use crate::engine::{QueryEngine, QueryEngineConfig};
use crate::env::EnvConfig;
use crate::error::AgentError;
use crate::tools::bash::BashTool;
use crate::tools::edit::FileEditTool;
use crate::tools::glob::GlobTool;
use crate::tools::grep::GrepTool;
use crate::tools::read::FileReadTool as ReadTool;
use crate::tools::write::FileWriteTool as WriteTool;
use crate::types::*;
fn register_all_tool_executors(engine: &mut QueryEngine) {
type BoxFuture<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send>>;
let bash_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = BashTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("Bash".to_string(), bash_executor);
let read_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = ReadTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("FileRead".to_string(), read_executor);
let write_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = WriteTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("FileWrite".to_string(), write_executor);
let glob_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = GlobTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("Glob".to_string(), glob_executor);
let grep_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = GrepTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("Grep".to_string(), grep_executor);
let edit_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = FileEditTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("FileEdit".to_string(), edit_executor);
use crate::tools::skill::register_skills_from_dir;
use crate::tools::skill::SkillTool;
use std::path::Path;
register_skills_from_dir(Path::new("examples/skills"));
let skill_executor = move |input: serde_json::Value,
ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_clone = SkillTool::new();
let cwd = ctx.cwd.clone();
Box::pin(async move {
let ctx2 = ToolContext {
cwd,
abort_signal: None,
};
tool_clone.execute(input, &ctx2).await
})
};
engine.register_tool("Skill".to_string(), skill_executor);
let stub_executor = |input: serde_json::Value,
_ctx: &ToolContext|
-> BoxFuture<Result<ToolResult, AgentError>> {
let tool_name = input
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("unknown")
.to_string();
Box::pin(async move {
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: tool_name.clone(),
content: format!("Tool '{}' is not fully implemented yet", tool_name),
is_error: Some(false),
})
})
};
for tool_name in &[
"TaskCreate",
"TaskList",
"TaskUpdate",
"TaskGet",
"TeamCreate",
"TeamDelete",
"SendMessage",
"EnterWorktree",
"ExitWorktree",
"EnterPlanMode",
"ExitPlanMode",
"AskUserQuestion",
"ToolSearch",
"CronCreate",
"CronDelete",
"CronList",
"Config",
"TodoWrite",
"NotebookEdit",
"WebFetch",
"WebSearch",
"Agent",
] {
engine.register_tool(tool_name.to_string(), stub_executor);
}
}
pub struct Agent {
config: AgentOptions,
model: String,
api_key: Option<String>,
base_url: Option<String>,
tool_pool: Vec<ToolDefinition>,
messages: Vec<Message>,
session_id: String,
}
impl From<AgentOptions> for Agent {
fn from(options: AgentOptions) -> Self {
Agent::create(options)
}
}
impl Agent {
pub fn new(model: &str, max_turns: u32) -> Self {
Self::create(AgentOptions {
model: Some(model.to_string()),
max_turns: Some(max_turns),
..Default::default()
})
}
pub fn with_event_callback<F>(model: &str, max_turns: u32, on_event: F) -> Self
where
F: Fn(AgentEvent) + Send + Sync + 'static,
{
let mut agent = Self::new(model, max_turns);
agent.config.on_event = Some(std::sync::Arc::new(on_event));
agent
}
pub fn create(options: AgentOptions) -> Self {
let env_config = EnvConfig::load();
let model = env_config
.model
.clone()
.or_else(|| options.model.clone())
.unwrap_or_else(|| "claude-sonnet-4-6".to_string());
let api_key = env_config
.auth_token
.clone()
.or_else(|| options.api_key.clone());
let base_url = env_config
.base_url
.clone()
.or_else(|| options.base_url.clone());
let session_id = uuid::Uuid::new_v4().to_string();
Self {
config: options.clone(),
model,
api_key,
base_url,
tool_pool: options.tools.clone(),
messages: vec![],
session_id,
}
}
pub fn get_model(&self) -> &str {
&self.model
}
pub fn get_session_id(&self) -> &str {
&self.session_id
}
pub fn get_messages(&self) -> &[Message] {
&self.messages
}
pub fn get_tools(&self) -> &[ToolDefinition] {
&self.tool_pool
}
pub fn set_system_prompt(&mut self, prompt: &str) {
self.config.system_prompt = Some(prompt.to_string());
}
pub fn set_cwd(&mut self, cwd: &str) {
self.config.cwd = Some(cwd.to_string());
}
pub fn set_event_callback<F>(&mut self, callback: F)
where
F: Fn(AgentEvent) + Send + Sync + 'static,
{
self.config.on_event = Some(std::sync::Arc::new(callback));
}
pub async fn execute_tool(
&mut self,
name: &str,
input: serde_json::Value,
) -> Result<ToolResult, AgentError> {
let cwd = self.config.cwd.clone().unwrap_or_else(|| {
std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string())
});
let model = self.model.clone();
let api_key = self.api_key.clone();
let base_url = self.base_url.clone();
let mut engine = QueryEngine::new(QueryEngineConfig {
cwd: cwd.clone(),
model: model.clone(),
api_key: api_key.clone(),
base_url: base_url.clone(),
tools: vec![],
system_prompt: None,
max_turns: 10,
max_budget_usd: None,
max_tokens: 16384,
can_use_tool: None,
on_event: None,
});
register_all_tool_executors(&mut engine);
let agent_tool_executor = move |input: serde_json::Value,
_ctx: &ToolContext|
-> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>,
> {
let cwd = cwd.clone();
let api_key = api_key.clone();
let base_url = base_url.clone();
let model = model.clone();
Box::pin(async move {
let description = input["description"].as_str().unwrap_or("subagent");
let subagent_prompt = input["prompt"].as_str().unwrap_or("");
let subagent_model = input["model"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| model.clone());
let max_turns = input["max_turns"]
.as_u64()
.or_else(|| input["maxTurns"].as_u64()) .unwrap_or(10) as u32;
let subagent_type = input["subagent_type"]
.as_str()
.or_else(|| input["subagentType"].as_str())
.map(|s| s.to_string());
let _run_in_background = input["run_in_background"]
.as_bool()
.or_else(|| input["runInBackground"].as_bool())
.unwrap_or(false);
let agent_name = input["name"].as_str().map(|s| s.to_string());
let _team_name = input["team_name"]
.as_str()
.or_else(|| input["teamName"].as_str())
.map(|s| s.to_string());
let _mode = input["mode"].as_str().map(|s| s.to_string());
let subagent_cwd = input["cwd"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| cwd.clone());
let _isolation = input["isolation"].as_str().map(|s| s.to_string());
let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
let mut sub_engine = QueryEngine::new(QueryEngineConfig {
cwd: subagent_cwd,
model: subagent_model.to_string(),
api_key,
base_url,
tools: vec![],
system_prompt: Some(system_prompt),
max_turns,
max_budget_usd: None,
max_tokens: 16384,
can_use_tool: None,
on_event: None,
});
match sub_engine.submit_message(subagent_prompt).await {
Ok((result_text, _)) => {
let mut content = format!("[Subagent: {}]", description);
if let Some(ref name) = agent_name {
content = format!("[Subagent: {} ({})]", description, name);
}
content = format!("{}\n\n{}", content, result_text);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content,
is_error: Some(false),
})
}
Err(e) => Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content: format!("[Subagent: {}] Error: {}", description, e),
is_error: Some(true),
}),
}
})
};
engine.register_tool("Agent".to_string(), agent_tool_executor);
engine.execute_tool(name, input).await
}
pub async fn prompt(&mut self, prompt: &str) -> Result<QueryResult, AgentError> {
self.query(prompt).await
}
pub async fn query(&mut self, prompt: &str) -> Result<QueryResult, AgentError> {
use crate::ai_md::load_ai_md;
use crate::memory::load_memory_prompt;
use crate::prompts::build_system_prompt;
use crate::tools::get_all_base_tools;
let cwd = self.config.cwd.clone().unwrap_or_else(|| {
std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string())
});
let cwd_path = std::path::Path::new(&cwd);
let model = self.model.clone();
let api_key = self.api_key.clone();
let base_url = self.base_url.clone();
let ai_md_prompt = load_ai_md(cwd_path).ok().flatten();
let memory_prompt = load_memory_prompt();
let base_system_prompt = build_system_prompt();
let system_prompt = match (&ai_md_prompt, &memory_prompt, &self.config.system_prompt) {
(Some(ai_md), Some(mem), Some(custom)) => Some(format!(
"{}\n\n{}\n\n{}\n\n{}",
ai_md, mem, base_system_prompt, custom
)),
(Some(ai_md), Some(mem), None) => {
Some(format!("{}\n\n{}\n\n{}", ai_md, mem, base_system_prompt))
}
(Some(ai_md), None, Some(custom)) => {
Some(format!("{}\n\n{}\n\n{}", ai_md, base_system_prompt, custom))
}
(Some(ai_md), None, None) => Some(format!("{}\n\n{}", ai_md, base_system_prompt)),
(None, Some(mem), Some(custom)) => {
Some(format!("{}\n\n{}\n\n{}", mem, base_system_prompt, custom))
}
(None, Some(mem), None) => Some(format!("{}\n\n{}", mem, base_system_prompt)),
(None, None, Some(custom)) => Some(format!("{}\n\n{}", base_system_prompt, custom)),
(None, None, None) => Some(base_system_prompt),
};
let tools = if self.tool_pool.is_empty() {
get_all_base_tools()
} else {
self.tool_pool.clone()
};
let on_event = self.config.on_event.clone();
let mut engine = QueryEngine::new(QueryEngineConfig {
cwd: cwd.clone(),
model: model.clone(),
api_key: api_key.clone(),
base_url: base_url.clone(),
tools,
system_prompt,
max_turns: self.config.max_turns.unwrap_or(10),
max_budget_usd: self.config.max_budget_usd,
max_tokens: self.config.max_tokens.unwrap_or(16384),
can_use_tool: None,
on_event,
});
register_all_tool_executors(&mut engine);
let tool_pool = self.tool_pool.clone();
let agent_tool_executor = move |input: serde_json::Value,
_ctx: &ToolContext|
-> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>,
> {
let cwd = cwd.clone();
let api_key = api_key.clone();
let base_url = base_url.clone();
let model = model.clone();
let tool_pool = tool_pool.clone();
Box::pin(async move {
let description = input["description"].as_str().unwrap_or("subagent");
let subagent_prompt = input["prompt"].as_str().unwrap_or("");
let subagent_model = input["model"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| model.clone());
let max_turns = input["max_turns"]
.as_u64()
.or_else(|| input["maxTurns"].as_u64()) .unwrap_or(10) as u32;
let subagent_type = input["subagent_type"]
.as_str()
.or_else(|| input["subagentType"].as_str())
.map(|s| s.to_string());
let _run_in_background = input["run_in_background"]
.as_bool()
.or_else(|| input["runInBackground"].as_bool())
.unwrap_or(false);
let agent_name = input["name"].as_str().map(|s| s.to_string());
let _team_name = input["team_name"]
.as_str()
.or_else(|| input["teamName"].as_str())
.map(|s| s.to_string());
let _mode = input["mode"].as_str().map(|s| s.to_string());
let subagent_cwd = input["cwd"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| cwd.clone());
let _isolation = input["isolation"].as_str().map(|s| s.to_string());
let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
let parent_tools = tool_pool;
let mut sub_engine = QueryEngine::new(QueryEngineConfig {
cwd: subagent_cwd,
model: subagent_model.to_string(),
api_key,
base_url,
tools: parent_tools,
system_prompt: Some(system_prompt),
max_turns,
max_budget_usd: None,
max_tokens: 16384,
can_use_tool: None,
on_event: None,
});
match sub_engine.submit_message(subagent_prompt).await {
Ok((result_text, _)) => {
let mut content = format!("[Subagent: {}]", description);
if let Some(ref name) = agent_name {
content = format!("[Subagent: {} ({})]", description, name);
}
content = format!("{}\n\n{}", content, result_text);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content,
is_error: Some(false),
})
}
Err(e) => Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content: format!("[Subagent: {}] Error: {}", description, e),
is_error: Some(true),
}),
}
})
};
register_all_tool_executors(&mut engine);
engine.register_tool("Agent".to_string(), agent_tool_executor);
engine.set_messages(self.messages.clone());
let start = std::time::Instant::now();
let (response_text, exit_reason) = engine.submit_message(prompt).await?;
let messages = engine.get_messages();
let engine_usage = engine.get_usage();
let usage = TokenUsage {
input_tokens: engine_usage.input_tokens,
output_tokens: engine_usage.output_tokens,
cache_creation_input_tokens: engine_usage.cache_creation_input_tokens,
cache_read_input_tokens: engine_usage.cache_read_input_tokens,
};
self.messages = messages;
Ok(QueryResult {
text: response_text,
usage,
num_turns: engine.get_turn_count(),
duration_ms: start.elapsed().as_millis() as u64,
exit_reason,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::{QueryEngine, QueryEngineConfig};
use crate::types::ToolContext;
use std::sync::Arc;
#[tokio::test]
async fn test_agent_tool_parses_all_parameters() {
let input1 = serde_json::json!({
"description": "explore-agent",
"prompt": "Explore the codebase",
"subagent_type": "Explore"
});
assert_eq!(input1["subagent_type"].as_str(), Some("Explore"));
assert_eq!(input1["subagentType"].as_str(), None);
let input2 = serde_json::json!({
"description": "explore-agent",
"prompt": "Explore the codebase",
"subagentType": "Plan"
});
assert_eq!(input2["subagentType"].as_str(), Some("Plan"));
let input3 = serde_json::json!({
"description": "background-agent",
"prompt": "Run in background",
"run_in_background": true
});
assert_eq!(input3["run_in_background"].as_bool(), Some(true));
let input4 = serde_json::json!({
"description": "background-agent",
"runInBackground": true
});
assert_eq!(input4["runInBackground"].as_bool(), Some(true));
let input5 = serde_json::json!({
"description": "test",
"max_turns": 5
});
assert_eq!(input5["max_turns"].as_u64(), Some(5));
let input6 = serde_json::json!({
"description": "test",
"maxTurns": 10
});
assert_eq!(input6["maxTurns"].as_u64(), Some(10));
let input7 = serde_json::json!({
"description": "team-agent",
"team_name": "my-team"
});
assert_eq!(input7["team_name"].as_str(), Some("my-team"));
let input8 = serde_json::json!({
"description": "team-agent",
"teamName": "my-team"
});
assert_eq!(input8["teamName"].as_str(), Some("my-team"));
let input9 = serde_json::json!({
"description": "custom-cwd",
"cwd": "/custom/path"
});
assert_eq!(input9["cwd"].as_str(), Some("/custom/path"));
let input10 = serde_json::json!({
"name": "my-agent",
"description": "named-agent"
});
assert_eq!(input10["name"].as_str(), Some("my-agent"));
let input11 = serde_json::json!({
"description": "plan-mode",
"mode": "plan"
});
assert_eq!(input11["mode"].as_str(), Some("plan"));
let input12 = serde_json::json!({
"description": "isolated",
"isolation": "worktree"
});
assert_eq!(input12["isolation"].as_str(), Some("worktree"));
}
#[tokio::test]
async fn test_agent_tool_system_prompt_by_type() {
let explore_prompt = build_agent_system_prompt("Explore task", Some("Explore"));
assert!(explore_prompt.contains("Explore agent"));
let plan_prompt = build_agent_system_prompt("Plan task", Some("Plan"));
assert!(plan_prompt.contains("Plan agent"));
let review_prompt = build_agent_system_prompt("Review task", Some("Review"));
assert!(review_prompt.contains("Review agent"));
let general_prompt = build_agent_system_prompt("General task", None);
assert!(general_prompt.contains("Task description: General task"));
}
#[tokio::test]
async fn test_agent_tool_creates_subagent_with_system_prompt() {
let mut engine = QueryEngine::new(QueryEngineConfig {
cwd: "/tmp".to_string(),
model: "test-model".to_string(),
api_key: Some("test-key".to_string()),
base_url: Some("http://localhost:8080".to_string()),
tools: vec![],
system_prompt: Some("Parent system prompt".to_string()),
max_turns: 10,
max_budget_usd: None,
max_tokens: 4096,
can_use_tool: None,
on_event: None,
});
let agent_tool_executor = create_agent_tool_executor(
"/tmp".to_string(),
Some("test-key".to_string()),
Some("http://localhost:8080".to_string()),
"test-model".to_string(),
vec![],
);
engine.register_tool("Agent".to_string(), agent_tool_executor);
let input = serde_json::json!({
"description": "test-subagent",
"prompt": "What is 1+1?"
});
let result = engine.execute_tool("Agent", input).await;
assert!(result.is_ok(), "Agent tool should execute with system prompt");
}
#[tokio::test]
async fn test_create_agent() {
let agent = Agent::create(AgentOptions {
model: Some("claude-sonnet-4-6".to_string()),
..Default::default()
});
assert!(!agent.get_model().is_empty());
}
fn has_required_env_vars() -> bool {
let config = EnvConfig::load();
config.base_url.is_some() && config.model.is_some() && config.auth_token.is_some()
}
#[tokio::test]
async fn test_agent_tool_calling_with_real_env_config() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
assert!(config.base_url.is_some(), "Base URL should be configured");
assert!(config.auth_token.is_some(), "Auth token should be configured");
assert!(config.model.is_some(), "Model should be configured");
let agent = Agent::create(AgentOptions {
model: config.model.clone(),
tools: vec![],
..Default::default()
});
let model = agent.get_model();
assert!(!model.is_empty(), "Agent should have a model set");
println!("Using model: {}", model);
}
#[tokio::test]
async fn test_agent_prompt_with_real_api() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
if config.base_url.is_none() || config.auth_token.is_none() {
eprintln!("Skipping test: no API config found");
return;
}
use crate::get_all_tools;
let tools = get_all_tools();
let mut agent = Agent::create(AgentOptions {
model: config.model.clone(),
max_turns: Some(3),
tools,
..Default::default()
});
let result = agent.prompt("What is 2 + 2? Just give me the answer.").await;
assert!(result.is_ok(), "Agent should respond successfully");
let response = result.unwrap();
assert!(!response.text.is_empty(), "Response should not be empty");
println!("Agent response: {}", response.text);
}
#[tokio::test]
async fn test_agent_with_multiple_tools_real_config() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
if config.base_url.is_none() || config.auth_token.is_none() {
eprintln!("Skipping test: no API config found");
return;
}
use crate::get_all_tools;
let tools = get_all_tools();
assert!(!tools.is_empty(), "Should have tools available");
let mut agent = Agent::create(AgentOptions {
model: config.model.clone(),
max_turns: Some(3),
tools,
..Default::default()
});
let result = agent.prompt("List all Rust files in the current directory using glob").await;
assert!(result.is_ok(), "Agent should respond");
let response = result.unwrap();
assert!(!response.text.is_empty(), "Response should not be empty");
println!("Agent response: {}", response.text);
}
#[tokio::test]
async fn test_tool_executors_registered() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
if config.base_url.is_none() || config.auth_token.is_none() {
eprintln!("Skipping test: no API config found");
return;
}
use crate::get_all_tools;
let tools = get_all_tools();
let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(tool_names.contains(&"Bash"), "Should have Bash tool");
assert!(tool_names.contains(&"FileRead"), "Should have FileRead tool");
assert!(tool_names.contains(&"Glob"), "Should have Glob tool");
println!("Available tools: {:?}", tool_names);
let mut agent = Agent::create(AgentOptions {
model: config.model.clone(),
max_turns: Some(3),
tools,
..Default::default()
});
let result = agent
.prompt("Run this command: echo 'hello from tool test'")
.await;
assert!(result.is_ok(), "Agent should respond successfully");
let response = result.unwrap();
assert!(!response.text.is_empty(), "Response should not be empty");
let text_lower = response.text.to_lowercase();
let tool_was_used =
text_lower.contains("hello from tool test") || text_lower.contains("tool");
println!(
"Tool calling test - Response: {} (tool_used: {})",
response.text, tool_was_used
);
}
#[tokio::test]
async fn test_glob_tool_via_agent() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
if config.base_url.is_none() || config.auth_token.is_none() {
eprintln!("Skipping test: no API config found");
return;
}
use crate::get_all_tools;
let tools = get_all_tools();
let mut agent = Agent::create(AgentOptions {
model: config.model.clone(),
max_turns: Some(3),
tools,
..Default::default()
});
let result = agent
.prompt("List all .rs files in the src directory using the Glob tool")
.await;
assert!(result.is_ok(), "Agent should respond");
let response = result.unwrap();
assert!(!response.text.is_empty(), "Response should not be empty");
println!("Glob tool test response: {}", response.text);
}
#[tokio::test]
async fn test_fileread_tool_via_agent() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
if config.base_url.is_none() || config.auth_token.is_none() {
eprintln!("Skipping test: no API config found");
return;
}
use crate::get_all_tools;
let tools = get_all_tools();
let mut agent = Agent::create(AgentOptions {
model: config.model.clone(),
max_turns: Some(3),
tools,
..Default::default()
});
let result = agent
.prompt("Read the Cargo.toml file from the current directory")
.await;
assert!(result.is_ok(), "Agent should respond");
let response = result.unwrap();
assert!(!response.text.is_empty(), "Response should not be empty");
println!("FileRead tool test response: {}", response.text);
}
#[tokio::test]
async fn test_multiple_tool_calls() {
if !tests::has_required_env_vars() {
eprintln!("Skipping test: AI_BASE_URL, AI_MODEL, or AI_AUTH_TOKEN not set");
return;
}
let config = EnvConfig::load();
if config.base_url.is_none() || config.auth_token.is_none() {
eprintln!("Skipping test: no API config found");
return;
}
use crate::get_all_tools;
let tools = get_all_tools();
let mut agent = Agent::create(AgentOptions {
model: config.model.clone(),
max_turns: Some(5),
tools,
..Default::default()
});
let result = agent
.prompt("First list all files in the current directory, then read the README.md file if it exists")
.await;
assert!(result.is_ok(), "Agent should respond");
let response = result.unwrap();
assert!(!response.text.is_empty(), "Response should not be empty");
println!("Multiple tool calls test response: {}", response.text);
}
}
fn create_agent_tool_executor(
cwd: String,
api_key: Option<String>,
base_url: Option<String>,
model: String,
tool_pool: Vec<crate::tools::ToolDefinition>,
) -> impl Fn(serde_json::Value, &ToolContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>> + Send + 'static {
move |input: serde_json::Value,
_ctx: &ToolContext|
-> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ToolResult, AgentError>> + Send>,
> {
let cwd = cwd.clone();
let api_key = api_key.clone();
let base_url = base_url.clone();
let model = model.clone();
let tool_pool = tool_pool.clone();
Box::pin(async move {
let description = input["description"].as_str().unwrap_or("subagent");
let subagent_prompt = input["prompt"].as_str().unwrap_or("");
let subagent_model = input["model"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| model.clone());
let max_turns = input["max_turns"]
.as_u64()
.or_else(|| input["maxTurns"].as_u64()) .unwrap_or(10) as u32;
let subagent_type = input["subagent_type"]
.as_str()
.or_else(|| input["subagentType"].as_str())
.map(|s| s.to_string());
let run_in_background = input["run_in_background"]
.as_bool()
.or_else(|| input["runInBackground"].as_bool())
.unwrap_or(false);
let agent_name = input["name"]
.as_str()
.map(|s| s.to_string());
let team_name = input["team_name"]
.as_str()
.or_else(|| input["teamName"].as_str())
.map(|s| s.to_string());
let mode = input["mode"]
.as_str()
.map(|s| s.to_string());
let subagent_cwd = input["cwd"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| cwd.clone());
let isolation = input["isolation"]
.as_str()
.map(|s| s.to_string());
let params_log = format!(
"Agent tool params: description={}, subagent_type={:?}, run_in_background={}, name={:?}, team_name={:?}, mode={:?}, cwd={}, isolation={:?}",
description,
subagent_type,
run_in_background,
agent_name,
team_name,
mode,
subagent_cwd,
isolation
);
eprintln!("{}", params_log);
let actual_cwd = subagent_cwd.clone();
let system_prompt = build_agent_system_prompt(description, subagent_type.as_deref());
let tools = tool_pool;
let mut sub_engine = QueryEngine::new(QueryEngineConfig {
cwd: actual_cwd,
model: subagent_model.to_string(),
api_key,
base_url,
tools,
system_prompt: Some(system_prompt),
max_turns,
max_budget_usd: None,
max_tokens: 16384,
can_use_tool: None,
on_event: None,
});
match sub_engine.submit_message(subagent_prompt).await {
Ok((result_text, _)) => {
let mut content = format!("[Subagent: {}]", description);
if let Some(ref name) = agent_name {
content = format!("[Subagent: {} ({}))]", description, name);
}
content = format!("{}\n\n{}", content, result_text);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content,
is_error: Some(false),
})
}
Err(e) => Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "agent_tool".to_string(),
content: format!("[Subagent: {}] Error: {}", description, e),
is_error: Some(true),
}),
}
})
}
}
fn build_agent_system_prompt(agent_description: &str, agent_type: Option<&str>) -> String {
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.";
match agent_type {
Some("Explore") => {
format!(
"{}\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.",
base_prompt
)
}
Some("Plan") => {
format!(
"{}\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.",
base_prompt
)
}
Some("Review") => {
format!(
"{}\n\nYou are a Review agent. Your goal is to review code and provide constructive feedback. Be thorough and focus on best practices.",
base_prompt
)
}
_ => {
format!(
"{}\n\nTask description: {}",
base_prompt, agent_description
)
}
}
}