Skip to main content

synaps_cli/tools/subagent/
steer.rs

1//! SubagentSteerTool — inject a guidance message into a running reactive subagent.
2//!
3//! Sends a message over the subagent's steering channel. The message is surfaced
4//! to the subagent's event loop as additional context mid-run, allowing the
5//! orchestrating agent to correct course without killing and restarting.
6//!
7//! Fails (non-fatally) if the subagent has already finished or the steering
8//! channel is unavailable — the caller receives a structured error payload.
9
10use serde_json::{json, Value};
11use crate::{Result, RuntimeError};
12use super::super::{Tool, ToolContext};
13use crate::runtime::subagent::SubagentStatus;
14
15pub struct SubagentSteerTool;
16
17#[async_trait::async_trait]
18impl Tool for SubagentSteerTool {
19    fn name(&self) -> &str { "subagent_steer" }
20
21    fn description(&self) -> &str {
22        "Inject a guidance message into a running reactive subagent. Use this to \
23         correct course, provide new context, or impose constraints mid-run without \
24         stopping the subagent. Returns {\"acknowledged\": true} on success or an \
25         error payload if the subagent is no longer running."
26    }
27
28    fn parameters(&self) -> Value {
29        json!({
30            "type": "object",
31            "properties": {
32                "handle_id": {
33                    "type": "string",
34                    "description": "Handle ID returned by subagent_start (e.g. \"sa_3\")."
35                },
36                "message": {
37                    "type": "string",
38                    "description": "Guidance message to inject into the subagent's context. \
39                                    Keep it concise — the subagent sees this as a mid-run \
40                                    user message."
41                }
42            },
43            "required": ["handle_id", "message"]
44        })
45    }
46
47    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
48        let handle_id = params["handle_id"].as_str()
49            .ok_or_else(|| RuntimeError::Tool("Missing 'handle_id' parameter".to_string()))?
50            .to_string();
51
52        let message = params["message"].as_str()
53            .ok_or_else(|| RuntimeError::Tool("Missing 'message' parameter".to_string()))?
54            .to_string();
55
56        // ── Registry lookup ────────────────────────────────────────────────────
57
58        let registry = ctx.capabilities.subagent_registry.as_ref()
59            .ok_or_else(|| RuntimeError::Tool(
60                "SubagentRegistry not available on this ToolContext".to_string()
61            ))?;
62
63        let reg = registry.lock().unwrap();
64
65        let handle = reg.get(&handle_id)
66            .ok_or_else(|| RuntimeError::Tool(
67                format!("No subagent found with handle_id '{}'", handle_id)
68            ))?;
69
70        // ── Guard: only steer a running subagent ───────────────────────────────
71
72        if handle.status() != SubagentStatus::Running {
73            return Ok(json!({
74                "acknowledged": false,
75                "error": format!(
76                    "Subagent '{}' is '{}' — steering is only possible while running.",
77                    handle_id,
78                    handle.status().as_str()
79                )
80            }).to_string());
81        }
82
83        // ── Send over steering channel ─────────────────────────────────────────
84        //
85        // The `steer_tx` is connected by the background task in subagent_start.
86        // Until that wiring is done the channel will be None — return a clear
87        // stub response so callers know the shape of the success path.
88
89        match handle.steer(&message) {
90            Ok(()) => Ok(json!({ "acknowledged": true }).to_string()),
91            Err(e) => {
92                tracing::debug!(
93                    "subagent_steer: steer failed for handle '{}': {}",
94                    handle_id, e
95                );
96                Ok(json!({
97                    "acknowledged": false,
98                    "error": e
99                }).to_string())
100            }
101        }
102    }
103}