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:
parent
76b8e69749
commit
bcf13c564a
3 changed files with 63 additions and 6 deletions
|
|
@ -233,6 +233,8 @@ pub fn apply_consolidation(store: &mut Store, do_apply: bool, report_key: Option
|
||||||
println!(" REFINE {}", key),
|
println!(" REFINE {}", key),
|
||||||
knowledge::ActionKind::Demote { key } =>
|
knowledge::ActionKind::Demote { key } =>
|
||||||
println!(" DEMOTE {}", key),
|
println!(" DEMOTE {}", key),
|
||||||
|
knowledge::ActionKind::Delete { key } =>
|
||||||
|
println!(" DELETE {}", key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("\nTo apply: poc-memory apply-consolidation --apply");
|
println!("\nTo apply: poc-memory apply-consolidation --apply");
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ pub enum ActionKind {
|
||||||
Demote {
|
Demote {
|
||||||
key: String,
|
key: String,
|
||||||
},
|
},
|
||||||
|
Delete {
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
|
@ -180,11 +183,28 @@ pub fn parse_demotes(text: &str) -> Vec<Action> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_deletes(text: &str) -> Vec<Action> {
|
||||||
|
let re = Regex::new(r"(?m)^DELETE\s+(\S+)").unwrap();
|
||||||
|
re.captures_iter(text)
|
||||||
|
.map(|cap| Action {
|
||||||
|
kind: ActionKind::Delete {
|
||||||
|
key: cap[1].to_string(),
|
||||||
|
},
|
||||||
|
confidence: Confidence::High,
|
||||||
|
weight: 1.0,
|
||||||
|
depth: 0,
|
||||||
|
applied: None,
|
||||||
|
rejected_reason: None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_all_actions(text: &str) -> Vec<Action> {
|
pub fn parse_all_actions(text: &str) -> Vec<Action> {
|
||||||
let mut actions = parse_write_nodes(text);
|
let mut actions = parse_write_nodes(text);
|
||||||
actions.extend(parse_links(text));
|
actions.extend(parse_links(text));
|
||||||
actions.extend(parse_refines(text));
|
actions.extend(parse_refines(text));
|
||||||
actions.extend(parse_demotes(text));
|
actions.extend(parse_demotes(text));
|
||||||
|
actions.extend(parse_deletes(text));
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,7 +263,7 @@ fn agent_base_depth(agent: &str) -> Option<i32> {
|
||||||
|
|
||||||
pub fn compute_action_depth(db: &DepthDb, action: &Action, agent: &str) -> i32 {
|
pub fn compute_action_depth(db: &DepthDb, action: &Action, agent: &str) -> i32 {
|
||||||
match &action.kind {
|
match &action.kind {
|
||||||
ActionKind::Link { .. } | ActionKind::Demote { .. } => -1,
|
ActionKind::Link { .. } | ActionKind::Demote { .. } | ActionKind::Delete { .. } => -1,
|
||||||
ActionKind::Refine { key, .. } => db.get(key),
|
ActionKind::Refine { key, .. } => db.get(key),
|
||||||
ActionKind::WriteNode { covers, .. } => {
|
ActionKind::WriteNode { covers, .. } => {
|
||||||
if !covers.is_empty() {
|
if !covers.is_empty() {
|
||||||
|
|
@ -336,6 +356,9 @@ pub fn apply_action(
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ActionKind::Delete { key } => {
|
||||||
|
store.delete_node(key).is_ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -582,7 +605,7 @@ pub fn run_one_agent(
|
||||||
.ok_or_else(|| format!("no .agent file for {}", agent_name))?;
|
.ok_or_else(|| format!("no .agent file for {}", agent_name))?;
|
||||||
let agent_batch = super::defs::run_agent(store, &def, batch_size)?;
|
let agent_batch = super::defs::run_agent(store, &def, batch_size)?;
|
||||||
|
|
||||||
let output = llm::call_sonnet(llm_tag, &agent_batch.prompt)?;
|
let output = llm::call_for_def(&def, &agent_batch.prompt)?;
|
||||||
|
|
||||||
// Store raw output for audit trail
|
// Store raw output for audit trail
|
||||||
let ts = store::compact_timestamp();
|
let ts = store::compact_timestamp();
|
||||||
|
|
@ -920,6 +943,9 @@ fn run_cycle(
|
||||||
ActionKind::Demote { key } => {
|
ActionKind::Demote { key } => {
|
||||||
eprintln!(" DEMOTE {}", key);
|
eprintln!(" DEMOTE {}", key);
|
||||||
}
|
}
|
||||||
|
ActionKind::Delete { key } => {
|
||||||
|
eprintln!(" DELETE {}", key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if apply_action(&mut store, action, agent_name, ×tamp, depth) {
|
if apply_action(&mut store, action, agent_name, ×tamp, depth) {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// 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
|
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.
|
/// 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.
|
/// parent daemon exits — no more orphaned claude processes.
|
||||||
/// Times out after 5 minutes to prevent blocking the daemon forever.
|
/// Times out after 5 minutes to prevent blocking the daemon forever.
|
||||||
fn call_model(agent: &str, model: &str, prompt: &str) -> Result<String, String> {
|
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)
|
// Write prompt to temp file (claude CLI needs file input for large prompts)
|
||||||
let tmp = std::env::temp_dir().join(format!("poc-llm-{}-{:?}.txt",
|
let tmp = std::env::temp_dir().join(format!("poc-llm-{}-{:?}.txt",
|
||||||
std::process::id(), std::thread::current().id()));
|
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))?;
|
.map_err(|e| format!("write temp prompt: {}", e))?;
|
||||||
|
|
||||||
let mut cmd = Command::new("claude");
|
let mut cmd = Command::new("claude");
|
||||||
|
if tools.is_empty() {
|
||||||
cmd.args(["-p", "--model", model, "--tools", "", "--no-session-persistence",
|
cmd.args(["-p", "--model", model, "--tools", "", "--no-session-persistence",
|
||||||
"--strict-mcp-config"])
|
"--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))?)
|
.stdin(fs::File::open(&tmp).map_err(|e| format!("open temp: {}", e))?)
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
.stderr(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 cancel_flag = cancel.clone();
|
||||||
let watchdog = std::thread::spawn(move || {
|
let watchdog = std::thread::spawn(move || {
|
||||||
// Sleep in 1s increments so we can check the cancel flag
|
// 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 {
|
while std::time::Instant::now() < deadline {
|
||||||
if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) {
|
if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -119,7 +143,7 @@ fn call_model(agent: &str, model: &str, prompt: &str) -> Result<String, String>
|
||||||
match result {
|
match result {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
let elapsed = start.elapsed().as_millis();
|
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);
|
log_usage(agent, model, prompt, "TIMEOUT", elapsed, false);
|
||||||
return Err(format!("claude timed out after {:.0}s", elapsed as f64 / 1000.0));
|
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_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.
|
/// Parse a JSON response, handling markdown fences.
|
||||||
pub(crate) fn parse_json_response(response: &str) -> Result<serde_json::Value, String> {
|
pub(crate) fn parse_json_response(response: &str) -> Result<serde_json::Value, String> {
|
||||||
let cleaned = response.trim();
|
let cleaned = response.trim();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue