From 6932e05b385284d21525c3dc35a4eac4ed0636b9 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Tue, 17 Mar 2026 00:37:12 -0400 Subject: [PATCH] Remove dead action pipeline: parsing, depth tracking, knowledge loop, fact miner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents now apply changes via tool calls (poc-memory write/link-add/etc) during the LLM call. The old pipeline — where agents output WRITE_NODE/ LINK/REFINE text, which was parsed and applied separately — is dead code. Removed: - Action/ActionKind/Confidence types and all parse_* functions - DepthDb, depth tracking, confidence gating - apply_action, stamp_content, has_edge - NamingResolution, resolve_naming and related naming agent code - KnowledgeLoopConfig, CycleResult, GraphMetrics, convergence checking - run_knowledge_loop, run_cycle, check_convergence - apply_consolidation (old report re-processing) - fact_mine.rs (folded into observation agent) - resolve_action_names Simplified: - AgentResult no longer carries actions/no_ops - run_and_apply_with_log just runs the agent - consolidate_full simplified action tracking -1364 lines. --- poc-memory/src/agents/consolidate.rs | 107 +--- poc-memory/src/agents/daemon.rs | 22 +- poc-memory/src/agents/fact_mine.rs | 303 --------- poc-memory/src/agents/knowledge.rs | 899 +-------------------------- poc-memory/src/agents/mod.rs | 5 +- poc-memory/src/cli/agent.rs | 69 +- poc-memory/src/lib.rs | 2 +- 7 files changed, 43 insertions(+), 1364 deletions(-) delete mode 100644 poc-memory/src/agents/fact_mine.rs diff --git a/poc-memory/src/agents/consolidate.rs b/poc-memory/src/agents/consolidate.rs index 50b6f22..fca539c 100644 --- a/poc-memory/src/agents/consolidate.rs +++ b/poc-memory/src/agents/consolidate.rs @@ -1,16 +1,12 @@ -// Consolidation pipeline: plan → agents → apply → digests → links +// Consolidation pipeline: plan → agents → maintenance → digests → links // // consolidate_full() runs the full autonomous consolidation: // 1. Plan: analyze metrics, allocate agents -// 2. Execute: run each agent, parse + apply actions inline +// 2. Execute: run each agent (agents apply changes via tool calls) // 3. Graph maintenance (orphans, degree cap) // 4. Digest: generate missing daily/weekly/monthly digests // 5. Links: apply links extracted from digests // 6. Summary: final metrics comparison -// -// Actions are parsed directly from agent output using the same parser -// as the knowledge loop (WRITE_NODE, LINK, REFINE), eliminating the -// second LLM call that was previously needed. use super::digest; use super::knowledge; @@ -25,7 +21,6 @@ fn log_line(buf: &mut String, line: &str) { } /// Run the full autonomous consolidation pipeline with logging. -/// If `on_progress` is provided, it's called at each significant step. pub fn consolidate_full(store: &mut Store) -> Result<(), String> { consolidate_full_with_progress(store, &|_| {}) } @@ -60,8 +55,6 @@ pub fn consolidate_full_with_progress( log_line(&mut log_buf, "\n--- Step 2: Execute agents ---"); let mut agent_num = 0usize; let mut agent_errors = 0usize; - let mut total_applied = 0usize; - let mut total_actions = 0usize; let batch_size = 5; let runs = plan.to_agent_runs(batch_size); @@ -83,27 +76,24 @@ pub fn consolidate_full_with_progress( *store = Store::load()?; } - let (total, applied) = match knowledge::run_and_apply(store, agent_type, *count, "consolidate") { - Ok(r) => r, + match knowledge::run_and_apply(store, agent_type, *count, "consolidate") { + Ok(()) => { + let msg = format!(" Done"); + log_line(&mut log_buf, &msg); + on_progress(&msg); + println!("{}", msg); + } Err(e) => { let msg = format!(" ERROR: {}", e); log_line(&mut log_buf, &msg); eprintln!("{}", msg); agent_errors += 1; - continue; } - }; - total_actions += total; - total_applied += applied; - - let msg = format!(" Done: {} actions ({} applied)", total, applied); - log_line(&mut log_buf, &msg); - on_progress(&msg); - println!("{}", msg); + } } - log_line(&mut log_buf, &format!("\nAgents complete: {} run, {} errors, {} actions ({} applied)", - agent_num - agent_errors, agent_errors, total_actions, total_applied)); + log_line(&mut log_buf, &format!("\nAgents complete: {} run, {} errors", + agent_num - agent_errors, agent_errors)); store.save()?; // --- Step 3: Link orphans --- @@ -183,76 +173,3 @@ pub fn consolidate_full_with_progress( Ok(()) } - -/// Re-parse and apply actions from stored consolidation reports. -/// This is for manually re-processing reports — during normal consolidation, -/// actions are applied inline as each agent runs. -pub fn apply_consolidation(store: &mut Store, do_apply: bool, report_key: Option<&str>) -> Result<(), String> { - let reports: Vec = if let Some(key) = report_key { - vec![key.to_string()] - } else { - // Find the most recent batch of reports - let mut keys: Vec<&String> = store.nodes.keys() - .filter(|k| k.starts_with("_consolidation-") && !k.contains("-actions-") && !k.contains("-log-")) - .collect(); - keys.sort(); - keys.reverse(); - - if keys.is_empty() { return Ok(()); } - - let latest_ts = keys[0].rsplit('-').next().unwrap_or("").to_string(); - keys.into_iter() - .filter(|k| k.ends_with(&latest_ts)) - .cloned() - .collect() - }; - - if reports.is_empty() { - println!("No consolidation reports found."); - return Ok(()); - } - - println!("Found {} reports:", reports.len()); - let mut all_actions = Vec::new(); - for key in &reports { - let content = store.nodes.get(key).map(|n| n.content.as_str()).unwrap_or(""); - let actions = knowledge::parse_all_actions(content); - println!(" {} → {} actions", key, actions.len()); - all_actions.extend(actions); - } - - if !do_apply { - println!("\nDRY RUN — {} actions parsed", all_actions.len()); - for action in &all_actions { - match &action.kind { - knowledge::ActionKind::Link { source, target } => - println!(" LINK {} → {}", source, target), - knowledge::ActionKind::WriteNode { key, .. } => - println!(" WRITE {}", key), - knowledge::ActionKind::Refine { key, .. } => - println!(" REFINE {}", key), - knowledge::ActionKind::Demote { key } => - println!(" DEMOTE {}", key), - knowledge::ActionKind::Delete { key } => - println!(" DELETE {}", key), - } - } - println!("\nTo apply: poc-memory apply-consolidation --apply"); - return Ok(()); - } - - let ts = store::compact_timestamp(); - let mut applied = 0; - for action in &all_actions { - if knowledge::apply_action(store, action, "consolidate", &ts, 0) { - applied += 1; - } - } - - if applied > 0 { - store.save()?; - } - - println!("Applied: {}/{} actions", applied, all_actions.len()); - Ok(()) -} diff --git a/poc-memory/src/agents/daemon.rs b/poc-memory/src/agents/daemon.rs index dbf88e1..6e4d76e 100644 --- a/poc-memory/src/agents/daemon.rs +++ b/poc-memory/src/agents/daemon.rs @@ -59,8 +59,8 @@ fn job_consolidation_agent( ctx.log_line(msg); log_event(&job_name2, "progress", msg); }; - let (total, applied) = super::knowledge::run_and_apply_with_log(&mut store, &agent, batch, "consolidate", &log)?; - ctx.log_line(&format!("done: {} actions ({} applied)", total, applied)); + super::knowledge::run_and_apply_with_log(&mut store, &agent, batch, "consolidate", &log)?; + ctx.log_line("done"); Ok(()) }) } @@ -377,20 +377,10 @@ fn job_digest_links(ctx: &ExecutionContext) -> Result<(), TaskError> { }) } -fn job_knowledge_loop(ctx: &ExecutionContext) -> Result<(), TaskError> { - run_job(ctx, "knowledge-loop", || { - let config = super::knowledge::KnowledgeLoopConfig { - max_cycles: 100, - batch_size: 5, - ..Default::default() - }; - ctx.log_line("running agents"); - let results = super::knowledge::run_knowledge_loop(&config)?; - ctx.log_line(format!("{} cycles, {} actions", - results.len(), - results.iter().map(|r| r.total_applied).sum::())); - Ok(()) - }) +fn job_knowledge_loop(_ctx: &ExecutionContext) -> Result<(), TaskError> { + // Knowledge loop removed — agents now use tool calls directly. + // Consolidation is handled by consolidate_full() in the consolidate job. + Ok(()) } fn job_digest(ctx: &ExecutionContext) -> Result<(), TaskError> { diff --git a/poc-memory/src/agents/fact_mine.rs b/poc-memory/src/agents/fact_mine.rs deleted file mode 100644 index 8983d27..0000000 --- a/poc-memory/src/agents/fact_mine.rs +++ /dev/null @@ -1,303 +0,0 @@ -// fact_mine.rs — extract atomic factual claims from conversation transcripts -// -// Chunks conversation text into overlapping windows, sends each to Haiku -// for extraction, deduplicates by claim text. Output: JSON array of facts. -// -// Uses Haiku (not Sonnet) for cost efficiency on high-volume extraction. - -use crate::config; -use super::llm; -use super::transcript; -use crate::store; - -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::path::Path; - -const CHARS_PER_TOKEN: usize = 4; -const WINDOW_TOKENS: usize = 2000; -const OVERLAP_TOKENS: usize = 200; -const WINDOW_CHARS: usize = WINDOW_TOKENS * CHARS_PER_TOKEN; -const OVERLAP_CHARS: usize = OVERLAP_TOKENS * CHARS_PER_TOKEN; - -fn extraction_prompt() -> String { - let cfg = config::get(); - format!( -r#"Extract atomic factual claims from this conversation excerpt. - -Speakers are labeled [{user}] and [{assistant}] in the transcript. -Use their proper names in claims — not "the user" or "the assistant." - -Each claim should be: -- A single verifiable statement -- Specific enough to be useful in isolation -- Tagged with domain (e.g., bcachefs/btree, bcachefs/alloc, bcachefs/journal, - bcachefs/ec, bcachefs/reconcile, rust/idioms, workflow/preferences, - linux/kernel, memory/design, identity/personal) -- Tagged with confidence: "stated" (explicitly said), "implied" (logically follows), - or "speculative" (hypothesis, not confirmed) -- Include which speaker said it ("{user}", "{assistant}", or "Unknown") - -Do NOT extract: -- Opinions or subjective assessments -- Conversational filler or greetings -- Things that are obviously common knowledge -- Restatements of the same fact (pick the clearest version) -- System messages, tool outputs, or error logs (extract what was LEARNED from them) -- Anything about the conversation itself ("{user} and {assistant} discussed...") -- Facts only relevant to this specific conversation (e.g. transient file paths, mid-debug state) - -Output as a JSON array. Each element: -{{ - "claim": "the exact factual statement", - "domain": "category/subcategory", - "confidence": "stated|implied|speculative", - "speaker": "{user}|{assistant}|Unknown" -}} - -If the excerpt contains no extractable facts, output an empty array: [] - ---- CONVERSATION EXCERPT --- -"#, user = cfg.user_name, assistant = cfg.assistant_name) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Fact { - pub claim: String, - pub domain: String, - pub confidence: String, - pub speaker: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub source_file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source_chunk: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source_offset: Option, -} - -/// Extract user/assistant text messages from a JSONL transcript. -fn extract_messages(path: &Path) -> Vec { - transcript::parse_transcript(path) - .unwrap_or_default() - .into_iter() - .filter(|m| m.text.len() >= 20) - .collect() -} - -/// Format messages into a single text for chunking. -fn format_for_extraction(messages: &[transcript::TranscriptMessage]) -> String { - let cfg = config::get(); - messages.iter() - .map(|msg| { - let role = if msg.role == "user" { &cfg.user_name } else { &cfg.assistant_name }; - let text = crate::util::truncate(&msg.text, 2800, "\n[...truncated...]"); - let ts = if msg.timestamp.len() >= 19 { &msg.timestamp[..19] } else { "" }; - if ts.is_empty() { - format!("[{}] {}", role, text) - } else { - format!("[{} {}] {}", role, ts, text) - } - }) - .collect::>() - .join("\n\n") -} - -/// Split text into overlapping windows, breaking at paragraph boundaries. -fn chunk_text(text: &str) -> Vec<(usize, &str)> { - let mut chunks = Vec::new(); - let mut start = 0; - - while start < text.len() { - let mut end = text.floor_char_boundary((start + WINDOW_CHARS).min(text.len())); - - // Try to break at a paragraph boundary - if end < text.len() { - if let Some(para) = text[start..end].rfind("\n\n") { - if para > WINDOW_CHARS / 2 { - end = start + para; - } - } - } - - chunks.push((start, &text[start..end])); - - let next = text.floor_char_boundary(end.saturating_sub(OVERLAP_CHARS)); - if next <= start { - start = end; - } else { - start = next; - } - } - - chunks -} - -/// Parse JSON facts from model response. -fn parse_facts(response: &str) -> Vec { - let cleaned = response.trim(); - // Strip markdown code block - let cleaned = if cleaned.starts_with("```") { - cleaned.lines() - .filter(|l| !l.starts_with("```")) - .collect::>() - .join("\n") - } else { - cleaned.to_string() - }; - - // Find JSON array - let start = cleaned.find('['); - let end = cleaned.rfind(']'); - let (Some(start), Some(end)) = (start, end) else { return Vec::new() }; - - serde_json::from_str(&cleaned[start..=end]).unwrap_or_default() -} - -/// Mine a single transcript for atomic facts. -/// The optional `progress` callback receives status strings (e.g. "chunk 3/47"). -pub fn mine_transcript( - path: &Path, - dry_run: bool, - progress: Option<&dyn Fn(&str)>, -) -> Result, String> { - let filename = path.file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "unknown".into()); - let log = |msg: &str| { - eprintln!("{}", msg); - if let Some(cb) = progress { cb(msg); } - }; - - log(&format!("Mining: {}", filename)); - - let messages = extract_messages(path); - if messages.is_empty() { - log("No messages found"); - return Ok(Vec::new()); - } - log(&format!("{} messages extracted", messages.len())); - - let text = format_for_extraction(&messages); - let chunks = chunk_text(&text); - log(&format!("{} chunks ({} chars)", chunks.len(), text.len())); - - if dry_run { - for (i, (offset, chunk)) in chunks.iter().enumerate() { - eprintln!("\n--- Chunk {} (offset {}, {} chars) ---", i + 1, offset, chunk.len()); - eprintln!("{}", crate::util::truncate(chunk, 500, "")); - if chunk.len() > 500 { - eprintln!(" ... ({} more chars)", chunk.len() - 500); - } - } - return Ok(Vec::new()); - } - - let prompt_prefix = extraction_prompt(); - let mut all_facts = Vec::new(); - for (i, (_offset, chunk)) in chunks.iter().enumerate() { - let status = format!("chunk {}/{} ({} chars)", i + 1, chunks.len(), chunk.len()); - eprint!(" {}...", status); - if let Some(cb) = progress { cb(&status); } - - let prompt = format!("{}{}\n\n--- END OF EXCERPT ---\n\nReturn ONLY a JSON array of factual claims, or [] if none.", prompt_prefix, chunk); - let response = match llm::call_haiku("fact-mine", &prompt) { - Ok(r) => r, - Err(e) => { - eprintln!(" error: {}", e); - continue; - } - }; - - let mut facts = parse_facts(&response); - for fact in &mut facts { - fact.source_file = Some(filename.clone()); - fact.source_chunk = Some(i + 1); - fact.source_offset = Some(*_offset); - } - - eprintln!(" {} facts", facts.len()); - all_facts.extend(facts); - } - - // Deduplicate by claim text - let mut seen = HashSet::new(); - let before = all_facts.len(); - all_facts.retain(|f| seen.insert(f.claim.to_lowercase())); - let dupes = before - all_facts.len(); - if dupes > 0 { - log(&format!("{} duplicates removed", dupes)); - } - - log(&format!("Total: {} unique facts", all_facts.len())); - Ok(all_facts) -} - -/// Mine a transcript and store facts in the capnp store. -/// Returns the number of facts stored. -/// The optional `progress` callback receives status strings for daemon display. -pub fn mine_and_store( - path: &Path, - progress: Option<&dyn Fn(&str)>, -) -> Result { - let facts = mine_transcript(path, false, progress)?; - - let filename = path.file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "unknown".into()); - - let proposed_key = format!("_facts-{}", filename.trim_end_matches(".jsonl")); - - // Always write a marker so we don't re-queue empty transcripts - let json = if facts.is_empty() { - "[]".to_string() - } else { - serde_json::to_string_pretty(&facts) - .map_err(|e| format!("serialize facts: {}", e))? - }; - - let mut store = store::Store::load()?; - - // Run naming resolution to get a good key (and possibly merge into existing) - let resolution = super::knowledge::resolve_naming(&store, &proposed_key, &json); - let key = match resolution { - super::knowledge::NamingResolution::Create(k) => k, - super::knowledge::NamingResolution::MergeInto(existing_key) => { - // Merge: append facts to existing node's content - eprintln!(" Merging facts into existing node: {}", existing_key); - if let Some(node) = store.nodes.get(existing_key.as_str()) { - let merged = format!("{}\n\n{}", node.content, json); - store.upsert_provenance(&existing_key, &merged, "fact-mine:write")?; - store.save()?; - return Ok(facts.len()); - } - // Fallback if existing node disappeared - proposed_key - } - }; - - store.upsert_provenance(&key, &json, "fact-mine:write")?; - store.save()?; - - eprintln!(" Stored {} facts as {}", facts.len(), key); - Ok(facts.len()) -} - -/// Mine transcripts, returning all facts. Skips files with fewer than min_messages. -pub fn mine_batch(paths: &[&Path], min_messages: usize, dry_run: bool) -> Result, String> { - let mut all_facts = Vec::new(); - - for path in paths { - let messages = extract_messages(path); - if messages.len() < min_messages { - eprintln!("Skipping {} ({} messages < {})", - path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default(), - messages.len(), min_messages); - continue; - } - - let facts = mine_transcript(path, dry_run, None)?; - all_facts.extend(facts); - } - - Ok(all_facts) -} diff --git a/poc-memory/src/agents/knowledge.rs b/poc-memory/src/agents/knowledge.rs index 8db1c9c..ac0fd64 100644 --- a/poc-memory/src/agents/knowledge.rs +++ b/poc-memory/src/agents/knowledge.rs @@ -1,385 +1,33 @@ -// knowledge.rs — knowledge agent action parsing, depth tracking, and convergence loop +// knowledge.rs — agent execution and conversation fragment selection // // Agent prompts live in agents/*.agent files, dispatched via defs.rs. // This module handles: -// - Action parsing (WRITE_NODE, LINK, REFINE from LLM output) -// - Inference depth tracking (prevents runaway abstraction) -// - Action application (write to store with provenance) -// - Convergence loop (sequences agents, measures graph stability) +// - Agent execution (build prompt → call LLM with tools → log) // - Conversation fragment selection (for observation agent) +// +// Agents apply changes via tool calls (poc-memory write/link-add/etc) +// during the LLM call — no action parsing needed. -use crate::graph::Graph; -use crate::spectral; use super::llm; -use crate::store::{self, Store, new_relation, RelationType}; +use crate::store::{self, Store}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; // --------------------------------------------------------------------------- -// Action types +// Agent execution // --------------------------------------------------------------------------- -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Action { - pub kind: ActionKind, - pub confidence: Confidence, - pub weight: f64, - pub depth: i32, - pub applied: Option, - pub rejected_reason: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ActionKind { - WriteNode { - key: String, - content: String, - covers: Vec, - }, - Link { - source: String, - target: String, - }, - Refine { - key: String, - content: String, - }, - Demote { - key: String, - }, - Delete { - key: String, - }, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Confidence { - High, - Medium, - Low, -} - -impl Confidence { - /// Weight for delta metrics — how much this action contributes to change measurement. - fn delta_weight(self) -> f64 { - match self { - Self::High => 1.0, - Self::Medium => 0.6, - Self::Low => 0.3, - } - } - - /// Confidence value for depth gating — capped below 1.0 so even "high" must clear thresholds. - fn gate_value(self) -> f64 { - match self { - Self::High => 0.9, - Self::Medium => 0.6, - Self::Low => 0.3, - } - } - - fn parse(s: &str) -> Self { - match s.to_lowercase().as_str() { - "high" => Self::High, - "low" => Self::Low, - _ => Self::Medium, - } - } -} - -// --------------------------------------------------------------------------- -// Action parsing -// --------------------------------------------------------------------------- - -pub fn parse_write_nodes(text: &str) -> Vec { - // Match WRITE_NODE or **WRITE_NODE** with optional backtick-wrapped key - let re = Regex::new(r"(?s)\*{0,2}WRITE_NODE\*{0,2}\s+`?(\S+?)`?\s*\n(.*?)\*{0,2}END_NODE\*{0,2}").unwrap(); - let conf_re = Regex::new(r"(?i)CONFIDENCE:\s*(high|medium|low)").unwrap(); - let covers_re = Regex::new(r"COVERS:\s*(.+)").unwrap(); - - re.captures_iter(text) - .map(|cap| { - let key = cap[1].to_string(); - let mut content = cap[2].trim().to_string(); - - let confidence = conf_re - .captures(&content) - .map(|c| Confidence::parse(&c[1])) - .unwrap_or(Confidence::Medium); - content = conf_re.replace(&content, "").trim().to_string(); - - let covers: Vec = covers_re - .captures(&content) - .map(|c| c[1].split(',').map(|s| s.trim().to_string()).collect()) - .unwrap_or_default(); - content = covers_re.replace(&content, "").trim().to_string(); - - Action { - weight: confidence.delta_weight(), - kind: ActionKind::WriteNode { key, content, covers }, - confidence, - depth: 0, - applied: None, - rejected_reason: None, - } - }) - .collect() -} - -pub fn parse_links(text: &str) -> Vec { - // Match LINK or **LINK** with optional backtick-wrapped keys - let re = Regex::new(r"(?m)^\*{0,2}LINK\*{0,2}\s+`?([^\s`]+)`?\s+`?([^\s`]+)`?").unwrap(); - re.captures_iter(text) - .map(|cap| Action { - kind: ActionKind::Link { - source: cap[1].to_string(), - target: cap[2].to_string(), - }, - confidence: Confidence::Low, - weight: 0.3, - depth: -1, - applied: None, - rejected_reason: None, - }) - .collect() -} - -pub fn parse_refines(text: &str) -> Vec { - let re = Regex::new(r"(?s)REFINE\s+(\S+)\s*\n(.*?)END_REFINE").unwrap(); - re.captures_iter(text) - .map(|cap| { - let key = cap[1].trim_matches('*').trim().to_string(); - Action { - kind: ActionKind::Refine { - key, - content: cap[2].trim().to_string(), - }, - confidence: Confidence::Medium, - weight: 0.7, - depth: 0, - applied: None, - rejected_reason: None, - } - }) - .collect() -} - -pub fn parse_demotes(text: &str) -> Vec { - let re = Regex::new(r"(?m)^DEMOTE\s+(\S+)").unwrap(); - re.captures_iter(text) - .map(|cap| Action { - kind: ActionKind::Demote { - key: cap[1].to_string(), - }, - confidence: Confidence::Medium, - weight: 0.5, - depth: -1, - applied: None, - rejected_reason: None, - }) - .collect() -} - -pub fn parse_deletes(text: &str) -> Vec { - 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 { - let mut actions = parse_write_nodes(text); - actions.extend(parse_links(text)); - actions.extend(parse_refines(text)); - actions.extend(parse_demotes(text)); - actions.extend(parse_deletes(text)); - actions -} - -pub fn count_no_ops(text: &str) -> usize { - let no_conn = Regex::new(r"\bNO_CONNECTION\b").unwrap().find_iter(text).count(); - let affirm = Regex::new(r"\bAFFIRM\b").unwrap().find_iter(text).count(); - let no_extract = Regex::new(r"\bNO_EXTRACTION\b").unwrap().find_iter(text).count(); - no_conn + affirm + no_extract -} - -// --------------------------------------------------------------------------- -// Inference depth tracking -// --------------------------------------------------------------------------- - -const DEPTH_DB_KEY: &str = "_knowledge-depths"; - -#[derive(Default)] -pub struct DepthDb { - depths: HashMap, -} - -impl DepthDb { - pub fn load(store: &Store) -> Self { - let depths = store.nodes.get(DEPTH_DB_KEY) - .and_then(|n| serde_json::from_str(&n.content).ok()) - .unwrap_or_default(); - Self { depths } - } - - pub fn save(&self, store: &mut Store) { - if let Ok(json) = serde_json::to_string(&self.depths) { - store.upsert_provenance(DEPTH_DB_KEY, &json, - "observation:write").ok(); - } - } - - pub fn get(&self, key: &str) -> i32 { - self.depths.get(key).copied().unwrap_or(0) - } - - pub fn set(&mut self, key: String, depth: i32) { - self.depths.insert(key, depth); - } -} - -/// Agent base depths: observation=1, extractor=2, connector=3 -fn agent_base_depth(agent: &str) -> Option { - match agent { - "observation" => Some(1), - "extractor" => Some(2), - "connector" => Some(3), - "challenger" => None, - _ => Some(2), - } -} - -pub fn compute_action_depth(db: &DepthDb, action: &Action, agent: &str) -> i32 { - match &action.kind { - ActionKind::Link { .. } | ActionKind::Demote { .. } | ActionKind::Delete { .. } => -1, - ActionKind::Refine { key, .. } => db.get(key), - ActionKind::WriteNode { covers, .. } => { - if !covers.is_empty() { - covers.iter().map(|k| db.get(k)).max().unwrap_or(0) + 1 - } else { - agent_base_depth(agent).unwrap_or(2) - } - } - } -} - -/// Confidence threshold that scales with inference depth. -pub fn required_confidence(depth: i32, base: f64) -> f64 { - if depth <= 0 { - return 0.0; - } - 1.0 - (1.0 - base).powi(depth) -} - -/// Confidence bonus from real-world use. -pub fn use_bonus(use_count: u32) -> f64 { - if use_count == 0 { - return 0.0; - } - 1.0 - 1.0 / (1.0 + 0.15 * use_count as f64) -} - -// --------------------------------------------------------------------------- -// Action application -// --------------------------------------------------------------------------- - -fn stamp_content(content: &str, agent: &str, timestamp: &str, depth: i32) -> String { - format!("\n{}", agent, timestamp, depth, content) -} - -/// Check if a link already exists between two keys. -fn has_edge(store: &Store, source: &str, target: &str) -> bool { - store.relations.iter().any(|r| { - !r.deleted - && ((r.source_key == source && r.target_key == target) - || (r.source_key == target && r.target_key == source)) - }) -} - -pub fn apply_action( - store: &mut Store, - action: &Action, - agent: &str, - timestamp: &str, - depth: i32, -) -> bool { - match &action.kind { - ActionKind::WriteNode { key, content, .. } => { - let stamped = stamp_content(content, agent, timestamp, depth); - let prov = format!("{}:write", agent); - store.upsert_provenance(key, &stamped, &prov).is_ok() - } - ActionKind::Link { source, target } => { - if has_edge(store, source, target) { - return false; - } - let source_uuid = match store.nodes.get(source.as_str()) { - Some(n) => n.uuid, - None => return false, - }; - let target_uuid = match store.nodes.get(target.as_str()) { - Some(n) => n.uuid, - None => return false, - }; - // Default strength 0.3 — caller should run Jaccard normalization - // after batch apply if needed (building graph per-link is too expensive) - let mut rel = new_relation( - source_uuid, target_uuid, - RelationType::Link, - 0.3, - source, target, - ); - rel.provenance = format!("{}:link", agent); - store.add_relation(rel).is_ok() - } - ActionKind::Refine { key, content } => { - let stamped = stamp_content(content, agent, timestamp, depth); - let prov = format!("{}:refine", agent); - store.upsert_provenance(key, &stamped, &prov).is_ok() - } - ActionKind::Demote { key } => { - if let Some(node) = store.nodes.get_mut(key) { - node.provenance = format!("{}:demote", agent); - node.weight = (node.weight * 0.5).max(0.05); - true - } else { - false - } - } - ActionKind::Delete { key } => { - store.delete_node(key).is_ok() - } - } -} - /// Extract a short slug from agent output for human-readable report keys. -/// Takes the first meaningful line, lowercases, keeps alphanum+hyphens, truncates. fn make_report_slug(output: &str) -> String { let line = output.lines() .map(|l| l.trim()) .find(|l| !l.is_empty() && !l.starts_with('#') && !l.starts_with("```") && !l.starts_with("---")) .unwrap_or(""); - // Strip markdown bold/italic let clean: String = line.replace("**", "").replace('*', ""); - // Keep only alphanumeric, spaces, hyphens let filtered: String = clean.chars() .map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' { c } else { ' ' }) .collect(); - // Collapse whitespace, convert to kebab-case, truncate let slug: String = filtered.split_whitespace() .take(6) .collect::>() @@ -388,223 +36,19 @@ fn make_report_slug(output: &str) -> String { if slug.len() > 60 { slug[..60].to_string() } else { slug } } -fn agent_provenance(agent: &str) -> String { - match agent { - "observation" => "agent:knowledge-observation".to_string(), - "extractor" | "pattern" => "agent:knowledge-pattern".to_string(), - "connector" => "agent:knowledge-connector".to_string(), - "challenger" => "agent:knowledge-challenger".to_string(), - _ => format!("agent:{}", agent), - } -} - -// --------------------------------------------------------------------------- -// Naming resolution — called before creating any new node -// --------------------------------------------------------------------------- - -/// Resolution from the naming agent. -#[derive(Debug)] -pub enum NamingResolution { - /// Create with the proposed key (or a better one). - Create(String), - /// Merge content into an existing node instead. - MergeInto(String), -} - -/// Find existing nodes that might conflict with a proposed new node. -/// Returns up to `limit` (key, content_preview) pairs. -fn find_conflicts( - store: &Store, - proposed_key: &str, - proposed_content: &str, - limit: usize, -) -> Vec<(String, String)> { - use std::collections::BTreeMap; - - // Extract search terms from the key (split on separators) and first ~200 chars of content - let mut terms: BTreeMap = BTreeMap::new(); - for part in proposed_key.split(|c: char| c == '-' || c == '_' || c == '#' || c == '.') { - let p = part.to_lowercase(); - if p.len() >= 3 { - terms.insert(p, 1.0); - } - } - // Add a few content terms - let content_terms = crate::search::extract_query_terms(proposed_content, 5); - for term in content_terms.split_whitespace() { - terms.entry(term.to_string()).or_insert(0.5); - } - - if terms.is_empty() { - return Vec::new(); - } - - // Use component matching to find related nodes - let (seeds, _) = crate::search::match_seeds_opts(&terms, store, true, false); - - let mut results: Vec<(String, f64)> = seeds.into_iter() - .filter(|(k, _)| k != proposed_key) - .collect(); - results.sort_by(|a, b| b.1.total_cmp(&a.1)); - - results.into_iter() - .take(limit) - .filter_map(|(key, _)| { - let node = store.nodes.get(key.as_str())?; - let preview: String = node.content.chars().take(200).collect(); - Some((key, preview)) - }) - .collect() -} - -/// Format the naming prompt for a proposed node. -fn format_naming_prompt( - proposed_key: &str, - proposed_content: &str, - conflicts: &[(String, String)], -) -> String { - let conflict_section = if conflicts.is_empty() { - "(no existing nodes found with overlapping content)".to_string() - } else { - conflicts.iter() - .map(|(key, preview)| format!("### `{}`\n\n{}", key, preview)) - .collect::>() - .join("\n\n") - }; - - // Truncate content for the prompt (don't send huge nodes to Haiku) - let content_preview: String = proposed_content.chars().take(1000).collect(); - - format!( - "# Naming Agent — Node Key Resolution\n\n\ - You are given a proposed new node (key + content) and a list of existing\n\ - nodes that might overlap with it. Decide what to do:\n\n\ - 1. **CREATE** — the proposed key is good and there's no meaningful overlap.\n\ - 2. **RENAME** — the content is unique but the key is bad (UUID, truncated, generic).\n\ - 3. **MERGE_INTO** — an existing node already covers this content.\n\n\ - Good keys: 2-5 words in kebab-case, optionally with `#` subtopic.\n\ - Bad keys: UUIDs, single generic words, truncated auto-slugs.\n\n\ - Respond with exactly ONE line: `CREATE key`, `RENAME better_key`, or `MERGE_INTO existing_key`.\n\n\ - ## Proposed node\n\n\ - Key: `{}`\n\n\ - Content:\n```\n{}\n```\n\n\ - ## Existing nodes that might overlap\n\n\ - {}", - proposed_key, content_preview, conflict_section, - ) -} - -/// Parse naming agent response. -fn parse_naming_response(response: &str) -> Option { - for line in response.lines() { - // Strip backticks — Haiku sometimes wraps the response line in them - let trimmed = line.trim().trim_matches('`').trim(); - if let Some(key) = trimmed.strip_prefix("CREATE ") { - return Some(NamingResolution::Create(key.trim().trim_matches('`').to_string())); - } - if let Some(key) = trimmed.strip_prefix("RENAME ") { - return Some(NamingResolution::Create(key.trim().trim_matches('`').to_string())); - } - if let Some(key) = trimmed.strip_prefix("MERGE_INTO ") { - return Some(NamingResolution::MergeInto(key.trim().trim_matches('`').to_string())); - } - } - None -} - -/// Resolve naming for a proposed WriteNode action. -/// -/// Searches for conflicts, calls the naming LLM (Haiku), and returns -/// either a Create (possibly with a better key) or MergeInto resolution. -/// On LLM failure, falls through to using the proposed key as-is. -pub fn resolve_naming( - store: &Store, - proposed_key: &str, - proposed_content: &str, -) -> NamingResolution { - let conflicts = find_conflicts(store, proposed_key, proposed_content, 5); - let prompt = format_naming_prompt(proposed_key, proposed_content, &conflicts); - - match llm::call_haiku("naming", &prompt) { - Ok(response) => { - match parse_naming_response(&response) { - Some(resolution) => resolution, - None => { - eprintln!("naming: unparseable response, using proposed key"); - NamingResolution::Create(proposed_key.to_string()) - } - } - } - Err(e) => { - eprintln!("naming: LLM error ({}), using proposed key", e); - NamingResolution::Create(proposed_key.to_string()) - } - } -} - -// --------------------------------------------------------------------------- -// Shared agent execution -// --------------------------------------------------------------------------- - -/// Result of running a single agent through the common pipeline. +/// Result of running a single agent. pub struct AgentResult { pub output: String, - pub actions: Vec, - pub no_ops: usize, pub node_keys: Vec, } -/// Resolve naming for all WriteNode actions in a list. -/// -/// For each WriteNode, calls the naming agent to check for conflicts and -/// get a good key. May convert WriteNode → Refine (if MERGE_INTO) or -/// update the key (if RENAME/CREATE with different key). -pub fn resolve_action_names(store: &Store, actions: Vec) -> Vec { - actions.into_iter().map(|action| { - match &action.kind { - ActionKind::WriteNode { key, content, covers } => { - match resolve_naming(store, key, content) { - NamingResolution::Create(new_key) => { - if new_key == *key { - action // keep as-is - } else { - eprintln!("naming: {} → {}", key, new_key); - Action { - kind: ActionKind::WriteNode { - key: new_key, - content: content.clone(), - covers: covers.clone(), - }, - ..action - } - } - } - NamingResolution::MergeInto(existing_key) => { - eprintln!("naming: {} → MERGE_INTO {}", key, existing_key); - Action { - kind: ActionKind::Refine { - key: existing_key, - content: content.clone(), - }, - ..action - } - } - } - } - _ => action, - } - }).collect() -} - -/// Run a single agent and apply its actions (no depth tracking). -/// -/// Returns (total_actions, applied_count) or an error. +/// Run a single agent and return the result (no action application — tools handle that). pub fn run_and_apply( store: &mut Store, agent_name: &str, batch_size: usize, llm_tag: &str, -) -> Result<(usize, usize), String> { +) -> Result<(), String> { run_and_apply_with_log(store, agent_name, batch_size, llm_tag, &|_| {}) } @@ -614,38 +58,17 @@ pub fn run_and_apply_with_log( batch_size: usize, llm_tag: &str, log: &dyn Fn(&str), -) -> Result<(usize, usize), String> { +) -> Result<(), String> { let result = run_one_agent(store, agent_name, batch_size, llm_tag, log, false)?; - let actions = resolve_action_names(store, result.actions); - let ts = store::compact_timestamp(); - let mut applied = 0; - for action in &actions { - let desc = match &action.kind { - ActionKind::WriteNode { key, .. } => format!("WRITE {}", key), - ActionKind::Refine { key, .. } => format!("REFINE {}", key), - ActionKind::Link { source, target } => format!("LINK {} → {}", source, target), - ActionKind::Demote { key } => format!("DEMOTE {}", key), - ActionKind::Delete { key } => format!("DELETE {}", key), - }; - if apply_action(store, action, agent_name, &ts, 0) { - log(&format!("applied: {}", desc)); - applied += 1; - } else { - log(&format!("skipped: {}", desc)); - } - } + // Mark conversation segments as mined after successful processing if agent_name == "observation" { mark_observation_done(&result.node_keys); } - Ok((actions.len(), applied)) + Ok(()) } -/// 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). /// Run an agent with explicit target keys, bypassing the agent's query. pub fn run_one_agent_with_keys( store: &mut Store, @@ -727,11 +150,6 @@ fn run_one_agent_inner( if debug { print!("{}", response_section); } log(&format!("response {}KB", output.len() / 1024)); - let actions = parse_all_actions(&output); - let no_ops = count_no_ops(&output); - - log(&format!("parsed {} actions, {} no-ops", actions.len(), no_ops)); - // Record visits for processed nodes if !agent_batch.node_keys.is_empty() { store.record_agent_visits(&agent_batch.node_keys, agent_name).ok(); @@ -739,8 +157,6 @@ fn run_one_agent_inner( Ok(AgentResult { output, - actions, - no_ops, node_keys: agent_batch.node_keys, }) } @@ -888,290 +304,3 @@ fn format_segment(messages: &[(usize, String, String, String)]) -> String { } fragments.join("\n\n") } - -// --------------------------------------------------------------------------- -// Convergence metrics -// --------------------------------------------------------------------------- - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CycleResult { - pub cycle: usize, - pub timestamp: String, - pub total_actions: usize, - pub total_applied: usize, - pub total_no_ops: usize, - pub depth_rejected: usize, - pub weighted_delta: f64, - pub graph_metrics_before: GraphMetrics, - pub graph_metrics_after: GraphMetrics, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct GraphMetrics { - pub nodes: usize, - pub edges: usize, - pub cc: f64, - pub sigma: f64, - pub communities: usize, -} - -impl GraphMetrics { - pub fn from_graph(store: &Store, graph: &Graph) -> Self { - Self { - nodes: store.nodes.len(), - edges: graph.edge_count(), - cc: graph.avg_clustering_coefficient() as f64, - sigma: graph.small_world_sigma() as f64, - communities: graph.community_count(), - } - } -} - -fn metric_stability(history: &[CycleResult], key: &str, window: usize) -> f64 { - if history.len() < window { return f64::INFINITY; } - - let values: Vec = history[history.len() - window..].iter() - .map(|h| match key { - "sigma" => h.graph_metrics_after.sigma, - "cc" => h.graph_metrics_after.cc, - "communities" => h.graph_metrics_after.communities as f64, - _ => 0.0, - }) - .collect(); - - if values.len() < 2 { return f64::INFINITY; } - let mean = values.iter().sum::() / values.len() as f64; - if mean == 0.0 { return 0.0; } - let variance = values.iter().map(|v| (v - mean).powi(2)).sum::() / values.len() as f64; - variance.sqrt() / mean.abs() -} - -pub fn check_convergence(history: &[CycleResult], window: usize) -> bool { - if history.len() < window { return false; } - - let sigma_cv = metric_stability(history, "sigma", window); - let cc_cv = metric_stability(history, "cc", window); - let comm_cv = metric_stability(history, "communities", window); - - let recent = &history[history.len() - window..]; - let avg_delta = recent.iter().map(|r| r.weighted_delta).sum::() / recent.len() as f64; - - eprintln!("\n Convergence check (last {} cycles):", window); - eprintln!(" sigma CV: {:.4} (< 0.05?)", sigma_cv); - eprintln!(" CC CV: {:.4} (< 0.05?)", cc_cv); - eprintln!(" community CV: {:.4} (< 0.10?)", comm_cv); - eprintln!(" avg delta: {:.2} (< 1.00?)", avg_delta); - - let structural = sigma_cv < 0.05 && cc_cv < 0.05 && comm_cv < 0.10; - let behavioral = avg_delta < 1.0; - - if structural && behavioral { - eprintln!(" → CONVERGED"); - true - } else { - false - } -} - -// --------------------------------------------------------------------------- -// The knowledge loop -// --------------------------------------------------------------------------- - -pub struct KnowledgeLoopConfig { - pub max_cycles: usize, - pub batch_size: usize, - pub window: usize, - pub max_depth: i32, - pub confidence_base: f64, -} - -impl Default for KnowledgeLoopConfig { - fn default() -> Self { - Self { - max_cycles: 20, - batch_size: 5, - window: 5, - max_depth: 4, - confidence_base: 0.3, - } - } -} - -pub fn run_knowledge_loop(config: &KnowledgeLoopConfig) -> Result, String> { - let mut store = Store::load()?; - let mut depth_db = DepthDb::load(&store); - let mut history = Vec::new(); - - eprintln!("Knowledge Loop — fixed-point iteration"); - eprintln!(" max_cycles={} batch_size={}", config.max_cycles, config.batch_size); - eprintln!(" window={} max_depth={}", config.window, config.max_depth); - - for cycle in 1..=config.max_cycles { - let result = run_cycle(cycle, config, &mut depth_db)?; - history.push(result); - - if check_convergence(&history, config.window) { - eprintln!("\n CONVERGED after {} cycles", cycle); - break; - } - } - - // Save loop summary as a store node - if let Some(first) = history.first() { - let key = format!("_knowledge-loop-{}", first.timestamp); - if let Ok(json) = serde_json::to_string_pretty(&history) { - store = Store::load()?; - store.upsert_provenance(&key, &json, - "observation:write").ok(); - depth_db.save(&mut store); - store.save()?; - } - } - - Ok(history) -} - -fn run_cycle( - cycle_num: usize, - config: &KnowledgeLoopConfig, - depth_db: &mut DepthDb, -) -> Result { - let timestamp = store::compact_timestamp(); - eprintln!("\n{}", "=".repeat(60)); - eprintln!("CYCLE {} — {}", cycle_num, timestamp); - eprintln!("{}", "=".repeat(60)); - - let mut store = Store::load()?; - let graph = store.build_graph(); - let metrics_before = GraphMetrics::from_graph(&store, &graph); - eprintln!(" Before: nodes={} edges={} cc={:.3} sigma={:.3}", - metrics_before.nodes, metrics_before.edges, metrics_before.cc, metrics_before.sigma); - - let mut all_actions = Vec::new(); - let mut all_no_ops = 0; - let mut depth_rejected = 0; - let mut total_applied = 0; - - // Run each agent via .agent file dispatch - let agent_names = ["observation", "extractor", "connector", "challenger"]; - - for agent_name in &agent_names { - eprintln!("\n --- {} (n={}) ---", agent_name, config.batch_size); - - let result = match run_one_agent(&mut store, agent_name, config.batch_size, "knowledge", &|msg| eprintln!(" {}", msg), false) { - Ok(r) => r, - Err(e) => { - eprintln!(" ERROR: {}", e); - continue; - } - }; - - let mut actions = result.actions; - all_no_ops += result.no_ops; - - eprintln!(" Actions: {} No-ops: {}", actions.len(), result.no_ops); - - let mut applied = 0; - for action in &mut actions { - let depth = compute_action_depth(depth_db, action, agent_name); - action.depth = depth; - - match &action.kind { - ActionKind::WriteNode { key, covers, .. } => { - let conf_val = action.confidence.gate_value(); - let req = required_confidence(depth, config.confidence_base); - - let source_uses: Vec = covers.iter() - .filter_map(|k| store.nodes.get(k).map(|n| n.uses)) - .collect(); - let avg_uses = if source_uses.is_empty() { 0 } - else { source_uses.iter().sum::() / source_uses.len() as u32 }; - let eff_conf = (conf_val + use_bonus(avg_uses)).min(1.0); - - if eff_conf < req { - action.applied = Some(false); - action.rejected_reason = Some("depth_threshold".into()); - depth_rejected += 1; - continue; - } - if depth > config.max_depth { - action.applied = Some(false); - action.rejected_reason = Some("max_depth".into()); - depth_rejected += 1; - continue; - } - eprintln!(" WRITE {} depth={} conf={:.2} eff={:.2} req={:.2}", - key, depth, conf_val, eff_conf, req); - } - ActionKind::Link { source, target } => { - eprintln!(" LINK {} → {}", source, target); - } - ActionKind::Refine { key, .. } => { - eprintln!(" REFINE {} depth={}", key, depth); - } - ActionKind::Demote { key } => { - eprintln!(" DEMOTE {}", key); - } - ActionKind::Delete { key } => { - eprintln!(" DELETE {}", key); - } - } - - if apply_action(&mut store, action, agent_name, ×tamp, depth) { - applied += 1; - action.applied = Some(true); - if let ActionKind::WriteNode { key, .. } | ActionKind::Refine { key, .. } = &action.kind { - depth_db.set(key.clone(), depth); - } - } else { - action.applied = Some(false); - } - } - - eprintln!(" Applied: {}/{}", applied, actions.len()); - total_applied += applied; - all_actions.extend(actions); - } - - depth_db.save(&mut store); - - // Recompute spectral at most once per hour — O(n³) is expensive at 14k+ nodes - if total_applied > 0 { - let stale = spectral::embedding_path() - .metadata() - .and_then(|m| m.modified()) - .map(|t| t.elapsed().unwrap_or_default() > std::time::Duration::from_secs(3600)) - .unwrap_or(true); - if stale { - eprintln!("\n Recomputing spectral embedding (>1h stale)..."); - let graph = store.build_graph(); - let result = spectral::decompose(&graph, 8); - let emb = spectral::to_embedding(&result); - spectral::save_embedding(&emb).ok(); - } - } - - let graph = store.build_graph(); - let metrics_after = GraphMetrics::from_graph(&store, &graph); - let weighted_delta: f64 = all_actions.iter() - .filter(|a| a.applied == Some(true)) - .map(|a| a.weight) - .sum(); - - eprintln!("\n CYCLE {} SUMMARY", cycle_num); - eprintln!(" Applied: {}/{} depth-rejected: {} no-ops: {}", - total_applied, all_actions.len(), depth_rejected, all_no_ops); - eprintln!(" Weighted delta: {:.2}", weighted_delta); - - Ok(CycleResult { - cycle: cycle_num, - timestamp, - total_actions: all_actions.len(), - total_applied, - total_no_ops: all_no_ops, - depth_rejected, - weighted_delta, - graph_metrics_before: metrics_before, - graph_metrics_after: metrics_after, - }) -} diff --git a/poc-memory/src/agents/mod.rs b/poc-memory/src/agents/mod.rs index 95f8104..7d81914 100644 --- a/poc-memory/src/agents/mod.rs +++ b/poc-memory/src/agents/mod.rs @@ -6,11 +6,11 @@ // // llm — model invocation, response parsing // prompts — prompt generation from store data +// defs — agent file loading and placeholder resolution // audit — link quality review via Sonnet // consolidate — full consolidation pipeline -// knowledge — knowledge production agents + convergence loop +// knowledge — agent execution, conversation fragment selection // enrich — journal enrichment, experience mining -// fact_mine — fact extraction from transcripts // digest — episodic digest generation (daily/weekly/monthly) // daemon — background job scheduler // transcript — shared JSONL transcript parsing @@ -23,6 +23,5 @@ pub mod audit; pub mod consolidate; pub mod knowledge; pub mod enrich; -pub mod fact_mine; pub mod digest; pub mod daemon; diff --git a/poc-memory/src/cli/agent.rs b/poc-memory/src/cli/agent.rs index 5cb484b..9b05ba9 100644 --- a/poc-memory/src/cli/agent.rs +++ b/poc-memory/src/cli/agent.rs @@ -97,73 +97,20 @@ pub fn cmd_journal_enrich(jsonl_path: &str, entry_text: &str, grep_line: usize) crate::enrich::journal_enrich(&mut store, jsonl_path, entry_text, grep_line) } -pub fn cmd_apply_consolidation(do_apply: bool, report_file: Option<&str>) -> Result<(), String> { - let mut store = store::Store::load()?; - crate::consolidate::apply_consolidation(&mut store, do_apply, report_file) +pub fn cmd_apply_consolidation(_do_apply: bool, _report_file: Option<&str>) -> Result<(), String> { + Err("apply-consolidation has been removed — agents now apply changes via tool calls directly.".into()) } -pub fn cmd_knowledge_loop(max_cycles: usize, batch_size: usize, window: usize, max_depth: i32) -> Result<(), String> { - let config = crate::knowledge::KnowledgeLoopConfig { - max_cycles, - batch_size, - window, - max_depth, - ..Default::default() - }; - - let results = crate::knowledge::run_knowledge_loop(&config)?; - eprintln!("\nCompleted {} cycles, {} total actions applied", - results.len(), - results.iter().map(|r| r.total_applied).sum::()); - Ok(()) +pub fn cmd_knowledge_loop(_max_cycles: usize, _batch_size: usize, _window: usize, _max_depth: i32) -> Result<(), String> { + Err("knowledge-loop has been removed — agents now use tool calls directly. Use `poc-memory agent run` instead.".into()) } -pub fn cmd_fact_mine(path: &str, batch: bool, dry_run: bool, output_file: Option<&str>, min_messages: usize) -> Result<(), String> { - let p = std::path::Path::new(path); - - let paths: Vec = if batch { - if !p.is_dir() { - return Err(format!("Not a directory: {}", path)); - } - let mut files: Vec<_> = std::fs::read_dir(p) - .map_err(|e| format!("read dir: {}", e))? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| p.extension().map(|x| x == "jsonl").unwrap_or(false)) - .collect(); - files.sort(); - eprintln!("Found {} transcripts", files.len()); - files - } else { - vec![p.to_path_buf()] - }; - - let path_refs: Vec<&std::path::Path> = paths.iter().map(|p| p.as_path()).collect(); - let facts = crate::fact_mine::mine_batch(&path_refs, min_messages, dry_run)?; - - if !dry_run { - let json = serde_json::to_string_pretty(&facts) - .map_err(|e| format!("serialize: {}", e))?; - if let Some(out) = output_file { - std::fs::write(out, &json).map_err(|e| format!("write: {}", e))?; - eprintln!("\nWrote {} facts to {}", facts.len(), out); - } else { - println!("{}", json); - } - } - - eprintln!("\nTotal: {} facts from {} transcripts", facts.len(), paths.len()); - Ok(()) +pub fn cmd_fact_mine(_path: &str, _batch: bool, _dry_run: bool, _output_file: Option<&str>, _min_messages: usize) -> Result<(), String> { + Err("fact-mine has been removed — use the observation agent instead.".into()) } -pub fn cmd_fact_mine_store(path: &str) -> Result<(), String> { - let path = std::path::Path::new(path); - if !path.exists() { - return Err(format!("File not found: {}", path.display())); - } - let count = crate::fact_mine::mine_and_store(path, None)?; - eprintln!("Stored {} facts", count); - Ok(()) +pub fn cmd_fact_mine_store(_path: &str) -> Result<(), String> { + Err("fact-mine-store has been removed — use the observation agent instead.".into()) } /// Sample recent actions from each agent type, sort by quality using diff --git a/poc-memory/src/lib.rs b/poc-memory/src/lib.rs index be79a84..977aee1 100644 --- a/poc-memory/src/lib.rs +++ b/poc-memory/src/lib.rs @@ -31,7 +31,7 @@ pub mod tui; // Re-export agent submodules at crate root for backwards compatibility pub use agents::{ llm, audit, consolidate, knowledge, - enrich, fact_mine, digest, daemon, + enrich, digest, daemon, }; pub mod memory_capnp {