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:
parent
d167b11283
commit
12798eeae2
15 changed files with 74 additions and 51 deletions
|
|
@ -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 })),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 })) },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 })) },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue