WIP: Output tool via Arc<Mutex<Subconscious>>, ToolHandler to Arc<dyn Fn>

- ToolHandler changed to Arc<dyn Fn(...)> (supports closures)
- Subconscious wrapped in Arc<Mutex<>> on Mind
- init_output_tool() pushes output tool closure capturing the Arc
- Output removed from static memory_tools()
- Most tool handlers wrapped in Arc::new() but some have paren issues

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-08 20:37:19 -04:00
parent d167b11283
commit 12798eeae2
15 changed files with 74 additions and 51 deletions

View file

@ -37,7 +37,7 @@ pub fn tool() -> super::Tool {
name: "bash", name: "bash",
description: "Execute a bash command and return its output. Use for git operations, building, running tests, and other terminal tasks.", description: "Execute a bash command and return its output. Use for git operations, building, running tests, and other terminal tasks.",
parameters_json: r#"{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute"},"timeout_secs":{"type":"integer","description":"Timeout in seconds (default 120)"}},"required":["command"]}"#, parameters_json: r#"{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute"},"timeout_secs":{"type":"integer","description":"Timeout in seconds (default 120)"}},"required":["command"]}"#,
handler: |_a, v| Box::pin(async move { run_bash(&v).await }), handler: Arc::new(|_a, v| Box::pin(async move { run_bash(&v).await })),
} }
} }

View file

