consciousness/src/agent/tools/mod.rs

325 lines
11 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 serde::{Serialize, Deserialize};
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.
pub struct Tool {
pub def: ToolDef,
pub handler: ToolHandler,
}
/// Function call within a tool call — name + JSON arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
/// Function definition for tool schema.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionDef {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
/// Partial function call within a streaming delta.
#[derive(Debug, Deserialize)]
pub struct FunctionCallDelta {
pub name: Option<String>,
pub arguments: Option<String>,
}
/// Tool definition sent to the model.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDef,
}
impl ToolDef {
pub fn new(name: &str, description: &str, parameters: serde_json::Value) -> Self {
Self {
tool_type: "function".to_string(),
function: FunctionDef {
name: name.to_string(),
description: description.to_string(),
parameters,
},
}
}
}
/// A tool call requested by the model.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
/// A partial tool call within a streaming delta. The first chunk for a
/// given tool call carries the id and function name; subsequent chunks
/// carry argument fragments.
#[derive(Debug, Deserialize)]
pub struct ToolCallDelta {
pub index: usize,
pub id: Option<String>,
#[serde(rename = "type")]
pub call_type: Option<String>,
pub function: Option<FunctionCallDelta>,
}
/// 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,
}
impl ToolOutput {
pub fn error(e: impl std::fmt::Display) -> Self {
Self {
text: format!("Error: {}", e),
is_yield: false,
images: Vec::new(),
model_switch: None,
dmn_pause: false,
}
}
pub fn text(s: String) -> Self {
Self {
text: s,
is_yield: false,
images: Vec::new(),
model_switch: None,
dmn_pause: false,
}
}
}
/// 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, ToolOutput)>,
}
/// 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.
pub async fn dispatch(
name: &str,
args: &serde_json::Value,
) -> ToolOutput {
dispatch_with_agent(name, args, None).await
}
/// Dispatch a tool call with optional agent context.
pub async fn dispatch_with_agent(
name: &str,
args: &serde_json::Value,
agent: Option<std::sync::Arc<tokio::sync::Mutex<super::Agent>>>,
) -> ToolOutput {
for tool in tools() {
if tool.def.function.name == name {
return match (tool.handler)(agent, args.clone()).await {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
};
}
}
ToolOutput::error(format!("Unknown tool: {}", name))
}
/// Dispatch shared tools — used by subconscious agents.
pub async fn dispatch_shared(
name: &str,
args: &serde_json::Value,
_provenance: Option<&str>,
) -> Option<ToolOutput> {
for tool in tools() {
if tool.def.function.name == name {
return Some(match (tool.handler)(None, args.clone()).await {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
});
}
}
None
}
/// Return all registered tools with definitions + handlers.
pub fn tools() -> Vec<Tool> {
vec![
// File tools
Tool { def: read::definition(), handler: |_a, v| Box::pin(async move { read::read_file(&v) }) },
Tool { def: write::definition(), handler: |_a, v| Box::pin(async move { write::write_file(&v) }) },
Tool { def: edit::definition(), handler: |_a, v| Box::pin(async move { edit::edit_file(&v) }) },
Tool { def: grep::definition(), handler: |_a, v| Box::pin(async move { grep::grep(&v) }) },
Tool { def: glob::definition(), handler: |_a, v| Box::pin(async move { glob::glob_search(&v) }) },
// Execution tools
Tool { def: bash::definition(), handler: |_a, v| Box::pin(async move { bash::run_bash(&v).await }) },
Tool { def: web::fetch_definition(), handler: |_a, v| Box::pin(async move { web::web_fetch(&v).await }) },
Tool { def: web::search_definition(), handler: |_a, v| Box::pin(async move { web::web_search(&v).await }) },
// Vision
Tool { def: vision::definition(), handler: |_a, v| Box::pin(async move { vision::view_image_text(&v) }) },
// Memory tools
Tool { def: memory::render_def(), handler: |_a, v| Box::pin(async move { memory::render(&v) }) },
Tool { def: memory::write_def(), handler: |_a, v| Box::pin(async move { memory::write(&v) }) },
Tool { def: memory::search_def(), handler: |_a, v| Box::pin(async move { memory::search(&v) }) },
Tool { def: memory::links_def(), handler: |_a, v| Box::pin(async move { memory::links(&v) }) },
Tool { def: memory::link_set_def(), handler: |_a, v| Box::pin(async move { memory::link_set(&v) }) },
Tool { def: memory::link_add_def(), handler: |_a, v| Box::pin(async move { memory::link_add(&v) }) },
Tool { def: memory::used_def(), handler: |_a, v| Box::pin(async move { memory::used(&v) }) },
Tool { def: memory::weight_set_def(), handler: |_a, v| Box::pin(async move { memory::weight_set(&v) }) },
Tool { def: memory::rename_def(), handler: |_a, v| Box::pin(async move { memory::rename(&v) }) },
Tool { def: memory::supersede_def(), handler: |_a, v| Box::pin(async move { memory::supersede(&v) }) },
Tool { def: memory::query_def(), handler: |_a, v| Box::pin(async move { memory::query(&v) }) },
Tool { def: memory::output_def(), handler: |_a, v| Box::pin(async move { memory::output(&v) }) },
// Channel tools
Tool { def: channels::definitions()[0].clone(), handler: |_a, v| Box::pin(async move { channels::channel_list().await }) },
Tool { def: channels::definitions()[1].clone(), handler: |_a, v| Box::pin(async move { channels::channel_recv(&v).await }) },
Tool { def: channels::definitions()[2].clone(), handler: |_a, v| Box::pin(async move { channels::channel_send(&v).await }) },
Tool { def: channels::definitions()[3].clone(), handler: |_a, v| Box::pin(async move { channels::channel_notifications().await }) },
// Control tools
Tool { def: control::definitions()[0].clone(),
handler: |_a, _v| Box::pin(async { Ok("Pausing autonomous behavior. Only user input will wake you.".into()) }) },
Tool { def: control::definitions()[1].clone(),
handler: |_a, v| Box::pin(async move {
let model = v.get("model").and_then(|v| v.as_str()).unwrap_or("");
Ok(format!("Switching to model: {}", model))
}) },
Tool { def: control::definitions()[2].clone(),
handler: |_a, v| Box::pin(async move {
let msg = v.get("message").and_then(|v| v.as_str()).unwrap_or("(yielding to user)");
Ok(msg.to_string())
}) },
]
}
/// Return all tool definitions (extracted from tools()).
pub fn definitions() -> Vec<ToolDef> {
tools().into_iter().map(|t| t.def).collect()
}
/// Return memory + journal tool definitions only.
pub fn memory_and_journal_definitions() -> Vec<ToolDef> {
let mut defs = memory::definitions();
defs.extend(memory::journal_definitions());
defs
}
/// 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(),
}
}