From 257b06bbd4f7f43e2b6f3731248c1a4444095301 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 25 Aug 2026 15:46:26 -0600 Subject: [PATCH] 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. --- src/config/prompts.rs | 2 +- src/function/supervisor.rs | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/config/prompts.rs b/src/config/prompts.rs index 8af226b..cb2e163 100644 --- a/src/config/prompts.rs +++ b/src/config/prompts.rs @@ -82,7 +82,7 @@ pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {" | Tool | Purpose | |------|----------| | `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__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. | diff --git a/src/function/supervisor.rs b/src/function/supervisor.rs index ad09808..2e766e2 100644 --- a/src/function/supervisor.rs +++ b/src/function/supervisor.rs @@ -255,7 +255,7 @@ pub fn supervisor_function_declarations() -> Vec { }, FunctionDeclaration { 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 { type_value: Some("object".to_string()), properties: Some(IndexMap::from([( @@ -934,7 +934,15 @@ async fn handle_check(ctx: &mut RequestContext, args: &Value) -> Result { }; 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) => { let mut result = json!({ "status": "pending", @@ -2711,23 +2719,34 @@ mod tests { assert!(err.to_string().contains("No supervisor active")); } - /// Pins current behavior: checking a finished agent does not report a - /// "finished, ready to collect" status; it silently delegates to collect, - /// returning the full result and consuming the handle. + /// Checking a finished agent is a pure status probe: it reports the + /// agent as finished, points at agent__collect, and leaves the handle + /// registered so a subsequent collect still returns the result. #[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); register_fake_agent(&mut ctx, "a1", "explore"); wait_until_finished(&ctx, "a1"); let result = run_async(handle_check(&mut ctx, &json!({"id": "a1"}))).unwrap(); - assert_eq!(result["status"], "completed"); - assert_eq!(result["output"], "fake output"); + assert_eq!(result["status"], "finished"); + assert_eq!(result["id"], "a1"); + assert!( + result["message"] + .as_str() + .unwrap() + .contains("agent__collect") + ); assert_eq!( 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]