consciousness/poc-agent/src/tools/mod.rs
Kent Overstreet 9517b1b310 refactor: move working_stack tool to tools/working_stack.rs
The working_stack tool was defined in tools/mod.rs but implemented
in agent.rs as Agent::handle_working_stack(). This orphaned the tool
from the rest of the tool infrastructure.

Move the implementation to tools/working_stack.rs so it follows the
same pattern as other tools. The tool still needs special handling
in agent.rs because it requires mutable access to context state,
but the implementation is now in the right place.

Changes:
- Created tools/working_stack.rs with handle() and format_stack()
- Updated tools/mod.rs to use working_stack::definition()
- Removed handle_working_stack() and format_stack() from Agent
- Agent now calls tools::working_stack::handle() directly
2026-03-20 13:15:01 -04:00

196 lines
6.4 KiB
Rust

// 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;
pub mod memory;
mod read;
mod vision;
mod write;
pub mod working_stack;
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),
"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"))
}
n if n.starts_with("memory_") => memory::dispatch(n, args, None),
"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(),
working_stack::definition(),
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')"
}
}
}),
),
].into_iter()
.chain(memory::definitions())
.collect()
}