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) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-20 12:16:55 -04:00
parent 5ef9098deb
commit 34e74ca2c5
5 changed files with 106 additions and 49 deletions

View file

@ -1,7 +1,5 @@
{"agent":"distill","query":"all | type:semantic | sort:degree | limit:10","model":"sonnet","schedule":"daily","tools":["Bash(poc-memory:*)"]} {"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}} {{node:core-personality}}
You are an agent of Proof of Concept's subconscious, and these are your You are an agent of Proof of Concept's subconscious, and these are your

View file

@ -1,35 +1,29 @@
{"agent":"organize","query":"all | not-visited:organize,86400 | 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 seed nodes with their
neighbors — your job is to explore outward, find what needs linking or
refining, and act on it.
{{node:core-personality}} {{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}} {{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. {{neighborhood}}
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}}

View file

@ -239,6 +239,7 @@ fn resolve(
"siblings" | "neighborhood" => { "siblings" | "neighborhood" => {
let mut out = String::new(); let mut out = String::new();
let mut all_keys: Vec<String> = Vec::new(); let mut all_keys: Vec<String> = Vec::new();
const MAX_NEIGHBORS: usize = 25;
for key in keys { for key in keys {
let Some(node) = store.nodes.get(key.as_str()) else { continue }; let Some(node) = store.nodes.get(key.as_str()) else { continue };
@ -248,25 +249,63 @@ fn resolve(
out.push_str(&format!("## {} (seed)\n\n{}\n\n", key, node.content)); out.push_str(&format!("## {} (seed)\n\n{}\n\n", key, node.content));
all_keys.push(key.clone()); all_keys.push(key.clone());
// All neighbors with full content and link strength // Rank neighbors by link_strength * node_weight
if !neighbors.is_empty() { // 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"); out.push_str("### Neighbors\n\n");
for (nbr, strength) in &neighbors { }
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()) { if let Some(n) = store.nodes.get(nbr.as_str()) {
out.push_str(&format!("#### {} (link: {:.2})\n\n{}\n\n", out.push_str(&format!("#### {} (link: {:.2})\n\n{}\n\n",
nbr, strength, n.content)); nbr, strength, n.content));
all_keys.push(nbr.to_string()); all_keys.push(nbr.to_string());
} }
} }
}
// Cross-links between neighbors (local subgraph structure) // Cross-links between included neighbors
let nbr_set: std::collections::HashSet<&str> = neighbors.iter()
.map(|(k, _)| k.as_str()).collect();
let mut cross_links = Vec::new(); let mut cross_links = Vec::new();
for (nbr, _) in &neighbors { for (nbr, _, _) in &included {
for (nbr2, strength) in graph.neighbors(nbr) { for (nbr2, strength) in graph.neighbors(nbr) {
if nbr2.as_str() != key && nbr_set.contains(nbr2.as_str()) && nbr.as_str() < nbr2.as_str() { if nbr2.as_str() != key
&& included_keys.contains(nbr2.as_str())
&& nbr.as_str() < nbr2.as_str()
{
cross_links.push((nbr.clone(), nbr2, strength)); cross_links.push((nbr.clone(), nbr2, strength));
} }
} }
@ -279,6 +318,7 @@ fn resolve(
out.push_str("\n"); out.push_str("\n");
} }
} }
}
Some(Resolved { text: out, keys: all_keys }) Some(Resolved { text: out, keys: all_keys })
} }

View file

@ -83,6 +83,22 @@ pub fn cmd_not_useful(key: &str) -> Result<(), String> {
Ok(()) 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> { pub fn cmd_gap(description: &[String]) -> Result<(), String> {
if description.is_empty() { if description.is_empty() {
return Err("gap requires a description".into()); return Err("gap requires a description".into());

View file

@ -160,6 +160,14 @@ EXAMPLES:
/// Node key /// Node key
key: String, 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 /// Record a gap in memory coverage
Gap { Gap {
/// Gap description /// Gap description
@ -769,6 +777,7 @@ fn main() {
Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context), Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context),
Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key), Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key),
Command::NotUseful { key } => cli::node::cmd_not_useful(&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), Command::Gap { description } => cli::node::cmd_gap(&description),
// Node // Node