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, ToolError, ToolExecutionContext, 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    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
104        self.execute_with_context(args, ToolExecutionContext::none("tool_call"))
105            .await
106    }
107
108    async fn execute_with_context(
109        &self,
110        args: serde_json::Value,
111        ctx: ToolExecutionContext<'_>,
112    ) -> Result<ToolResult, ToolError> {
113        let caller = ctx.session_id.ok_or_else(|| {
114            ToolError::Execution("ask_agent requires a session_id in tool context".to_string())
115        })?;
116        let parsed: AskArgs = serde_json::from_value(args)
117            .map_err(|e| ToolError::InvalidArguments(format!("Invalid ask_agent args: {e}")))?;
118
119        let mode = match parsed.mode.as_deref() {
120            Some("steer") => AskMode::Steer,
121            Some("query") | None => AskMode::Query,
122            Some(other) => {
123                return Err(ToolError::InvalidArguments(format!(
124                    "unknown mode '{other}' (use 'query' or 'steer')"
125                )))
126            }
127        };
128        let timeout = Duration::from_secs(
129            parsed
130                .timeout_secs
131                .unwrap_or(DEFAULT_TIMEOUT_SECS)
132                .clamp(1, MAX_TIMEOUT_SECS),
133        );
134        let me = AgentRef {
135            session_id: caller.to_string(),
136            role: None,
137        };
138
139        let answer = bamboo_broker::ask_agent(
140            &self.endpoint,
141            me,
142            &self.token,
143            &parsed.target,
144            &parsed.question,
145            mode,
146            timeout,
147        )
148        .await
149        .map_err(|e| ToolError::Execution(format!("ask_agent failed: {e}")))?;
150
151        let mode_str = if matches!(mode, AskMode::Steer) {
152            "steer"
153        } else {
154            "query"
155        };
156        Ok(ToolResult {
157            success: true,
158            result: json!({ "from": parsed.target, "mode": mode_str, "answer": answer })
159                .to_string(),
160            display_preference: None,
161            images: Vec::new(),
162        })
163    }
164}