bamboo_server_tools/
ask_agent.rs1use 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
22const 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}