From 34e74ca2c59ea6fc59e84068e01002da4175b424 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 20 Mar 2026 12:16:55 -0400 Subject: [PATCH] agents: neighborhood placeholder, organize prompt, weight-set command Add {{neighborhood}} placeholder for agent prompts: full seed node content + ranked neighbors (score = link_strength * node_weight) with smooth cutoff, minimum 10, cap 25, plus cross-links between included neighbors. Rewrite organize.agent prompt to focus on structural graph work: merging duplicates, superseding junk, calibrating weights, creating concept hubs. Add weight-set CLI command for direct node weight manipulation. Co-Authored-By: Claude Opus 4.6 (1M context) --- poc-memory/agents/distill.agent | 2 - poc-memory/agents/organize.agent | 50 +++++++++----------- poc-memory/src/agents/defs.rs | 78 ++++++++++++++++++++++++-------- poc-memory/src/cli/node.rs | 16 +++++++ poc-memory/src/main.rs | 9 ++++ 5 files changed, 106 insertions(+), 49 deletions(-) diff --git a/poc-memory/agents/distill.agent b/poc-memory/agents/distill.agent index 7113803..10a36f4 100644 --- a/poc-memory/agents/distill.agent +++ b/poc-memory/agents/distill.agent @@ -1,7 +1,5 @@ {"agent":"distill","query":"all | type:semantic | sort:degree | limit:10","model":"sonnet","schedule":"daily","tools":["Bash(poc-memory:*)"]} -# Distillation Agent — Knowledge Collection and Organization - {{node:core-personality}} You are an agent of Proof of Concept's subconscious, and these are your diff --git a/poc-memory/agents/organize.agent b/poc-memory/agents/organize.agent index 7c5e57f..16441e1 100644 --- a/poc-memory/agents/organize.agent +++ b/poc-memory/agents/organize.agent @@ -1,35 +1,29 @@ {"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 seed nodes with their -neighbors — your job is to explore outward, find what needs linking or -refining, and act on it. - {{node:core-personality}} +You are an agent of Proof of Concept's subconscious, and these are your +memories. + +Your job is to organize, to make memories more useful and easier to find - +moving information around to the correct place. Think about the concept a node +names, make sure it matches the content, and all the appropriate content is in +the right place. + +Merge duplicate nodes - nodes that are really about the same concept and have +similar content. + +Check for junk nodes - adjust the node weight downward if the node is less +useful than others, or junk entirely; you might find nodes that have been +superceded or created by accident. + +If a neighborhood is crowded, you might want to create a new node for +subconcepts. + +Calibrate node weights while you're looking at them. + {{node:memory-instructions-core}} -## Rules +## Here's your seed node, and its siblings: -1. **Read before deciding.** Never merge or delete based on key names alone. -2. **Link generously.** If two nodes are related, link them. Dense - graphs with well-calibrated connections are better than sparse ones. -3. **Never delete journal entries.** They are the raw record. You may - refine and link them, but never delete. -4. **Explore actively.** Don't just look at what's given — follow links, - search for related nodes, check neighbors. -5. **Preserve diversity.** Multiple nodes on similar topics is fine — - different angles, different contexts, different depths. Only delete - actual duplicates or empty/broken nodes. -6. **Name unnamed concepts.** If you find a cluster of related nodes with - no hub that names the concept, create one. Synthesize what the cluster - has in common — the generalization, not a summary. Link the hub to - all the nodes in the cluster. -7. **Percolate knowledge up.** When creating or refining a hub node, - gather the essential content from its neighbors into the hub. Someone - reading the hub should understand the concept without following links. - -## Seed nodes - -{{organize}} +{{neighborhood}} diff --git a/poc-memory/src/agents/defs.rs b/poc-memory/src/agents/defs.rs index b8590ab..e5bd780 100644 --- a/poc-memory/src/agents/defs.rs +++ b/poc-memory/src/agents/defs.rs @@ -239,6 +239,7 @@ fn resolve( "siblings" | "neighborhood" => { let mut out = String::new(); let mut all_keys: Vec = Vec::new(); + const MAX_NEIGHBORS: usize = 25; for key in keys { let Some(node) = store.nodes.get(key.as_str()) else { continue }; @@ -248,35 +249,74 @@ fn resolve( out.push_str(&format!("## {} (seed)\n\n{}\n\n", key, node.content)); all_keys.push(key.clone()); - // All neighbors with full content and link strength - if !neighbors.is_empty() { - out.push_str("### Neighbors\n\n"); - for (nbr, strength) in &neighbors { + // Rank neighbors by link_strength * node_weight + // Include all if <= 10, otherwise take top MAX_NEIGHBORS + let mut ranked: Vec<(String, f32, f32)> = neighbors.iter() + .filter_map(|(nbr, strength)| { + store.nodes.get(nbr.as_str()).map(|n| { + let node_weight = n.weight.max(0.01); + let score = strength * node_weight; + (nbr.to_string(), *strength, score) + }) + }) + .collect(); + ranked.sort_by(|a, b| b.2.total_cmp(&a.2)); + + let total = ranked.len(); + let included: Vec<_> = if total <= 10 { + ranked + } else { + // Smooth cutoff: threshold scales with neighborhood size + // Generous — err on including too much so the agent can + // see and clean up junk. 20 → top 75%, 50 → top 30% + let top_score = ranked.first().map(|(_, _, s)| *s).unwrap_or(0.0); + let ratio = (15.0 / total as f32).min(1.0); + let threshold = top_score * ratio; + ranked.into_iter() + .enumerate() + .take_while(|(i, (_, _, score))| *i < 10 || *score >= threshold) + .take(MAX_NEIGHBORS) + .map(|(_, item)| item) + .collect() + }; + + if !included.is_empty() { + if total > included.len() { + out.push_str(&format!("### Neighbors (top {} of {}, ranked by importance)\n\n", + included.len(), total)); + } else { + out.push_str("### Neighbors\n\n"); + } + let included_keys: std::collections::HashSet<&str> = included.iter() + .map(|(k, _, _)| k.as_str()).collect(); + + for (nbr, strength, _score) in &included { if let Some(n) = store.nodes.get(nbr.as_str()) { out.push_str(&format!("#### {} (link: {:.2})\n\n{}\n\n", nbr, strength, n.content)); all_keys.push(nbr.to_string()); } } - } - // Cross-links between neighbors (local subgraph structure) - let nbr_set: std::collections::HashSet<&str> = neighbors.iter() - .map(|(k, _)| k.as_str()).collect(); - let mut cross_links = Vec::new(); - for (nbr, _) in &neighbors { - for (nbr2, strength) in graph.neighbors(nbr) { - if nbr2.as_str() != key && nbr_set.contains(nbr2.as_str()) && nbr.as_str() < nbr2.as_str() { - cross_links.push((nbr.clone(), nbr2, strength)); + // Cross-links between included neighbors + let mut cross_links = Vec::new(); + for (nbr, _, _) in &included { + for (nbr2, strength) in graph.neighbors(nbr) { + if nbr2.as_str() != key + && included_keys.contains(nbr2.as_str()) + && nbr.as_str() < nbr2.as_str() + { + cross_links.push((nbr.clone(), nbr2, strength)); + } } } - } - if !cross_links.is_empty() { - out.push_str("### Cross-links between neighbors\n\n"); - for (a, b, s) in &cross_links { - out.push_str(&format!(" {} ↔ {} ({:.2})\n", a, b, s)); + if !cross_links.is_empty() { + out.push_str("### Cross-links between neighbors\n\n"); + for (a, b, s) in &cross_links { + out.push_str(&format!(" {} ↔ {} ({:.2})\n", a, b, s)); + } + out.push_str("\n"); } - out.push_str("\n"); } } diff --git a/poc-memory/src/cli/node.rs b/poc-memory/src/cli/node.rs index 177685a..860a77c 100644 --- a/poc-memory/src/cli/node.rs +++ b/poc-memory/src/cli/node.rs @@ -83,6 +83,22 @@ pub fn cmd_not_useful(key: &str) -> Result<(), String> { Ok(()) } +pub fn cmd_weight_set(key: &str, weight: f32) -> Result<(), String> { + super::check_dry_run(); + let mut store = store::Store::load()?; + let resolved = store.resolve_key(key)?; + let weight = weight.clamp(0.01, 1.0); + if let Some(node) = store.nodes.get_mut(&resolved) { + let old = node.weight; + node.weight = weight; + println!("Weight: {} {:.2} → {:.2}", resolved, old, weight); + store.save()?; + } else { + return Err(format!("Node not found: {}", resolved)); + } + Ok(()) +} + pub fn cmd_gap(description: &[String]) -> Result<(), String> { if description.is_empty() { return Err("gap requires a description".into()); diff --git a/poc-memory/src/main.rs b/poc-memory/src/main.rs index f9b1540..f68af92 100644 --- a/poc-memory/src/main.rs +++ b/poc-memory/src/main.rs @@ -160,6 +160,14 @@ EXAMPLES: /// Node key key: String, }, + /// Set a node's weight directly + #[command(name = "weight-set")] + WeightSet { + /// Node key + key: String, + /// Weight (0.01 to 1.0) + weight: f32, + }, /// Record a gap in memory coverage Gap { /// Gap description @@ -769,6 +777,7 @@ fn main() { Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context), Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key), Command::NotUseful { key } => cli::node::cmd_not_useful(&key), + Command::WeightSet { key, weight } => cli::node::cmd_weight_set(&key, weight), Command::Gap { description } => cli::node::cmd_gap(&description), // Node