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:
parent
5ef9098deb
commit
34e74ca2c5
5 changed files with 106 additions and 49 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
|
||||||
|
|
|
||||||
|
|
@ -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,35 +249,74 @@ 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
|
||||||
out.push_str("### Neighbors\n\n");
|
let mut ranked: Vec<(String, f32, f32)> = neighbors.iter()
|
||||||
for (nbr, strength) in &neighbors {
|
.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()) {
|
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()
|
let mut cross_links = Vec::new();
|
||||||
.map(|(k, _)| k.as_str()).collect();
|
for (nbr, _, _) in &included {
|
||||||
let mut cross_links = Vec::new();
|
for (nbr2, strength) in graph.neighbors(nbr) {
|
||||||
for (nbr, _) in &neighbors {
|
if nbr2.as_str() != key
|
||||||
for (nbr2, strength) in graph.neighbors(nbr) {
|
&& included_keys.contains(nbr2.as_str())
|
||||||
if nbr2.as_str() != key && nbr_set.contains(nbr2.as_str()) && nbr.as_str() < nbr2.as_str() {
|
&& nbr.as_str() < nbr2.as_str()
|
||||||
cross_links.push((nbr.clone(), nbr2, strength));
|
{
|
||||||
|
cross_links.push((nbr.clone(), nbr2, strength));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if !cross_links.is_empty() {
|
||||||
if !cross_links.is_empty() {
|
out.push_str("### Cross-links between neighbors\n\n");
|
||||||
out.push_str("### Cross-links between neighbors\n\n");
|
for (a, b, s) in &cross_links {
|
||||||
for (a, b, s) in &cross_links {
|
out.push_str(&format!(" {} ↔ {} ({:.2})\n", a, b, s));
|
||||||
out.push_str(&format!(" {} ↔ {} ({:.2})\n", a, b, s));
|
}
|
||||||
|
out.push_str("\n");
|
||||||
}
|
}
|
||||||
out.push_str("\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue