From c316d0a3e76fd4a3ec7d1d1b97760366e0957ef0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 28 Feb 2026 16:30:25 -0500 Subject: [PATCH] fix(chatd): improve subagent tool descriptions and strip tools from child agents (#22441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: 1. **Gate subagent tools behind `!chat.ParentChatID.Valid`** so child agents never receive `spawn_agent`, `wait_agent`, `message_agent`, or `close_agent`. Previously all 4 tools were given to every chat. `spawn_agent` would fail at runtime ("delegated chats cannot create child subagents") but the other 3 had no guard at all — meaning a child could theoretically operate on sibling chats. Removing the tools entirely is cleaner and saves context window. 2. **Rewrite tool descriptions to explain *when* to use them**, not just what they do. `spawn_agent` now says to use it for clearly scoped, independent, self-contained tasks (e.g. fixing a specific bug, writing a single module, running a migration) and explicitly says *not* to use it for simple operations you can handle with `execute`/`read_file`/`write_file`. It also states that child agents cannot spawn their own subagents. The other 3 tools get similar guidance-oriented descriptions. Co-authored-by: Coder --- coderd/chatd/chatd.go | 11 ++++++++--- coderd/chatd/subagent.go | 25 +++++++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/coderd/chatd/chatd.go b/coderd/chatd/chatd.go index 262bbe0828..f2a4db6ab9 100644 --- a/coderd/chatd/chatd.go +++ b/coderd/chatd/chatd.go @@ -2093,9 +2093,14 @@ func (p *Server) runChat( GetWorkspaceConn: getWorkspaceConn, }), } - tools = append(tools, p.subagentTools(func() database.Chat { - return chat - })...) + // Only root chats (not delegated subagents) get subagent tools. + // Child agents must not spawn further subagents — they should + // focus on completing their delegated task. + if !chat.ParentChatID.Valid { + tools = append(tools, p.subagentTools(func() database.Chat { + return chat + })...) + } _, err = chatloop.Run(ctx, chatloop.RunOptions{ Model: model, diff --git a/coderd/chatd/subagent.go b/coderd/chatd/subagent.go index f818f29f50..bb41d1d47f 100644 --- a/coderd/chatd/subagent.go +++ b/coderd/chatd/subagent.go @@ -46,7 +46,15 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent return []fantasy.AgentTool{ fantasy.NewAgentTool( "spawn_agent", - "Spawn a delegated child agent chat from the root chat.", + "Spawn a delegated child agent to work on a clearly scoped, "+ + "independent task in parallel. Use this when the task is "+ + "self-contained and would benefit from a separate agent "+ + "(e.g. fixing a specific bug, writing a single module, "+ + "running a migration). Do NOT use for simple or quick "+ + "operations you can handle directly with execute, "+ + "read_file, or write_file. The child agent receives the "+ + "same workspace tools but cannot spawn its own subagents. "+ + "After spawning, use wait_agent to collect the result.", func(ctx context.Context, args spawnAgentArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { if currentChat == nil { return fantasy.NewTextErrorResponse("subagent callbacks are not configured"), nil @@ -80,7 +88,10 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent ), fantasy.NewAgentTool( "wait_agent", - "Wait until a delegated descendant agent reaches a non-streaming status.", + "Wait until a spawned child agent finishes its task. "+ + "Returns the agent's final response and status. "+ + "Call this after spawn_agent to collect the result "+ + "before continuing your own work.", func(ctx context.Context, args waitAgentArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { if currentChat == nil { return fantasy.NewTextErrorResponse("subagent callbacks are not configured"), nil @@ -117,7 +128,11 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent ), fantasy.NewAgentTool( "message_agent", - "Send a message to a delegated descendant agent. Use wait_agent to collect a response.", + "Send a follow-up message to a previously spawned child "+ + "agent. Use this to provide additional instructions, "+ + "corrections, or context to a running or completed "+ + "agent. After sending, use wait_agent to collect the "+ + "updated response.", func(ctx context.Context, args messageAgentArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { if currentChat == nil { return fantasy.NewTextErrorResponse("subagent callbacks are not configured"), nil @@ -154,7 +169,9 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent ), fantasy.NewAgentTool( "close_agent", - "Interrupt a delegated descendant agent immediately.", + "Immediately stop a spawned child agent. Use this to "+ + "cancel a subagent that is stuck, no longer needed, "+ + "or working on the wrong approach.", func(ctx context.Context, args closeAgentArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { if currentChat == nil { return fantasy.NewTextErrorResponse("subagent callbacks are not configured"), nil