1use 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
13fn register_all_tool_executors(engine: &mut QueryEngine) {
15 type BoxFuture<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send>>;
16
17 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 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 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 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 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 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 use crate::tools::skill::register_skills_from_dir;
115 use crate::tools::skill::SkillTool;
116 use std::path::Path;
117
118 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 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 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 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 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 pub fn create(options: AgentOptions) -> Self {
222 let env_config = EnvConfig::load();
224
225 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 pub fn get_messages(&self) -> &[Message] {
265 &self.messages
266 }
267
268 pub fn get_tools(&self) -> &[ToolDefinition] {
270 &self.tool_pool
271 }
272
273 pub fn set_system_prompt(&mut self, prompt: &str) {
275 self.config.system_prompt = Some(prompt.to_string());
276 }
277
278 pub fn set_cwd(&mut self, cwd: &str) {
280 self.config.cwd = Some(cwd.to_string());
281 }
282
283 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 pub async fn execute_tool(
294 &mut self,
295 name: &str,
296 input: serde_json::Value,
297 ) -> Result<ToolResult, AgentError> {
298 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(&mut engine);
324
325 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 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()) .unwrap_or(10) as u32;
348
349 let subagent_type = input["subagent_type"]
351 .as_str()
352 .or_else(|| input["subagentType"].as_str())
353 .map(|s| s.to_string());
354
355 let _run_in_background = input["run_in_background"]
357 .as_bool()
358 .or_else(|| input["runInBackground"].as_bool())
359 .unwrap_or(false);
360
361 let agent_name = input["name"].as_str().map(|s| s.to_string());
363
364 let _team_name = input["team_name"]
366 .as_str()
367 .or_else(|| input["teamName"].as_str())
368 .map(|s| s.to_string());
369
370 let _mode = input["mode"].as_str().map(|s| s.to_string());
372
373 let subagent_cwd = input["cwd"]
375 .as_str()
376 .map(|s| s.to_string())
377 .unwrap_or_else(|| cwd.clone());
378
379 let _isolation = input["isolation"].as_str().map(|s| s.to_string());
381
382 let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
384
385 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 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 let ai_md_prompt = load_ai_md(cwd_path).ok().flatten();
452 let memory_prompt = load_memory_prompt();
453
454 let base_system_prompt = build_system_prompt();
456
457 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 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(&mut engine);
502
503 let tool_pool = self.tool_pool.clone();
505
506 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 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()) .unwrap_or(10) as u32;
532
533 let subagent_type = input["subagent_type"]
535 .as_str()
536 .or_else(|| input["subagentType"].as_str())
537 .map(|s| s.to_string());
538
539 let _run_in_background = input["run_in_background"]
541 .as_bool()
542 .or_else(|| input["runInBackground"].as_bool())
543 .unwrap_or(false);
544
545 let agent_name = input["name"].as_str().map(|s| s.to_string());
547
548 let _team_name = input["team_name"]
550 .as_str()
551 .or_else(|| input["teamName"].as_str())
552 .map(|s| s.to_string());
553
554 let _mode = input["mode"].as_str().map(|s| s.to_string());
556
557 let subagent_cwd = input["cwd"]
559 .as_str()
560 .map(|s| s.to_string())
561 .unwrap_or_else(|| cwd.clone());
562
563 let _isolation = input["isolation"].as_str().map(|s| s.to_string());
565
566 let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
568
569 let parent_tools = tool_pool;
571
572 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 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(&mut engine);
614 engine.register_tool("Agent".to_string(), agent_tool_executor);
615
616 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 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 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 #[tokio::test]
654 async fn test_agent_tool_parses_all_parameters() {
655 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); 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 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 let input4 = serde_json::json!({
685 "description": "background-agent",
686 "runInBackground": true
687 });
688 assert_eq!(input4["runInBackground"].as_bool(), Some(true));
689
690 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 let input6 = serde_json::json!({
699 "description": "test",
700 "maxTurns": 10
701 });
702 assert_eq!(input6["maxTurns"].as_u64(), Some(10));
703
704 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 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 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 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 let input11 = serde_json::json!({
734 "description": "plan-mode",
735 "mode": "plan"
736 });
737 assert_eq!(input11["mode"].as_str(), Some("plan"));
738
739 let input12 = serde_json::json!({
741 "description": "isolated",
742 "isolation": "worktree"
743 });
744 assert_eq!(input12["isolation"].as_str(), Some("worktree"));
745
746 }
749
750 #[tokio::test]
752 async fn test_agent_tool_system_prompt_by_type() {
753 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 #[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 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 assert!(result.is_ok(), "Agent tool should execute with system prompt");
803 }
804
805 #[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 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 #[tokio::test]
825 async fn test_agent_tool_calling_with_real_env_config() {
826 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 let config = EnvConfig::load();
834
835 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 let agent = Agent::create(AgentOptions {
842 model: config.model.clone(),
843 tools: vec![],
844 ..Default::default()
845 });
846
847 let model = agent.get_model();
849 assert!(!model.is_empty(), "Agent should have a model set");
850 println!("Using model: {}", model);
851 }
852
853 #[tokio::test]
856 async fn test_agent_prompt_with_real_api() {
857 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 let config = EnvConfig::load();
865
866 if config.base_url.is_none() || config.auth_token.is_none() {
868 eprintln!("Skipping test: no API config found");
869 return;
870 }
871
872 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 let result = agent.prompt("What is 2 + 2? Just give me the answer.").await;
885
886 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 #[tokio::test]
896 async fn test_agent_with_multiple_tools_real_config() {
897 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 let config = EnvConfig::load();
905
906 if config.base_url.is_none() || config.auth_token.is_none() {
908 eprintln!("Skipping test: no API config found");
909 return;
910 }
911
912 use crate::get_all_tools;
914 let tools = get_all_tools();
915
916 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 let result = agent.prompt("List all Rust files in the current directory using glob").await;
928
929 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 #[tokio::test]
939 async fn test_tool_executors_registered() {
940 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 let config = EnvConfig::load();
948
949 if config.base_url.is_none() || config.auth_token.is_none() {
951 eprintln!("Skipping test: no API config found");
952 return;
953 }
954
955 use crate::get_all_tools;
957 let tools = get_all_tools();
958
959 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 let mut agent = Agent::create(AgentOptions {
968 model: config.model.clone(),
969 max_turns: Some(3),
970 tools,
971 ..Default::default()
972 });
973
974 let result = agent
976 .prompt("Run this command: echo 'hello from tool test'")
977 .await;
978
979 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 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 #[tokio::test]
996 async fn test_glob_tool_via_agent() {
997 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 let config = EnvConfig::load();
1005
1006 if config.base_url.is_none() || config.auth_token.is_none() {
1008 eprintln!("Skipping test: no API config found");
1009 return;
1010 }
1011
1012 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 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 #[tokio::test]
1036 async fn test_fileread_tool_via_agent() {
1037 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 let config = EnvConfig::load();
1045
1046 if config.base_url.is_none() || config.auth_token.is_none() {
1048 eprintln!("Skipping test: no API config found");
1049 return;
1050 }
1051
1052 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 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 println!("FileRead tool test response: {}", response.text);
1073 }
1074
1075 #[tokio::test]
1077 async fn test_multiple_tool_calls() {
1078 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 let config = EnvConfig::load();
1086
1087 if config.base_url.is_none() || config.auth_token.is_none() {
1089 eprintln!("Skipping test: no API config found");
1090 return;
1091 }
1092
1093 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 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
1116fn 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 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()) .unwrap_or(10) as u32;
1147
1148 let subagent_type = input["subagent_type"]
1150 .as_str()
1151 .or_else(|| input["subagentType"].as_str())
1152 .map(|s| s.to_string());
1153
1154 let run_in_background = input["run_in_background"]
1156 .as_bool()
1157 .or_else(|| input["runInBackground"].as_bool())
1158 .unwrap_or(false);
1159
1160 let agent_name = input["name"]
1162 .as_str()
1163 .map(|s| s.to_string());
1164
1165 let team_name = input["team_name"]
1167 .as_str()
1168 .or_else(|| input["teamName"].as_str())
1169 .map(|s| s.to_string());
1170
1171 let mode = input["mode"]
1173 .as_str()
1174 .map(|s| s.to_string());
1175
1176 let subagent_cwd = input["cwd"]
1178 .as_str()
1179 .map(|s| s.to_string())
1180 .unwrap_or_else(|| cwd.clone());
1181
1182 let isolation = input["isolation"]
1184 .as_str()
1185 .map(|s| s.to_string());
1186
1187 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 let actual_cwd = subagent_cwd.clone();
1203
1204 let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
1206
1207 let tools = tool_pool;
1209
1210 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 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
1251fn 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 format!(
1277 "{}\n\nTask description: {}",
1278 base_prompt, agent_description
1279 )
1280 }
1281 }
1282}