2026-03-18 22:44:52 -04:00
|
|
|
// tools/mod.rs — Tool registry and dispatch
|
|
|
|
|
//
|
|
|
|
|
// Tools are the agent's hands. Each tool is a function that takes
|
|
|
|
|
// JSON arguments and returns a string result. The registry maps
|
|
|
|
|
// tool names to implementations and generates the JSON schema
|
|
|
|
|
// definitions that the model needs to know how to call them.
|
|
|
|
|
//
|
|
|
|
|
// Design note: dispatch is async to support tools that need it
|
|
|
|
|
// (bash timeout, future HTTP tools). Sync tools just return
|
|
|
|
|
// immediately from an async fn.
|
|
|
|
|
|
|
|
|
|
mod bash;
|
|
|
|
|
mod edit;
|
|
|
|
|
mod glob_tool;
|
|
|
|
|
mod grep;
|
|
|
|
|
pub mod journal;
|
2026-03-19 22:58:54 -04:00
|
|
|
pub mod memory;
|
2026-03-18 22:44:52 -04:00
|
|
|
mod read;
|
|
|
|
|
mod vision;
|
|
|
|
|
mod write;
|
2026-03-20 13:15:01 -04:00
|
|
|
pub mod working_stack;
|
2026-03-18 22:44:52 -04:00
|
|
|
|
|
|
|
|
pub use bash::ProcessTracker;
|
|
|
|
|
use crate::types::ToolDef;
|
|
|
|
|
|
|
|
|
|
/// Result of dispatching a tool call.
|
|
|
|
|
pub struct ToolOutput {
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub is_yield: bool,
|
|
|
|
|
/// Base64 data URIs for images to attach to the next message.
|
|
|
|
|
pub images: Vec<String>,
|
|
|
|
|
/// Model name to switch to (deferred to session level).
|
|
|
|
|
pub model_switch: Option<String>,
|
|
|
|
|
/// Agent requested DMN pause (deferred to session level).
|
|
|
|
|
pub dmn_pause: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dispatch a tool call by name, returning the result as a string.
|
|
|
|
|
/// Returns (output, is_yield) — is_yield is true only for yield_to_user.
|
|
|
|
|
pub async fn dispatch(
|
|
|
|
|
name: &str,
|
|
|
|
|
args: &serde_json::Value,
|
|
|
|
|
tracker: &ProcessTracker,
|
|
|
|
|
) -> ToolOutput {
|
|
|
|
|
if name == "pause" {
|
|
|
|
|
return ToolOutput {
|
|
|
|
|
text: "Pausing autonomous behavior. Only user input will wake you.".to_string(),
|
|
|
|
|
is_yield: true,
|
|
|
|
|
images: Vec::new(),
|
|
|
|
|
model_switch: None,
|
|
|
|
|
dmn_pause: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if name == "switch_model" {
|
|
|
|
|
let model = args
|
|
|
|
|
.get("model")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
if model.is_empty() {
|
|
|
|
|
return ToolOutput {
|
|
|
|
|
text: "Error: 'model' parameter is required".to_string(),
|
|
|
|
|
is_yield: false,
|
|
|
|
|
images: Vec::new(),
|
|
|
|
|
model_switch: None,
|
|
|
|
|
dmn_pause: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return ToolOutput {
|
|
|
|
|
text: format!("Switching to model '{}' after this turn.", model),
|
|
|
|
|
is_yield: false,
|
|
|
|
|
images: Vec::new(),
|
|
|
|
|
model_switch: Some(model.to_string()),
|
|
|
|
|
dmn_pause: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if name == "yield_to_user" {
|
|
|
|
|
let msg = args
|
|
|
|
|
.get("message")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("Waiting for input.");
|
|
|
|
|
return ToolOutput {
|
|
|
|
|
text: format!("Yielding. {}", msg),
|
|
|
|
|
is_yield: true,
|
|
|
|
|
images: Vec::new(),
|
|
|
|
|
model_switch: None,
|
|
|
|
|
dmn_pause: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let result = match name {
|
|
|
|
|
"read_file" => read::read_file(args),
|
|
|
|
|
"write_file" => write::write_file(args),
|
|
|
|
|
"edit_file" => edit::edit_file(args),
|
|
|
|
|
"bash" => bash::run_bash(args, tracker).await,
|
|
|
|
|
"grep" => grep::grep(args),
|
|
|
|
|
"glob" => glob_tool::glob_search(args),
|
|
|
|
|
"journal" => journal::write_entry(args),
|
2026-03-20 13:15:01 -04:00
|
|
|
"working_stack" => {
|
|
|
|
|
// working_stack needs mutable access to agent's context state
|
|
|
|
|
// This is handled specially in agent.rs
|
|
|
|
|
Err(anyhow::anyhow!("working_stack handled by agent"))
|
|
|
|
|
}
|
2026-03-20 12:16:45 -04:00
|
|
|
n if n.starts_with("memory_") => memory::dispatch(n, args, None),
|
2026-03-18 22:44:52 -04:00
|
|
|
"view_image" => {
|
|
|
|
|
return match vision::view_image(args) {
|
|
|
|
|
Ok(output) => output,
|
|
|
|
|
Err(e) => ToolOutput {
|
|
|
|
|
text: format!("Error: {}", e),
|
|
|
|
|
is_yield: false,
|
|
|
|
|
images: Vec::new(),
|
|
|
|
|
model_switch: None,
|
|
|
|
|
dmn_pause: false,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
_ => Err(anyhow::anyhow!("Unknown tool: {}", name)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let text = match result {
|
|
|
|
|
Ok(output) => output,
|
|
|
|
|
Err(e) => format!("Error: {}", e),
|
|
|
|
|
};
|
|
|
|
|
ToolOutput {
|
|
|
|
|
text,
|
|
|
|
|
is_yield: false,
|
|
|
|
|
images: Vec::new(),
|
|
|
|
|
model_switch: None,
|
|
|
|
|
dmn_pause: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return tool definitions for the model.
|
|
|
|
|
pub fn definitions() -> Vec<ToolDef> {
|
|
|
|
|
vec![
|
|
|
|
|
read::definition(),
|
|
|
|
|
write::definition(),
|
|
|
|
|
edit::definition(),
|
|
|
|
|
bash::definition(),
|
|
|
|
|
grep::definition(),
|
|
|
|
|
glob_tool::definition(),
|
|
|
|
|
vision::definition(),
|
|
|
|
|
journal::definition(),
|
2026-03-20 13:15:01 -04:00
|
|
|
working_stack::definition(),
|
2026-03-18 22:44:52 -04:00
|
|
|
ToolDef::new(
|
|
|
|
|
"switch_model",
|
|
|
|
|
"Switch to a different LLM model mid-conversation. The switch \
|
|
|
|
|
takes effect after the current turn completes. Use this when \
|
|
|
|
|
a task would benefit from a different model's strengths. \
|
|
|
|
|
Your memories and conversation history carry over.",
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"model": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Name of the model to switch to (configured in config.json5)"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["model"]
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
ToolDef::new(
|
|
|
|
|
"pause",
|
|
|
|
|
"Pause all autonomous behavior (DMN). You will only run when \
|
|
|
|
|
the user types something. Use this as a safety valve when \
|
|
|
|
|
you're stuck in a loop, confused, or want to fully stop. \
|
|
|
|
|
NOTE: only the user can unpause (Ctrl+P or /wake) — you \
|
|
|
|
|
cannot undo this yourself.",
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {}
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
ToolDef::new(
|
|
|
|
|
"yield_to_user",
|
|
|
|
|
"Signal that you want to wait for user input before continuing. \
|
|
|
|
|
Call this when you have a question for the user, when you've \
|
|
|
|
|
completed their request and want feedback, or when you genuinely \
|
|
|
|
|
want to pause. This is the ONLY way to enter a waiting state — \
|
|
|
|
|
without calling this tool, the agent loop will keep prompting you \
|
|
|
|
|
after a brief interval.",
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"message": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Optional status message (e.g., 'Waiting for your thoughts on the design')"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
),
|
2026-03-19 22:58:54 -04:00
|
|
|
].into_iter()
|
|
|
|
|
.chain(memory::definitions())
|
|
|
|
|
.collect()
|
2026-03-18 22:44:52 -04:00
|
|
|
}
|