Skip to main content

agent_engine/tools/subagent/
status.rs

1//! SubagentStatusTool — poll the current state of a running or completed reactive subagent.
2//!
3//! Returns a lightweight snapshot: lifecycle status, last 500 chars of output,
4//! elapsed wall-clock seconds, and tool-use count. Non-blocking — always returns
5//! immediately regardless of the subagent's progress.
6
7use super::super::{Tool, ToolContext};
8use crate::{Result, RuntimeError};
9use serde_json::{json, Value};
10
11pub struct SubagentStatusTool;
12
13#[async_trait::async_trait]
14impl Tool for SubagentStatusTool {
15    fn origin(&self) -> crate::tools::ToolOrigin {
16        crate::tools::ToolOrigin::Builtin
17    }
18
19    fn name(&self) -> &str {
20        "subagent_status"
21    }
22
23    fn description(&self) -> &str {
24        "Poll the current state of a reactive subagent. Returns status \
25         (running/completed/cancelled/timed_out/failed), the last 500 characters of output \
26         produced so far, elapsed time in seconds, and the number of tool calls \
27         made. If the status is 'failed', an 'error' field contains the failure reason. \
28         Non-blocking — returns immediately. Use for an occasional progress peek only: \
29         subagents are reactive and push a completion event that wakes you when they \
30         finish, so never call this in a sleep/poll loop."
31    }
32
33    fn parameters(&self) -> Value {
34        json!({
35            "type": "object",
36            "properties": {
37                "handle_id": {
38                    "type": "string",
39                    "description": "Handle ID returned by subagent_start (e.g. \"sa_3\")."
40                }
41            },
42            "required": ["handle_id"]
43        })
44    }
45
46    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
47        let handle_id = params["handle_id"]
48            .as_str()
49            .ok_or_else(|| RuntimeError::Tool("Missing 'handle_id' parameter".to_string()))?
50            .to_string();
51
52        // ── Registry lookup ────────────────────────────────────────────────────
53
54        let registry = ctx.capabilities.subagent_registry.as_ref().ok_or_else(|| {
55            RuntimeError::Tool("SubagentRegistry not available on this ToolContext".to_string())
56        })?;
57
58        let reg = registry.lock().unwrap();
59
60        let handle = reg.get(&handle_id).ok_or_else(|| {
61            RuntimeError::Tool(format!("No subagent found with handle_id '{}'", handle_id))
62        })?;
63
64        // ── Build response ─────────────────────────────────────────────────────
65        // Clone all needed data under the lock, then drop lock before char traversal
66        let full: String = handle.partial_output();
67        let agent_name = handle.agent_name.clone();
68        let status = handle.status();
69        let status_str = status.as_str().to_string();
70        let elapsed = handle.elapsed_secs();
71        let tool_count = handle.tool_log().len();
72        let model = handle.model.clone();
73        let terminal = handle.terminal_diagnostic();
74        let authorization = handle.authorization.clone();
75        let archive_error = handle.archive_error();
76        let _ = handle;
77        drop(reg);
78
79        let char_count = full.chars().count();
80        let partial_output: String = if char_count > 500 {
81            full.chars().skip(char_count - 500).collect()
82        } else {
83            full
84        };
85
86        let output_length = partial_output.chars().count();
87        if let Some(orchestration) = &ctx.capabilities.orchestration {
88            orchestration
89                .poll(&handle_id, &format!("{}:{}", status_str, tool_count))
90                .map_err(RuntimeError::Tool)?;
91        }
92
93        let mut resp = json!({
94            "handle_id":      handle_id,
95            "agent_name":     agent_name,
96            "status":         status_str,
97            "partial_output": partial_output,
98            "elapsed_secs":   (elapsed * 10.0).round() / 10.0,
99            "tool_count":     tool_count,
100            "model":          model,
101            "terminal_cause": terminal,
102            "authorization":   authorization,
103            "archiveError": archive_error,
104            "output_length":  output_length
105        });
106        if let Some(reason) = status.failure_reason() {
107            resp["error"] = json!(reason);
108        }
109        Ok(resp.to_string())
110    }
111}