Skip to main content

codetether_agent/tool/
relay_autochat.rs

1//! Relay AutoChat Tool - Autonomous relay communication between agents
2//!
3//! Enables task delegation and result aggregation between agents using
4//! the protocol-first relay runtime. This tool allows LLMs to trigger
5//! agent handoffs and coordinate multi-agent workflows.
6
7use super::{Tool, ToolResult};
8use crate::bus::AgentBus;
9use crate::bus::relay::ProtocolRelayRuntime;
10use anyhow::Result;
11use async_trait::async_trait;
12use parking_lot::RwLock;
13use serde_json::{Value, json};
14use std::collections::HashMap;
15use std::sync::Arc;
16use tokio::sync::OnceCell;
17use uuid::Uuid;
18
19lazy_static::lazy_static! {
20    static ref RELAY_STORE: RwLock<HashMap<String, Arc<ProtocolRelayRuntime>>> = RwLock::new(HashMap::new());
21    static ref AGENT_BUS: OnceCell<Arc<AgentBus>> = OnceCell::const_new();
22}
23
24async fn get_agent_bus() -> Result<Arc<AgentBus>> {
25    let bus = AGENT_BUS
26        .get_or_try_init(|| async {
27            let bus = AgentBus::new().into_arc();
28            Ok::<_, anyhow::Error>(bus)
29        })
30        .await?;
31    Ok(bus.clone())
32}
33
34pub struct RelayAutoChatTool;
35
36impl RelayAutoChatTool {
37    pub fn new() -> Self {
38        Self
39    }
40}
41
42#[async_trait]
43impl Tool for RelayAutoChatTool {
44    fn id(&self) -> &str {
45        "relay_autochat"
46    }
47
48    fn name(&self) -> &str {
49        "Relay AutoChat"
50    }
51
52    fn description(&self) -> &str {
53        "Autonomous relay communication between agents for task delegation and result aggregation. \
54         Actions: delegate (send task to target agent), handoff (pass context between agents), \
55         status (check relay status), list_agents (show available agents in relay), \
56         init (initialize a new relay with task), complete (finish relay and aggregate results)."
57    }
58
59    fn parameters(&self) -> Value {
60        json!({
61            "type": "object",
62            "properties": {
63                "action": {
64                    "type": "string",
65                    "enum": ["delegate", "handoff", "status", "list_agents", "init", "complete"],
66                    "description": "Action to perform"
67                },
68                "target_agent": {
69                    "type": "string",
70                    "description": "Target agent name for delegation/handoff"
71                },
72                "message": {
73                    "type": "string",
74                    "description": "Message to send to the target agent"
75                },
76                "context": {
77                    "type": "object",
78                    "description": "Additional context to pass along (JSON object)"
79                },
80                "relay_id": {
81                    "type": "string",
82                    "description": "Relay ID to use (auto-generated if not provided)"
83                },
84                "okr_id": {
85                    "type": "string",
86                    "description": "Optional OKR ID to associate with this relay"
87                },
88                "task": {
89                    "type": "string",
90                    "description": "Task description for initializing a new relay"
91                }
92            },
93            "required": ["action"]
94        })
95    }
96
97    async fn execute(&self, params: Value) -> Result<ToolResult> {
98        let action = match params.get("action").and_then(|v| v.as_str()) {
99            Some(s) if !s.is_empty() => s.to_string(),
100            _ => {
101                return Ok(ToolResult::structured_error(
102                    "MISSING_FIELD",
103                    "relay_autochat",
104                    "action is required. Valid actions: init, delegate, handoff, status, list_agents, complete",
105                    Some(vec!["action"]),
106                    Some(json!({
107                        "action": "init",
108                        "task": "description of the relay task"
109                    })),
110                ));
111            }
112        };
113
114        let relay_id = params
115            .get("relay_id")
116            .and_then(|v| v.as_str())
117            .map(String::from);
118        let target_agent = params
119            .get("target_agent")
120            .and_then(|v| v.as_str())
121            .map(String::from);
122        let message = params
123            .get("message")
124            .and_then(|v| v.as_str())
125            .map(String::from);
126        let context = params.get("context").cloned();
127        let okr_id = params
128            .get("okr_id")
129            .and_then(|v| v.as_str())
130            .map(String::from);
131        let task = params
132            .get("task")
133            .and_then(|v| v.as_str())
134            .map(String::from);
135
136        match action.as_str() {
137            "init" => self.init_relay(relay_id, task, context, okr_id).await,
138            "delegate" => {
139                self.delegate_task(relay_id, target_agent, message, context, okr_id)
140                    .await
141            }
142            "handoff" => {
143                self.handoff_context(relay_id, target_agent, message, context)
144                    .await
145            }
146            "status" => self.get_status(relay_id).await,
147            "list_agents" => self.list_agents(relay_id).await,
148            "complete" => self.complete_relay(relay_id).await,
149            _ => Ok(ToolResult::structured_error(
150                "INVALID_ACTION",
151                "relay_autochat",
152                &format!(
153                    "Unknown action: '{action}'. Valid actions: init, delegate, handoff, status, list_agents, complete"
154                ),
155                None,
156                Some(json!({
157                    "action": "init",
158                    "task": "description of the relay task"
159                })),
160            )),
161        }
162    }
163}
164
165impl RelayAutoChatTool {
166    /// Initialize a new relay with a task
167    async fn init_relay(
168        &self,
169        relay_id: Option<String>,
170        task: Option<String>,
171        _context: Option<Value>,
172        okr_id: Option<String>,
173    ) -> Result<ToolResult> {
174        let task = task.unwrap_or_else(|| "Unspecified task".to_string());
175        let relay_id =
176            relay_id.unwrap_or_else(|| format!("relay-{}", &Uuid::new_v4().to_string()[..8]));
177
178        let bus = get_agent_bus().await?;
179        let runtime = ProtocolRelayRuntime::with_relay_id(bus, relay_id.clone());
180
181        // Store the runtime
182        {
183            let mut store = RELAY_STORE.write();
184            store.insert(relay_id.clone(), Arc::new(runtime.clone()));
185        }
186
187        let response = json!({
188            "status": "initialized",
189            "relay_id": relay_id,
190            "task": task,
191            "okr_id": okr_id,
192            "message": "Relay initialized. Use 'delegate' to assign tasks to agents, or 'list_agents' to see available agents."
193        });
194
195        let mut result = ToolResult::success(
196            serde_json::to_string_pretty(&response).unwrap_or_else(|_| format!("{:?}", response)),
197        )
198        .with_metadata("relay_id", json!(relay_id));
199        if let Some(okr_id) = response.get("okr_id").and_then(|v| v.as_str()) {
200            result = result.with_metadata("okr_id", json!(okr_id));
201        }
202
203        Ok(result)
204    }
205
206    /// Delegate a task to a target agent
207    async fn delegate_task(
208        &self,
209        relay_id: Option<String>,
210        target_agent: Option<String>,
211        message: Option<String>,
212        context: Option<Value>,
213        okr_id: Option<String>,
214    ) -> Result<ToolResult> {
215        let relay_id = match relay_id {
216            Some(id) => id,
217            None => {
218                return Ok(ToolResult::structured_error(
219                    "MISSING_FIELD",
220                    "relay_autochat",
221                    "relay_id is required for delegate action",
222                    Some(vec!["relay_id"]),
223                    Some(
224                        json!({"action": "delegate", "relay_id": "relay-xxx", "target_agent": "agent-name", "message": "task description"}),
225                    ),
226                ));
227            }
228        };
229        let target_agent = match target_agent {
230            Some(a) => a,
231            None => {
232                return Ok(ToolResult::structured_error(
233                    "MISSING_FIELD",
234                    "relay_autochat",
235                    "target_agent is required for delegate action",
236                    Some(vec!["target_agent"]),
237                    Some(
238                        json!({"action": "delegate", "relay_id": relay_id, "target_agent": "agent-name", "message": "task description"}),
239                    ),
240                ));
241            }
242        };
243        let message = message.unwrap_or_else(|| "New task assigned".to_string());
244
245        // Get or create the runtime
246        let runtime = {
247            let store = RELAY_STORE.read();
248            store.get(&relay_id).cloned()
249        };
250
251        let runtime = match runtime {
252            Some(r) => r,
253            None => {
254                // Create a new runtime if it doesn't exist
255                let bus = get_agent_bus().await?;
256                let new_runtime = ProtocolRelayRuntime::with_relay_id(bus, relay_id.clone());
257                let arc_runtime = Arc::new(new_runtime);
258                {
259                    let mut store = RELAY_STORE.write();
260                    store.insert(relay_id.clone(), arc_runtime.clone());
261                }
262                arc_runtime
263            }
264        };
265
266        // Build context payload if provided
267        let context_msg = if let Some(ref ctx) = context {
268            format!(
269                "{}\n\nContext: {}",
270                message,
271                serde_json::to_string_pretty(ctx).unwrap_or_default()
272            )
273        } else {
274            message.clone()
275        };
276
277        // Send the delegation message
278        runtime.send_handoff("system", &target_agent, &context_msg);
279
280        let response = json!({
281            "status": "delegated",
282            "relay_id": relay_id,
283            "target_agent": target_agent,
284            "okr_id": okr_id,
285            "message": message,
286            "initial_results": {
287                "task_assigned": true,
288                "agent_notified": true
289            }
290        });
291
292        let mut result = ToolResult::success(
293            serde_json::to_string_pretty(&response).unwrap_or_else(|_| format!("{:?}", response)),
294        )
295        .with_metadata("relay_id", json!(relay_id))
296        .with_metadata("target_agent", json!(target_agent));
297        if let Some(okr_id) = response.get("okr_id").and_then(|v| v.as_str()) {
298            result = result.with_metadata("okr_id", json!(okr_id));
299        }
300
301        Ok(result)
302    }
303
304    /// Hand off context between agents
305    async fn handoff_context(
306        &self,
307        relay_id: Option<String>,
308        target_agent: Option<String>,
309        message: Option<String>,
310        context: Option<Value>,
311    ) -> Result<ToolResult> {
312        let relay_id = match relay_id {
313            Some(id) => id,
314            None => {
315                return Ok(ToolResult::structured_error(
316                    "MISSING_FIELD",
317                    "relay_autochat",
318                    "relay_id is required for handoff action",
319                    Some(vec!["relay_id"]),
320                    Some(
321                        json!({"action": "handoff", "relay_id": "relay-xxx", "target_agent": "agent-name"}),
322                    ),
323                ));
324            }
325        };
326        let target_agent = match target_agent {
327            Some(a) => a,
328            None => {
329                return Ok(ToolResult::structured_error(
330                    "MISSING_FIELD",
331                    "relay_autochat",
332                    "target_agent is required for handoff action",
333                    Some(vec!["target_agent"]),
334                    Some(
335                        json!({"action": "handoff", "relay_id": relay_id, "target_agent": "agent-name"}),
336                    ),
337                ));
338            }
339        };
340        let message = message.unwrap_or_else(|| "Context handoff".to_string());
341
342        let store = RELAY_STORE.read();
343        let runtime = match store.get(&relay_id) {
344            Some(r) => r.clone(),
345            None => {
346                return Ok(ToolResult::structured_error(
347                    "NOT_FOUND",
348                    "relay_autochat",
349                    &format!(
350                        "Relay not found: {relay_id}. Use 'init' action to create a relay first."
351                    ),
352                    None,
353                    Some(json!({"action": "init", "task": "description of the relay task"})),
354                ));
355            }
356        };
357        // need to drop the lock before await
358        drop(store);
359
360        // Build context payload
361        let context_msg = if let Some(ref ctx) = context {
362            format!(
363                "{}\n\nContext: {}",
364                message,
365                serde_json::to_string_pretty(ctx).unwrap_or_default()
366            )
367        } else {
368            message
369        };
370
371        // Send handoff
372        runtime.send_handoff("previous_agent", &target_agent, &context_msg);
373
374        let response = json!({
375            "status": "handoff_complete",
376            "relay_id": relay_id,
377            "target_agent": target_agent,
378            "message": "Context successfully handed off to target agent"
379        });
380
381        Ok(ToolResult::success(
382            serde_json::to_string_pretty(&response).unwrap_or_else(|_| format!("{:?}", response)),
383        ))
384    }
385
386    /// Get status of a relay
387    async fn get_status(&self, relay_id: Option<String>) -> Result<ToolResult> {
388        let relay_id = match relay_id {
389            Some(id) => id,
390            None => {
391                return Ok(ToolResult::structured_error(
392                    "MISSING_FIELD",
393                    "relay_autochat",
394                    "relay_id is required for status action",
395                    Some(vec!["relay_id"]),
396                    Some(json!({"action": "status", "relay_id": "relay-xxx"})),
397                ));
398            }
399        };
400
401        let store = RELAY_STORE.read();
402
403        if store.contains_key(&relay_id) {
404            let response = json!({
405                "status": "active",
406                "relay_id": relay_id,
407                "message": "Relay is active"
408            });
409
410            Ok(ToolResult::success(
411                serde_json::to_string_pretty(&response)
412                    .unwrap_or_else(|_| format!("{:?}", response)),
413            ))
414        } else {
415            Ok(ToolResult::error(format!("Relay not found: {}", relay_id)))
416        }
417    }
418
419    /// List agents in a relay
420    async fn list_agents(&self, relay_id: Option<String>) -> Result<ToolResult> {
421        let relay_id = match relay_id {
422            Some(id) => id,
423            None => {
424                return Ok(ToolResult::structured_error(
425                    "MISSING_FIELD",
426                    "relay_autochat",
427                    "relay_id is required for list_agents action",
428                    Some(vec!["relay_id"]),
429                    Some(json!({"action": "list_agents", "relay_id": "relay-xxx"})),
430                ));
431            }
432        };
433
434        let relay_exists = {
435            let store = RELAY_STORE.read();
436            store.contains_key(&relay_id)
437        };
438
439        if relay_exists {
440            let bus = get_agent_bus().await?;
441            let agents: Vec<Value> = bus
442                .registry
443                .agent_ids()
444                .iter()
445                .map(|name| json!({ "name": name }))
446                .collect();
447
448            let response = json!({
449                "relay_id": relay_id,
450                "agents": agents,
451                "count": agents.len()
452            });
453
454            Ok(ToolResult::success(
455                serde_json::to_string_pretty(&response)
456                    .unwrap_or_else(|_| format!("{:?}", response)),
457            ))
458        } else {
459            Ok(ToolResult::error(format!("Relay not found: {}", relay_id)))
460        }
461    }
462
463    /// Complete a relay and aggregate results
464    async fn complete_relay(&self, relay_id: Option<String>) -> Result<ToolResult> {
465        let relay_id = match relay_id {
466            Some(id) => id,
467            None => {
468                return Ok(ToolResult::structured_error(
469                    "MISSING_FIELD",
470                    "relay_autochat",
471                    "relay_id is required for complete action",
472                    Some(vec!["relay_id"]),
473                    Some(json!({"action": "complete", "relay_id": "relay-xxx"})),
474                ));
475            }
476        };
477
478        // Get the runtime and shutdown agents
479        let runtime = {
480            let mut store = RELAY_STORE.write();
481            store.remove(&relay_id)
482        };
483
484        if let Some(runtime) = runtime {
485            runtime.shutdown_agents(&[]); // Shutdown all registered agents
486        }
487
488        let response = json!({
489            "status": "completed",
490            "relay_id": relay_id,
491            "message": "Relay completed successfully. Results aggregated.",
492            "aggregated_results": {
493                "completed": true,
494                "total_agents": 0
495            }
496        });
497
498        Ok(ToolResult::success(
499            serde_json::to_string_pretty(&response).unwrap_or_else(|_| format!("{:?}", response)),
500        ))
501    }
502}