From 12798eeae2dd0963c7bb96e3b201b00923ff451f Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Wed, 8 Apr 2026 20:37:19 -0400 Subject: [PATCH] WIP: Output tool via Arc>, ToolHandler to Arc - ToolHandler changed to Arc (supports closures) - Subconscious wrapped in Arc> 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 --- src/agent/tools/bash.rs | 2 +- src/agent/tools/channels.rs | 12 ++++++------ src/agent/tools/control.rs | 6 +++--- src/agent/tools/edit.rs | 2 +- src/agent/tools/glob.rs | 2 +- src/agent/tools/grep.rs | 2 +- src/agent/tools/memory.rs | 31 ++++++++++++++----------------- src/agent/tools/mod.rs | 13 +++++-------- src/agent/tools/read.rs | 2 +- src/agent/tools/vision.rs | 2 +- src/agent/tools/web.rs | 4 ++-- src/agent/tools/write.rs | 2 +- src/mind/dmn.rs | 27 +++++++++++++++++++++++++++ src/mind/mod.rs | 8 +++++--- src/subconscious/digest.rs | 10 +++++----- 15 files changed, 74 insertions(+), 51 deletions(-) diff --git a/src/agent/tools/bash.rs b/src/agent/tools/bash.rs index 9bffbcf..2358c34 100644 --- a/src/agent/tools/bash.rs +++ b/src/agent/tools/bash.rs @@ -37,7 +37,7 @@ pub fn tool() -> super::Tool { name: "bash", 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"]}"#, - handler: |_a, v| Box::pin(async move { run_bash(&v).await }), + handler: Arc::new(|_a, v| Box::pin(async move { run_bash(&v).await })), } } diff --git a/src/agent/tools/channels.rs b/src/agent/tools/channels.rs index 479bf91..ca18a57 100644 --- a/src/agent/tools/channels.rs +++ b/src/agent/tools/channels.rs @@ -15,27 +15,27 @@ pub fn tools() -> [Tool; 6] { Tool { name: "channel_list", description: "List all available channels and their status (connected, unread count).", 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", 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"]}"#, - 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", 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"]}"#, - 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", description: "Get pending channel notifications (unread signals). Does not consume messages — use channel_recv for that.", 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", 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"]}"#, - 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", 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"]}"#, - handler: |_a, v| Box::pin(async move { channel_close(&v).await }) }, + handler: Arc::new(|_a, v| Box::pin(async move { channel_close(&v).await })) }, ] } diff --git a/src/agent/tools/control.rs b/src/agent/tools/control.rs index 6ada846..02e5d95 100644 --- a/src/agent/tools/control.rs +++ b/src/agent/tools/control.rs @@ -9,7 +9,7 @@ pub(super) fn tools() -> [super::Tool; 3] { Tool { name: "switch_model", 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"]}"#, - 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()) .ok_or_else(|| anyhow::anyhow!("'model' parameter is required"))?; 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", description: "Pause all autonomous behavior. Only the user can unpause (Ctrl+P or /wake).", 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 { let mut a = agent.state.lock().await; a.pending_yield = true; @@ -33,7 +33,7 @@ pub(super) fn tools() -> [super::Tool; 3] { Tool { name: "yield_to_user", 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"}}}"#, - 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."); if let Some(agent) = agent { let mut a = agent.state.lock().await; diff --git a/src/agent/tools/edit.rs b/src/agent/tools/edit.rs index 52f13ee..f136f00 100644 --- a/src/agent/tools/edit.rs +++ b/src/agent/tools/edit.rs @@ -8,7 +8,7 @@ pub fn tool() -> super::Tool { 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.", 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)) }), } } diff --git a/src/agent/tools/glob.rs b/src/agent/tools/glob.rs index 18752bf..ce51ad4 100644 --- a/src/agent/tools/glob.rs +++ b/src/agent/tools/glob.rs @@ -22,7 +22,7 @@ pub fn tool() -> super::Tool { name: "glob", 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"]}"#, - handler: |_a, v| Box::pin(async move { glob_search(&v) }), + handler: Arc::new(|_a, v| Box::pin(async move { glob_search(&v)) }), } } diff --git a/src/agent/tools/grep.rs b/src/agent/tools/grep.rs index b02379e..213e3b4 100644 --- a/src/agent/tools/grep.rs +++ b/src/agent/tools/grep.rs @@ -25,7 +25,7 @@ pub fn tool() -> super::Tool { name: "grep", 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"]}"#, - handler: |_a, v| Box::pin(async move { grep(&v) }), + handler: Arc::new(|_a, v| Box::pin(async move { grep(&v)) }), } } diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 9abc712..515ce9e 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -37,40 +37,37 @@ pub fn memory_tools() -> [super::Tool; 12] { [ 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"]}"#, - 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.", 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.", 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.", 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.", 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.", 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).", 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).", 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.", 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).", 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.", parameters_json: r#"{"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]}"#, - handler: |_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) }) }, + handler: Arc::new(|_a, v| Box::pin(async move { query(&v).await })) }, ] } @@ -79,13 +76,13 @@ pub fn journal_tools() -> [super::Tool; 3] { [ 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)"}}}"#, - 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.", 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.", 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 })) }, ] } diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index ee72975..31cabba 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -21,21 +21,18 @@ mod vision; use std::future::Future; use std::pin::Pin; +use std::sync::Arc; use std::time::Instant; fn default_timeout() -> u64 { 120 } -/// Async tool handler function. -/// Agent is None when called from contexts without an agent (MCP server, subconscious). -pub type ToolHandler = fn( +pub type ToolHandler = Arc>, serde_json::Value, -) -> Pin> + Send>>; +) -> Pin> + Send>> + + Send + Sync>; -/// A tool with its definition and handler — single source of truth. -/// Strings are static — the tool list JSON can be built without -/// serialization by interpolating these directly. -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct Tool { pub name: &'static str, pub description: &'static str, diff --git a/src/agent/tools/read.rs b/src/agent/tools/read.rs index 0320798..330205d 100644 --- a/src/agent/tools/read.rs +++ b/src/agent/tools/read.rs @@ -8,7 +8,7 @@ pub fn tool() -> super::Tool { name: "read_file", 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"]}"#, - handler: |_a, v| Box::pin(async move { read_file(&v) }), + handler: Arc::new(|_a, v| Box::pin(async move { read_file(&v)) }), } } diff --git a/src/agent/tools/vision.rs b/src/agent/tools/vision.rs index e340db2..965ddd6 100644 --- a/src/agent/tools/vision.rs +++ b/src/agent/tools/vision.rs @@ -23,7 +23,7 @@ pub fn tool() -> super::Tool { 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.", 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)) }), } } diff --git a/src/agent/tools/web.rs b/src/agent/tools/web.rs index 9861d81..92f3e50 100644 --- a/src/agent/tools/web.rs +++ b/src/agent/tools/web.rs @@ -9,13 +9,13 @@ pub fn tools() -> [super::Tool; 2] { name: "web_fetch", 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"]}"#, - 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 { name: "web_search", 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"]}"#, - handler: |_a, v| Box::pin(async move { web_search(&v).await }), + handler: Arc::new(|_a, v| Box::pin(async move { web_search(&v).await }), }, ] } diff --git a/src/agent/tools/write.rs b/src/agent/tools/write.rs index ff1cdb0..48a99ee 100644 --- a/src/agent/tools/write.rs +++ b/src/agent/tools/write.rs @@ -8,7 +8,7 @@ pub fn tool() -> super::Tool { name: "write_file", 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"]}"#, - handler: |_a, v| Box::pin(async move { write_file(&v) }), + handler: Arc::new(|_a, v| Box::pin(async move { write_file(&v)) }), } } diff --git a/src/mind/dmn.rs b/src/mind/dmn.rs index 4a84130..da25906 100644 --- a/src/mind/dmn.rs +++ b/src/mind/dmn.rs @@ -389,6 +389,33 @@ impl Subconscious { 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> so the + /// closure can capture a reference back. + pub fn init_output_tool(&mut self, self_arc: std::sync::Arc>) { + 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. pub fn set_state_path(&mut self, path: std::path::PathBuf) { if let Ok(data) = std::fs::read_to_string(&path) { diff --git a/src/mind/mod.rs b/src/mind/mod.rs index c0e5da2..e2cfc55 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -251,7 +251,7 @@ pub struct Mind { pub agent: Arc, pub shared: Arc, pub config: SessionConfig, - subconscious: tokio::sync::Mutex, + subconscious: Arc>, turn_tx: mpsc::Sender<(Result, StreamTarget)>, turn_watch: tokio::sync::watch::Sender, bg_tx: mpsc::UnboundedSender, @@ -287,9 +287,11 @@ impl Mind { sup.load_config(); 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, - subconscious: tokio::sync::Mutex::new(Subconscious::new()), - turn_tx, turn_watch, bg_tx, + subconscious, turn_tx, turn_watch, bg_tx, bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup } } diff --git a/src/subconscious/digest.rs b/src/subconscious/digest.rs index acd5844..aab7bed 100644 --- a/src/subconscious/digest.rs +++ b/src/subconscious/digest.rs @@ -663,31 +663,31 @@ pub fn digest_tools() -> [super::super::agent::tools::Tool; 5] { name: "digest_daily", 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"]}"#, - 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 { name: "digest_weekly", 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"]}"#, - 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 { name: "digest_monthly", 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"]}"#, - 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 { name: "digest_auto", 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":{}}"#, - 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 { name: "digest_links", description: "Parse and apply structural links from digest nodes to the memory graph.", 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 }), }, ] }