Skip to main content

bamboo_server_tools/
ask_agent.rs

1//! `ask_agent` — the in-loop "command another agent" tool.
2//!
3//! Lets a running (root) agent ask another agent — deployed as a local
4//! subprocess, in Docker, or on a remote host — a question over the central
5//! message broker, and judge the answer. The caller's session id is the asker
6//! (replies route back to it); the `target` is the other agent's broker mailbox
7//! key. Two modes mirror `AskMode`: `query` (read-only summarize/extract) and
8//! `steer` (insert into the target's conversation to redirect its work).
9//!
10//! Only registered on the Root surface when a broker is configured
11//! (`subagents.broker` in config).
12
13use std::time::Duration;
14
15use async_trait::async_trait;
16use serde::Deserialize;
17use serde_json::json;
18
19use bamboo_agent_core::tools::{Tool, ToolClass, ToolCtx, ToolError, ToolOutcome, ToolResult};
20use bamboo_subagent::{AgentRef, AskMode};
21
22/// Default / max wait for an answer.
23const DEFAULT_TIMEOUT_SECS: u64 = 60;
24const MAX_TIMEOUT_SECS: u64 = 300;
25
26pub struct AskAgentTool {
27    endpoint: String,
28    token: String,
29}
30
31impl AskAgentTool {
32    pub fn new(endpoint: impl Into<String>, token: impl Into<String>) -> Self {
33        Self {
34            endpoint: endpoint.into(),
35            token: token.into(),
36        }
37    }
38}
39
40#[derive(Debug, Deserialize)]
41struct AskArgs {
42    target: String,
43    question: String,
44    #[serde(default)]
45    mode: Option<String>,
46    #[serde(default)]
47    timeout_secs: Option<u64>,
48}
49
50#[async_trait]
51impl Tool for AskAgentTool {
52    fn name(&self) -> &str {
53        "ask_agent"
54    }
55
56    fn description(&self) -> &str {
57        "Ask another agent — already running locally, in a Docker container, or on a remote host — \
58         a question over the message broker, and get its answer back synchronously. This is how you \
59         COMMAND a worker you (or a teammate) deployed: `target` is that agent's id (the broker \
60         mailbox key returned by deploy_agent, or a peer's session id). Replies route back to you \
61         automatically.\n\
62         \n\
63         TWO MODES (pick deliberately):\n\
64         - mode=query (default) — READ-ONLY. The target inspects its OWN current state and \
65         summarizes/extracts an answer WITHOUT changing what it is doing. Use it to poll progress, \
66         pull a result, or ask 'what did you find?'. Safe to call repeatedly.\n\
67         - mode=steer — WRITE. Your question is injected into the target's LIVE conversation, so it \
68         redirects or advances the target's work (a command, not a peek). Use it to assign the next \
69         task, change priorities, or hand off new context.\n\
70         \n\
71         WORKED EXAMPLE (deploy → poll → steer):\n\
72         1. deploy_agent(action=deploy, env=docker, image=\"bamboo:latest\", role=\"researcher\") \
73         → returns id \"agent-1a2b3c\".\n\
74         2. ask_agent(target=\"agent-1a2b3c\", question=\"Summarize the auth flow in this repo.\", \
75         mode=query) → wait for its findings.\n\
76         3. ask_agent(target=\"agent-1a2b3c\", question=\"Now write the fix to src/auth.rs and run \
77         the tests.\", mode=steer) → reassigns it to do the work.\n\
78         4. ask_agent(target=\"agent-1a2b3c\", question=\"Are the tests green yet?\", mode=query) → \
79         poll until done.\n\
80         \n\
81         Blocks until the target answers or `timeout_secs` elapses (default 60, max 300) — raise it \
82         for slow work. The target must be reachable on the broker; deploy it with deploy_agent \
83         first if it does not exist yet."
84    }
85
86    fn parameters_schema(&self) -> serde_json::Value {
87        json!({
88            "type": "object",
89            "properties": {
90                "target": { "type": "string", "description": "The target agent's id (broker mailbox key)." },
91                "question": { "type": "string", "description": "What to ask the target agent." },
92                "mode": {
93                    "type": "string",
94                    "enum": ["query", "steer"],
95                    "description": "query = read-only summarize/extract (default); steer = insert into the target's conversation / redirect its work."
96                },
97                "timeout_secs": { "type": "number", "description": "Max seconds to wait for the answer (default 60, max 300)." }
98            },
99            "required": ["target", "question"]
100        })
101    }
102
103    fn classify(&self, _args: &serde_json::Value) -> ToolClass {
104        ToolClass::MUTATING_SERIAL.promotable()
105    }
106
107    async fn invoke(
108        &self,
109        args: serde_json::Value,
110        ctx: ToolCtx,
111    ) -> Result<ToolOutcome, ToolError> {
112        let caller = ctx.session_id().ok_or_else(|| {
113            ToolError::Execution("ask_agent requires a session_id in tool context".to_string())
114        })?;
115        let parsed: AskArgs = serde_json::from_value(args)
116            .map_err(|e| ToolError::InvalidArguments(format!("Invalid ask_agent args: {e}")))?;
117
118        let mode = match parsed.mode.as_deref() {
119            Some("steer") => AskMode::Steer,
120            Some("query") | None => AskMode::Query,
121            Some(other) => {
122                return Err(ToolError::InvalidArguments(format!(
123                    "unknown mode '{other}' (use 'query' or 'steer')"
124                )))
125            }
126        };
127        let timeout = Duration::from_secs(
128            parsed
129                .timeout_secs
130                .unwrap_or(DEFAULT_TIMEOUT_SECS)
131                .clamp(1, MAX_TIMEOUT_SECS),
132        );
133        let me = AgentRef {
134            session_id: caller.to_string(),
135            role: None,
136        };
137
138        let answer = bamboo_broker::ask_agent(
139            &self.endpoint,
140            me,
141            &self.token,
142            &parsed.target,
143            &parsed.question,
144            mode,
145            timeout,
146        )
147        .await
148        .map_err(|e| ToolError::Execution(format!("ask_agent failed: {e}")))?;
149
150        let mode_str = if matches!(mode, AskMode::Steer) {
151            "steer"
152        } else {
153            "query"
154        };
155        Ok(ToolOutcome::Completed(ToolResult {
156            success: true,
157            result: json!({ "from": parsed.target, "mode": mode_str, "answer": answer })
158                .to_string(),
159            display_preference: None,
160            images: Vec::new(),
161        }))
162    }
163}