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."
29    }
30
31    fn parameters(&self) -> Value {
32        json!({
33            "type": "object",
34            "properties": {
35                "handle_id": {
36                    "type": "string",
37                    "description": "Handle ID returned by subagent_start (e.g. \"sa_3\")."
38                }
39            },
40            "required": ["handle_id"]
41        })
42    }
43
44    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
45        let handle_id = params["handle_id"]
46            .as_str()
47            .ok_or_else(|| RuntimeError::Tool("Missing 'handle_id' parameter".to_string()))?
48            .to_string();
49
50        // ── Registry lookup ────────────────────────────────────────────────────
51
52        let registry = ctx.capabilities.subagent_registry.as_ref().ok_or_else(|| {
53            RuntimeError::Tool("SubagentRegistry not available on this ToolContext".to_string())
54        })?;
55
56        let reg = registry.lock().unwrap();
57
58        let handle = reg.get(&handle_id).ok_or_else(|| {
59            RuntimeError::Tool(format!("No subagent found with handle_id '{}'", handle_id))
60        })?;
61
62        // ── Build response ─────────────────────────────────────────────────────
63        // Clone all needed data under the lock, then drop lock before char traversal
64        let full: String = handle.partial_output();
65        let agent_name = handle.agent_name.clone();
66        let status = handle.status();
67        let status_str = status.as_str().to_string();
68        let elapsed = handle.elapsed_secs();
69        let tool_count = handle.tool_log().len();
70        let model = handle.model.clone();
71        let terminal = handle.terminal_diagnostic();
72        let authorization = handle.authorization.clone();
73        let _ = handle;
74        drop(reg);
75
76        let char_count = full.chars().count();
77        let partial_output: String = if char_count > 500 {
78            full.chars().skip(char_count - 500).collect()
79        } else {
80            full
81        };
82
83        let output_length = partial_output.chars().count();
84        if let Some(orchestration) = &ctx.capabilities.orchestration {
85            orchestration
86                .poll(&handle_id, &format!("{}:{}", status_str, tool_count))
87                .map_err(RuntimeError::Tool)?;
88        }
89
90        let mut resp = json!({
91            "handle_id":      handle_id,
92            "agent_name":     agent_name,
93            "status":         status_str,
94            "partial_output": partial_output,
95            "elapsed_secs":   (elapsed * 10.0).round() / 10.0,
96            "tool_count":     tool_count,
97            "model":          model,
98            "terminal_cause": terminal,
99            "authorization":   authorization,
100            "output_length":  output_length
101        });
102        if let Some(reason) = status.failure_reason() {
103            resp["error"] = json!(reason);
104        }
105        Ok(resp.to_string())
106    }
107}