codetether_agent/tool/
agent.rs1use 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
19struct 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
29static 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 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 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 let handle = tokio::spawn(async move {
194 session_clone
195 .prompt_with_events(&msg_clone, tx_clone, registry_clone)
196 .await
197 });
198
199 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 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 tokio::task::yield_now().await;
214
215 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 agent_done = true;
256 }
257 Err(_) => {
258 if handle.is_finished() {
260 agent_done = true;
261 }
262 }
263 }
264 }
265
266 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 handle.abort();
286 if last_error.is_none() {
287 last_error = Some(format!("Agent @{name} timed out after 5 minutes"));
288 }
289 }
290
291 {
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 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}