agents: extract shared run_one_agent, standardize output formats

Three places duplicated the agent execution loop (build prompt → call
LLM → store output → parse actions → record visits): consolidate.rs,
knowledge.rs, and daemon.rs. Extract into run_one_agent() in
knowledge.rs that all three now call.

Also standardize consolidation agent prompts to use WRITE_NODE/LINK/REFINE
— the same commands the parser handles. Previously agents output
CATEGORIZE/NOTE/EXTRACT/DIGEST/DIFFERENTIATE/MERGE/COMPRESS which were
silently dropped after the second-LLM-call removal.
This commit is contained in:
ProofOfConcept 2026-03-10 17:33:12 -04:00
parent f6ea659975
commit fe7f636ad3
8 changed files with 124 additions and 189 deletions

View file

@ -13,7 +13,6 @@
// second LLM call that was previously needed.
use super::digest;
use super::llm::call_sonnet;
use super::knowledge;
use crate::neuro;
use crate::store::{self, Store};
@ -102,24 +101,10 @@ pub fn consolidate_full_with_progress(
*store = Store::load()?;
}
let agent_batch = match super::prompts::agent_prompt(store, agent_type, *count) {
Ok(b) => b,
Err(e) => {
let msg = format!(" ERROR building prompt: {}", e);
log_line(&mut log_buf, &msg);
eprintln!("{}", msg);
agent_errors += 1;
continue;
}
};
log_line(&mut log_buf, &format!(" Prompt: {} chars (~{} tokens), {} nodes",
agent_batch.prompt.len(), agent_batch.prompt.len() / 4, agent_batch.node_keys.len()));
let response = match call_sonnet("consolidate", &agent_batch.prompt) {
let result = match knowledge::run_one_agent(store, agent_type, *count, "consolidate") {
Ok(r) => r,
Err(e) => {
let msg = format!(" ERROR from Sonnet: {}", e);
let msg = format!(" ERROR: {}", e);
log_line(&mut log_buf, &msg);
eprintln!("{}", msg);
agent_errors += 1;
@ -127,34 +112,19 @@ pub fn consolidate_full_with_progress(
}
};
// Store report as a node (for audit trail)
let ts = store::format_datetime(store::now_epoch())
.replace([':', '-', 'T'], "");
let report_key = format!("_consolidation-{}-{}", agent_type, ts);
store.upsert_provenance(&report_key, &response,
store::Provenance::AgentConsolidate).ok();
// Parse and apply actions inline — same parser as knowledge loop
let actions = knowledge::parse_all_actions(&response);
let no_ops = knowledge::count_no_ops(&response);
let mut applied = 0;
for action in &actions {
for action in &result.actions {
if knowledge::apply_action(store, action, agent_type, &ts, 0) {
applied += 1;
}
}
total_actions += actions.len();
total_actions += result.actions.len();
total_applied += applied;
// Record visits for successfully processed nodes
if !agent_batch.node_keys.is_empty() {
if let Err(e) = store.record_agent_visits(&agent_batch.node_keys, agent_type) {
log_line(&mut log_buf, &format!(" Visit recording: {}", e));
}
}
let msg = format!(" Done: {} actions ({} applied, {} no-ops) → {}",
actions.len(), applied, no_ops, report_key);
let msg = format!(" Done: {} actions ({} applied, {} no-ops)",
result.actions.len(), applied, result.no_ops);
log_line(&mut log_buf, &msg);
on_progress(&msg);
println!("{}", msg);

View file

@ -130,43 +130,19 @@ fn job_consolidation_agent(
ctx.log_line("loading store");
let mut store = crate::store::Store::load()?;
let label = if batch > 0 {
format!("{} (batch={})", agent, batch)
} else {
agent.to_string()
};
ctx.log_line(&format!("building prompt: {}", label));
let agent_batch = super::prompts::agent_prompt(&store, &agent, batch)?;
ctx.log_line(&format!("prompt: {} chars ({} nodes), calling Sonnet",
agent_batch.prompt.len(), agent_batch.node_keys.len()));
let response = super::llm::call_sonnet("consolidate", &agent_batch.prompt)?;
ctx.log_line(&format!("running agent: {} (batch={})", agent, batch));
let result = super::knowledge::run_one_agent(&mut store, &agent, batch, "consolidate")?;
let ts = crate::store::format_datetime(crate::store::now_epoch())
.replace([':', '-', 'T'], "");
let report_key = format!("_consolidation-{}-{}", agent, ts);
store.upsert_provenance(&report_key, &response,
crate::store::Provenance::AgentConsolidate).ok();
// Parse and apply actions inline
let actions = super::knowledge::parse_all_actions(&response);
let mut applied = 0;
for action in &actions {
for action in &result.actions {
if super::knowledge::apply_action(&mut store, action, &agent, &ts, 0) {
applied += 1;
}
}
// Record visits for successfully processed nodes
if !agent_batch.node_keys.is_empty() {
if let Err(e) = store.record_agent_visits(&agent_batch.node_keys, &agent) {
ctx.log_line(&format!("visit recording: {}", e));
}
}
ctx.log_line(&format!("done: {} actions ({} applied) → {}",
actions.len(), applied, report_key));
ctx.log_line(&format!("done: {} actions ({} applied)", result.actions.len(), applied));
Ok(())
})
}

View file

@ -319,7 +319,58 @@ fn agent_provenance(agent: &str) -> store::Provenance {
}
// ---------------------------------------------------------------------------
// Agent runners
// Shared agent execution
// ---------------------------------------------------------------------------
/// Result of running a single agent through the common pipeline.
pub struct AgentResult {
pub output: String,
pub actions: Vec<Action>,
pub no_ops: usize,
pub node_keys: Vec<String>,
}
/// Run a single agent: build prompt → call LLM → store output → parse actions → record visits.
///
/// This is the common pipeline shared by the knowledge loop, consolidation pipeline,
/// and daemon. Callers handle action application (with or without depth tracking).
pub fn run_one_agent(
store: &mut Store,
agent_name: &str,
batch_size: usize,
llm_tag: &str,
) -> Result<AgentResult, String> {
let def = super::defs::get_def(agent_name)
.ok_or_else(|| format!("no .agent file for {}", agent_name))?;
let agent_batch = super::defs::run_agent(store, &def, batch_size)?;
let output = llm::call_sonnet(llm_tag, &agent_batch.prompt)?;
// Store raw output for audit trail
let ts = store::format_datetime(store::now_epoch())
.replace([':', '-', 'T'], "");
let report_key = format!("_{}-{}-{}", llm_tag, agent_name, ts);
let provenance = agent_provenance(agent_name);
store.upsert_provenance(&report_key, &output, provenance).ok();
let actions = parse_all_actions(&output);
let no_ops = count_no_ops(&output);
// Record visits for processed nodes
if !agent_batch.node_keys.is_empty() {
store.record_agent_visits(&agent_batch.node_keys, agent_name).ok();
}
Ok(AgentResult {
output,
actions,
no_ops,
node_keys: agent_batch.node_keys,
})
}
// ---------------------------------------------------------------------------
// Conversation fragment selection
// ---------------------------------------------------------------------------
/// Extract human-readable dialogue from a conversation JSONL
@ -573,51 +624,18 @@ fn run_cycle(
for agent_name in &agent_names {
eprintln!("\n --- {} (n={}) ---", agent_name, config.batch_size);
let def = match super::defs::get_def(agent_name) {
Some(d) => d,
None => {
eprintln!(" SKIP: no .agent file for {}", agent_name);
continue;
}
};
let agent_batch = match super::defs::run_agent(&store, &def, config.batch_size) {
Ok(b) => b,
Err(e) => {
eprintln!(" ERROR building prompt: {}", e);
continue;
}
};
eprintln!(" prompt: {} chars ({} nodes)", agent_batch.prompt.len(), agent_batch.node_keys.len());
let output = llm::call_sonnet("knowledge", &agent_batch.prompt);
// Record visits for processed nodes
if !agent_batch.node_keys.is_empty() {
if let Err(e) = store.record_agent_visits(&agent_batch.node_keys, agent_name) {
eprintln!(" visit recording: {}", e);
}
}
let output = match output {
Ok(o) => o,
let result = match run_one_agent(&mut store, agent_name, config.batch_size, "knowledge") {
Ok(r) => r,
Err(e) => {
eprintln!(" ERROR: {}", e);
continue;
}
};
// Store raw output as a node (for debugging/audit)
let raw_key = format!("_knowledge-{}-{}", agent_name, timestamp);
let raw_content = format!("# {} Agent Results — {}\n\n{}", agent_name, timestamp, output);
store.upsert_provenance(&raw_key, &raw_content,
agent_provenance(agent_name)).ok();
let mut actions = result.actions;
all_no_ops += result.no_ops;
let mut actions = parse_all_actions(&output);
let no_ops = count_no_ops(&output);
all_no_ops += no_ops;
eprintln!(" Actions: {} No-ops: {}", actions.len(), no_ops);
eprintln!(" Actions: {} No-ops: {}", actions.len(), result.no_ops);
let mut applied = 0;
for action in &mut actions {