@ -15,27 +15,27 @@ pub fn tools() -> [Tool; 6] {
Tool { name: "channel_list", Tool { name: "channel_list",
description: "List all available channels and their status (connected, unread count).", description: "List all available channels and their status (connected, unread count).",
parameters_json: r#"{"type":"object","properties":{}}"#, parameters_json: r#"{"type":"object","properties":{}}"#,
handler: |_a, _v| Box::pin(async { channel_list().await }) }, handler: Arc::new(|_a, _v| Box::pin(async { channel_list().await })) },
Tool { name: "channel_recv", Tool { name: "channel_recv",
description: "Read messages from a channel.", description: "Read messages from a channel.",
parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. irc.#bcachefs, telegram.kent)"},"all_new":{"type":"boolean","description":"If true, return all unconsumed messages","default":true},"min_count":{"type":"integer","description":"Minimum number of lines to return","default":20}},"required":["channel"]}"#, parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. irc.#bcachefs, telegram.kent)"},"all_new":{"type":"boolean","description":"If true, return all unconsumed messages","default":true},"min_count":{"type":"integer","description":"Minimum number of lines to return","default":20}},"required":["channel"]}"#,
handler: |_a, v| Box::pin(async move { channel_recv(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { channel_recv(&v).await })) },
Tool { name: "channel_send", Tool { name: "channel_send",
description: "Send a message to a channel.", description: "Send a message to a channel.",
parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. irc.#bcachefs, irc.pm.nick, telegram.kent)"},"message":{"type":"string","description":"Message to send"}},"required":["channel","message"]}"#, parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. irc.#bcachefs, irc.pm.nick, telegram.kent)"},"message":{"type":"string","description":"Message to send"}},"required":["channel","message"]}"#,
handler: |_a, v| Box::pin(async move { channel_send(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { channel_send(&v).await })) },
Tool { name: "channel_notifications", Tool { name: "channel_notifications",
description: "Get pending channel notifications (unread signals). Does not consume messages — use channel_recv for that.", description: "Get pending channel notifications (unread signals). Does not consume messages — use channel_recv for that.",
parameters_json: r#"{"type":"object","properties":{}}"#, parameters_json: r#"{"type":"object","properties":{}}"#,
handler: |_a, _v| Box::pin(async { channel_notifications().await }) }, handler: Arc::new(|_a, _v| Box::pin(async { channel_notifications().await })) },
Tool { name: "channel_open", Tool { name: "channel_open",
description: "Open a channel — start monitoring. For tmux: finds the pane by name and attaches pipe-pane.", description: "Open a channel — start monitoring. For tmux: finds the pane by name and attaches pipe-pane.",
parameters_json: r#"{"type":"object","properties":{"label":{"type":"string","description":"Channel label / tmux pane name"}},"required":["label"]}"#, parameters_json: r#"{"type":"object","properties":{"label":{"type":"string","description":"Channel label / tmux pane name"}},"required":["label"]}"#,
handler: |_a, v| Box::pin(async move { channel_open(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { channel_open(&v).await })) },
Tool { name: "channel_close", Tool { name: "channel_close",
description: "Close a channel — stop monitoring and clean up.", description: "Close a channel — stop monitoring and clean up.",
parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. tmux.ktest)"}},"required":["channel"]}"#, parameters_json: r#"{"type":"object","properties":{"channel":{"type":"string","description":"Channel path (e.g. tmux.ktest)"}},"required":["channel"]}"#,
handler: |_a, v| Box::pin(async move { channel_close(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { channel_close(&v).await })) },
] ]
} }

View file

@ -9,7 +9,7 @@ pub(super) fn tools() -> [super::Tool; 3] {
Tool { name: "switch_model", Tool { name: "switch_model",
description: "Switch to a different LLM model mid-conversation. Memories and history carry over.", description: "Switch to a different LLM model mid-conversation. Memories and history carry over.",
parameters_json: r#"{"type":"object","properties":{"model":{"type":"string","description":"Name of the model to switch to"}},"required":["model"]}"#, parameters_json: r#"{"type":"object","properties":{"model":{"type":"string","description":"Name of the model to switch to"}},"required":["model"]}"#,
handler: |agent, v| Box::pin(async move { handler: Arc::new(|agent, v| Box::pin(async move {
let model = v.get("model").and_then(|v| v.as_str()) let model = v.get("model").and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("'model' parameter is required"))?; .ok_or_else(|| anyhow::anyhow!("'model' parameter is required"))?;
if model.is_empty() { anyhow::bail!("'model' parameter cannot be empty"); } if model.is_empty() { anyhow::bail!("'model' parameter cannot be empty"); }
@ -22,7 +22,7 @@ pub(super) fn tools() -> [super::Tool; 3] {
Tool { name: "pause", Tool { name: "pause",
description: "Pause all autonomous behavior. Only the user can unpause (Ctrl+P or /wake).", description: "Pause all autonomous behavior. Only the user can unpause (Ctrl+P or /wake).",
parameters_json: r#"{"type":"object","properties":{}}"#, parameters_json: r#"{"type":"object","properties":{}}"#,
handler: |agent, _v| Box::pin(async move { handler: Arc::new(|agent, _v| Box::pin(async move {
if let Some(agent) = agent { if let Some(agent) = agent {
let mut a = agent.state.lock().await; let mut a = agent.state.lock().await;
a.pending_yield = true; a.pending_yield = true;
@ -33,7 +33,7 @@ pub(super) fn tools() -> [super::Tool; 3] {
Tool { name: "yield_to_user", Tool { name: "yield_to_user",
description: "Wait for user input before continuing. The only way to enter a waiting state.", description: "Wait for user input before continuing. The only way to enter a waiting state.",
parameters_json: r#"{"type":"object","properties":{"message":{"type":"string","description":"Optional status message"}}}"#, parameters_json: r#"{"type":"object","properties":{"message":{"type":"string","description":"Optional status message"}}}"#,
handler: |agent, v| Box::pin(async move { handler: Arc::new(|agent, v| Box::pin(async move {
let msg = v.get("message").and_then(|v| v.as_str()).unwrap_or("Waiting for input."); let msg = v.get("message").and_then(|v| v.as_str()).unwrap_or("Waiting for input.");
if let Some(agent) = agent { if let Some(agent) = agent {
let mut a = agent.state.lock().await; let mut a = agent.state.lock().await;

View file

@ -8,7 +8,7 @@ pub fn tool() -> super::Tool {
name: "edit_file", name: "edit_file",
description: "Perform exact string replacement in a file. The old_string must appear exactly once (unless replace_all is true). Use read_file first to see current contents.", description: "Perform exact string replacement in a file. The old_string must appear exactly once (unless replace_all is true). Use read_file first to see current contents.",
parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to the file to edit"},"old_string":{"type":"string","description":"The exact text to find and replace"},"new_string":{"type":"string","description":"The replacement text"},"replace_all":{"type":"boolean","description":"Replace all occurrences (default false)"}},"required":["file_path","old_string","new_string"]}"#, parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to the file to edit"},"old_string":{"type":"string","description":"The exact text to find and replace"},"new_string":{"type":"string","description":"The replacement text"},"replace_all":{"type":"boolean","description":"Replace all occurrences (default false)"}},"required":["file_path","old_string","new_string"]}"#,
handler: |_a, v| Box::pin(async move { edit_file(&v) }), handler: Arc::new(|_a, v| Box::pin(async move { edit_file(&v)) }),
} }
} }

View file

@ -22,7 +22,7 @@ pub fn tool() -> super::Tool {
name: "glob", name: "glob",
description: "Find files matching a glob pattern. Returns file paths sorted by modification time (newest first).", description: "Find files matching a glob pattern. Returns file paths sorted by modification time (newest first).",
parameters_json: r#"{"type":"object","properties":{"pattern":{"type":"string","description":"Glob pattern to match files (e.g. '**/*.rs')"},"path":{"type":"string","description":"Directory to search in (default: current directory)"}},"required":["pattern"]}"#, parameters_json: r#"{"type":"object","properties":{"pattern":{"type":"string","description":"Glob pattern to match files (e.g. '**/*.rs')"},"path":{"type":"string","description":"Directory to search in (default: current directory)"}},"required":["pattern"]}"#,
handler: |_a, v| Box::pin(async move { glob_search(&v) }), handler: Arc::new(|_a, v| Box::pin(async move { glob_search(&v)) }),
} }
} }

View file

@ -25,7 +25,7 @@ pub fn tool() -> super::Tool {
name: "grep", name: "grep",
description: "Search for a pattern in files. Returns matching file paths by default, or matching lines with context.", description: "Search for a pattern in files. Returns matching file paths by default, or matching lines with context.",
parameters_json: r#"{"type":"object","properties":{"pattern":{"type":"string","description":"Regex pattern to search for"},"path":{"type":"string","description":"Directory or file to search in (default: current directory)"},"glob":{"type":"string","description":"Glob pattern to filter files (e.g. '*.rs')"},"show_content":{"type":"boolean","description":"Show matching lines instead of just file paths"},"context_lines":{"type":"integer","description":"Lines of context around matches"}},"required":["pattern"]}"#, parameters_json: r#"{"type":"object","properties":{"pattern":{"type":"string","description":"Regex pattern to search for"},"path":{"type":"string","description":"Directory or file to search in (default: current directory)"},"glob":{"type":"string","description":"Glob pattern to filter files (e.g. '*.rs')"},"show_content":{"type":"boolean","description":"Show matching lines instead of just file paths"},"context_lines":{"type":"integer","description":"Lines of context around matches"}},"required":["pattern"]}"#,
handler: |_a, v| Box::pin(async move { grep(&v) }), handler: Arc::new(|_a, v| Box::pin(async move { grep(&v)) }),
} }
} }

View file

@ -37,40 +37,37 @@ pub fn memory_tools() -> [super::Tool; 12] {
[ [
Tool { name: "memory_render", description: "Read a memory node's content and links.", Tool { name: "memory_render", description: "Read a memory node's content and links.",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#, parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#,
handler: |_a, v| Box::pin(async move { render(&v) }) }, handler: Arc::new(|_a, v| Box::pin(async move { render(&v) })) },
Tool { name: "memory_write", description: "Create or update a memory node.", Tool { name: "memory_write", description: "Create or update a memory node.",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"},"content":{"type":"string","description":"Full content (markdown)"}},"required":["key","content"]}"#, parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"},"content":{"type":"string","description":"Full content (markdown)"}},"required":["key","content"]}"#,
handler: |a, v| Box::pin(async move { write(&a, &v).await }) }, handler: Arc::new(|a, v| Box::pin(async move { write(&a, &v).await })) },
Tool { name: "memory_search", description: "Search the memory graph via spreading activation. Give 2-4 seed node keys.", Tool { name: "memory_search", description: "Search the memory graph via spreading activation. Give 2-4 seed node keys.",
parameters_json: r#"{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"},"description":"Seed node keys to activate from"}},"required":["keys"]}"#, parameters_json: r#"{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"},"description":"Seed node keys to activate from"}},"required":["keys"]}"#,
handler: |_a, v| Box::pin(async move { search(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { search(&v).await })) },
Tool { name: "memory_links", description: "Show a node's neighbors with link strengths.", Tool { name: "memory_links", description: "Show a node's neighbors with link strengths.",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#, parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#,
handler: |_a, v| Box::pin(async move { links(&v) }) }, handler: Arc::new(|_a, v| Box::pin(async move { links(&v) })) },
Tool { name: "memory_link_set", description: "Set link strength between two nodes.", Tool { name: "memory_link_set", description: "Set link strength between two nodes.",
parameters_json: r#"{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"},"strength":{"type":"number","description":"0.01 to 1.0"}},"required":["source","target","strength"]}"#, parameters_json: r#"{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"},"strength":{"type":"number","description":"0.01 to 1.0"}},"required":["source","target","strength"]}"#,
handler: |_a, v| Box::pin(async move { link_set(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { link_set(&v).await })) },
Tool { name: "memory_link_add", description: "Add a new link between two nodes.", Tool { name: "memory_link_add", description: "Add a new link between two nodes.",
parameters_json: r#"{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"}},"required":["source","target"]}"#, parameters_json: r#"{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"}},"required":["source","target"]}"#,
handler: |a, v| Box::pin(async move { link_add(&a, &v).await }) }, handler: Arc::new(|a, v| Box::pin(async move { link_add(&a, &v).await })) },
Tool { name: "memory_used", description: "Mark a node as useful (boosts weight).", Tool { name: "memory_used", description: "Mark a node as useful (boosts weight).",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#, parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#,
handler: |_a, v| Box::pin(async move { used(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { used(&v).await })) },
Tool { name: "memory_weight_set", description: "Set a node's weight directly (0.01 to 1.0).", Tool { name: "memory_weight_set", description: "Set a node's weight directly (0.01 to 1.0).",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string"},"weight":{"type":"number","description":"0.01 to 1.0"}},"required":["key","weight"]}"#, parameters_json: r#"{"type":"object","properties":{"key":{"type":"string"},"weight":{"type":"number","description":"0.01 to 1.0"}},"required":["key","weight"]}"#,
handler: |_a, v| Box::pin(async move { weight_set(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { weight_set(&v).await })) },
Tool { name: "memory_rename", description: "Rename a node key in place.", Tool { name: "memory_rename", description: "Rename a node key in place.",
parameters_json: r#"{"type":"object","properties":{"old_key":{"type":"string"},"new_key":{"type":"string"}},"required":["old_key","new_key"]}"#, parameters_json: r#"{"type":"object","properties":{"old_key":{"type":"string"},"new_key":{"type":"string"}},"required":["old_key","new_key"]}"#,
handler: |_a, v| Box::pin(async move { rename(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { rename(&v).await })) },
Tool { name: "memory_supersede", description: "Mark a node as superseded by another (sets weight to 0.01).", Tool { name: "memory_supersede", description: "Mark a node as superseded by another (sets weight to 0.01).",
parameters_json: r#"{"type":"object","properties":{"old_key":{"type":"string"},"new_key":{"type":"string"},"reason":{"type":"string"}},"required":["old_key","new_key"]}"#, parameters_json: r#"{"type":"object","properties":{"old_key":{"type":"string"},"new_key":{"type":"string"},"reason":{"type":"string"}},"required":["old_key","new_key"]}"#,
handler: |a, v| Box::pin(async move { supersede(&a, &v).await }) }, handler: Arc::new(|a, v| Box::pin(async move { supersede(&a, &v).await })) },
Tool { name: "memory_query", description: "Run a structured query against the memory graph.", Tool { name: "memory_query", description: "Run a structured query against the memory graph.",
parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]}"#, parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]}"#,
handler: |_a, v| Box::pin(async move { query(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { query(&v).await })) },
Tool { name: "output", description: "Produce a named output value for passing between steps.",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Output name"},"value":{"type":"string","description":"Output value"}},"required":["key","value"]}"#,
handler: |_a, v| Box::pin(async move { output(&v) }) },
] ]
} }
@ -79,13 +76,13 @@ pub fn journal_tools() -> [super::Tool; 3] {
[ [
Tool { name: "journal_tail", description: "Read the last N journal entries (default 1).", Tool { name: "journal_tail", description: "Read the last N journal entries (default 1).",
parameters_json: r#"{"type":"object","properties":{"count":{"type":"integer","description":"Number of entries (default 1)"}}}"#, parameters_json: r#"{"type":"object","properties":{"count":{"type":"integer","description":"Number of entries (default 1)"}}}"#,
handler: |_a, v| Box::pin(async move { journal_tail(&v).await }) }, handler: Arc::new(|_a, v| Box::pin(async move { journal_tail(&v).await })) },
Tool { name: "journal_new", description: "Start a new journal entry.", Tool { name: "journal_new", description: "Start a new journal entry.",
parameters_json: r#"{"type":"object","properties":{"name":{"type":"string","description":"Short node name (becomes the key)"},"title":{"type":"string","description":"Descriptive title"},"body":{"type":"string","description":"Entry body"}},"required":["name","title","body"]}"#, parameters_json: r#"{"type":"object","properties":{"name":{"type":"string","description":"Short node name (becomes the key)"},"title":{"type":"string","description":"Descriptive title"},"body":{"type":"string","description":"Entry body"}},"required":["name","title","body"]}"#,
handler: |a, v| Box::pin(async move { journal_new(&a, &v).await }) }, handler: Arc::new(|a, v| Box::pin(async move { journal_new(&a, &v).await })) },
Tool { name: "journal_update", description: "Append text to the most recent journal entry.", Tool { name: "journal_update", description: "Append text to the most recent journal entry.",
parameters_json: r#"{"type":"object","properties":{"body":{"type":"string","description":"Text to append"}},"required":["body"]}"#, parameters_json: r#"{"type":"object","properties":{"body":{"type":"string","description":"Text to append"}},"required":["body"]}"#,
handler: |a, v| Box::pin(async move { journal_update(&a, &v).await }) }, handler: Arc::new(|a, v| Box::pin(async move { journal_update(&a, &v).await })) },
] ]
} }

View file

@ -21,21 +21,18 @@ mod vision;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
fn default_timeout() -> u64 { 120 } fn default_timeout() -> u64 { 120 }
/// Async tool handler function. pub type ToolHandler = Arc<dyn Fn(
/// Agent is None when called from contexts without an agent (MCP server, subconscious).
pub type ToolHandler = fn(
Option<std::sync::Arc<super::Agent>>, Option<std::sync::Arc<super::Agent>>,
serde_json::Value, serde_json::Value,
) -> Pin<Box<dyn Future<Output = anyhow::Result<String>> + Send>>; ) -> Pin<Box<dyn Future<Output = anyhow::Result<String>> + Send>>
+ Send + Sync>;
/// A tool with its definition and handler — single source of truth. #[derive(Clone)]
/// Strings are static — the tool list JSON can be built without
/// serialization by interpolating these directly.
#[derive(Clone, Copy)]
pub struct Tool { pub struct Tool {
pub name: &'static str, pub name: &'static str,
pub description: &'static str, pub description: &'static str,

View file

@ -8,7 +8,7 @@ pub fn tool() -> super::Tool {
name: "read_file", name: "read_file",
description: "Read the contents of a file. Returns the file contents with line numbers.", description: "Read the contents of a file. Returns the file contents with line numbers.",
parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to the file to read"},"offset":{"type":"integer","description":"Line number to start reading from (1-based)"},"limit":{"type":"integer","description":"Maximum number of lines to read"}},"required":["file_path"]}"#, parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to the file to read"},"offset":{"type":"integer","description":"Line number to start reading from (1-based)"},"limit":{"type":"integer","description":"Maximum number of lines to read"}},"required":["file_path"]}"#,
handler: |_a, v| Box::pin(async move { read_file(&v) }), handler: Arc::new(|_a, v| Box::pin(async move { read_file(&v)) }),
} }
} }

View file

@ -23,7 +23,7 @@ pub fn tool() -> super::Tool {
name: "view_image", name: "view_image",
description: "View an image file or capture a tmux pane screenshot. Supports PNG, JPEG, GIF, WebP. Use pane_id to capture a tmux pane instead.", description: "View an image file or capture a tmux pane screenshot. Supports PNG, JPEG, GIF, WebP. Use pane_id to capture a tmux pane instead.",
parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Path to an image file"},"pane_id":{"type":"string","description":"Tmux pane ID to capture (e.g. '0:1.0')"},"lines":{"type":"integer","description":"Lines to capture from tmux pane (default 50)"}}}"#, parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Path to an image file"},"pane_id":{"type":"string","description":"Tmux pane ID to capture (e.g. '0:1.0')"},"lines":{"type":"integer","description":"Lines to capture from tmux pane (default 50)"}}}"#,
handler: |_a, v| Box::pin(async move { view_image_text(&v) }), handler: Arc::new(|_a, v| Box::pin(async move { view_image_text(&v)) }),
} }
} }

View file

@ -9,13 +9,13 @@ pub fn tools() -> [super::Tool; 2] {
name: "web_fetch", name: "web_fetch",
description: "Fetch content from a URL and return it as text. Use for reading web pages, API responses, documentation.", description: "Fetch content from a URL and return it as text. Use for reading web pages, API responses, documentation.",
parameters_json: r#"{"type":"object","properties":{"url":{"type":"string","description":"The URL to fetch"}},"required":["url"]}"#, parameters_json: r#"{"type":"object","properties":{"url":{"type":"string","description":"The URL to fetch"}},"required":["url"]}"#,
handler: |_a, v| Box::pin(async move { web_fetch(&v).await }), handler: Arc::new(|_a, v| Box::pin(async move { web_fetch(&v).await }),
}, },
super::Tool { super::Tool {
name: "web_search", name: "web_search",
description: "Search the web and return results. Use for finding documentation, looking up APIs, researching topics.", description: "Search the web and return results. Use for finding documentation, looking up APIs, researching topics.",
parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"The search query"},"num_results":{"type":"integer","description":"Number of results to return (default 5)"}},"required":["query"]}"#, parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"The search query"},"num_results":{"type":"integer","description":"Number of results to return (default 5)"}},"required":["query"]}"#,
handler: |_a, v| Box::pin(async move { web_search(&v).await }), handler: Arc::new(|_a, v| Box::pin(async move { web_search(&v).await }),
}, },
] ]
} }

View file

@ -8,7 +8,7 @@ pub fn tool() -> super::Tool {
name: "write_file", name: "write_file",
description: "Create or overwrite a file with the given content.", description: "Create or overwrite a file with the given content.",
parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to write"},"content":{"type":"string","description":"File content"}},"required":["file_path","content"]}"#, parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to write"},"content":{"type":"string","description":"File content"}},"required":["file_path","content"]}"#,
handler: |_a, v| Box::pin(async move { write_file(&v) }), handler: Arc::new(|_a, v| Box::pin(async move { write_file(&v)) }),
} }
} }

View file

@ -389,6 +389,33 @@ impl Subconscious {
Self { agents, state: std::collections::BTreeMap::new(), state_path: None } Self { agents, state: std::collections::BTreeMap::new(), state_path: None }
} }
/// Late-init: push the output tool onto each agent's tool list.
/// Called after Subconscious is wrapped in Arc<Mutex<>> so the
/// closure can capture a reference back.
pub fn init_output_tool(&mut self, self_arc: std::sync::Arc<tokio::sync::Mutex<Self>>) {
for agent in &mut self.agents {
let sub = self_arc.clone();
agent.auto.tools.push(crate::agent::tools::Tool {
name: "output",
description: "Produce a named output value for passing between steps.",
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Output name"},"value":{"type":"string","description":"Output value"}},"required":["key","value"]}"#,
handler: std::sync::Arc::new(move |_agent, v| {
let sub = sub.clone();
Box::pin(async move {
let key = v["key"].as_str()
.ok_or_else(|| anyhow::anyhow!("output requires 'key'"))?;
let value = v["value"].as_str()
.ok_or_else(|| anyhow::anyhow!("output requires 'value'"))?;
let mut s = sub.lock().await;
s.state.insert(key.to_string(), value.to_string());
s.save_state();
Ok(format!("{}: {}", key, value))
})
}),
});
}
}
/// Set the state file path and load any existing state from disk. /// Set the state file path and load any existing state from disk.
pub fn set_state_path(&mut self, path: std::path::PathBuf) { pub fn set_state_path(&mut self, path: std::path::PathBuf) {
if let Ok(data) = std::fs::read_to_string(&path) { if let Ok(data) = std::fs::read_to_string(&path) {

View file

@ -251,7 +251,7 @@ pub struct Mind {
pub agent: Arc<Agent>, pub agent: Arc<Agent>,
pub shared: Arc<SharedMindState>, pub shared: Arc<SharedMindState>,
pub config: SessionConfig, pub config: SessionConfig,
subconscious: tokio::sync::Mutex<Subconscious>, subconscious: Arc<tokio::sync::Mutex<Subconscious>>,
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>, turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
turn_watch: tokio::sync::watch::Sender<bool>, turn_watch: tokio::sync::watch::Sender<bool>,
bg_tx: mpsc::UnboundedSender<BgEvent>, bg_tx: mpsc::UnboundedSender<BgEvent>,
@ -287,9 +287,11 @@ impl Mind {
sup.load_config(); sup.load_config();
sup.ensure_running(); sup.ensure_running();
let subconscious = Arc::new(tokio::sync::Mutex::new(Subconscious::new()));
subconscious.lock().await.init_output_tool(subconscious.clone());
Self { agent, shared, config, Self { agent, shared, config,
subconscious: tokio::sync::Mutex::new(Subconscious::new()), subconscious, turn_tx, turn_watch, bg_tx,
turn_tx, turn_watch, bg_tx,
bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup } bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup }
} }

View file

@ -663,31 +663,31 @@ pub fn digest_tools() -> [super::super::agent::tools::Tool; 5] {
name: "digest_daily", name: "digest_daily",
description: "Generate a daily digest from journal entries.", description: "Generate a daily digest from journal entries.",
parameters_json: r#"{"type":"object","properties":{"date":{"type":"string","description":"Date in YYYY-MM-DD format"}}, "required":["date"]}"#, parameters_json: r#"{"type":"object","properties":{"date":{"type":"string","description":"Date in YYYY-MM-DD format"}}, "required":["date"]}"#,
handler: |_a, v| Box::pin(async move { handle_digest_daily(_a, v).await }), handler: Arc::new(|_a, v| Box::pin(async move { handle_digest_daily(_a, v).await }),
}, },
Tool { Tool {
name: "digest_weekly", name: "digest_weekly",
description: "Generate a weekly digest from daily digests.", description: "Generate a weekly digest from daily digests.",
parameters_json: r#"{"type":"object","properties":{"week":{"type":"string","description":"Week label (YYYY-W##) or date (YYYY-MM-DD)"}}, "required":["week"]}"#, parameters_json: r#"{"type":"object","properties":{"week":{"type":"string","description":"Week label (YYYY-W##) or date (YYYY-MM-DD)"}}, "required":["week"]}"#,
handler: |_a, v| Box::pin(async move { handle_digest_weekly(_a, v).await }), handler: Arc::new(|_a, v| Box::pin(async move { handle_digest_weekly(_a, v).await }),
}, },
Tool { Tool {
name: "digest_monthly", name: "digest_monthly",
description: "Generate a monthly digest from weekly digests.", description: "Generate a monthly digest from weekly digests.",
parameters_json: r#"{"type":"object","properties":{"month":{"type":"string","description":"Month label (YYYY-MM) or date (YYYY-MM-DD)"}}, "required":["month"]}"#, parameters_json: r#"{"type":"object","properties":{"month":{"type":"string","description":"Month label (YYYY-MM) or date (YYYY-MM-DD)"}}, "required":["month"]}"#,
handler: |_a, v| Box::pin(async move { handle_digest_monthly(_a, v).await }), handler: Arc::new(|_a, v| Box::pin(async move { handle_digest_monthly(_a, v).await }),
}, },
Tool { Tool {
name: "digest_auto", name: "digest_auto",
description: "Auto-generate all missing digests (daily, weekly, monthly) for past dates that have content but no digest yet.", description: "Auto-generate all missing digests (daily, weekly, monthly) for past dates that have content but no digest yet.",
parameters_json: r#"{"type":"object","properties":{}}"#, parameters_json: r#"{"type":"object","properties":{}}"#,
handler: |_a, v| Box::pin(async move { handle_digest_auto(_a, v).await }), handler: Arc::new(|_a, v| Box::pin(async move { handle_digest_auto(_a, v).await }),
}, },
Tool { Tool {
name: "digest_links", name: "digest_links",
description: "Parse and apply structural links from digest nodes to the memory graph.", description: "Parse and apply structural links from digest nodes to the memory graph.",
parameters_json: r#"{"type":"object","properties":{}}"#, parameters_json: r#"{"type":"object","properties":{}}"#,
handler: |_a, v| Box::pin(async move { handle_digest_links(_a, v).await }), handler: Arc::new(|_a, v| Box::pin(async move { handle_digest_links(_a, v).await }),
}, },
] ]
} }