diff --git a/src/subconscious/defs.rs b/src/subconscious/defs.rs index 7039c6a..983b420 100644 --- a/src/subconscious/defs.rs +++ b/src/subconscious/defs.rs @@ -561,6 +561,12 @@ fn resolve( Some(Resolved { text, keys }) } + // tool:NAME ARGS — run a tool call and include its output + _ if name.starts_with("tool:") => { + let spec = name[5..].trim(); + resolve_tool(spec, store, graph) + } + // bash:COMMAND — run a shell command and include its stdout _ if name.starts_with("bash:") => { let cmd = &name[5..]; @@ -721,6 +727,44 @@ fn resolve_memory_ratio() -> String { pct, keys.len(), memory_bytes / 1024, transcript_size / 1024) } +/// Resolve a {{tool: name {args}}} placeholder by calling the tool +/// handler from the registry. Uses block_in_place to bridge sync→async. +fn resolve_tool(spec: &str, _store: &Store, _graph: &Graph) -> Option { + // Parse "tool_name {json args}" or "tool_name arg" + let (name, args) = match spec.find('{') { + Some(i) => { + let name = spec[..i].trim(); + let args: serde_json::Value = serde_json::from_str(&spec[i..]).ok()?; + (name, args) + } + None => { + let mut parts = spec.splitn(2, char::is_whitespace); + let name = parts.next()?; + match parts.next() { + Some(arg) => (name, serde_json::json!({"key": arg})), + None => (name, serde_json::json!({})), + } + } + }; + + let tools = crate::agent::tools::tools(); + let tool = tools.iter().find(|t| t.name == name)?; + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on( + (tool.handler)(None, args.clone()) + ) + }); + + match result { + Ok(text) => Some(Resolved { text, keys: vec![] }), + Err(e) => { + eprintln!("[defs] {{{{tool: {}}}}} failed: {}", name, e); + Some(Resolved { text: format!("(tool error: {})", e), keys: vec![] }) + } + } +} + /// Resolve all {{placeholder}} patterns in a prompt template. /// Returns the resolved text and all node keys collected from placeholders. pub fn resolve_placeholders(