thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
// tools/mod.rs — Agent-specific tool dispatch
|
2026-03-25 00:52:41 -04:00
|
|
|
//
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
// 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.
|
2026-03-25 00:52:41 -04:00
|
|
|
|
2026-04-03 21:59:14 -04:00
|
|
|
// Core tools
|
|
|
|
|
pub mod bash;
|
|
|
|
|
pub mod edit;
|
|
|
|
|
pub mod glob;
|
|
|
|
|
pub mod grep;
|
|
|
|
|
pub mod memory;
|
|
|
|
|
pub mod read;
|
|
|
|
|
pub mod write;
|
|
|
|
|
|
|
|
|
|
// Agent-specific tools
|
2026-03-25 00:52:41 -04:00
|
|
|
mod control;
|
|
|
|
|
mod vision;
|
|
|
|
|
pub mod working_stack;
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
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
|
|
|
|
|
}
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
/// Dispatch a tool call by name.
|
|
|
|
|
///
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
/// Tries agent-specific tools first (control, vision), then
|
|
|
|
|
/// delegates to thought::dispatch for shared tools.
|
2026-03-25 00:52:41 -04:00
|
|
|
///
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
/// Note: working_stack is handled in runner.rs before reaching this
|
2026-03-25 00:52:41 -04:00
|
|
|
/// function (it needs mutable context access).
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Dispatch a tool call by name. Handles all tools:
|
|
|
|
|
/// agent-specific (control, vision), memory/journal, file/bash.
|
2026-03-25 00:52:41 -04:00
|
|
|
pub async fn dispatch(
|
|
|
|
|
name: &str,
|
|
|
|
|
args: &serde_json::Value,
|
|
|
|
|
tracker: &ProcessTracker,
|
|
|
|
|
) -> ToolOutput {
|
2026-04-03 23:21:16 -04:00
|
|
|
// Agent-specific tools
|
2026-03-25 00:52:41 -04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
if let Some(output) = dispatch_shared(name, args, tracker, None).await {
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
return output;
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
|
|
|
|
|
ToolOutput::error(format!("Unknown tool: {}", name))
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
/// 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).
|
2026-03-25 00:52:41 -04:00
|
|
|
pub fn definitions() -> Vec<ToolDef> {
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
let mut defs = vec![
|
2026-03-25 00:52:41 -04:00
|
|
|
vision::definition(),
|
|
|
|
|
working_stack::definition(),
|
2026-04-03 23:21:16 -04:00
|
|
|
read::definition(),
|
|
|
|
|
write::definition(),
|
|
|
|
|
edit::definition(),
|
|
|
|
|
bash::definition(),
|
|
|
|
|
grep::definition(),
|
|
|
|
|
glob::definition(),
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
];
|
|
|
|
|
defs.extend(control::definitions());
|
2026-04-03 23:21:16 -04:00
|
|
|
defs.extend(memory::definitions());
|
thought: wire up agent and subconscious to use shared tools
- agent/tools/mod.rs: remove duplicated tool implementations, delegate
to thought::dispatch for shared tools, keep only agent-specific
tools (control, vision, working_stack)
- subconscious/api.rs: replace duplicated memory/tool dispatch with
thought::dispatch, use thought::all_definitions() for tool schemas
- Delete agent/tools/{bash,read,write,edit,grep,glob_tool,journal,memory}.rs
(now live in thought/)
Both poc-agent and subconscious agents now use the same tool
implementations through the thought layer. Agent-specific behavior
(node tracking in runner.rs, control tools) stays in agent/.
2026-03-27 15:27:33 -04:00
|
|
|
defs
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
|
|
|
|
/// 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(),
|
|
|
|
|
}
|
|
|
|
|
}
|