Skip to main content

ai_agent/
agent.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/model/agent.ts
2use crate::engine::{QueryEngine, QueryEngineConfig};
3use crate::env::EnvConfig;
4use crate::error::AgentError;
5use crate::tools::bash::BashTool;
6use crate::tools::edit::FileEditTool;
7use crate::tools::glob::GlobTool;
8use crate::tools::grep::GrepTool;
9use crate::tools::read::FileReadTool as ReadTool;
10use crate::tools::write::FileWriteTool as WriteTool;
11use crate::types::*;
12
13/// Register all built-in tool executors
14fn register_all_tool_executors(engine: &mut QueryEngine) {
15    type BoxFuture<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send>>;
16
17    // Bash tool - clone tool and ctx into async block
18    let bash_executor = move |input: serde_json::Value,
19                              ctx: &ToolContext|
20          -> BoxFuture<Result<ToolResult, AgentError>> {
21        let tool_clone = BashTool::new();
22        let cwd = ctx.cwd.clone();
23        Box::pin(async move {
24            let ctx2 = ToolContext {
25                cwd,
26                abort_signal: None,
27            };
28            tool_clone.execute(input, &ctx2).await
29        })
30    };
31    engine.register_tool("Bash".to_string(), bash_executor);
32
33    // FileRead tool
34    let read_executor = move |input: serde_json::Value,
35                              ctx: &ToolContext|
36          -> BoxFuture<Result<ToolResult, AgentError>> {
37        let tool_clone = ReadTool::new();
38        let cwd = ctx.cwd.clone();
39        Box::pin(async move {
40            let ctx2 = ToolContext {
41                cwd,
42                abort_signal: None,
43            };
44            tool_clone.execute(input, &ctx2).await
45        })
46    };
47    engine.register_tool("FileRead".to_string(), read_executor);
48
49    // FileWrite tool
50    let write_executor = move |input: serde_json::Value,
51                               ctx: &ToolContext|
52          -> BoxFuture<Result<ToolResult, AgentError>> {
53        let tool_clone = WriteTool::new();
54        let cwd = ctx.cwd.clone();
55        Box::pin(async move {
56            let ctx2 = ToolContext {
57                cwd,
58                abort_signal: None,
59            };
60            tool_clone.execute(input, &ctx2).await
61        })
62    };
63    engine.register_tool("FileWrite".to_string(), write_executor);
64
65    // Glob tool
66    let glob_executor = move |input: serde_json::Value,
67                              ctx: &ToolContext|
68          -> BoxFuture<Result<ToolResult, AgentError>> {
69        let tool_clone = GlobTool::new();
70        let cwd = ctx.cwd.clone();
71        Box::pin(async move {
72            let ctx2 = ToolContext {
73                cwd,
74                abort_signal: None,
75            };
76            tool_clone.execute(input, &ctx2).await
77        })
78    };
79    engine.register_tool("Glob".to_string(), glob_executor);
80
81    // Grep tool
82    let grep_executor = move |input: serde_json::Value,
83                              ctx: &ToolContext|
84          -> BoxFuture<Result<ToolResult, AgentError>> {
85        let tool_clone = GrepTool::new();
86        let cwd = ctx.cwd.clone();
87        Box::pin(async move {
88            let ctx2 = ToolContext {
89                cwd,
90                abort_signal: None,
91            };
92            tool_clone.execute(input, &ctx2).await
93        })
94    };
95    engine.register_tool("Grep".to_string(), grep_executor);
96
97    // FileEdit tool
98    let edit_executor = move |input: serde_json::Value,
99                              ctx: &ToolContext|
100          -> BoxFuture<Result<ToolResult, AgentError>> {
101        let tool_clone = FileEditTool::new();
102        let cwd = ctx.cwd.clone();
103        Box::pin(async move {
104            let ctx2 = ToolContext {
105                cwd,
106                abort_signal: None,
107            };
108            tool_clone.execute(input, &ctx2).await
109        })
110    };
111    engine.register_tool("FileEdit".to_string(), edit_executor);
112
113    // Skill tool
114    use crate::tools::skill::register_skills_from_dir;
115    use crate::tools::skill::SkillTool;
116    use std::path::Path;
117
118    // Register skills from examples/skills directory
119    register_skills_from_dir(Path::new("examples/skills"));
120
121    let skill_executor = move |input: serde_json::Value,
122                               ctx: &ToolContext|
123          -> BoxFuture<Result<ToolResult, AgentError>> {
124        let tool_clone = SkillTool::new();
125        let cwd = ctx.cwd.clone();
126        Box::pin(async move {
127            let ctx2 = ToolContext {
128                cwd,
129                abort_signal: None,
130            };
131            tool_clone.execute(input, &ctx2).await
132        })
133    };
134    engine.register_tool("Skill".to_string(), skill_executor);
135
136    // Add stub executors for other tools (they have definitions but no full implementation)
137    let stub_executor = |input: serde_json::Value,
138                         _ctx: &ToolContext|
139     -> BoxFuture<Result<ToolResult, AgentError>> {
140        let tool_name = input
141            .get("name")
142            .and_then(|n| n.as_str())
143            .unwrap_or("unknown")
144            .to_string();
145        Box::pin(async move {
146            Ok(ToolResult {
147                result_type: "text".to_string(),
148                tool_use_id: tool_name.clone(),
149                content: format!("Tool '{}' is not fully implemented yet", tool_name),
150                is_error: Some(false),
151            })
152        })
153    };
154
155    // Register stub executors for tools without full implementations
156    for tool_name in &[
157        "TaskCreate",
158        "TaskList",
159        "TaskUpdate",
160        "TaskGet",
161        "TeamCreate",
162        "TeamDelete",
163        "SendMessage",
164        "EnterWorktree",
165        "ExitWorktree",
166        "EnterPlanMode",
167        "ExitPlanMode",
168        "AskUserQuestion",
169        "ToolSearch",
170        "CronCreate",
171        "CronDelete",
172        "CronList",
173        "Config",
174        "TodoWrite",
175        "NotebookEdit",
176        "WebFetch",
177        "WebSearch",
178        "Agent",
179    ] {
180        engine.register_tool(tool_name.to_string(), stub_executor);
181    }
182}
183
184pub struct Agent {
185    config: AgentOptions,
186    model: String,
187    api_key: Option<String>,
188    base_url: Option<String>,
189    tool_pool: Vec<ToolDefinition>,
190    messages: Vec<Message>,
191    session_id: String,
192}
193
194impl From<AgentOptions> for Agent {
195    fn from(options: AgentOptions) -> Self {
196        Agent::create(options)
197    }
198}
199
200impl Agent {
201    /// Create a new agent with model name and max turns
202    pub fn new(model: &str, max_turns: u32) -> Self {
203        Self::create(AgentOptions {
204            model: Some(model.to_string()),
205            max_turns: Some(max_turns),
206            ..Default::default()
207        })
208    }
209
210    /// Create a new agent with model, max turns, and event callback for streaming
211    pub fn with_event_callback<F>(model: &str, max_turns: u32, on_event: F) -> Self
212    where
213        F: Fn(AgentEvent) + Send + Sync + 'static,
214    {
215        let mut agent = Self::new(model, max_turns);
216        agent.config.on_event = Some(std::sync::Arc::new(on_event));
217        agent
218    }
219
220    /// Create agent from AgentOptions
221    pub fn create(options: AgentOptions) -> Self {
222        // Load env config for defaults
223        let env_config = EnvConfig::load();
224
225        // Use env value, then options value, then default
226        let model = env_config
227            .model
228            .clone()
229            .or_else(|| options.model.clone())
230            .unwrap_or_else(|| "claude-sonnet-4-6".to_string());
231
232        let api_key = env_config
233            .auth_token
234            .clone()
235            .or_else(|| options.api_key.clone());
236
237        let base_url = env_config
238            .base_url
239            .clone()
240            .or_else(|| options.base_url.clone());
241
242        let session_id = uuid::Uuid::new_v4().to_string();
243
244        Self {
245            config: options.clone(),
246            model,
247            api_key,
248            base_url,
249            tool_pool: options.tools.clone(),
250            messages: vec![],
251            session_id,
252        }
253    }
254
255    pub fn get_model(&self) -> &str {
256        &self.model
257    }
258
259    pub fn get_session_id(&self) -> &str {
260        &self.session_id
261    }
262
263    /// Get all messages in the conversation history
264    pub fn get_messages(&self) -> &[Message] {
265        &self.messages
266    }
267
268    /// Get all tools available to the agent
269    pub fn get_tools(&self) -> &[ToolDefinition] {
270        &self.tool_pool
271    }
272
273    /// Set system prompt for the agent
274    pub fn set_system_prompt(&mut self, prompt: &str) {
275        self.config.system_prompt = Some(prompt.to_string());
276    }
277
278    /// Set the working directory for the agent
279    pub fn set_cwd(&mut self, cwd: &str) {
280        self.config.cwd = Some(cwd.to_string());
281    }
282
283    /// Set the event callback for agent events (tool start/complete/error, thinking, done)
284    /// Note: This must be called BEFORE query() - it sets the callback on the engine
285    pub fn set_event_callback<F>(&mut self, callback: F)
286    where
287        F: Fn(AgentEvent) + Send + Sync + 'static,
288    {
289        self.config.on_event = Some(std::sync::Arc::new(callback));
290    }
291
292    /// Execute a tool directly (for testing/demo purposes)
293    pub async fn execute_tool(
294        &mut self,
295        name: &str,
296        input: serde_json::Value,
297    ) -> Result<ToolResult, AgentError> {
298        // Create a temporary engine to execute the tool
299        let cwd = self.config.cwd.clone().unwrap_or_else(|| {
300            std::env::current_dir()
301                .map(|p| p.to_string_lossy().to_string())
302                .unwrap_or_else(|_| ".".to_string())
303        });
304        let model = self.model.clone();
305        let api_key = self.api_key.clone();
306        let base_url = self.base_url.clone();
307
308        let mut engine = QueryEngine::new(QueryEngineConfig {
309            cwd: cwd.clone(),
310            model: model.clone(),
311            api_key: api_key.clone(),
312            base_url: base_url.clone(),
313            tools: vec![],
314            system_prompt: None,
315            max_turns: 10,
316            max_budget_usd: None,
317            max_tokens: 16384,
318            can_use_tool: None,
319            on_event: None,
320        });
321
322        // Register all tool executors (including Bash, Read, Write, etc.)
323        register_all_tool_executors(&mut engine);
324
325        // Register Agent tool executor with full parameter support
326        let agent_tool_executor = move |input: serde_json::Value,
327                                        _ctx: &ToolContext|
328              -> std::pin::Pin<
329            Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>,
330        > {
331            let cwd = cwd.clone();
332            let api_key = api_key.clone();
333            let base_url = base_url.clone();
334            let model = model.clone();
335
336            Box::pin(async move {
337                // Extract ALL parameters from input
338                let description = input["description"].as_str().unwrap_or("subagent");
339                let subagent_prompt = input["prompt"].as_str().unwrap_or("");
340                let subagent_model = input["model"]
341                    .as_str()
342                    .map(|s| s.to_string())
343                    .unwrap_or_else(|| model.clone());
344                let max_turns = input["max_turns"]
345                    .as_u64()
346                    .or_else(|| input["maxTurns"].as_u64()) // Support camelCase too
347                    .unwrap_or(10) as u32;
348
349                // NEW: Extract subagent_type
350                let subagent_type = input["subagent_type"]
351                    .as_str()
352                    .or_else(|| input["subagentType"].as_str())
353                    .map(|s| s.to_string());
354
355                // NEW: Extract run_in_background (ignored for now, async not supported)
356                let _run_in_background = input["run_in_background"]
357                    .as_bool()
358                    .or_else(|| input["runInBackground"].as_bool())
359                    .unwrap_or(false);
360
361                // NEW: Extract name
362                let agent_name = input["name"].as_str().map(|s| s.to_string());
363
364                // NEW: Extract team_name
365                let _team_name = input["team_name"]
366                    .as_str()
367                    .or_else(|| input["teamName"].as_str())
368                    .map(|s| s.to_string());
369
370                // NEW: Extract mode (permission mode - ignored for now)
371                let _mode = input["mode"].as_str().map(|s| s.to_string());
372
373                // NEW: Extract cwd (working directory override)
374                let subagent_cwd = input["cwd"]
375                    .as_str()
376                    .map(|s| s.to_string())
377                    .unwrap_or_else(|| cwd.clone());
378
379                // NEW: Extract isolation
380                let _isolation = input["isolation"].as_str().map(|s| s.to_string());
381
382                // Build system prompt for subagent
383                let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
384
385                // Create sub-agent engine with proper system prompt
386                let mut sub_engine = QueryEngine::new(QueryEngineConfig {
387                    cwd: subagent_cwd,
388                    model: subagent_model.to_string(),
389                    api_key,
390                    base_url,
391                    tools: vec![],
392                    system_prompt: Some(system_prompt),
393                    max_turns,
394                    max_budget_usd: None,
395                    max_tokens: 16384,
396                    can_use_tool: None,
397                    on_event: None,
398                });
399
400                match sub_engine.submit_message(subagent_prompt).await {
401                    Ok((result_text, _)) => {
402                        let mut content = format!("[Subagent: {}]", description);
403                        if let Some(ref name) = agent_name {
404                            content = format!("[Subagent: {} ({})]", description, name);
405                        }
406                        content = format!("{}\n\n{}", content, result_text);
407                        Ok(ToolResult {
408                            result_type: "text".to_string(),
409                            tool_use_id: "agent_tool".to_string(),
410                            content,
411                            is_error: Some(false),
412                        })
413                    }
414                    Err(e) => Ok(ToolResult {
415                        result_type: "text".to_string(),
416                        tool_use_id: "agent_tool".to_string(),
417                        content: format!("[Subagent: {}] Error: {}", description, e),
418                        is_error: Some(true),
419                    }),
420                }
421            })
422        };
423
424        engine.register_tool("Agent".to_string(), agent_tool_executor);
425        engine.execute_tool(name, input).await
426    }
427
428    /// Simple blocking prompt method - sends a prompt and returns the result.
429    /// This matches the TypeScript SDK's agent.prompt() API.
430    pub async fn prompt(&mut self, prompt: &str) -> Result<QueryResult, AgentError> {
431        self.query(prompt).await
432    }
433
434    pub async fn query(&mut self, prompt: &str) -> Result<QueryResult, AgentError> {
435        use crate::ai_md::load_ai_md;
436        use crate::memory::load_memory_prompt;
437        use crate::prompts::build_system_prompt;
438        use crate::tools::get_all_base_tools;
439
440        let cwd = self.config.cwd.clone().unwrap_or_else(|| {
441            std::env::current_dir()
442                .map(|p| p.to_string_lossy().to_string())
443                .unwrap_or_else(|_| ".".to_string())
444        });
445        let cwd_path = std::path::Path::new(&cwd);
446        let model = self.model.clone();
447        let api_key = self.api_key.clone();
448        let base_url = self.base_url.clone();
449
450        // Build system prompt: AI.md + memory prompt + custom system prompt
451        let ai_md_prompt = load_ai_md(cwd_path).ok().flatten();
452        let memory_prompt = load_memory_prompt();
453
454        // Use the full system prompt from prompts module (matches TypeScript)
455        let base_system_prompt = build_system_prompt();
456
457        // Combine: AI.md (highest priority) -> memory -> base prompt -> custom
458        let system_prompt = match (&ai_md_prompt, &memory_prompt, &self.config.system_prompt) {
459            (Some(ai_md), Some(mem), Some(custom)) => Some(format!(
460                "{}\n\n{}\n\n{}\n\n{}",
461                ai_md, mem, base_system_prompt, custom
462            )),
463            (Some(ai_md), Some(mem), None) => {
464                Some(format!("{}\n\n{}\n\n{}", ai_md, mem, base_system_prompt))
465            }
466            (Some(ai_md), None, Some(custom)) => {
467                Some(format!("{}\n\n{}\n\n{}", ai_md, base_system_prompt, custom))
468            }
469            (Some(ai_md), None, None) => Some(format!("{}\n\n{}", ai_md, base_system_prompt)),
470            (None, Some(mem), Some(custom)) => {
471                Some(format!("{}\n\n{}\n\n{}", mem, base_system_prompt, custom))
472            }
473            (None, Some(mem), None) => Some(format!("{}\n\n{}", mem, base_system_prompt)),
474            (None, None, Some(custom)) => Some(format!("{}\n\n{}", base_system_prompt, custom)),
475            (None, None, None) => Some(base_system_prompt),
476        };
477
478        // Use base tools if tool_pool is empty
479        let tools = if self.tool_pool.is_empty() {
480            get_all_base_tools()
481        } else {
482            self.tool_pool.clone()
483        };
484
485        let on_event = self.config.on_event.clone();
486        let mut engine = QueryEngine::new(QueryEngineConfig {
487            cwd: cwd.clone(),
488            model: model.clone(),
489            api_key: api_key.clone(),
490            base_url: base_url.clone(),
491            tools,
492            system_prompt,
493            max_turns: self.config.max_turns.unwrap_or(10),
494            max_budget_usd: self.config.max_budget_usd,
495            max_tokens: self.config.max_tokens.unwrap_or(16384),
496            can_use_tool: None,
497            on_event,
498        });
499
500        // Register all tool executors on the engine so they can be called
501        register_all_tool_executors(&mut engine);
502
503        // Clone tool_pool before the closure to avoid capturing self
504        let tool_pool = self.tool_pool.clone();
505
506        // Register the Agent tool executor
507
508        // Register the Agent tool executor to spawn sub-agents with full parameter support
509        let agent_tool_executor = move |input: serde_json::Value,
510                                        _ctx: &ToolContext|
511              -> std::pin::Pin<
512            Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>,
513        > {
514            let cwd = cwd.clone();
515            let api_key = api_key.clone();
516            let base_url = base_url.clone();
517            let model = model.clone();
518            let tool_pool = tool_pool.clone();
519
520            Box::pin(async move {
521                // Extract ALL parameters from input
522                let description = input["description"].as_str().unwrap_or("subagent");
523                let subagent_prompt = input["prompt"].as_str().unwrap_or("");
524                let subagent_model = input["model"]
525                    .as_str()
526                    .map(|s| s.to_string())
527                    .unwrap_or_else(|| model.clone());
528                let max_turns = input["max_turns"]
529                    .as_u64()
530                    .or_else(|| input["maxTurns"].as_u64()) // Support camelCase too
531                    .unwrap_or(10) as u32;
532
533                // NEW: Extract subagent_type
534                let subagent_type = input["subagent_type"]
535                    .as_str()
536                    .or_else(|| input["subagentType"].as_str())
537                    .map(|s| s.to_string());
538
539                // NEW: Extract run_in_background (ignored for now)
540                let _run_in_background = input["run_in_background"]
541                    .as_bool()
542                    .or_else(|| input["runInBackground"].as_bool())
543                    .unwrap_or(false);
544
545                // NEW: Extract name
546                let agent_name = input["name"].as_str().map(|s| s.to_string());
547
548                // NEW: Extract team_name
549                let _team_name = input["team_name"]
550                    .as_str()
551                    .or_else(|| input["teamName"].as_str())
552                    .map(|s| s.to_string());
553
554                // NEW: Extract mode
555                let _mode = input["mode"].as_str().map(|s| s.to_string());
556
557                // NEW: Extract cwd (working directory override)
558                let subagent_cwd = input["cwd"]
559                    .as_str()
560                    .map(|s| s.to_string())
561                    .unwrap_or_else(|| cwd.clone());
562
563                // NEW: Extract isolation
564                let _isolation = input["isolation"].as_str().map(|s| s.to_string());
565
566                // Build system prompt for subagent based on agent type
567                let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
568
569                // Use parent agent's tool pool for the subagent
570                let parent_tools = tool_pool;
571
572                // Create a new engine for the subagent
573                let mut sub_engine = QueryEngine::new(QueryEngineConfig {
574                    cwd: subagent_cwd,
575                    model: subagent_model.to_string(),
576                    api_key,
577                    base_url,
578                    tools: parent_tools,
579                    system_prompt: Some(system_prompt),
580                    max_turns,
581                    max_budget_usd: None,
582                    max_tokens: 16384,
583                    can_use_tool: None,
584                    on_event: None,
585                });
586
587                // Run the subagent
588                match sub_engine.submit_message(subagent_prompt).await {
589                    Ok((result_text, _)) => {
590                        let mut content = format!("[Subagent: {}]", description);
591                        if let Some(ref name) = agent_name {
592                            content = format!("[Subagent: {} ({})]", description, name);
593                        }
594                        content = format!("{}\n\n{}", content, result_text);
595                        Ok(ToolResult {
596                            result_type: "text".to_string(),
597                            tool_use_id: "agent_tool".to_string(),
598                            content,
599                            is_error: Some(false),
600                        })
601                    }
602                    Err(e) => Ok(ToolResult {
603                        result_type: "text".to_string(),
604                        tool_use_id: "agent_tool".to_string(),
605                        content: format!("[Subagent: {}] Error: {}", description, e),
606                        is_error: Some(true),
607                    }),
608                }
609            })
610        };
611
612        // Register all tool executors
613        register_all_tool_executors(&mut engine);
614        engine.register_tool("Agent".to_string(), agent_tool_executor);
615
616        // Pass existing messages to engine for continuing conversation
617        engine.set_messages(self.messages.clone());
618
619        let start = std::time::Instant::now();
620        let (response_text, exit_reason) = engine.submit_message(prompt).await?;
621        let messages = engine.get_messages();
622
623        // Get actual usage from engine
624        let engine_usage = engine.get_usage();
625        let usage = TokenUsage {
626            input_tokens: engine_usage.input_tokens,
627            output_tokens: engine_usage.output_tokens,
628            cache_creation_input_tokens: engine_usage.cache_creation_input_tokens,
629            cache_read_input_tokens: engine_usage.cache_read_input_tokens,
630        };
631
632        // Store messages in agent
633        self.messages = messages;
634
635        Ok(QueryResult {
636            text: response_text,
637            usage,
638            num_turns: engine.get_turn_count(),
639            duration_ms: start.elapsed().as_millis() as u64,
640            exit_reason,
641        })
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use crate::engine::{QueryEngine, QueryEngineConfig};
649    use crate::types::ToolContext;
650    use std::sync::Arc;
651
652    /// Test that Agent tool correctly extracts all parameters from input
653    #[tokio::test]
654    async fn test_agent_tool_parses_all_parameters() {
655        // Test parameter extraction from various input formats
656        // This verifies all parameters are now properly parsed
657
658        // Test 1: subagent_type parameter (snake_case)
659        let input1 = serde_json::json!({
660            "description": "explore-agent",
661            "prompt": "Explore the codebase",
662            "subagent_type": "Explore"
663        });
664        assert_eq!(input1["subagent_type"].as_str(), Some("Explore"));
665        assert_eq!(input1["subagentType"].as_str(), None); // snake_case works
666
667        // Test 2: subagent_type parameter (camelCase)
668        let input2 = serde_json::json!({
669            "description": "explore-agent",
670            "prompt": "Explore the codebase",
671            "subagentType": "Plan"
672        });
673        assert_eq!(input2["subagentType"].as_str(), Some("Plan"));
674
675        // Test 3: run_in_background (snake_case)
676        let input3 = serde_json::json!({
677            "description": "background-agent",
678            "prompt": "Run in background",
679            "run_in_background": true
680        });
681        assert_eq!(input3["run_in_background"].as_bool(), Some(true));
682
683        // Test 4: runInBackground (camelCase)
684        let input4 = serde_json::json!({
685            "description": "background-agent",
686            "runInBackground": true
687        });
688        assert_eq!(input4["runInBackground"].as_bool(), Some(true));
689
690        // Test 5: max_turns (snake_case)
691        let input5 = serde_json::json!({
692            "description": "test",
693            "max_turns": 5
694        });
695        assert_eq!(input5["max_turns"].as_u64(), Some(5));
696
697        // Test 6: maxTurns (camelCase)
698        let input6 = serde_json::json!({
699            "description": "test",
700            "maxTurns": 10
701        });
702        assert_eq!(input6["maxTurns"].as_u64(), Some(10));
703
704        // Test 7: team_name (snake_case)
705        let input7 = serde_json::json!({
706            "description": "team-agent",
707            "team_name": "my-team"
708        });
709        assert_eq!(input7["team_name"].as_str(), Some("my-team"));
710
711        // Test 8: teamName (camelCase)
712        let input8 = serde_json::json!({
713            "description": "team-agent",
714            "teamName": "my-team"
715        });
716        assert_eq!(input8["teamName"].as_str(), Some("my-team"));
717
718        // Test 9: cwd parameter
719        let input9 = serde_json::json!({
720            "description": "custom-cwd",
721            "cwd": "/custom/path"
722        });
723        assert_eq!(input9["cwd"].as_str(), Some("/custom/path"));
724
725        // Test 10: name parameter
726        let input10 = serde_json::json!({
727            "name": "my-agent",
728            "description": "named-agent"
729        });
730        assert_eq!(input10["name"].as_str(), Some("my-agent"));
731
732        // Test 11: mode parameter
733        let input11 = serde_json::json!({
734            "description": "plan-mode",
735            "mode": "plan"
736        });
737        assert_eq!(input11["mode"].as_str(), Some("plan"));
738
739        // Test 12: isolation parameter
740        let input12 = serde_json::json!({
741            "description": "isolated",
742            "isolation": "worktree"
743        });
744        assert_eq!(input12["isolation"].as_str(), Some("worktree"));
745
746        // Verify all expected keys are now handled
747        // The agent tool executor should handle all these parameters
748    }
749
750    /// Test that Agent tool creates subagent with proper system prompt based on agent type
751    #[tokio::test]
752    async fn test_agent_tool_system_prompt_by_type() {
753        // Test system prompt generation for different agent types
754        let explore_prompt = build_agent_system_prompt("Explore task", Some("Explore"));
755        assert!(explore_prompt.contains("Explore agent"));
756
757        let plan_prompt = build_agent_system_prompt("Plan task", Some("Plan"));
758        assert!(plan_prompt.contains("Plan agent"));
759
760        let review_prompt = build_agent_system_prompt("Review task", Some("Review"));
761        assert!(review_prompt.contains("Review agent"));
762
763        let general_prompt = build_agent_system_prompt("General task", None);
764        assert!(general_prompt.contains("Task description: General task"));
765    }
766
767    /// Test that Agent tool creates subagent with proper system prompt
768    #[tokio::test]
769    async fn test_agent_tool_creates_subagent_with_system_prompt() {
770        let mut engine = QueryEngine::new(QueryEngineConfig {
771            cwd: "/tmp".to_string(),
772            model: "test-model".to_string(),
773            api_key: Some("test-key".to_string()),
774            base_url: Some("http://localhost:8080".to_string()),
775            tools: vec![],
776            system_prompt: Some("Parent system prompt".to_string()),
777            max_turns: 10,
778            max_budget_usd: None,
779            max_tokens: 4096,
780            can_use_tool: None,
781            on_event: None,
782        });
783
784        // Register Agent tool
785        let agent_tool_executor = create_agent_tool_executor(
786            "/tmp".to_string(),
787            Some("test-key".to_string()),
788            Some("http://localhost:8080".to_string()),
789            "test-model".to_string(),
790            vec![],
791        );
792        engine.register_tool("Agent".to_string(), agent_tool_executor);
793
794        let input = serde_json::json!({
795            "description": "test-subagent",
796            "prompt": "What is 1+1?"
797        });
798
799        let result = engine.execute_tool("Agent", input).await;
800        // The subagent should have a system prompt (not None)
801        // We verify by checking the tool executes without error
802        assert!(result.is_ok(), "Agent tool should execute with system prompt");
803    }
804
805    /// Test Agent creation with options
806    #[tokio::test]
807    async fn test_create_agent() {
808        let agent = Agent::create(AgentOptions {
809            model: Some("claude-sonnet-4-6".to_string()),
810            ..Default::default()
811        });
812        assert!(!agent.get_model().is_empty());
813    }
814
815    /// Check if required environment variables are present for real API tests
816    /// Returns true if AI_BASE_URL, AI_MODEL, and AI_AUTH_TOKEN can be loaded from .env
817    fn has_required_env_vars() -> bool {
818        let config = EnvConfig::load();
819        config.base_url.is_some() && config.model.is_some() && config.auth_token.is_some()
820    }
821
822    /// Test Agent tool calling with real .env config
823    /// This test makes an actual API call using the configured model
824    #[tokio::test]
825    async fn test_agent_tool_calling_with_real_env_config() {
826        // Only run if required env vars are set
827        if !tests::has_required_env_vars() {
828            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
829            return;
830        }
831
832        // Load config from .env file
833        let config = EnvConfig::load();
834
835        // Verify config is loaded
836        assert!(config.base_url.is_some(), "Base URL should be configured");
837        assert!(config.auth_token.is_some(), "Auth token should be configured");
838        assert!(config.model.is_some(), "Model should be configured");
839
840        // Create agent with real config
841        let agent = Agent::create(AgentOptions {
842            model: config.model.clone(),
843            tools: vec![],
844            ..Default::default()
845        });
846
847        // Verify agent was created with the configured model
848        let model = agent.get_model();
849        assert!(!model.is_empty(), "Agent should have a model set");
850        println!("Using model: {}", model);
851    }
852
853    /// Test agent prompt with real API call using .env config
854    /// This is an integration test that exercises the full agent flow
855    #[tokio::test]
856    async fn test_agent_prompt_with_real_api() {
857        // Only run if required env vars are set
858        if !tests::has_required_env_vars() {
859            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
860            return;
861        }
862
863        // Load config from .env file
864        let config = EnvConfig::load();
865
866        // Skip test if no API configured
867        if config.base_url.is_none() || config.auth_token.is_none() {
868            eprintln!("Skipping test: no API config found");
869            return;
870        }
871
872        // Create agent with all tools and real config
873        use crate::get_all_tools;
874        let tools = get_all_tools();
875
876        let mut agent = Agent::create(AgentOptions {
877            model: config.model.clone(),
878            max_turns: Some(3),
879            tools,
880            ..Default::default()
881        });
882
883        // Make a simple prompt that should trigger tool use
884        let result = agent.prompt("What is 2 + 2? Just give me the answer.").await;
885
886        // Verify we got a response
887        assert!(result.is_ok(), "Agent should respond successfully");
888        let response = result.unwrap();
889        assert!(!response.text.is_empty(), "Response should not be empty");
890        println!("Agent response: {}", response.text);
891    }
892
893    /// Test agent tool calling with multiple tools from .env config
894    /// This tests that the agent can use tools when configured via .env
895    #[tokio::test]
896    async fn test_agent_with_multiple_tools_real_config() {
897        // Only run if required env vars are set
898        if !tests::has_required_env_vars() {
899            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
900            return;
901        }
902
903        // Load config from .env file
904        let config = EnvConfig::load();
905
906        // Skip if no API configured
907        if config.base_url.is_none() || config.auth_token.is_none() {
908            eprintln!("Skipping test: no API config found");
909            return;
910        }
911
912        // Get all available tools
913        use crate::get_all_tools;
914        let tools = get_all_tools();
915
916        // Verify we have tools available
917        assert!(!tools.is_empty(), "Should have tools available");
918
919        let mut agent = Agent::create(AgentOptions {
920            model: config.model.clone(),
921            max_turns: Some(3),
922            tools,
923            ..Default::default()
924        });
925
926        // Prompt that might use tools
927        let result = agent.prompt("List all Rust files in the current directory using glob").await;
928
929        // Should get a response (may or may not use tools depending on model)
930        assert!(result.is_ok(), "Agent should respond");
931        let response = result.unwrap();
932        assert!(!response.text.is_empty(), "Response should not be empty");
933        println!("Agent response: {}", response.text);
934    }
935
936    /// Test that tool executors are registered and can be invoked
937    /// This verifies the fix for tool calling not working in TUI
938    #[tokio::test]
939    async fn test_tool_executors_registered() {
940        // Only run if required env vars are set
941        if !tests::has_required_env_vars() {
942            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
943            return;
944        }
945
946        // Load config from .env file
947        let config = EnvConfig::load();
948
949        // Skip if no API configured
950        if config.base_url.is_none() || config.auth_token.is_none() {
951            eprintln!("Skipping test: no API config found");
952            return;
953        }
954
955        // Get all available tools
956        use crate::get_all_tools;
957        let tools = get_all_tools();
958
959        // Verify tools are available
960        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
961        assert!(tool_names.contains(&"Bash"), "Should have Bash tool");
962        assert!(tool_names.contains(&"FileRead"), "Should have FileRead tool");
963        assert!(tool_names.contains(&"Glob"), "Should have Glob tool");
964        println!("Available tools: {:?}", tool_names);
965
966        // Create agent - this will call register_all_tool_executors internally
967        let mut agent = Agent::create(AgentOptions {
968            model: config.model.clone(),
969            max_turns: Some(3),
970            tools,
971            ..Default::default()
972        });
973
974        // Prompt that should definitely use the Bash tool
975        let result = agent
976            .prompt("Run this command: echo 'hello from tool test'")
977            .await;
978
979        // Verify we got a response
980        assert!(result.is_ok(), "Agent should respond successfully");
981        let response = result.unwrap();
982        assert!(!response.text.is_empty(), "Response should not be empty");
983
984        // Check that the tool was actually used (response should contain output)
985        let text_lower = response.text.to_lowercase();
986        let tool_was_used =
987            text_lower.contains("hello from tool test") || text_lower.contains("tool");
988        println!(
989            "Tool calling test - Response: {} (tool_used: {})",
990            response.text, tool_was_used
991        );
992    }
993
994    /// Test Glob tool directly via agent
995    #[tokio::test]
996    async fn test_glob_tool_via_agent() {
997        // Only run if required env vars are set
998        if !tests::has_required_env_vars() {
999            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
1000            return;
1001        }
1002
1003        // Load config from .env file
1004        let config = EnvConfig::load();
1005
1006        // Skip if no API configured
1007        if config.base_url.is_none() || config.auth_token.is_none() {
1008            eprintln!("Skipping test: no API config found");
1009            return;
1010        }
1011
1012        // Get all available tools
1013        use crate::get_all_tools;
1014        let tools = get_all_tools();
1015
1016        let mut agent = Agent::create(AgentOptions {
1017            model: config.model.clone(),
1018            max_turns: Some(3),
1019            tools,
1020            ..Default::default()
1021        });
1022
1023        // Prompt that should use Glob tool
1024        let result = agent
1025            .prompt("List all .rs files in the src directory using the Glob tool")
1026            .await;
1027
1028        assert!(result.is_ok(), "Agent should respond");
1029        let response = result.unwrap();
1030        assert!(!response.text.is_empty(), "Response should not be empty");
1031        println!("Glob tool test response: {}", response.text);
1032    }
1033
1034    /// Test FileRead tool directly via agent
1035    #[tokio::test]
1036    async fn test_fileread_tool_via_agent() {
1037        // Only run if required env vars are set
1038        if !tests::has_required_env_vars() {
1039            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
1040            return;
1041        }
1042
1043        // Load config from .env file
1044        let config = EnvConfig::load();
1045
1046        // Skip if no API configured
1047        if config.base_url.is_none() || config.auth_token.is_none() {
1048            eprintln!("Skipping test: no API config found");
1049            return;
1050        }
1051
1052        // Get all available tools
1053        use crate::get_all_tools;
1054        let tools = get_all_tools();
1055
1056        let mut agent = Agent::create(AgentOptions {
1057            model: config.model.clone(),
1058            max_turns: Some(3),
1059            tools,
1060            ..Default::default()
1061        });
1062
1063        // Prompt that should use FileRead tool
1064        let result = agent
1065            .prompt("Read the Cargo.toml file from the current directory")
1066            .await;
1067
1068        assert!(result.is_ok(), "Agent should respond");
1069        let response = result.unwrap();
1070        assert!(!response.text.is_empty(), "Response should not be empty");
1071        // The response should contain something from Cargo.toml
1072        println!("FileRead tool test response: {}", response.text);
1073    }
1074
1075    /// Test multiple tool calls in one turn
1076    #[tokio::test]
1077    async fn test_multiple_tool_calls() {
1078        // Only run if required env vars are set
1079        if !tests::has_required_env_vars() {
1080            eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
1081            return;
1082        }
1083
1084        // Load config from .env file
1085        let config = EnvConfig::load();
1086
1087        // Skip if no API configured
1088        if config.base_url.is_none() || config.auth_token.is_none() {
1089            eprintln!("Skipping test: no API config found");
1090            return;
1091        }
1092
1093        // Get all available tools
1094        use crate::get_all_tools;
1095        let tools = get_all_tools();
1096
1097        let mut agent = Agent::create(AgentOptions {
1098            model: config.model.clone(),
1099            max_turns: Some(5),
1100            tools,
1101            ..Default::default()
1102        });
1103
1104        // Prompt that should use multiple tools
1105        let result = agent
1106            .prompt("First list all files in the current directory, then read the README.md file if it exists")
1107            .await;
1108
1109        assert!(result.is_ok(), "Agent should respond");
1110        let response = result.unwrap();
1111        assert!(!response.text.is_empty(), "Response should not be empty");
1112        println!("Multiple tool calls test response: {}", response.text);
1113    }
1114}
1115
1116/// Helper function to create the Agent tool executor with all parameters
1117fn create_agent_tool_executor(
1118    cwd: String,
1119    api_key: Option<String>,
1120    base_url: Option<String>,
1121    model: String,
1122    tool_pool: Vec<crate::tools::ToolDefinition>,
1123) -> impl Fn(serde_json::Value, &ToolContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>> + Send + 'static {
1124    move |input: serde_json::Value,
1125          _ctx: &ToolContext|
1126          -> std::pin::Pin<
1127        Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>,
1128    > {
1129        let cwd = cwd.clone();
1130        let api_key = api_key.clone();
1131        let base_url = base_url.clone();
1132        let model = model.clone();
1133        let tool_pool = tool_pool.clone();
1134
1135        Box::pin(async move {
1136            // Extract ALL parameters from input (not just description, prompt, model, max_turns)
1137            let description = input["description"].as_str().unwrap_or("subagent");
1138            let subagent_prompt = input["prompt"].as_str().unwrap_or("");
1139            let subagent_model = input["model"]
1140                .as_str()
1141                .map(|s| s.to_string())
1142                .unwrap_or_else(|| model.clone());
1143            let max_turns = input["max_turns"]
1144                .as_u64()
1145                .or_else(|| input["maxTurns"].as_u64()) // Support camelCase too
1146                .unwrap_or(10) as u32;
1147
1148            // NEW: Extract subagent_type
1149            let subagent_type = input["subagent_type"]
1150                .as_str()
1151                .or_else(|| input["subagentType"].as_str())
1152                .map(|s| s.to_string());
1153
1154            // NEW: Extract run_in_background
1155            let run_in_background = input["run_in_background"]
1156                .as_bool()
1157                .or_else(|| input["runInBackground"].as_bool())
1158                .unwrap_or(false);
1159
1160            // NEW: Extract name
1161            let agent_name = input["name"]
1162                .as_str()
1163                .map(|s| s.to_string());
1164
1165            // NEW: Extract team_name
1166            let team_name = input["team_name"]
1167                .as_str()
1168                .or_else(|| input["teamName"].as_str())
1169                .map(|s| s.to_string());
1170
1171            // NEW: Extract mode (permission mode)
1172            let mode = input["mode"]
1173                .as_str()
1174                .map(|s| s.to_string());
1175
1176            // NEW: Extract cwd (working directory override)
1177            let subagent_cwd = input["cwd"]
1178                .as_str()
1179                .map(|s| s.to_string())
1180                .unwrap_or_else(|| cwd.clone());
1181
1182            // NEW: Extract isolation
1183            let isolation = input["isolation"]
1184                .as_str()
1185                .map(|s| s.to_string());
1186
1187            // Log all parameters for debugging
1188            let params_log = format!(
1189                "Agent tool params: description={}, subagent_type={:?}, run_in_background={}, name={:?}, team_name={:?}, mode={:?}, cwd={}, isolation={:?}",
1190                description,
1191                subagent_type,
1192                run_in_background,
1193                agent_name,
1194                team_name,
1195                mode,
1196                subagent_cwd,
1197                isolation
1198            );
1199            eprintln!("{}", params_log);
1200
1201            // Use the correct cwd for the subagent
1202            let actual_cwd = subagent_cwd.clone();
1203
1204            // Build system prompt for subagent (matching TypeScript getAgentSystemPrompt)
1205            let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
1206
1207            // Use parent agent's tool pool for the subagent
1208            let tools = tool_pool;
1209
1210            // Create a new engine for the subagent
1211            let mut sub_engine = QueryEngine::new(QueryEngineConfig {
1212                cwd: actual_cwd,
1213                model: subagent_model.to_string(),
1214                api_key,
1215                base_url,
1216                tools,
1217                system_prompt: Some(system_prompt),
1218                max_turns,
1219                max_budget_usd: None,
1220                max_tokens: 16384,
1221                can_use_tool: None,
1222                on_event: None,
1223            });
1224
1225            // Run the subagent
1226            match sub_engine.submit_message(subagent_prompt).await {
1227                Ok((result_text, _)) => {
1228                    let mut content = format!("[Subagent: {}]", description);
1229                    if let Some(ref name) = agent_name {
1230                        content = format!("[Subagent: {} ({}))]", description, name);
1231                    }
1232                    content = format!("{}\n\n{}", content, result_text);
1233                    Ok(ToolResult {
1234                        result_type: "text".to_string(),
1235                        tool_use_id: "agent_tool".to_string(),
1236                        content,
1237                        is_error: Some(false),
1238                    })
1239                }
1240                Err(e) => Ok(ToolResult {
1241                    result_type: "text".to_string(),
1242                    tool_use_id: "agent_tool".to_string(),
1243                    content: format!("[Subagent: {}] Error: {}", description, e),
1244                    is_error: Some(true),
1245                }),
1246            }
1247        })
1248    }
1249}
1250
1251/// Build system prompt for subagent based on agent type
1252fn build_agent_system_prompt(agent_description: &str, agent_type: Option<&str>) -> String {
1253    let base_prompt = "You are an agent that helps users with software engineering tasks. Use the tools available to you to assist the user.\n\nComplete the task fully—don't gold-plate, but don't leave it half-done. When you complete the task, respond with a concise report covering what was done and any key findings.";
1254
1255    match agent_type {
1256        Some("Explore") => {
1257            format!(
1258                "{}\n\nYou are an Explore agent. Your goal is to explore and understand the codebase thoroughly. Use search and read tools to investigate. Report your findings in detail.",
1259                base_prompt
1260            )
1261        }
1262        Some("Plan") => {
1263            format!(
1264                "{}\n\nYou are a Plan agent. Your goal is to plan and analyze tasks before execution. Break down complex tasks into steps. Provide a detailed plan.",
1265                base_prompt
1266            )
1267        }
1268        Some("Review") => {
1269            format!(
1270                "{}\n\nYou are a Review agent. Your goal is to review code and provide constructive feedback. Be thorough and focus on best practices.",
1271                base_prompt
1272            )
1273        }
1274        _ => {
1275            // General purpose agent
1276            format!(
1277                "{}\n\nTask description: {}",
1278                base_prompt, agent_description
1279            )
1280        }
1281    }
1282}