fixup: consolidate tool types, fix build after reorganization

Move FunctionCall, FunctionDef, FunctionCallDelta from user/types
to agent/tools. Re-export from user/types for backward compat.
Merge duplicate dispatch functions in tools/mod.rs into dispatch
(agent-specific) + dispatch_shared (with provenance). Fix orphaned
derive, missing imports, runner→agent module path.

Co-Developed-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
ProofOfConcept 2026-04-03 23:21:16 -04:00
parent 474b66c834
commit 17a018ff12
9 changed files with 1356 additions and 1380 deletions

View file

@ -18,8 +18,161 @@ mod control;
mod vision;
pub mod working_stack;
// Re-export
pub use crate::agent::{ToolDef, ToolOutput, ProcessTracker, truncate_output};
use serde::{Serialize, Deserialize};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::Mutex;
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,
}
/// 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,
}
}
}
/// Info about a running child process, visible to the TUI.
#[derive(Debug, Clone)]
pub struct ProcessInfo {
pub pid: u32,
pub command: String,
pub started: Instant,
}
/// Shared tracker for running child processes. Allows the TUI to
/// display what's running and kill processes by PID.
#[derive(Debug, Clone, Default)]
pub struct ProcessTracker {
inner: Arc<Mutex<Vec<ProcessInfo>>>,
}
impl ProcessTracker {
pub fn new() -> Self {
Self::default()
}
async fn register(&self, pid: u32, command: &str) {
self.inner.lock().await.push(ProcessInfo {
pid,
command: if command.len() > 120 {
format!("{}...", &command[..120])
} else {
command.to_string()
},
started: Instant::now(),
});
}
async fn unregister(&self, pid: u32) {
self.inner.lock().await.retain(|p| p.pid != pid);
}
/// Snapshot of currently running processes.
pub async fn list(&self) -> Vec<ProcessInfo> {
self.inner.lock().await.clone()
}
/// Kill a process by PID. Returns true if the signal was sent.
pub async fn kill(&self, pid: u32) -> bool {
// SIGTERM the process group (negative PID kills the group)
let ret = unsafe { libc::kill(-(pid as i32), libc::SIGTERM) };
if ret != 0 {
// Try just the process
unsafe { libc::kill(pid as i32, libc::SIGTERM) };
}
// Don't unregister — let the normal exit path do that
// so the tool result says "killed by user"
true
}
}
/// 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.
///
@ -28,12 +181,14 @@ pub use crate::agent::{ToolDef, ToolOutput, ProcessTracker, truncate_output};
///
/// 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,
tracker: &ProcessTracker,
) -> ToolOutput {
// Agent-specific tools that return Result<ToolOutput> directly
// Agent-specific tools
let rich_result = match name {
"pause" => Some(control::pause(args)),
"switch_model" => Some(control::switch_model(args)),
@ -45,21 +200,125 @@ pub async fn dispatch(
return result.unwrap_or_else(ToolOutput::error);
}
// Delegate to shared thought layer (poc-agent uses default provenance)
if let Some(output) = crate::agent::dispatch(name, args, tracker, None).await {
if let Some(output) = dispatch_shared(name, args, tracker, None).await {
return output;
}
ToolOutput::error(format!("Unknown tool: {}", name))
}
/// Return all tool definitions (agent-specific + shared).
/// 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,
tracker: &ProcessTracker,
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, tracker).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(),
grep::definition(),
glob::definition(),
];
defs.extend(control::definitions());
defs.extend(crate::agent::all_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(),
}
}