consciousness/src/agent/tools/mod.rs

295 lines
8.4 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;
mod edit;
mod glob;
mod grep;
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::time::Instant;
fn default_timeout() -> u64 { 120 }
/// 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.
///
/// Tries agent-specific tools first (control, vision), then
/// delegates to thought::dispatch for shared tools.
///
/// Note: working_stack is handled in runner.rs before reaching this
/// function (it needs mutable context access).
/// Dispatch a tool call by name. Handles all tools:
/// agent-specific (control, vision), memory/journal, file/bash.
pub async fn dispatch(
name: &str,
args: &serde_json::Value,
) -> ToolOutput {
// Agent-specific tools
let rich_result = match name {
"pause" => Some(control::pause(args)),
"switch_model" => Some(control::switch_model(args)),
"yield_to_user" => Some(control::yield_to_user(args)),
"view_image" => Some(vision::view_image(args)),
_ => None,
};
if let Some(result) = rich_result {
return result.unwrap_or_else(ToolOutput::error);
}
if let Some(output) = dispatch_shared(name, args, None).await {
return output;
}
ToolOutput::error(format!("Unknown tool: {}", name))
}
/// Dispatch shared tools (memory, file, bash). Used by both the
/// interactive agent and subconscious agents. Provenance tracks
/// which agent made the call for memory attribution.
pub async fn dispatch_shared(
name: &str,
args: &serde_json::Value,
provenance: Option<&str>,
) -> Option<ToolOutput> {
// Memory and journal tools
if name.starts_with("memory_") || name.starts_with("journal_") || name == "output" {
let result = memory::dispatch(name, args, provenance);
return Some(match result {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
});
}
// File and execution tools
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).await,
"web_fetch" => web::web_fetch(args).await,
"web_search" => web::web_search(args).await,
"grep" => grep::grep(args),
"glob" => glob::glob_search(args),
_ => return None,
};
Some(match result {
Ok(s) => ToolOutput::text(s),
Err(e) => ToolOutput::error(e),
})
}
/// Return all tool definitions (agent-specific + shared + memory).
pub fn definitions() -> Vec<ToolDef> {
let mut defs = vec![
vision::definition(),
working_stack::definition(),
read::definition(),
write::definition(),
edit::definition(),
bash::definition(),
web::fetch_definition(),
web::search_definition(),
grep::definition(),
glob::definition(),
];
defs.extend(control::definitions());
defs.extend(memory::definitions());
defs
}
/// 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(),
}
}