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:
@@ -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. |
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user