Skip to main content

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