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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,12 +10,9 @@ use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use std::process::Stdio; use std::process::Stdio;
use std::sync::Arc;
use std::time::Instant;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::Mutex;
use super::ToolDef; use super::{ToolDef, ProcessTracker, default_timeout};
#[derive(Deserialize)] #[derive(Deserialize)]
struct Args { struct Args {
@ -24,63 +21,6 @@ struct Args {
timeout_secs: u64, timeout_secs: u64,
} }
fn default_timeout() -> u64 { 120 }
/// 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
}
}
pub fn definition() -> ToolDef { pub fn definition() -> ToolDef {
ToolDef::new( ToolDef::new(
"bash", "bash",

View file

@ -18,8 +18,161 @@ mod control;
mod vision; mod vision;
pub mod working_stack; pub mod working_stack;
// Re-export use serde::{Serialize, Deserialize};
pub use crate::agent::{ToolDef, ToolOutput, ProcessTracker, truncate_output}; 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. /// 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 /// Note: working_stack is handled in runner.rs before reaching this
/// function (it needs mutable context access). /// 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( pub async fn dispatch(
name: &str, name: &str,
args: &serde_json::Value, args: &serde_json::Value,
tracker: &ProcessTracker, tracker: &ProcessTracker,
) -> ToolOutput { ) -> ToolOutput {
// Agent-specific tools that return Result<ToolOutput> directly // Agent-specific tools
let rich_result = match name { let rich_result = match name {
"pause" => Some(control::pause(args)), "pause" => Some(control::pause(args)),
"switch_model" => Some(control::switch_model(args)), "switch_model" => Some(control::switch_model(args)),
@ -45,21 +200,125 @@ pub async fn dispatch(
return result.unwrap_or_else(ToolOutput::error); return result.unwrap_or_else(ToolOutput::error);
} }
// Delegate to shared thought layer (poc-agent uses default provenance) if let Some(output) = dispatch_shared(name, args, tracker, None).await {
if let Some(output) = crate::agent::dispatch(name, args, tracker, None).await {
return output; return output;
} }
ToolOutput::error(format!("Unknown tool: {}", name)) 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> { pub fn definitions() -> Vec<ToolDef> {
let mut defs = vec![ let mut defs = vec![
vision::definition(), vision::definition(),
working_stack::definition(), working_stack::definition(),
read::definition(),
write::definition(),
edit::definition(),
bash::definition(),
grep::definition(),
glob::definition(),
]; ];
defs.extend(control::definitions()); defs.extend(control::definitions());
defs.extend(crate::agent::all_definitions()); defs.extend(memory::definitions());
defs 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(),
}
}

View file

@ -1,3 +1,4 @@
#![warn(unreachable_pub)]
// poc-agent — Substrate-independent AI agent // poc-agent — Substrate-independent AI agent
// //
// A minimal but complete agent framework designed for identity // A minimal but complete agent framework designed for identity
@ -32,7 +33,7 @@ use clap::Parser;
use poc_memory::dbglog; use poc_memory::dbglog;
use poc_memory::user::*; use poc_memory::user::*;
use poc_memory::agent::{tools, runner::{Agent, TurnResult}}; use poc_memory::agent::{tools, Agent, TurnResult};
use poc_memory::user::api::ApiClient; use poc_memory::user::api::ApiClient;
use poc_memory::user::tui::HotkeyAction; use poc_memory::user::tui::HotkeyAction;
use poc_memory::config::{self, AppConfig, SessionConfig}; use poc_memory::config::{self, AppConfig, SessionConfig};

View file

@ -9,7 +9,7 @@
use crate::user::api::ApiClient; use crate::user::api::ApiClient;
use crate::user::types::*; use crate::user::types::*;
use crate::agent::{self, ProcessTracker}; use crate::agent::tools::{self as agent_tools, ProcessTracker, ToolOutput};
use std::sync::OnceLock; use std::sync::OnceLock;
@ -46,7 +46,7 @@ pub async fn call_api_with_tools(
let (ui_tx, mut ui_rx) = crate::user::ui_channel::channel(); let (ui_tx, mut ui_rx) = crate::user::ui_channel::channel();
// All available native tools for subconscious agents // All available native tools for subconscious agents
let all_tools = agent::memory_and_journal_definitions(); let all_tools = agent_tools::memory_and_journal_definitions();
// If agent header specifies a tools whitelist, filter to only those // If agent header specifies a tools whitelist, filter to only those
let tool_defs: Vec<_> = if tools.is_empty() { let tool_defs: Vec<_> = if tools.is_empty() {
all_tools all_tools
@ -175,9 +175,9 @@ pub async fn call_api_with_tools(
}; };
let prov = provenance.borrow().clone(); let prov = provenance.borrow().clone();
let output = match agent::dispatch(&call.function.name, &args, &tracker, Some(&prov)).await { let output = match agent_tools::dispatch_shared(&call.function.name, &args, &tracker, Some(&prov)).await {
Some(out) => out, Some(out) => out,
None => agent::ToolOutput::error(format!("Unknown tool: {}", call.function.name)), None => ToolOutput::error(format!("Unknown tool: {}", call.function.name)),
}; };
if std::env::var("POC_AGENT_VERBOSE").is_ok() { if std::env::var("POC_AGENT_VERBOSE").is_ok() {

View file

@ -295,7 +295,7 @@ fn run_one_agent_inner(
_llm_tag: &str, _llm_tag: &str,
log: &(dyn Fn(&str) + Sync), log: &(dyn Fn(&str) + Sync),
) -> Result<AgentResult, String> { ) -> Result<AgentResult, String> {
let all_tools = crate::agent::memory_and_journal_definitions(); let all_tools = crate::agent::tools::memory_and_journal_definitions();
let effective_tools: Vec<String> = if def.tools.is_empty() { let effective_tools: Vec<String> = if def.tools.is_empty() {
all_tools.iter().map(|t| t.function.name.clone()).collect() all_tools.iter().map(|t| t.function.name.clone()).collect()
} else { } else {

View file

@ -307,10 +307,6 @@ pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
.collect() .collect()
} }
/// A tool call currently in flight — shown above the status bar.
// ActiveTool moved to ui_channel — shared between Agent and TUI
pub(crate) use crate::user::ui_channel::ActiveTool;
/// Main TUI application state. /// Main TUI application state.
pub struct App { pub struct App {
pub(crate) autonomous: PaneState, pub(crate) autonomous: PaneState,

View file

@ -9,6 +9,12 @@
use chrono::Utc; use chrono::Utc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// Re-export tool types that moved to agent::tools
pub use crate::agent::tools::{
ToolDef, ToolCall, ToolCallDelta, ToolOutput,
FunctionCall, FunctionDef, FunctionCallDelta,
};
/// Message content — either plain text or an array of content parts /// Message content — either plain text or an array of content parts
/// (for multimodal messages with images). Serializes as a JSON string /// (for multimodal messages with images). Serializes as a JSON string
/// for text-only, or a JSON array for multimodal. /// for text-only, or a JSON array for multimodal.
@ -79,35 +85,7 @@ pub enum Role {
Tool, Tool,
} }
/// A tool call requested by the model. // FunctionCall, FunctionDef moved to agent::tools
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String, // JSON 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,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionDef {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
/// Chat completion request. /// Chat completion request.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -202,23 +180,7 @@ pub struct Delta {
pub tool_calls: Option<Vec<ToolCallDelta>>, pub tool_calls: Option<Vec<ToolCallDelta>>,
} }
/// A partial tool call within a streaming delta. The first chunk for a // FunctionCallDelta moved to agent::tools
/// 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>,
}
#[derive(Debug, Deserialize)]
pub struct FunctionCallDelta {
pub name: Option<String>,
pub arguments: Option<String>,
}
// --- Convenience constructors --- // --- Convenience constructors ---