1use crate::error::AgentError;
9use crate::types::*;
10use std::collections::HashMap;
11use std::sync::{Mutex, OnceLock};
12
13pub const TEAM_CREATE_TOOL_NAME: &str = "TeamCreate";
14pub const TEAM_DELETE_TOOL_NAME: &str = "TeamDelete";
15pub const SEND_MESSAGE_TOOL_NAME: &str = "SendMessage";
16
17static TEAMS: OnceLock<Mutex<HashMap<String, Team>>> = OnceLock::new();
19static INBOX: OnceLock<Mutex<Vec<AgentMessage>>> = OnceLock::new();
21
22fn get_teams_map() -> &'static Mutex<HashMap<String, Team>> {
23 TEAMS.get_or_init(|| Mutex::new(HashMap::new()))
24}
25
26fn get_inbox() -> &'static Mutex<Vec<AgentMessage>> {
27 INBOX.get_or_init(|| Mutex::new(Vec::new()))
28}
29
30#[derive(Debug, Clone)]
31struct Team {
32 name: String,
33 description: String,
34 agents: Vec<AgentInfo>,
35 team_file_path: Option<String>,
36}
37
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39pub struct AgentInfo {
40 pub name: String,
41 pub description: Option<String>,
42 pub model: Option<String>,
43}
44
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct AgentMessage {
47 pub to: String,
48 pub from: Option<String>,
49 pub message: String,
50 pub timestamp: u64,
51}
52
53pub struct TeamCreateTool;
55
56impl TeamCreateTool {
57 pub fn new() -> Self {
58 Self
59 }
60
61 pub fn name(&self) -> &str {
62 TEAMCREATE_TOOL_NAME
63 }
64
65 pub fn description(&self) -> &str {
66 "Create a team of agents that can work in parallel. Teams enable swarm mode where agents collaborate on complex tasks."
67 }
68
69 pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
70 "TeamCreate".to_string()
71 }
72
73 pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
74 input.and_then(|inp| inp["name"].as_str().map(String::from))
75 }
76
77 pub fn render_tool_result_message(
78 &self,
79 content: &serde_json::Value,
80 ) -> Option<String> {
81 content["content"].as_str().map(|s| s.to_string())
82 }
83
84 pub fn input_schema(&self) -> ToolInputSchema {
85 ToolInputSchema {
86 schema_type: "object".to_string(),
87 properties: serde_json::json!({
88 "name": {
89 "type": "string",
90 "description": "Name of the team to create"
91 },
92 "description": {
93 "type": "string",
94 "description": "Description of what the team does"
95 },
96 "agents": {
97 "type": "array",
98 "items": {
99 "type": "object",
100 "properties": {
101 "name": { "type": "string", "description": "Agent name" },
102 "description": { "type": "string", "description": "Agent description" },
103 "model": { "type": "string", "description": "Agent model" }
104 }
105 },
106 "description": "List of agents in the team"
107 }
108 }),
109 required: Some(vec!["name".to_string()]),
110 }
111 }
112
113 pub async fn execute(
114 &self,
115 input: serde_json::Value,
116 _context: &ToolContext,
117 ) -> Result<ToolResult, AgentError> {
118 let name = input["name"]
119 .as_str()
120 .ok_or_else(|| AgentError::Tool("name is required".to_string()))?
121 .to_string();
122
123 let description = input["description"].as_str().unwrap_or("").to_string();
124
125 let agents: Vec<AgentInfo> = input["agents"]
126 .as_array()
127 .map(|arr| {
128 arr.iter()
129 .filter_map(|v| {
130 let name = v.get("name")?.as_str()?.to_string();
131 let description = v
132 .get("description")
133 .and_then(|v| v.as_str())
134 .map(|s| s.to_string());
135 let model = v
136 .get("model")
137 .and_then(|v| v.as_str())
138 .map(|s| s.to_string());
139 Some(AgentInfo {
140 name,
141 description,
142 model,
143 })
144 })
145 .collect()
146 })
147 .unwrap_or_default();
148
149 let mut guard = get_teams_map().lock().unwrap();
151 if guard.contains_key(&name) {
152 return Ok(ToolResult {
153 result_type: "text".to_string(),
154 tool_use_id: "".to_string(),
155 content: format!("Error: Team '{}' already exists.", name),
156 is_error: Some(true),
157 was_persisted: None,
158 });
159 }
160 drop(guard);
161
162 let team = Team {
169 name: name.clone(),
170 description: description.clone(),
171 agents,
172 team_file_path: None,
173 };
174
175 let mut guard = get_teams_map().lock().unwrap();
176 guard.insert(name.clone(), team);
177 let agent_count = guard.get(&name).map(|t| t.agents.len()).unwrap_or(0);
178 drop(guard);
179
180 Ok(ToolResult {
181 result_type: "text".to_string(),
182 tool_use_id: "".to_string(),
183 content: format!(
184 "Team '{}' created successfully.\n\
185 Description: {}\n\
186 Agents: {}\n\n\
187 The team is ready for coordination. \
188 Team members can communicate using the SendMessage tool.",
189 name, description, agent_count
190 ),
191 is_error: Some(false),
192 was_persisted: None,
193 })
194 }
195}
196
197impl Default for TeamCreateTool {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203pub struct TeamDeleteTool;
205
206impl TeamDeleteTool {
207 pub fn new() -> Self {
208 Self
209 }
210
211 pub fn name(&self) -> &str {
212 TEAM_DELETE_TOOL_NAME
213 }
214
215 pub fn description(&self) -> &str {
216 "Delete a previously created team. All team members will be stopped and the team configuration will be removed."
217 }
218
219 pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
220 "TeamDelete".to_string()
221 }
222
223 pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
224 input.and_then(|inp| inp["name"].as_str().map(String::from))
225 }
226
227 pub fn render_tool_result_message(
228 &self,
229 content: &serde_json::Value,
230 ) -> Option<String> {
231 content["content"].as_str().map(|s| s.to_string())
232 }
233
234 pub fn input_schema(&self) -> ToolInputSchema {
235 ToolInputSchema {
236 schema_type: "object".to_string(),
237 properties: serde_json::json!({
238 "name": {
239 "type": "string",
240 "description": "Name of the team to delete"
241 }
242 }),
243 required: Some(vec!["name".to_string()]),
244 }
245 }
246
247 pub async fn execute(
248 &self,
249 input: serde_json::Value,
250 _context: &ToolContext,
251 ) -> Result<ToolResult, AgentError> {
252 let name = input["name"]
253 .as_str()
254 .ok_or_else(|| AgentError::Tool("name is required".to_string()))?;
255
256 let mut guard = get_teams_map().lock().unwrap();
257 let team = guard.remove(name);
258 drop(guard);
259
260 let team = team.ok_or_else(|| AgentError::Tool(format!("Team '{}' not found", name)))?;
261
262 let agent_names: Vec<String> = team.agents.iter().map(|a| a.name.clone()).collect();
271
272 Ok(ToolResult {
273 result_type: "text".to_string(),
274 tool_use_id: "".to_string(),
275 content: format!(
276 "Team '{}' deleted successfully.\n\
277 Stopped {} agent(s): {}",
278 name,
279 agent_names.len(),
280 agent_names.join(", ")
281 ),
282 is_error: Some(false),
283 was_persisted: None,
284 })
285 }
286}
287
288impl Default for TeamDeleteTool {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294pub struct SendMessageTool;
296
297impl SendMessageTool {
298 pub fn new() -> Self {
299 Self
300 }
301
302 pub fn name(&self) -> &str {
303 SEND_MESSAGE_TOOL_NAME
304 }
305
306 pub fn description(&self) -> &str {
307 "Send a message to another agent. Use 'to: *' to broadcast to all agents. Supports direct messages, shutdown requests, and plan approvals."
308 }
309
310 pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
311 "SendMessage".to_string()
312 }
313
314 pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
315 input.and_then(|inp| inp["to"].as_str().map(String::from))
316 }
317
318 pub fn render_tool_result_message(
319 &self,
320 content: &serde_json::Value,
321 ) -> Option<String> {
322 content["content"].as_str().map(|s| s.to_string())
323 }
324
325 pub fn input_schema(&self) -> ToolInputSchema {
326 ToolInputSchema {
327 schema_type: "object".to_string(),
328 properties: serde_json::json!({
329 "to": {
330 "type": "string",
331 "description": "Agent name to send message to. Use '*' to broadcast to all agents."
332 },
333 "message": {
334 "type": "string",
335 "description": "Message content"
336 }
337 }),
338 required: Some(vec!["to".to_string(), "message".to_string()]),
339 }
340 }
341
342 pub async fn execute(
343 &self,
344 input: serde_json::Value,
345 _context: &ToolContext,
346 ) -> Result<ToolResult, AgentError> {
347 let to = input["to"]
348 .as_str()
349 .ok_or_else(|| AgentError::Tool("to is required".to_string()))?;
350
351 let message = input["message"]
352 .as_str()
353 .ok_or_else(|| AgentError::Tool("message is required".to_string()))?;
354
355 let timestamp = std::time::SystemTime::now()
356 .duration_since(std::time::UNIX_EPOCH)
357 .map(|d| d.as_secs())
358 .unwrap_or(0);
359
360 let msg = AgentMessage {
361 to: to.to_string(),
362 from: None,
363 message: message.to_string(),
364 timestamp,
365 };
366
367 let mut inbox = get_inbox().lock().unwrap();
375 inbox.push(msg);
376 let inbox_len = inbox.len();
377 drop(inbox);
378
379 let recipient = if to == "*" {
380 "all agents (broadcast)".to_string()
381 } else {
382 format!("agent '{}'", to)
383 };
384
385 Ok(ToolResult {
386 result_type: "text".to_string(),
387 tool_use_id: "".to_string(),
388 content: format!(
389 "Message sent to {}.\n\
390 Message: {}\n\
391 Inbox size: {}",
392 recipient, message, inbox_len
393 ),
394 is_error: Some(false),
395 was_persisted: None,
396 })
397 }
398}
399
400impl Default for SendMessageTool {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406const TEAMCREATE_TOOL_NAME: &str = "TeamCreate";
408
409pub fn reset_teams_for_testing() {
411 let mut guard = get_teams_map().lock().unwrap();
412 guard.clear();
413 drop(guard);
414 let mut inbox = get_inbox().lock().unwrap();
415 inbox.clear();
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 use crate::tests::common::clear_all_test_state;
423
424 #[tokio::test]
425 async fn test_team_create_and_delete() {
426 clear_all_test_state();
427 let create = TeamCreateTool::new();
428 let result = create
429 .execute(
430 serde_json::json!({
431 "name": "test-team",
432 "description": "A test team",
433 "agents": [
434 { "name": "agent1", "description": "First agent" }
435 ]
436 }),
437 &ToolContext::default(),
438 )
439 .await;
440 assert!(result.is_ok());
441
442 let delete = TeamDeleteTool::new();
443 let result = delete
444 .execute(
445 serde_json::json!({ "name": "test-team" }),
446 &ToolContext::default(),
447 )
448 .await;
449 assert!(result.is_ok());
450 assert!(result.unwrap().content.contains("deleted"));
451 }
452
453 #[tokio::test]
454 async fn test_send_message() {
455 clear_all_test_state();
456 let send = SendMessageTool::new();
457 let result = send
458 .execute(
459 serde_json::json!({
460 "to": "agent1",
461 "message": "Hello from test"
462 }),
463 &ToolContext::default(),
464 )
465 .await;
466 assert!(result.is_ok());
467 assert!(result.unwrap().content.contains("agent 'agent1'"));
468 }
469
470 #[tokio::test]
471 async fn test_send_message_broadcast() {
472 clear_all_test_state();
473 let send = SendMessageTool::new();
474 let result = send
475 .execute(
476 serde_json::json!({
477 "to": "*",
478 "message": "Broadcast message"
479 }),
480 &ToolContext::default(),
481 )
482 .await;
483 assert!(result.is_ok());
484 assert!(result.unwrap().content.contains("broadcast"));
485 }
486}