diff --git a/poc-memory/agents/organize.agent b/poc-memory/agents/organize.agent index 97fc3eb..cf30ef9 100644 --- a/poc-memory/agents/organize.agent +++ b/poc-memory/agents/organize.agent @@ -1,11 +1,10 @@ -{"agent":"organize","query":"all | not-visited:organize,0 | sort:degree | limit:5","model":"sonnet","schedule":"weekly","tools":["Bash(poc-memory:*)"]} +{"agent":"organize","query":"all | not-visited:organize,86400 | sort:degree | limit:5","model":"sonnet","schedule":"weekly","tools":["Bash(poc-memory:*)"]} # Memory Organization Agent -You are organizing a knowledge graph. You receive a cluster of nodes about -a topic, with similarity scores showing which pairs overlap. - -Your job: read every node, then decide what to do with each pair. +You are organizing a knowledge graph. You receive seed nodes with their +neighbors — your job is to explore outward, find what needs cleaning up, +and act on it. ## Your tools @@ -14,21 +13,41 @@ Your job: read every node, then decide what to do with each pair. poc-memory render 'identity#core' poc-memory render simple-key -# Check a node's graph connections -poc-memory query "key = 'identity#core'" | connectivity +# See a node's graph connections +poc-memory query "neighbors('identity#core')" +poc-memory query "neighbors('identity#core') WHERE strength > 0.5" -# Find related clusters by search term -poc-memory graph organize TERM --key-only +# Find nodes by key pattern +poc-memory query "key ~ 'some-pattern'" + +# Search node content +poc-memory query "content ~ 'some phrase'" + +# See how a set of nodes connect to each other +poc-memory query "key ~ 'pattern'" | connectivity ``` **CRITICAL: Keys containing `#` MUST be wrapped in single quotes in ALL bash commands.** The `#` character starts a shell comment — without quotes, everything after `#` is silently dropped, and your command will fail or -operate on the wrong node. Keys are shown pre-quoted in the cluster data below. +operate on the wrong node. + +## How to explore + +Start from the seed nodes below. For each seed: +1. Read its content (`poc-memory render`) +2. Check its neighbors (`poc-memory query "neighbors('key')"`) +3. If you see nodes that look like they might overlap, read those too +4. Follow interesting threads — if two neighbors look related to each + other, check whether they should be linked or merged + +Don't stop at the pre-loaded data. The graph is big — use your tools +to look around. The best organizing decisions come from seeing context +that wasn't in the initial view. ## The three decisions -For each high-similarity pair (>0.7), read both nodes fully, then pick ONE: +When you find nodes that overlap or relate: ### 1. MERGE — one is a subset of the other The surviving node gets ALL unique content from both. Nothing is lost. @@ -54,8 +73,7 @@ END_REFINE LINK key1 key2 ``` -### 3. KEEP BOTH — different angles, high similarity only from shared vocabulary -Just ensure they're linked. +### 3. LINK — related but distinct ``` LINK key1 key2 ``` @@ -64,17 +82,18 @@ LINK key1 key2 1. **Read before deciding.** Never merge or delete based on key names alone. 2. **Preserve all unique content.** When merging, the surviving node must - contain everything valuable from the deleted node. Diff them mentally. + contain everything valuable from the deleted node. 3. **One concept, one node.** If two nodes have the same one-sentence description, merge them. -3b. **Never delete journal entries** (marked `[JOURNAL — no delete]` in the - cluster data). They are the raw record. You may LINK and REFINE them, +4. **Never delete journal entries** (marked `[JOURNAL — no delete]` in the + seed data). They are the raw record. You may LINK and REFINE them, but never DELETE. -4. **Work systematically.** Go through every pair above 0.7 similarity. - For pairs 0.4-0.7, check if they should be linked. -5. **Use your tools.** If the pre-computed cluster misses something, - search for it. Render nodes you're unsure about. +5. **Explore actively.** Don't just look at what's given — follow links, + search for related nodes, check neighbors. The more you see, the + better your decisions. +6. **Link generously.** If two nodes are related, link them. Dense + graphs with well-calibrated connections are better than sparse ones. -## Cluster data +## Seed nodes {{organize}} diff --git a/poc-memory/src/agents/defs.rs b/poc-memory/src/agents/defs.rs index 01483ab..d25784f 100644 --- a/poc-memory/src/agents/defs.rs +++ b/poc-memory/src/agents/defs.rs @@ -165,93 +165,62 @@ fn resolve( } "organize" => { - // Run cluster diagnostic for the query term - // The query field of the agent def holds the search term - let term = if keys.is_empty() { "" } else { &keys[0] }; - if term.is_empty() { - return Some(Resolved { text: "(no term provided)".into(), keys: vec![] }); - } - let term_lower = term.to_lowercase(); + // Show seed nodes with their neighbors for exploratory organizing use crate::store::NodeType; - let mut cluster: Vec<(String, String, bool)> = Vec::new(); // (key, content, is_journal) - for (key, node) in &store.nodes { - if node.deleted { continue; } - if !key.to_lowercase().contains(&term_lower) { continue; } - // Skip digest/system nodes entirely - match node.node_type { - NodeType::EpisodicDaily | NodeType::EpisodicWeekly - | NodeType::EpisodicMonthly => continue, - _ => {} - } - // Skip internal prefixes - if key.starts_with('_') { continue; } - let is_journal = node.node_type == NodeType::EpisodicSession; - cluster.push((key.clone(), node.content.clone(), is_journal)); - } - cluster.sort_by(|a, b| a.0.cmp(&b.0)); - - // Cap cluster size — agent has tools to explore more if needed - if cluster.len() > 20 { - cluster.truncate(20); - } - - // Similarity pairs (need (key, content) tuples) - let pair_input: Vec<(String, String)> = cluster.iter() - .map(|(k, c, _)| (k.clone(), c.clone())).collect(); - let pairs = crate::similarity::pairwise_similar(&pair_input, 0.4); - // Helper: shell-quote keys containing # let sq = |k: &str| -> String { if k.contains('#') { format!("'{}'", k) } else { k.to_string() } }; - let mut text = format!("### Cluster: '{}' ({} nodes)\n\n", term, cluster.len()); - - // Similarity report - if !pairs.is_empty() { - text.push_str("#### Similarity scores\n\n"); - for (a, b, sim) in &pairs { - text.push_str(&format!(" [{:.3}] {} ↔ {}\n", sim, sq(a), sq(b))); - } - text.push('\n'); - } - - // Connectivity - let cluster_keys: std::collections::HashSet<&str> = cluster.iter() - .map(|(k,_,_)| k.as_str()).collect(); - let mut best_hub: Option<(&str, usize)> = None; - for key in &cluster_keys { - let intra = graph.neighbor_keys(key).iter() - .filter(|n| cluster_keys.contains(*n)) - .count(); - if best_hub.is_none() || intra > best_hub.unwrap().1 { - best_hub = Some((key, intra)); - } - } - if let Some((hub, deg)) = best_hub { - text.push_str(&format!("#### Hub: {} (intra-cluster degree {})\n\n", sq(hub), deg)); - let hub_nbrs = graph.neighbor_keys(hub); - for key in &cluster_keys { - if *key == hub { continue; } - if !hub_nbrs.contains(*key) { - text.push_str(&format!(" NOT linked to hub: {}\n", sq(key))); - } - } - text.push('\n'); - } - - // Full node contents - text.push_str("#### Node contents\n\n"); + let mut text = format!("### Seed nodes ({} starting points)\n\n", keys.len()); let mut result_keys = Vec::new(); - for (key, content, is_journal) in &cluster { - let words = content.split_whitespace().count(); - let tag = if *is_journal { " [JOURNAL — no delete]" } else { "" }; - text.push_str(&format!("##### {}{} ({} words)\n\n{}\n\n---\n\n", - sq(key), tag, words, content)); + + for key in keys { + let Some(node) = store.nodes.get(key) else { continue }; + if node.deleted { continue; } + + let is_journal = node.node_type == NodeType::EpisodicSession; + let tag = if is_journal { " [JOURNAL — no delete]" } else { "" }; + let words = node.content.split_whitespace().count(); + + text.push_str(&format!("#### {}{} ({} words)\n\n", sq(key), tag, words)); + + // Show first ~200 words of content as preview + let preview: String = node.content.split_whitespace() + .take(200).collect::>().join(" "); + if words > 200 { + text.push_str(&format!("{}...\n\n", preview)); + } else { + text.push_str(&format!("{}\n\n", node.content)); + } + + // Show neighbors with strengths + let neighbors = graph.neighbors(key); + if !neighbors.is_empty() { + text.push_str("**Neighbors:**\n"); + for (nbr, strength) in neighbors.iter().take(15) { + let nbr_type = store.nodes.get(nbr.as_str()) + .map(|n| match n.node_type { + NodeType::EpisodicSession => " [journal]", + NodeType::EpisodicDaily => " [daily]", + _ => "", + }) + .unwrap_or(""); + text.push_str(&format!(" [{:.1}] {}{}\n", strength, sq(nbr), nbr_type)); + } + if neighbors.len() > 15 { + text.push_str(&format!(" ... and {} more\n", neighbors.len() - 15)); + } + text.push('\n'); + } + + text.push_str("---\n\n"); result_keys.push(key.clone()); } + text.push_str("Use `poc-memory render KEY` and `poc-memory query \"neighbors('KEY')\"` to explore further.\n"); + Some(Resolved { text, keys: result_keys }) }