consciousness/src/agent/tools/mod.rs

193 lines
5.7 KiB
Rust
Raw Normal View History

// tools/mod.rs — Agent-specific tool dispatch
//
// Shared tools (memory, files, bash, journal) live in thought/.
// This module handles agent-specific tools (control, vision,
// working_stack) and delegates everything else to thought::dispatch.
// Core tools
mod bash;
pub mod channels;
mod edit;
mod glob;
mod grep;
pub mod memory;
mod read;
mod web;
mod write;
// Agent-specific tools
mod control;
mod vision;
pub mod working_stack;
use std::future::Future;
use std::pin::Pin;
use std::time::Instant;
fn default_timeout() -> u64 { 120 }
/// Async tool handler function.
/// Agent is None when called from contexts without an agent (MCP server, subconscious).
pub type ToolHandler = fn(
Option<std::sync::Arc<tokio::sync::Mutex<super::Agent>>>,
serde_json::Value,
) -> Pin<Box<dyn Future<Output = anyhow::Result<String>> + Send>>;
/// A tool with its definition and handler — single source of truth.
/// Strings are static — the tool list JSON can be built without
/// serialization by interpolating these directly.
#[derive(Clone, Copy)]
pub struct Tool {
pub name: &'static str,
pub description: &'static str,
pub parameters_json: &'static str,
pub handler: ToolHandler,
}
impl Tool {
/// Build the JSON for this tool's definition (for the API tools array).
pub fn to_json(&self) -> String {
format!(
r#"{{"type":"function","function":{{"name":"{}","description":"{}","parameters":{}}}}}"#,
self.name,
self.description.replace('"', r#"\""#),
self.parameters_json,
)
}
}
// Re-export API wire types used by the agent turn loop
use super::api::types::ToolCall;
/// A tool call in flight — metadata for TUI + JoinHandle for
/// result collection and cancellation.
pub struct ActiveToolCall {
pub id: String,
pub name: String,
pub detail: String,
pub started: Instant,
pub background: bool,
pub handle: tokio::task::JoinHandle<(ToolCall, String)>,
}
/// Truncate output if it exceeds max length, appending a truncation notice.
pub fn truncate_output(mut s: String, max: usize) -> String {
if s.len() > max {
s.truncate(max);
s.push_str("\n... (output truncated)");
}
s
}
/// Dispatch a tool call by name through the registry.
/// Dispatch a tool call by name. Returns the result text,
/// or an error string prefixed with "Error: ".
pub async fn dispatch(
name: &str,
args: &serde_json::Value,
) -> String {
dispatch_with_agent(name, args, None).await
}
/// Dispatch a tool call with optional agent context.
/// If agent is provided, uses the agent's tool list.
pub async fn dispatch_with_agent(
name: &str,
args: &serde_json::Value,
agent: Option<std::sync::Arc<tokio::sync::Mutex<super::Agent>>>,
) -> String {
// Look up in agent's tools if available, otherwise global
let tool = if let Some(ref a) = agent {
let guard = a.lock().await;
guard.tools.iter().find(|t| t.name == name).copied()
} else {
None
};
let tool = tool.or_else(|| tools().into_iter().find(|t| t.name == name));
match tool {
Some(t) => (t.handler)(agent, args.clone()).await
.unwrap_or_else(|e| format!("Error: {}", e)),
None => format!("Error: Unknown tool: {}", name),
}
}
/// Return all registered tools with definitions + handlers.
pub fn tools() -> Vec<Tool> {
let mut all = vec![
read::tool(), write::tool(), edit::tool(),
grep::tool(), glob::tool(), bash::tool(),
vision::tool(),
working_stack::tool(),
];
all.extend(web::tools());
all.extend(memory::memory_tools());
all.extend(memory::journal_tools());
all.extend(channels::tools());
all.extend(control::tools());
all
}
/// Memory + journal tools only — for subconscious agents.
pub fn memory_and_journal_tools() -> Vec<Tool> {
let mut all = memory::memory_tools().to_vec();
all.extend(memory::journal_tools());
all
}
/// Create a short summary of tool args for the tools pane header.
pub fn summarize_args(tool_name: &str, args: &serde_json::Value) -> String {
match tool_name {
"read_file" | "write_file" | "edit_file" => args["file_path"]
.as_str()
.unwrap_or("")
.to_string(),
"bash" => {
let cmd = args["command"].as_str().unwrap_or("");
if cmd.len() > 60 {
let end = cmd.char_indices()
.map(|(i, _)| i)
.take_while(|&i| i <= 60)
.last()
.unwrap_or(0);
format!("{}...", &cmd[..end])
} else {
cmd.to_string()
}
}
"grep" => {
let pattern = args["pattern"].as_str().unwrap_or("");
let path = args["path"].as_str().unwrap_or(".");
format!("{} in {}", pattern, path)
}
"glob" => args["pattern"]
.as_str()
.unwrap_or("")
.to_string(),
"view_image" => {
if let Some(pane) = args["pane_id"].as_str() {
format!("pane {}", pane)
} else {
args["file_path"].as_str().unwrap_or("").to_string()
}
}
"journal" => {
let entry = args["entry"].as_str().unwrap_or("");
if entry.len() > 60 {
format!("{}...", &entry[..60])
} else {
entry.to_string()
}
}
"yield_to_user" => args["message"]
.as_str()
.unwrap_or("")
.to_string(),
"switch_model" => args["model"]
.as_str()
.unwrap_or("")
.to_string(),
"pause" => String::new(),
_ => String::new(),
}
}