fix(chatd): improve subagent tool descriptions and strip tools from child agents (#22441)

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 <coder@users.noreply.github.com>
This commit is contained in:
Kyle Carberry
2026-02-28 16:30:25 -05:00
committed by GitHub
co-authored by Coder
parent eec0b299e8
commit c316d0a3e7
2 changed files with 29 additions and 7 deletions
+8 -3
View File
@@ -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,
+21 -4
View File
@@ -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