Skip to main content

codetether_agent/tool/
agent.rs

1//! Agent Tool - Spawn and communicate with sub-agents
2//!
3//! Allows the main agent to create specialized sub-agents, send them messages,
4//! and receive their responses. Sub-agents maintain conversation history and
5//! can use all available tools.
6
7use super::{Tool, ToolResult};
8use crate::provider::{ContentPart, ProviderRegistry, Role};
9use crate::session::{Session, SessionEvent};
10use anyhow::{Context, Result};
11use async_trait::async_trait;
12use parking_lot::RwLock;
13use serde::Deserialize;
14use serde_json::{Value, json};
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::{OnceCell, mpsc};
18
19/// A spawned sub-agent with its own session and identity.
20struct AgentEntry {
21    instructions: String,
22    session: Session,
23}
24
25lazy_static::lazy_static! {
26    static ref AGENT_STORE: RwLock<HashMap<String, AgentEntry>> = RwLock::new(HashMap::new());
27}
28
29/// Lazily loaded provider registry — initialized on first agent interaction.
30static PROVIDER_REGISTRY: OnceCell<Arc<ProviderRegistry>> = OnceCell::const_new();
31
32async fn get_registry() -> Result<Arc<ProviderRegistry>> {
33    let reg = PROVIDER_REGISTRY
34        .get_or_try_init(|| async {
35            let registry = ProviderRegistry::from_vault().await?;
36            Ok::<_, anyhow::Error>(Arc::new(registry))
37        })
38        .await?;
39    Ok(reg.clone())
40}
41
42pub struct AgentTool;
43
44impl AgentTool {
45    pub fn new() -> Self {
46        Self
47    }
48}
49
50#[derive(Deserialize)]
51struct Params {
52    action: String,
53    #[serde(default)]
54    name: Option<String>,
55    #[serde(default)]
56    instructions: Option<String>,
57    #[serde(default)]
58    message: Option<String>,
59    #[serde(default)]
60    model: Option<String>,
61}
62
63#[async_trait]
64impl Tool for AgentTool {
65    fn id(&self) -> &str {
66        "agent"
67    }
68
69    fn name(&self) -> &str {
70        "Sub-Agent"
71    }
72
73    fn description(&self) -> &str {
74        "Spawn and communicate with specialized sub-agents. Each sub-agent has its own conversation \
75         history, system prompt, and access to all tools. Use this to delegate tasks to focused agents. \
76         Actions: spawn (create agent), message (send message and get response), list (show agents), \
77         kill (remove agent)."
78    }
79
80    fn parameters(&self) -> Value {
81        json!({
82            "type": "object",
83            "properties": {
84                "action": {
85                    "type": "string",
86                    "enum": ["spawn", "message", "list", "kill"],
87                    "description": "Action to perform"
88                },
89                "name": {
90                    "type": "string",
91                    "description": "Agent name (required for spawn, message, kill)"
92                },
93                "instructions": {
94                    "type": "string",
95                    "description": "System instructions for the agent (required for spawn). Describe the agent's role and expertise."
96                },
97                "message": {
98                    "type": "string",
99                    "description": "Message to send to the agent (required for message action)"
100                },
101                "model": {
102                    "type": "string",
103                    "description": "Model to use for the agent (optional, defaults to current model). Format: provider/model"
104                }
105            },
106            "required": ["action"]
107        })
108    }
109
110    async fn execute(&self, params: Value) -> Result<ToolResult> {
111        let p: Params = serde_json::from_value(params).context("Invalid params")?;
112
113        match p.action.as_str() {
114            "spawn" => {
115                let name = p
116                    .name
117                    .ok_or_else(|| anyhow::anyhow!("name required for spawn"))?;
118                let instructions = p
119                    .instructions
120                    .ok_or_else(|| anyhow::anyhow!("instructions required for spawn"))?;
121
122                {
123                    let store = AGENT_STORE.read();
124                    if store.contains_key(&name) {
125                        return Ok(ToolResult::error(format!(
126                            "Agent @{name} already exists. Use kill first, or message it directly."
127                        )));
128                    }
129                }
130
131                let mut session = Session::new()
132                    .await
133                    .context("Failed to create session for sub-agent")?;
134
135                session.agent = name.clone();
136                if let Some(ref model) = p.model {
137                    session.metadata.model = Some(model.clone());
138                }
139
140                session.add_message(crate::provider::Message {
141                    role: Role::System,
142                    content: vec![ContentPart::Text {
143                        text: format!(
144                            "You are @{name}, a specialized sub-agent. {instructions}\n\n\
145                             You have access to all tools. Be thorough, focused, and concise. \
146                             Complete the task fully before responding."
147                        ),
148                    }],
149                });
150
151                AGENT_STORE.write().insert(
152                    name.clone(),
153                    AgentEntry {
154                        instructions: instructions.clone(),
155                        session,
156                    },
157                );
158
159                tracing::info!(agent = %name, "Sub-agent spawned");
160                Ok(ToolResult::success(format!(
161                    "Spawned agent @{name}: {instructions}\nSend it a message with action \"message\"."
162                )))
163            }
164
165            "message" => {
166                let name = p
167                    .name
168                    .ok_or_else(|| anyhow::anyhow!("name required for message"))?;
169                let message = p
170                    .message
171                    .ok_or_else(|| anyhow::anyhow!("message required for message action"))?;
172
173                // Take the session out of the store so we can mutably use it
174                let mut session = {
175                    let mut store = AGENT_STORE.write();
176                    let entry = store.get_mut(&name).ok_or_else(|| {
177                        anyhow::anyhow!("Agent @{name} not found. Spawn it first.")
178                    })?;
179                    entry.session.clone()
180                };
181
182                let (tx, mut rx) = mpsc::channel::<SessionEvent>(256);
183                let registry = get_registry().await?;
184
185                // Use tokio::spawn to run the agent in the background
186                // This allows the main event loop to remain responsive
187                let mut session_clone = session.clone();
188                let msg_clone = message.clone();
189                let registry_clone = registry.clone();
190                let tx_clone = tx.clone();
191
192                // Spawn the agent prompt task
193                let handle = tokio::spawn(async move {
194                    session_clone
195                        .prompt_with_events(&msg_clone, tx_clone, registry_clone)
196                        .await
197                });
198
199                // Wait for completion events with yielding to stay responsive
200                let mut response_text = String::new();
201                let mut thinking_text = String::new();
202                let mut tool_calls = Vec::new();
203                let mut agent_done = false;
204                let mut last_error: Option<String> = None;
205
206                // Maximum wait time: 5 minutes per agent
207                let max_wait = std::time::Duration::from_secs(300);
208                let start = std::time::Instant::now();
209
210                while !agent_done && start.elapsed() < max_wait {
211                    // Yield immediately on each iteration to keep the TUI event loop responsive
212                    // This prevents UI freezes when multiple agents are spawned
213                    tokio::task::yield_now().await;
214
215                    // Use tokio::select! with small timeout to stay responsive
216                    match tokio::time::timeout(std::time::Duration::from_millis(20), rx.recv())
217                        .await
218                    {
219                        Ok(Some(event)) => match event {
220                            SessionEvent::TextComplete(text) => {
221                                response_text.push_str(&text);
222                            }
223                            SessionEvent::ThinkingComplete(text) => {
224                                thinking_text.push_str(&text);
225                            }
226                            SessionEvent::ToolCallComplete {
227                                name: tool_name,
228                                output,
229                                success,
230                            } => {
231                                tool_calls.push(json!({
232                                    "tool": tool_name,
233                                    "success": success,
234                                    "output_preview": if output.len() > 200 {
235                                        format!("{}...", &output[..200])
236                                    } else {
237                                        output
238                                    }
239                                }));
240                            }
241                            SessionEvent::Error(err) => {
242                                response_text.push_str(&format!("\n[Error: {err}]"));
243                                last_error = Some(err);
244                            }
245                            SessionEvent::Done => {
246                                agent_done = true;
247                            }
248                            SessionEvent::SessionSync(synced) => {
249                                session = synced;
250                            }
251                            _ => {}
252                        },
253                        Ok(None) => {
254                            // Channel closed
255                            agent_done = true;
256                        }
257                        Err(_) => {
258                            // Timeout - check if spawn is done and yield to other tasks
259                            if handle.is_finished() {
260                                agent_done = true;
261                            }
262                        }
263                    }
264                }
265
266                // Check the result of the spawned task
267                if handle.is_finished() {
268                    match handle.await {
269                        Ok(Ok(_)) => {}
270                        Ok(Err(err)) => {
271                            if last_error.is_none() {
272                                last_error = Some(err.to_string());
273                            }
274                        }
275                        Err(err) => {
276                            if err.is_cancelled() {
277                                last_error = Some("Agent task was cancelled".to_string());
278                            } else {
279                                last_error = Some(format!("Agent task panicked: {}", err));
280                            }
281                        }
282                    }
283                } else {
284                    // Agent didn't finish in time - abort it
285                    handle.abort();
286                    if last_error.is_none() {
287                        last_error = Some(format!("Agent @{name} timed out after 5 minutes"));
288                    }
289                }
290
291                // Put the updated session back
292                {
293                    let mut store = AGENT_STORE.write();
294                    if let Some(entry) = store.get_mut(&name) {
295                        entry.session = session;
296                    }
297                }
298
299                if let Some(ref err) = last_error {
300                    if response_text.is_empty() {
301                        return Ok(ToolResult::error(format!("Agent @{name} failed: {err}")));
302                    }
303                    // Partial response with error
304                    response_text.push_str(&format!("\n\n[Warning: {err}]"));
305                }
306
307                let mut output = json!({
308                    "agent": name,
309                    "response": response_text,
310                });
311                if !thinking_text.is_empty() {
312                    output["thinking"] = json!(thinking_text);
313                }
314                if !tool_calls.is_empty() {
315                    output["tool_calls"] = json!(tool_calls);
316                }
317
318                Ok(ToolResult::success(
319                    serde_json::to_string_pretty(&output).unwrap_or(response_text),
320                ))
321            }
322
323            "list" => {
324                let store = AGENT_STORE.read();
325                if store.is_empty() {
326                    return Ok(ToolResult::success(
327                        "No sub-agents spawned. Use action \"spawn\" to create one.",
328                    ));
329                }
330
331                let agents: Vec<Value> = store
332                    .iter()
333                    .map(|(name, entry)| {
334                        json!({
335                            "name": name,
336                            "instructions": entry.instructions,
337                            "messages": entry.session.messages.len(),
338                        })
339                    })
340                    .collect();
341
342                Ok(ToolResult::success(
343                    serde_json::to_string_pretty(&agents).unwrap_or_default(),
344                ))
345            }
346
347            "kill" => {
348                let name = p
349                    .name
350                    .ok_or_else(|| anyhow::anyhow!("name required for kill"))?;
351
352                let removed = AGENT_STORE.write().remove(&name);
353                match removed {
354                    Some(_) => {
355                        tracing::info!(agent = %name, "Sub-agent killed");
356                        Ok(ToolResult::success(format!("Removed agent @{name}")))
357                    }
358                    None => Ok(ToolResult::error(format!("Agent @{name} not found"))),
359                }
360            }
361
362            _ => Ok(ToolResult::error(format!(
363                "Unknown action: {}. Valid: spawn, message, list, kill",
364                p.action
365            ))),
366        }
367    }
368}