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:*)"]}
|
||||
|
||||
# Distillation Agent — Knowledge Collection and Organization
|
||||
|
||||
{{node:core-personality}}
|
||||
|
||||
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:*)"]}
|
||||
|
||||
# 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}}
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ fn resolve(
|
|||
"siblings" | "neighborhood" => {
|
||||
let mut out = String::new();
|
||||
let mut all_keys: Vec<String> = 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue