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()
|
defer cancel()
|
||||||
|
|
||||||
session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".new", "")
|
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()
|
ag.EnsureTrailingNewline()
|
||||||
reply := ag.Reply()
|
reply := ag.Reply()
|
||||||
s.RemoveAgent(ag.ID())
|
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=`.
|
Optional parameters: `profile=`, `backend=`, `model=`, `name=`, `timeout=`, `max_depth=`, `max_parallel=`, `fork_at=`.
|
||||||
|
|
||||||
**Guardrails** (enforced server-side):
|
**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_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).
|
- `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 outputWriter io.Writer
|
||||||
var startedCh chan exec.StartResult
|
var startedCh chan exec.StartResult
|
||||||
timeout := 30 // default for foreground
|
timeout := 30 // default for foreground
|
||||||
|
if info.Timeout >= 0 {
|
||||||
|
timeout = info.Timeout // tool declares its own default
|
||||||
|
}
|
||||||
if background {
|
if background {
|
||||||
outputWriter = &procWriter{proc: proc}
|
outputWriter = &procWriter{proc: proc}
|
||||||
startedCh = make(chan exec.StartResult, 1)
|
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.",
|
"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",
|
"scope": "read",
|
||||||
"tier": "hot",
|
"tier": "hot",
|
||||||
|
"timeout": 0,
|
||||||
"args": {
|
"args": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -32,6 +33,10 @@
|
||||||
"cwd": {
|
"cwd": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Working directory (optional, default: current)"
|
"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"`
|
Tier string `json:"tier,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
OutputFormat string `json:"outputFormat,omitempty"`
|
OutputFormat string `json:"outputFormat,omitempty"`
|
||||||
|
Timeout *int `json:"timeout,omitempty"`
|
||||||
Cmd string `json:"cmd,omitempty"`
|
Cmd string `json:"cmd,omitempty"`
|
||||||
Variants []Variant `json:"variants,omitempty"`
|
Variants []Variant `json:"variants,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +157,14 @@ func checkCondition(key, val string) bool {
|
||||||
return false
|
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.
|
// ToolInfoFromMeta builds a ToolInfo from a MetaFile.
|
||||||
// Applies variant resolution first. Returns zero ToolInfo if no variant matches.
|
// Applies variant resolution first. Returns zero ToolInfo if no variant matches.
|
||||||
func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
|
func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
|
||||||
|
|
@ -179,6 +188,7 @@ func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
|
||||||
Tier: tier,
|
Tier: tier,
|
||||||
Scope: resolved.Scope,
|
Scope: resolved.Scope,
|
||||||
OutputFormat: resolved.OutputFormat,
|
OutputFormat: resolved.OutputFormat,
|
||||||
|
Timeout: resolveTimeout(m.Timeout),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type ToolInfo struct {
|
||||||
Tier string
|
Tier string
|
||||||
Scope string
|
Scope string
|
||||||
OutputFormat string
|
OutputFormat string
|
||||||
|
Timeout int // default execution timeout; 0 = no timeout, -1 = use server default
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolResult is the structured result from a tool execution.
|
// ToolResult is the structured result from a tool execution.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue