fix(supervisor): make agent__check a pure status probe that never consumes the handle

agent__check on a finished agent delegated to agent__collect, which
returned the full (unbounded) result and consumed the handle. That
contradicted the tool's own docs and broke the check-then-collect
pattern: a second collect on the same id failed.

check now reports { status: finished } with a pointer to
agent__collect and leaves the handle registered; collect is the single
retrieval verb. The tool description and prompt table are updated to
stop promising that check returns the result.
This commit is contained in:
2026-08-25 17:36:11 -06:00
parent 7cf88c030f
commit 257b06bbd4
2 changed files with 29 additions and 10 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {"
| Tool | Purpose | | Tool | Purpose |
|------|----------| |------|----------|
| `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. | | `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. |
| `agent__check` | Non-blocking check: is the agent done yet? Returns PENDING or result. | | `agent__check` | Non-blocking status probe: running or finished. Never returns/consumes the result — use `agent__collect`. |
| `agent__collect` | Blocking wait: wait for an agent to finish, return its output. | | `agent__collect` | Blocking wait: wait for an agent to finish, return its output. |
| `agent__list_available` | List all agent types you can spawn (name + description). Use this to discover specialists before calling `agent__spawn`. | | `agent__list_available` | List all agent types you can spawn (name + description). Use this to discover specialists before calling `agent__spawn`. |
| `agent__list_running` | List all subagents YOU have spawned, with their status. | | `agent__list_running` | List all subagents YOU have spawned, with their status. |
+28 -9
View File
@@ -255,7 +255,7 @@ pub fn supervisor_function_declarations() -> Vec<FunctionDeclaration> {
}, },
FunctionDeclaration { FunctionDeclaration {
name: format!("{SUPERVISOR_FUNCTION_PREFIX}check"), name: format!("{SUPERVISOR_FUNCTION_PREFIX}check"),
description: "Check if a spawned agent has finished. Non-blocking; returns PENDING if still running, or the result if complete.".to_string(), description: "Non-blocking status probe: reports whether a spawned agent is still running or finished. NEVER returns or consumes the result — when finished, call agent__collect to retrieve it.".to_string(),
parameters: JsonSchema { parameters: JsonSchema {
type_value: Some("object".to_string()), type_value: Some("object".to_string()),
properties: Some(IndexMap::from([( properties: Some(IndexMap::from([(
@@ -934,7 +934,15 @@ async fn handle_check(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
}; };
match is_finished { match is_finished {
Some(true) => handle_collect(ctx, args).await, Some(true) => Ok(json!({
"status": "finished",
"id": id,
"message": format!(
"Agent '{id}' has finished; its result is ready and has NOT been consumed. \
Call `agent__collect --id {id}` to retrieve it (returns instantly on a \
finished agent). The handle stays registered until collected."
),
})),
Some(false) => { Some(false) => {
let mut result = json!({ let mut result = json!({
"status": "pending", "status": "pending",
@@ -2711,23 +2719,34 @@ mod tests {
assert!(err.to_string().contains("No supervisor active")); assert!(err.to_string().contains("No supervisor active"));
} }
/// Pins current behavior: checking a finished agent does not report a /// Checking a finished agent is a pure status probe: it reports the
/// "finished, ready to collect" status; it silently delegates to collect, /// agent as finished, points at agent__collect, and leaves the handle
/// returning the full result and consuming the handle. /// registered so a subsequent collect still returns the result.
#[test] #[test]
fn handle_check_finished_agent_delegates_to_collect_and_consumes_handle() { fn handle_check_finished_agent_reports_status_and_keeps_handle() {
let mut ctx = ctx_with_supervisor(4, 3); let mut ctx = ctx_with_supervisor(4, 3);
register_fake_agent(&mut ctx, "a1", "explore"); register_fake_agent(&mut ctx, "a1", "explore");
wait_until_finished(&ctx, "a1"); wait_until_finished(&ctx, "a1");
let result = run_async(handle_check(&mut ctx, &json!({"id": "a1"}))).unwrap(); let result = run_async(handle_check(&mut ctx, &json!({"id": "a1"}))).unwrap();
assert_eq!(result["status"], "completed"); assert_eq!(result["status"], "finished");
assert_eq!(result["output"], "fake output"); assert_eq!(result["id"], "a1");
assert!(
result["message"]
.as_str()
.unwrap()
.contains("agent__collect")
);
assert_eq!( assert_eq!(
ctx.supervisor.as_ref().unwrap().read().is_finished("a1"), ctx.supervisor.as_ref().unwrap().read().is_finished("a1"),
None Some(true)
); );
let collected = run_async(handle_collect(&mut ctx, &json!({"id": "a1"}))).unwrap();
assert_eq!(collected["status"], "completed");
assert_eq!(collected["output"], "fake output");
} }
#[test] #[test]