fix(subagent): report resumable children as ready

list_agents called a storage-only continuable child 'complete', inviting
parents to poll it for a result that will never appear there. Rename the
status to 'ready' — resumable, not terminal — and rewrite the tool
description to say a parent is told when a child finishes, so listing is
for recall, not polling.
This commit is contained in:
Hypatia May
2026-08-11 12:38:57 +08:00
parent 85dd22fd4f
commit 567fcadba0
50 changed files with 215 additions and 109 deletions
@@ -32,7 +32,7 @@ type ListAgentsEntry =
readonly kind: 'child'
readonly id: SessionId
readonly label: string
readonly status: 'running' | 'idle' | 'complete'
readonly status: 'running' | 'idle' | 'ready'
readonly parent?: SessionId
readonly depth?: number
}
@@ -52,11 +52,13 @@ function resolveListAgentsRequest(request: ListAgentsRequest): ListAgentsSpec {
/**
* Refine one candidate's status through the live Agent registry: `running`
* for an active driver, `idle` for a resident Agent between turns (possibly
* waiting on agents it started), and `complete` when no live Agent remains.
* waiting on agents it started), and `ready` when no live Agent remains.
* `ready` preserves resumability without presenting an inactive conversation
* as a terminal result to collect.
*/
function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: SessionId): 'running' | 'idle' | 'complete' {
function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: SessionId): 'running' | 'idle' | 'ready' {
const agent = agents.get(id)
if (agent === undefined) return 'complete'
if (agent === undefined) return 'ready'
return agent.status === 'running' ? 'running' : 'idle'
}
@@ -90,10 +92,12 @@ export function apply(ctx: Context): void {
ctx.tools.register(defineTool({
name: 'list_agents',
description:
'List your continuable background subagents by durable id and label. Status comes from the live '
'List your continuable background subagents by durable id and label. Use it to recall which ones '
+ 'you started, not to poll for completion — you are told when one finishes. Status comes from the live '
+ 'registry: running means the agent is working right now, idle means it is loaded but between turns '
+ '(it may be waiting on agents it started), and complete means it exists only in storage — a '
+ 'direct child remains a `send_message` candidate in every status. The snapshot is not a delivery '
+ '(it may be waiting on agents it started), and ready means it exists only in storage — resumable, not '
+ 'terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same '
+ 'conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery '
+ 'promise — `send_message` performs the authoritative check and may still fail. Children that could '
+ 'not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` '
+ 'walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent '
@@ -118,7 +122,7 @@ export function apply(ctx: Context): void {
kind: { type: 'string', required: true, enum: ['child'] },
id: { type: 'string', required: true },
label: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['running', 'idle', 'complete'] },
status: { type: 'string', required: true, enum: ['running', 'idle', 'ready'] },
parent: { type: 'string' },
depth: { type: 'number' },
},