agents: tool-enabled LLM calls + DELETE action support

Add call_for_def() that threads model and tools from agent definitions
through to claude CLI. Tool-enabled agents get --allowedTools instead
of --tools "" and a longer 15-minute timeout for multi-turn work.

Add ActionKind::Delete with parse/apply support so agents can delete
nodes (used by organize agent for deduplication).

Use call_for_def() in run_one_agent instead of hardcoded call_sonnet.
This commit is contained in:
ProofOfConcept 2026-03-13 18:50:06 -04:00
parent 76b8e69749
commit bcf13c564a
3 changed files with 63 additions and 6 deletions

View file

@ -40,6 +40,8 @@ fn log_usage(agent: &str, model: &str, prompt: &str, response: &str,
/// Maximum time to wait for a claude subprocess before killing it.
const SUBPROCESS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300); // 5 minutes
/// Longer timeout for agents with tool access (multi-turn conversations).
const TOOL_AGENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900); // 15 minutes
/// Call a model via claude CLI. Returns the response text.
///
@ -47,6 +49,19 @@ const SUBPROCESS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3
/// parent daemon exits — no more orphaned claude processes.
/// Times out after 5 minutes to prevent blocking the daemon forever.
fn call_model(agent: &str, model: &str, prompt: &str) -> Result<String, String> {
call_model_with_tools(agent, model, prompt, &[])
}
/// Call a model via claude CLI, optionally with allowed tools.
///
/// When `tools` is empty, all tools are disabled (`--tools ""`).
/// When `tools` has entries, they're passed as `--allowedTools` patterns
/// (e.g. `["Bash(poc-memory:*)"]`), letting the agent call those tools
/// in Claude's native tool loop.
fn call_model_with_tools(agent: &str, model: &str, prompt: &str,
tools: &[String]) -> Result<String, String> {
let timeout = if tools.is_empty() { SUBPROCESS_TIMEOUT } else { TOOL_AGENT_TIMEOUT };
// Write prompt to temp file (claude CLI needs file input for large prompts)
let tmp = std::env::temp_dir().join(format!("poc-llm-{}-{:?}.txt",
std::process::id(), std::thread::current().id()));
@ -54,8 +69,17 @@ fn call_model(agent: &str, model: &str, prompt: &str) -> Result<String, String>
.map_err(|e| format!("write temp prompt: {}", e))?;
let mut cmd = Command::new("claude");
cmd.args(["-p", "--model", model, "--tools", "", "--no-session-persistence",
"--strict-mcp-config"])
if tools.is_empty() {
cmd.args(["-p", "--model", model, "--tools", "", "--no-session-persistence",
"--strict-mcp-config"]);
} else {
cmd.args(["-p", "--model", model, "--no-session-persistence",
"--strict-mcp-config", "--allowedTools"]);
for tool in tools {
cmd.arg(tool);
}
}
cmd
.stdin(fs::File::open(&tmp).map_err(|e| format!("open temp: {}", e))?)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
@ -87,7 +111,7 @@ fn call_model(agent: &str, model: &str, prompt: &str) -> Result<String, String>
let cancel_flag = cancel.clone();
let watchdog = std::thread::spawn(move || {
// Sleep in 1s increments so we can check the cancel flag
let deadline = std::time::Instant::now() + SUBPROCESS_TIMEOUT;
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) {
return;
@ -119,7 +143,7 @@ fn call_model(agent: &str, model: &str, prompt: &str) -> Result<String, String>
match result {
Ok(output) => {
let elapsed = start.elapsed().as_millis();
if elapsed > SUBPROCESS_TIMEOUT.as_millis() - 1000 {
if elapsed > timeout.as_millis() - 1000 {
log_usage(agent, model, prompt, "TIMEOUT", elapsed, false);
return Err(format!("claude timed out after {:.0}s", elapsed as f64 / 1000.0));
}
@ -148,6 +172,11 @@ pub(crate) fn call_haiku(agent: &str, prompt: &str) -> Result<String, String> {
call_model(agent, "haiku", prompt)
}
/// Call a model using an agent definition's model and tool configuration.
pub(crate) fn call_for_def(def: &super::defs::AgentDef, prompt: &str) -> Result<String, String> {
call_model_with_tools(&def.agent, &def.model, prompt, &def.tools)
}
/// Parse a JSON response, handling markdown fences.
pub(crate) fn parse_json_response(response: &str) -> Result<serde_json::Value, String> {
let cleaned = response.trim();