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, ToolClass, ToolCtx, ToolError, ToolOutcome, 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 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}