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:
parent
24621294b0
commit
4ff37741e2
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue