subagent: fix timeout and premature response issues

Timeout fix:
- Add Timeout field to ToolInfo (protocol) and MetaFile (metadata)
- proc.go respects tool-declared timeout before falling back to 30s default
- subagent_spawn.meta declares timeout=0 (no timeout) so the tool is
  never killed prematurely while waiting for the sub-agent to finish
- Tool schema declares timeout with 'do not set' guidance to prevent
  the LLM from adding a short timeout

Premature response fix:
- Inject behavioral prefix into sub-agent prompt: complete all work
  before responding, report results not intentions
- Sub-agent's final text is returned to parent; this instruction ensures
  it contains accomplished work, not a plan
This commit is contained in:
Levi Neely 2026-08-20 10:59:23 +02:00
parent 24621294b0
commit 4ff37741e2
6 changed files with 24 additions and 2 deletions

View File

@ -666,7 +666,10 @@ func buildSessionChildren(
defer cancel()
session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".new", "")
ag.Submit(subCtx, req.Prompt)
subPrompt := "You are a sub-agent. Complete ALL work using tools before producing your final response. " +
"Your response is returned to the caller — it must be a summary of what you accomplished, not a plan of what you intend to do. " +
"Do not narrate your intentions; act, then report results.\n\n" + req.Prompt
ag.Submit(subCtx, subPrompt)
ag.EnsureTrailingNewline()
reply := ag.Reply()
s.RemoveAgent(ag.ID())

View File

@ -208,7 +208,7 @@ The sub-agent gets its own context window and tool access and runs independently
Optional parameters: `profile=`, `backend=`, `model=`, `name=`, `timeout=`, `max_depth=`, `max_parallel=`, `fork_at=`.
**Guardrails** (enforced server-side):
- `timeout=600` — sub-agent is killed after 10 minutes (default). Top-level agents are never timed out.
- `timeout=600` — sub-agent's internal execution limit (default 10 minutes). The tool itself has no execution timeout.
- `max_depth=1` — sub-agents cannot spawn their own sub-agents by default.
- `max_parallel=5` — up to 5 concurrent sub-agents per parent (set to -1 for unlimited).

View File

@ -310,6 +310,9 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) (
var outputWriter io.Writer
var startedCh chan exec.StartResult
timeout := 30 // default for foreground
if info.Timeout >= 0 {
timeout = info.Timeout // tool declares its own default
}
if background {
outputWriter = &procWriter{proc: proc}
startedCh = make(chan exec.StartResult, 1)

View File

@ -3,6 +3,7 @@
"prompt": "## subagent_spawn\n\nSpawn a sub-agent with a task. Blocks until the sub-agent completes and returns its reply.\nThe sub-agent gets its own context window and tool access. It is destroyed when done.\n\n```\nsubagent_spawn(prompt=\"fix the failing tests in auth/\")\nsubagent_spawn(prompt=\"implement the login page\", cwd=\"$PWD\", model=\"sonnet\")\n```\n\nMultiple subagent_spawn calls in the same turn run in parallel automatically.",
"scope": "read",
"tier": "hot",
"timeout": 0,
"args": {
"type": "object",
"required": [
@ -32,6 +33,10 @@
"cwd": {
"type": "string",
"description": "Working directory (optional, default: current)"
},
"timeout": {
"type": "integer",
"description": "Timeout in seconds (default: 0, no timeout). Do not set a timeout on sub-agents."
}
}
}

View File

@ -18,6 +18,7 @@ type MetaFile struct {
Tier string `json:"tier,omitempty"`
Scope string `json:"scope,omitempty"`
OutputFormat string `json:"outputFormat,omitempty"`
Timeout *int `json:"timeout,omitempty"`
Cmd string `json:"cmd,omitempty"`
Variants []Variant `json:"variants,omitempty"`
}
@ -156,6 +157,14 @@ func checkCondition(key, val string) bool {
return false
}
// resolveTimeout returns -1 (use server default) if nil, or the value if set.
func resolveTimeout(t *int) int {
if t == nil {
return -1
}
return *t
}
// ToolInfoFromMeta builds a ToolInfo from a MetaFile.
// Applies variant resolution first. Returns zero ToolInfo if no variant matches.
func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
@ -179,6 +188,7 @@ func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
Tier: tier,
Scope: resolved.Scope,
OutputFormat: resolved.OutputFormat,
Timeout: resolveTimeout(m.Timeout),
}
}

View File

@ -13,6 +13,7 @@ type ToolInfo struct {
Tier string
Scope string
OutputFormat string
Timeout int // default execution timeout; 0 = no timeout, -1 = use server default
}
// ToolResult is the structured result from a tool execution.