From 0105bab2d5b2e4d8cbe5f0c1c2b029f8c0ca1191 Mon Sep 17 00:00:00 2001 From: Michael <108142320+xthanhn91@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:06:18 +0700 Subject: [PATCH] fix(spawn): resolve parent agent from context in managed mode (#4) In managed mode, multiple agents share a single tool registry. The spawn and subagent tools were constructed with a hardcoded parentID of "default", which does not exist as an agent in managed mode deployments. This caused subagent announces to fail with "agent default not found" when any non-default agent (e.g. a secondary agent) tried to spawn a subagent. Fix: add ctxAgentKey to tool context keys (same pattern as channel/chatID), inject the calling agent's key from the loop, and read it in spawn/subagent tools with a fallback to the construction-time default for backward compatibility with standalone mode. --- internal/agent/loop.go | 3 +++ internal/tools/context_keys.go | 13 +++++++++++++ internal/tools/subagent_spawn_tool.go | 8 +++++++- internal/tools/subagent_tool.go | 8 +++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 1f276ff0..5133f87c 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -434,6 +434,9 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } } + // Inject agent key into context for tool-level resolution (managed mode: multiple agents share tool registry) + ctx = tools.WithToolAgentKey(ctx, l.id) + // Security: truncate oversized user messages gracefully (feed truncation notice into LLM) maxChars := l.maxMessageChars if maxChars <= 0 { diff --git a/internal/tools/context_keys.go b/internal/tools/context_keys.go index bceff734..8a9b8f88 100644 --- a/internal/tools/context_keys.go +++ b/internal/tools/context_keys.go @@ -20,6 +20,7 @@ const ( ctxSandboxKey toolContextKey = "tool_sandbox_key" ctxAsyncCB toolContextKey = "tool_async_cb" ctxWorkspace toolContextKey = "tool_workspace" + ctxAgentKey toolContextKey = "tool_agent_key" ) func WithToolChannel(ctx context.Context, channel string) context.Context { @@ -76,6 +77,18 @@ func ToolWorkspaceFromCtx(ctx context.Context) string { return v } +// WithToolAgentKey injects the calling agent's key into context. +// In managed mode multiple agents share a single tool registry; the agent key +// lets tools like spawn/subagent identify which agent is the parent. +func WithToolAgentKey(ctx context.Context, key string) context.Context { + return context.WithValue(ctx, ctxAgentKey, key) +} + +func ToolAgentKeyFromCtx(ctx context.Context) string { + v, _ := ctx.Value(ctxAgentKey).(string) + return v +} + // --- Vision / ImageGen config (per-agent overrides) --- const ( diff --git a/internal/tools/subagent_spawn_tool.go b/internal/tools/subagent_spawn_tool.go index 3ae1b7f5..0e114f84 100644 --- a/internal/tools/subagent_spawn_tool.go +++ b/internal/tools/subagent_spawn_tool.go @@ -61,7 +61,13 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *R peerKind := ToolPeerKindFromCtx(ctx) callback := ToolAsyncCBFromCtx(ctx) - msg, err := t.manager.Spawn(ctx, t.parentID, t.depth, task, label, modelOverride, + // Resolve parent agent from ctx (managed mode) with fallback to construction-time default + parentID := ToolAgentKeyFromCtx(ctx) + if parentID == "" { + parentID = t.parentID + } + + msg, err := t.manager.Spawn(ctx, parentID, t.depth, task, label, modelOverride, channel, chatID, peerKind, callback) if err != nil { return ErrorResult(err.Error()) diff --git a/internal/tools/subagent_tool.go b/internal/tools/subagent_tool.go index b6884257..a1593bc8 100644 --- a/internal/tools/subagent_tool.go +++ b/internal/tools/subagent_tool.go @@ -148,7 +148,13 @@ func (t *SubagentTool) executeRun(ctx context.Context, args map[string]interface channel := ToolChannelFromCtx(ctx) chatID := ToolChatIDFromCtx(ctx) - result, iterations, err := t.manager.RunSync(ctx, t.parentID, t.depth, task, label, + // Resolve parent agent from ctx (managed mode) with fallback to construction-time default + parentID := ToolAgentKeyFromCtx(ctx) + if parentID == "" { + parentID = t.parentID + } + + result, iterations, err := t.manager.RunSync(ctx, parentID, t.depth, task, label, channel, chatID) if err != nil { return ErrorResult(fmt.Sprintf("Subagent '%s' failed: %v", label, err))