agent_code_lib/tools/
agent.rs1use async_trait::async_trait;
14use serde_json::json;
15use std::path::PathBuf;
16
17use super::{Tool, ToolContext, ToolResult};
18use crate::error::ToolError;
19
20pub struct AgentTool;
21
22#[async_trait]
23impl Tool for AgentTool {
24 fn name(&self) -> &'static str {
25 "Agent"
26 }
27
28 fn description(&self) -> &'static str {
29 "Launch a subagent to handle a complex task autonomously. The agent \
30 runs with its own conversation context and can execute tools in parallel \
31 with the main session."
32 }
33
34 fn prompt(&self) -> String {
35 "Launch a subagent for complex, multi-step tasks. Each agent gets its own \
36 conversation context and tool access. Use for:\n\
37 - Parallel research or code exploration\n\
38 - Tasks that would clutter the main conversation\n\
39 - Independent subtasks that don't depend on each other\n\n\
40 Provide a clear, complete prompt so the agent can work autonomously."
41 .to_string()
42 }
43
44 fn input_schema(&self) -> serde_json::Value {
45 json!({
46 "type": "object",
47 "required": ["description", "prompt"],
48 "properties": {
49 "description": {
50 "type": "string",
51 "description": "Short (3-5 word) description of the task"
52 },
53 "prompt": {
54 "type": "string",
55 "description": "The complete task for the agent to perform"
56 },
57 "model": {
58 "type": "string",
59 "enum": ["sonnet", "opus", "haiku"],
60 "description": "Optional model override for this agent"
61 },
62 "isolation": {
63 "type": "string",
64 "enum": ["worktree"],
65 "description": "Run in an isolated git worktree"
66 },
67 "run_in_background": {
68 "type": "boolean",
69 "description": "Run the agent in the background"
70 }
71 }
72 })
73 }
74
75 fn is_read_only(&self) -> bool {
76 false
77 }
78
79 fn is_concurrency_safe(&self) -> bool {
80 false
81 }
82
83 fn max_result_size_chars(&self) -> usize {
84 200_000
85 }
86
87 async fn call(
88 &self,
89 input: serde_json::Value,
90 ctx: &ToolContext,
91 ) -> Result<ToolResult, ToolError> {
92 let description = input
93 .get("description")
94 .and_then(|v| v.as_str())
95 .ok_or_else(|| ToolError::InvalidInput("'description' is required".into()))?;
96
97 let prompt = input
98 .get("prompt")
99 .and_then(|v| v.as_str())
100 .ok_or_else(|| ToolError::InvalidInput("'prompt' is required".into()))?;
101
102 let isolation = input.get("isolation").and_then(|v| v.as_str());
103
104 let agent_cwd = if isolation == Some("worktree") {
106 match create_worktree(&ctx.cwd).await {
107 Ok(path) => path,
108 Err(e) => {
109 return Ok(ToolResult::error(format!("Failed to create worktree: {e}")));
110 }
111 }
112 } else {
113 ctx.cwd.clone()
114 };
115
116 let agent_binary = std::env::current_exe()
119 .map(|p| p.display().to_string())
120 .unwrap_or_else(|_| "agent".to_string());
121
122 let mut cmd = tokio::process::Command::new(&agent_binary);
123 cmd.arg("--prompt")
124 .arg(prompt)
125 .current_dir(&agent_cwd)
126 .stdout(std::process::Stdio::piped())
127 .stderr(std::process::Stdio::piped());
128
129 for var in &[
131 "AGENT_CODE_API_KEY",
132 "ANTHROPIC_API_KEY",
133 "OPENAI_API_KEY",
134 "XAI_API_KEY",
135 "GOOGLE_API_KEY",
136 "DEEPSEEK_API_KEY",
137 "GROQ_API_KEY",
138 "MISTRAL_API_KEY",
139 "TOGETHER_API_KEY",
140 "AGENT_CODE_API_BASE_URL",
141 "AGENT_CODE_MODEL",
142 ] {
143 if let Ok(val) = std::env::var(var) {
144 cmd.env(var, val);
145 }
146 }
147
148 let timeout = std::time::Duration::from_secs(300); let result = tokio::select! {
151 r = cmd.output() => {
152 match r {
153 Ok(output) => {
154 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
155 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
156
157 let mut content = format!("Agent ({description}) completed.\n\n");
158 if !stdout.is_empty() {
159 content.push_str(&stdout);
160 }
161 if !stderr.is_empty() && !output.status.success() {
162 content.push_str(&format!("\nAgent errors:\n{stderr}"));
163 }
164
165 if isolation == Some("worktree") {
167 let _ = cleanup_worktree(&agent_cwd).await;
168 }
169
170 Ok(ToolResult {
171 content,
172 is_error: !output.status.success(),
173 })
174 }
175 Err(e) => Err(ToolError::ExecutionFailed(format!(
176 "Failed to spawn agent: {e}"
177 ))),
178 }
179 }
180 _ = tokio::time::sleep(timeout) => {
181 Err(ToolError::Timeout(300_000))
182 }
183 _ = ctx.cancel.cancelled() => {
184 Err(ToolError::Cancelled)
185 }
186 };
187
188 result
189 }
190}
191
192async fn create_worktree(base_cwd: &PathBuf) -> Result<PathBuf, String> {
194 let branch_name = format!(
195 "agent-{}",
196 uuid::Uuid::new_v4()
197 .to_string()
198 .split('-')
199 .next()
200 .unwrap_or("tmp")
201 );
202 let worktree_path = std::env::temp_dir().join(format!("agent-wt-{branch_name}"));
203
204 let output = tokio::process::Command::new("git")
205 .args(["worktree", "add", "-b", &branch_name])
206 .arg(&worktree_path)
207 .current_dir(base_cwd)
208 .output()
209 .await
210 .map_err(|e| format!("git worktree add failed: {e}"))?;
211
212 if !output.status.success() {
213 let stderr = String::from_utf8_lossy(&output.stderr);
214 return Err(format!("git worktree add failed: {stderr}"));
215 }
216
217 Ok(worktree_path)
218}
219
220async fn cleanup_worktree(worktree_path: &PathBuf) -> Result<(), String> {
222 let status = tokio::process::Command::new("git")
224 .args(["status", "--porcelain"])
225 .current_dir(worktree_path)
226 .output()
227 .await
228 .map_err(|e| format!("git status failed: {e}"))?;
229
230 let has_changes = !String::from_utf8_lossy(&status.stdout).trim().is_empty();
231
232 if !has_changes {
233 let _ = tokio::process::Command::new("git")
235 .args(["worktree", "remove", "--force"])
236 .arg(worktree_path)
237 .output()
238 .await;
239 }
240 Ok(())
243}