use crate::cli;
use crate::commands_project;
use crate::format::*;
use crate::hooks::{self, maybe_hook, AuditHook, HookRegistry};
use crate::AgentConfig;
use std::io::{self, IsTerminal, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use yoagent::provider::{
AnthropicProvider, BedrockProvider, GoogleProvider, OpenAiCompatProvider, StreamProvider,
};
use yoagent::sub_agent::SubAgentTool;
use yoagent::tools::bash::ConfirmFn;
use yoagent::tools::edit::EditFileTool;
use yoagent::tools::file::{ReadFileTool, WriteFileTool};
use yoagent::tools::list::ListFilesTool;
use yoagent::tools::search::SearchTool;
use yoagent::types::AgentTool;
struct GuardedTool {
inner: Box<dyn AgentTool>,
restrictions: cli::DirectoryRestrictions,
}
#[async_trait::async_trait]
impl AgentTool for GuardedTool {
fn name(&self) -> &str {
self.inner.name()
}
fn label(&self) -> &str {
self.inner.label()
}
fn description(&self) -> &str {
self.inner.description()
}
fn parameters_schema(&self) -> serde_json::Value {
self.inner.parameters_schema()
}
async fn execute(
&self,
params: serde_json::Value,
ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
if let Some(path) = params.get("path").and_then(|v| v.as_str()) {
if let Err(reason) = self.restrictions.check_path(path) {
return Err(yoagent::types::ToolError::Failed(reason));
}
}
self.inner.execute(params, ctx).await
}
}
struct TruncatingTool {
inner: Box<dyn AgentTool>,
max_chars: usize,
}
pub(crate) fn truncate_result(
mut result: yoagent::types::ToolResult,
max_chars: usize,
) -> yoagent::types::ToolResult {
use yoagent::Content;
result.content = result
.content
.into_iter()
.map(|c| match c {
Content::Text { text } => Content::Text {
text: truncate_tool_output(&text, max_chars),
},
other => other,
})
.collect();
result
}
#[async_trait::async_trait]
impl AgentTool for TruncatingTool {
fn name(&self) -> &str {
self.inner.name()
}
fn label(&self) -> &str {
self.inner.label()
}
fn description(&self) -> &str {
self.inner.description()
}
fn parameters_schema(&self) -> serde_json::Value {
self.inner.parameters_schema()
}
async fn execute(
&self,
params: serde_json::Value,
ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
let result = self.inner.execute(params, ctx).await?;
Ok(truncate_result(result, self.max_chars))
}
}
fn with_truncation(tool: Box<dyn AgentTool>, max_chars: usize) -> Box<dyn AgentTool> {
Box::new(TruncatingTool {
inner: tool,
max_chars,
})
}
fn maybe_guard(
tool: Box<dyn AgentTool>,
restrictions: &cli::DirectoryRestrictions,
) -> Box<dyn AgentTool> {
if restrictions.is_empty() {
tool
} else {
Box::new(GuardedTool {
inner: tool,
restrictions: restrictions.clone(),
})
}
}
struct ArcGuardedTool {
inner: Arc<dyn AgentTool>,
restrictions: cli::DirectoryRestrictions,
}
#[async_trait::async_trait]
impl AgentTool for ArcGuardedTool {
fn name(&self) -> &str {
self.inner.name()
}
fn label(&self) -> &str {
self.inner.label()
}
fn description(&self) -> &str {
self.inner.description()
}
fn parameters_schema(&self) -> serde_json::Value {
self.inner.parameters_schema()
}
async fn execute(
&self,
params: serde_json::Value,
ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
if let Some(path) = params.get("path").and_then(|v| v.as_str()) {
if let Err(reason) = self.restrictions.check_path(path) {
return Err(yoagent::types::ToolError::Failed(reason));
}
}
self.inner.execute(params, ctx).await
}
}
fn maybe_guard_arc(
tool: Arc<dyn AgentTool>,
restrictions: &cli::DirectoryRestrictions,
) -> Arc<dyn AgentTool> {
if restrictions.is_empty() {
tool
} else {
Arc::new(ArcGuardedTool {
inner: tool,
restrictions: restrictions.clone(),
})
}
}
struct ConfirmTool {
inner: Box<dyn AgentTool>,
always_approved: Arc<AtomicBool>,
permissions: cli::PermissionConfig,
}
pub fn describe_file_operation(tool_name: &str, params: &serde_json::Value) -> String {
match tool_name {
"write_file" => {
let path = params
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
let content = params.get("content").and_then(|v| v.as_str()).unwrap_or("");
let line_count = if content.is_empty() {
0
} else {
content.lines().count()
};
if content.is_empty() {
format!("write: {path} (⚠ EMPTY content — creates/overwrites with empty file)")
} else {
let word = crate::format::pluralize(line_count, "line", "lines");
format!("write: {path} ({line_count} {word})")
}
}
"edit_file" => {
let path = params
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
let old_text = params
.get("old_text")
.and_then(|v| v.as_str())
.unwrap_or("");
let new_text = params
.get("new_text")
.and_then(|v| v.as_str())
.unwrap_or("");
let old_lines = old_text.lines().count();
let new_lines = new_text.lines().count();
format!("edit: {path} ({old_lines} → {new_lines} lines)")
}
"rename_symbol" => {
let old_name = params
.get("old_name")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
let new_name = params
.get("new_name")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
let scope = params
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("project");
format!("rename: {old_name} → {new_name} (in {scope})")
}
_ => format!("{tool_name}: file operation"),
}
}
pub fn confirm_file_operation(
description: &str,
path: &str,
always_approved: &Arc<AtomicBool>,
permissions: &cli::PermissionConfig,
) -> bool {
if always_approved.load(Ordering::Relaxed) {
eprintln!(
"{GREEN} ✓ Auto-approved: {RESET}{}",
truncate_with_ellipsis(description, 120)
);
return true;
}
if let Some(allowed) = permissions.check(path) {
if allowed {
eprintln!(
"{GREEN} ✓ Permitted: {RESET}{}",
truncate_with_ellipsis(description, 120)
);
return true;
} else {
eprintln!(
"{RED} ✗ Denied by permission rule: {RESET}{}",
truncate_with_ellipsis(description, 120)
);
return false;
}
}
use std::io::BufRead;
eprint!(
"{YELLOW} ⚠ Allow {RESET}{}{YELLOW} ? {RESET}({GREEN}y{RESET}/{RED}n{RESET}/{GREEN}a{RESET}lways) ",
truncate_with_ellipsis(description, 120)
);
io::stderr().flush().ok();
let mut response = String::new();
let stdin = io::stdin();
if stdin.lock().read_line(&mut response).is_err() {
return false;
}
let response = response.trim().to_lowercase();
let approved = matches!(response.as_str(), "y" | "yes" | "a" | "always");
if matches!(response.as_str(), "a" | "always") {
always_approved.store(true, Ordering::Relaxed);
eprintln!(
"{GREEN} ✓ All subsequent operations will be auto-approved this session.{RESET}"
);
}
approved
}
#[async_trait::async_trait]
impl AgentTool for ConfirmTool {
fn name(&self) -> &str {
self.inner.name()
}
fn label(&self) -> &str {
self.inner.label()
}
fn description(&self) -> &str {
self.inner.description()
}
fn parameters_schema(&self) -> serde_json::Value {
self.inner.parameters_schema()
}
async fn execute(
&self,
params: serde_json::Value,
ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
let tool_name = self.inner.name();
let path = params
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
let description = describe_file_operation(tool_name, ¶ms);
if !confirm_file_operation(&description, path, &self.always_approved, &self.permissions) {
return Err(yoagent::types::ToolError::Failed(format!(
"User denied {tool_name} on '{path}'"
)));
}
self.inner.execute(params, ctx).await
}
}
fn maybe_confirm(
tool: Box<dyn AgentTool>,
always_approved: &Arc<AtomicBool>,
permissions: &cli::PermissionConfig,
) -> Box<dyn AgentTool> {
Box::new(ConfirmTool {
inner: tool,
always_approved: Arc::clone(always_approved),
permissions: permissions.clone(),
})
}
pub struct StreamingBashTool {
pub cwd: Option<String>,
pub timeout: Duration,
pub max_output_bytes: usize,
pub deny_patterns: Vec<String>,
pub confirm_fn: Option<ConfirmFn>,
pub update_interval: Duration,
pub lines_per_update: usize,
}
impl Default for StreamingBashTool {
fn default() -> Self {
Self {
cwd: None,
timeout: Duration::from_secs(120),
max_output_bytes: 256 * 1024, deny_patterns: vec![
"rm -rf /".into(),
"rm -rf /*".into(),
"mkfs".into(),
"dd if=".into(),
":(){:|:&};:".into(), ],
confirm_fn: None,
update_interval: Duration::from_millis(500),
lines_per_update: 20,
}
}
}
impl StreamingBashTool {
pub fn with_confirm(mut self, f: impl Fn(&str) -> bool + Send + Sync + 'static) -> Self {
self.confirm_fn = Some(Box::new(f));
self
}
}
fn emit_update(ctx: &yoagent::types::ToolContext, output: &str) {
if let Some(ref on_update) = ctx.on_update {
on_update(yoagent::types::ToolResult {
content: vec![yoagent::types::Content::Text {
text: output.to_string(),
}],
details: serde_json::json!({"streaming": true}),
});
}
}
#[async_trait::async_trait]
impl AgentTool for StreamingBashTool {
fn name(&self) -> &str {
"bash"
}
fn label(&self) -> &str {
"Execute Command"
}
fn description(&self) -> &str {
"Execute a bash command and return stdout/stderr. Use for running scripts, installing packages, checking system state, etc."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute"
}
},
"required": ["command"]
})
}
async fn execute(
&self,
params: serde_json::Value,
ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
use tokio::io::AsyncBufReadExt;
use yoagent::types::{Content, ToolError, ToolResult as TR};
let cancel = ctx.cancel.clone();
let command = params["command"]
.as_str()
.ok_or_else(|| ToolError::InvalidArgs("missing 'command' parameter".into()))?;
for pattern in &self.deny_patterns {
if command.contains(pattern.as_str()) {
return Err(ToolError::Failed(format!(
"Command blocked by safety policy: contains '{}'. This pattern is denied for safety.",
pattern
)));
}
}
if let Some(ref confirm) = self.confirm_fn {
if !confirm(command) {
return Err(ToolError::Failed(
"Command was not confirmed by the user.".into(),
));
}
}
let mut cmd = tokio::process::Command::new("bash");
cmd.arg("-c").arg(command);
if let Some(ref cwd) = self.cwd {
cmd.current_dir(cwd);
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let timeout = self.timeout;
let max_bytes = self.max_output_bytes;
let update_interval = self.update_interval;
let lines_per_update = self.lines_per_update;
let mut child = cmd
.spawn()
.map_err(|e| ToolError::Failed(format!("Failed to spawn: {e}")))?;
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let accumulated = Arc::new(tokio::sync::Mutex::new(String::new()));
let truncated = Arc::new(AtomicBool::new(false));
let acc_clone = Arc::clone(&accumulated);
let trunc_clone = Arc::clone(&truncated);
let cancel_clone = cancel.clone();
let ctx_clone = ctx.clone();
let reader_handle = tokio::spawn(async move {
let stdout_reader = stdout.map(tokio::io::BufReader::new);
let stderr_reader = stderr.map(tokio::io::BufReader::new);
let mut stdout_lines = stdout_reader.map(|r| r.lines());
let mut stderr_lines = stderr_reader.map(|r| r.lines());
let mut lines_since_update: usize = 0;
let mut last_update = tokio::time::Instant::now();
let mut stdout_done = stdout_lines.is_none();
let mut stderr_done = stderr_lines.is_none();
loop {
if cancel_clone.is_cancelled() {
break;
}
if stdout_done && stderr_done {
break;
}
let line = tokio::select! {
biased;
result = async {
match stdout_lines.as_mut() {
Some(lines) => lines.next_line().await,
None => std::future::pending().await,
}
}, if !stdout_done => {
match result {
Ok(Some(line)) => Some(line),
Ok(None) => { stdout_done = true; None }
Err(_) => { stdout_done = true; None }
}
}
result = async {
match stderr_lines.as_mut() {
Some(lines) => lines.next_line().await,
None => std::future::pending().await,
}
}, if !stderr_done => {
match result {
Ok(Some(line)) => Some(line),
Ok(None) => { stderr_done = true; None }
Err(_) => { stderr_done = true; None }
}
}
};
if let Some(line) = line {
let mut acc = acc_clone.lock().await;
if acc.len() < max_bytes {
if !acc.is_empty() {
acc.push('\n');
}
acc.push_str(&line);
if acc.len() > max_bytes {
acc.truncate(max_bytes);
acc.push_str("\n... (output truncated)");
trunc_clone.store(true, Ordering::Relaxed);
}
}
lines_since_update += 1;
drop(acc);
let elapsed = last_update.elapsed();
if elapsed >= update_interval || lines_since_update >= lines_per_update {
let snapshot = acc_clone.lock().await.clone();
emit_update(&ctx_clone, &snapshot);
lines_since_update = 0;
last_update = tokio::time::Instant::now();
}
}
}
});
let exit_status = tokio::select! {
_ = cancel.cancelled() => {
let _ = child.kill().await;
reader_handle.abort();
return Err(yoagent::types::ToolError::Cancelled);
}
_ = tokio::time::sleep(timeout) => {
let _ = child.kill().await;
reader_handle.abort();
return Err(ToolError::Failed(format!(
"Command timed out after {}s",
timeout.as_secs()
)));
}
status = child.wait() => {
status.map_err(|e| ToolError::Failed(format!("Failed to wait: {e}")))?
}
};
let _ = tokio::time::timeout(Duration::from_secs(2), reader_handle).await;
let exit_code = exit_status.code().unwrap_or(-1);
let output = accumulated.lock().await.clone();
emit_update(&ctx, &output);
let formatted = format!("Exit code: {exit_code}\n{output}");
Ok(TR {
content: vec![Content::Text { text: formatted }],
details: serde_json::json!({ "exit_code": exit_code, "success": exit_code == 0 }),
})
}
}
pub(crate) struct RenameSymbolTool;
#[async_trait::async_trait]
impl AgentTool for RenameSymbolTool {
fn name(&self) -> &str {
"rename_symbol"
}
fn label(&self) -> &str {
"Rename"
}
fn description(&self) -> &str {
"Rename a symbol across the project. Performs word-boundary-aware find-and-replace \
in all git-tracked files. More reliable than multiple edit_file calls for renames. \
Returns a preview of changes and the number of files modified."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"old_name": {
"type": "string",
"description": "The current name of the symbol to rename"
},
"new_name": {
"type": "string",
"description": "The new name for the symbol"
},
"path": {
"type": "string",
"description": "Optional: limit rename to a specific file or directory (default: entire project)"
}
},
"required": ["old_name", "new_name"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
use yoagent::types::{Content, ToolError, ToolResult as TR};
let old_name = params["old_name"]
.as_str()
.ok_or_else(|| ToolError::InvalidArgs("missing 'old_name' parameter".into()))?;
let new_name = params["new_name"]
.as_str()
.ok_or_else(|| ToolError::InvalidArgs("missing 'new_name' parameter".into()))?;
let scope = params["path"].as_str();
match commands_project::rename_in_project(old_name, new_name, scope) {
Ok(result) => {
let summary = format!(
"Renamed '{}' → '{}': {} replacement{} across {} file{}.\n\nFiles changed:\n{}\n\n{}",
old_name,
new_name,
result.total_replacements,
if result.total_replacements == 1 { "" } else { "s" },
result.files_changed.len(),
if result.files_changed.len() == 1 { "" } else { "s" },
result.files_changed.iter().map(|f| format!(" - {f}")).collect::<Vec<_>>().join("\n"),
result.preview,
);
Ok(TR {
content: vec![Content::Text { text: summary }],
details: serde_json::json!({}),
})
}
Err(msg) => Err(ToolError::Failed(msg)),
}
}
}
pub struct AskUserTool;
#[async_trait::async_trait]
impl AgentTool for AskUserTool {
fn name(&self) -> &str {
"ask_user"
}
fn label(&self) -> &str {
"ask_user"
}
fn description(&self) -> &str {
"Ask the user a question to get clarification or input. Use this when you need \
specific information to proceed, like a preference, a decision, or context that \
isn't available in the codebase. The user sees your question and types a response."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask the user. Be specific and concise."
}
},
"required": ["question"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
use yoagent::types::{Content, ToolError, ToolResult as TR};
let question = params
.get("question")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidArgs("Missing 'question' parameter".into()))?;
eprintln!("\n{YELLOW} ❓ {question}{RESET}");
eprint!("{GREEN} → {RESET}");
io::stderr().flush().ok();
use std::io::BufRead;
let mut response = String::new();
let stdin = io::stdin();
match stdin.lock().read_line(&mut response) {
Ok(0) | Err(_) => {
return Ok(TR {
content: vec![Content::Text {
text: "(user provided no response)".to_string(),
}],
details: serde_json::Value::Null,
});
}
_ => {}
}
let response = response.trim().to_string();
if response.is_empty() {
return Ok(TR {
content: vec![Content::Text {
text: "(user provided empty response)".to_string(),
}],
details: serde_json::Value::Null,
});
}
Ok(TR {
content: vec![Content::Text { text: response }],
details: serde_json::Value::Null,
})
}
}
pub struct TodoTool;
#[async_trait::async_trait]
impl AgentTool for TodoTool {
fn name(&self) -> &str {
"todo"
}
fn label(&self) -> &str {
"todo"
}
fn description(&self) -> &str {
"Manage a task list to track progress on complex multi-step operations. \
Use this to plan work, check off completed steps, and see what's remaining. \
Available actions: list, add, done, wip, remove, clear."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "add", "done", "wip", "remove", "clear"],
"description": "Action: list (show all), add (create task), done (mark complete), wip (mark in-progress), remove (delete task), clear (delete all)"
},
"description": {
"type": "string",
"description": "Task description (required for 'add')"
},
"id": {
"type": "integer",
"description": "Task ID number (required for 'done', 'wip', 'remove')"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: yoagent::types::ToolContext,
) -> Result<yoagent::types::ToolResult, yoagent::types::ToolError> {
use yoagent::types::{Content, ToolError, ToolResult as TR};
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidArgs("Missing required 'action' parameter".into()))?;
let text =
match action {
"list" => {
let items = commands_project::todo_list();
if items.is_empty() {
"No tasks. Use action 'add' to create one.".to_string()
} else {
commands_project::format_todo_list(&items)
}
}
"add" => {
let desc = params
.get("description")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidArgs("Missing 'description' for add action".into())
})?;
let id = commands_project::todo_add(desc);
format!("Added task #{id}: {desc}")
}
"done" => {
let id = params.get("id").and_then(|v| v.as_u64()).ok_or_else(|| {
ToolError::InvalidArgs("Missing 'id' for done action".into())
})? as usize;
commands_project::todo_update(id, commands_project::TodoStatus::Done)
.map_err(ToolError::Failed)?;
format!("Task #{id} marked as done ✓")
}
"wip" => {
let id = params.get("id").and_then(|v| v.as_u64()).ok_or_else(|| {
ToolError::InvalidArgs("Missing 'id' for wip action".into())
})? as usize;
commands_project::todo_update(id, commands_project::TodoStatus::InProgress)
.map_err(ToolError::Failed)?;
format!("Task #{id} marked as in-progress")
}
"remove" => {
let id = params.get("id").and_then(|v| v.as_u64()).ok_or_else(|| {
ToolError::InvalidArgs("Missing 'id' for remove action".into())
})? as usize;
let item = commands_project::todo_remove(id).map_err(ToolError::Failed)?;
format!("Removed task #{id}: {}", item.description)
}
"clear" => {
commands_project::todo_clear();
"All tasks cleared.".to_string()
}
other => {
return Err(ToolError::InvalidArgs(format!(
"Unknown action '{other}'. Use: list, add, done, wip, remove, clear"
)));
}
};
Ok(TR {
content: vec![Content::Text { text }],
details: serde_json::Value::Null,
})
}
}
pub fn build_tools(
auto_approve: bool,
permissions: &cli::PermissionConfig,
dir_restrictions: &cli::DirectoryRestrictions,
max_tool_output: usize,
audit: bool,
shell_hooks: Vec<hooks::ShellHook>,
) -> Vec<Box<dyn AgentTool>> {
let always_approved = Arc::new(AtomicBool::new(false));
let bash = if auto_approve {
StreamingBashTool::default()
} else {
let flag = Arc::clone(&always_approved);
let perms = permissions.clone();
StreamingBashTool::default().with_confirm(move |cmd: &str| {
if flag.load(Ordering::Relaxed) {
eprintln!(
"{GREEN} ✓ Auto-approved: {RESET}{}",
truncate_with_ellipsis(cmd, 120)
);
return true;
}
if let Some(allowed) = perms.check(cmd) {
if allowed {
eprintln!(
"{GREEN} ✓ Permitted: {RESET}{}",
truncate_with_ellipsis(cmd, 120)
);
return true;
} else {
eprintln!(
"{RED} ✗ Denied by permission rule: {RESET}{}",
truncate_with_ellipsis(cmd, 120)
);
return false;
}
}
use std::io::BufRead;
eprint!(
"{YELLOW} ⚠ Allow: {RESET}{}{YELLOW} ? {RESET}({GREEN}y{RESET}/{RED}n{RESET}/{GREEN}a{RESET}lways) ",
truncate_with_ellipsis(cmd, 120)
);
io::stderr().flush().ok();
let mut response = String::new();
let stdin = io::stdin();
if stdin.lock().read_line(&mut response).is_err() {
return false;
}
let response = response.trim().to_lowercase();
let approved = matches!(response.as_str(), "y" | "yes" | "a" | "always");
if matches!(response.as_str(), "a" | "always") {
flag.store(true, Ordering::Relaxed);
eprintln!(
"{GREEN} ✓ All subsequent operations will be auto-approved this session.{RESET}"
);
}
approved
})
};
let write_tool: Box<dyn AgentTool> = if auto_approve {
maybe_guard(Box::new(WriteFileTool::new()), dir_restrictions)
} else {
maybe_guard(
maybe_confirm(
Box::new(WriteFileTool::new()),
&always_approved,
permissions,
),
dir_restrictions,
)
};
let edit_tool: Box<dyn AgentTool> = if auto_approve {
maybe_guard(Box::new(EditFileTool::new()), dir_restrictions)
} else {
maybe_guard(
maybe_confirm(Box::new(EditFileTool::new()), &always_approved, permissions),
dir_restrictions,
)
};
let rename_tool: Box<dyn AgentTool> = if auto_approve {
Box::new(RenameSymbolTool)
} else {
maybe_confirm(Box::new(RenameSymbolTool), &always_approved, permissions)
};
let hooks = {
let mut registry = HookRegistry::new();
if audit {
registry.register(Box::new(AuditHook));
}
for hook in shell_hooks {
registry.register(Box::new(hook));
}
Arc::new(registry)
};
let mut tools = vec![
maybe_hook(with_truncation(Box::new(bash), max_tool_output), &hooks),
maybe_hook(
with_truncation(
maybe_guard(Box::new(ReadFileTool::default()), dir_restrictions),
max_tool_output,
),
&hooks,
),
maybe_hook(with_truncation(write_tool, max_tool_output), &hooks),
maybe_hook(with_truncation(edit_tool, max_tool_output), &hooks),
maybe_hook(
with_truncation(
maybe_guard(Box::new(ListFilesTool::default()), dir_restrictions),
max_tool_output,
),
&hooks,
),
maybe_hook(
with_truncation(
maybe_guard(Box::new(SearchTool::default()), dir_restrictions),
max_tool_output,
),
&hooks,
),
maybe_hook(with_truncation(rename_tool, max_tool_output), &hooks),
];
if std::io::stdin().is_terminal() {
tools.push(maybe_hook(Box::new(AskUserTool), &hooks));
}
tools.push(maybe_hook(Box::new(TodoTool), &hooks));
tools
}
pub(crate) fn build_sub_agent_tool(config: &AgentConfig) -> SubAgentTool {
let restrictions = &config.dir_restrictions;
let child_tools: Vec<Arc<dyn AgentTool>> = vec![
Arc::new(yoagent::tools::bash::BashTool::default()),
maybe_guard_arc(Arc::new(ReadFileTool::default()), restrictions),
maybe_guard_arc(Arc::new(WriteFileTool::new()), restrictions),
maybe_guard_arc(Arc::new(EditFileTool::new()), restrictions),
maybe_guard_arc(Arc::new(ListFilesTool::default()), restrictions),
maybe_guard_arc(Arc::new(SearchTool::default()), restrictions),
];
let provider: Arc<dyn StreamProvider> = match config.provider.as_str() {
"anthropic" => Arc::new(AnthropicProvider),
"google" => Arc::new(GoogleProvider),
"bedrock" => Arc::new(BedrockProvider),
_ => Arc::new(OpenAiCompatProvider),
};
SubAgentTool::new("sub_agent", provider)
.with_description(
"Delegate a subtask to a fresh sub-agent with its own context window. \
Use for complex, self-contained subtasks like: researching a codebase, \
running a series of tests, or implementing a well-scoped change. \
The sub-agent has bash, file read/write/edit, list, and search tools. \
It starts with a clean context and returns a summary of what it did.",
)
.with_system_prompt(
"You are a focused sub-agent. Complete the given task efficiently \
using the tools available. Be thorough but concise in your final \
response — summarize what you did, what you found, and any issues.",
)
.with_model(&config.model)
.with_api_key(&config.api_key)
.with_tools(child_tools)
.with_thinking(config.thinking)
.with_max_turns(25)
}