organize: exploratory agent with neighbor context
Previously the organize agent received a pre-computed cluster from a term search — 69% of runs produced 0 actions because the same clusters kept being found via different entry points. Now: seed nodes shown with content previews and neighbor lists. Agent uses tools (render, query neighbors, search) to explore outward and discover what needs organizing. Visit filter set to 24h cooldown. Prompt rewritten to encourage active exploration rather than static cluster analysis.
This commit is contained in:
parent
7c1b96293f
commit
958cf9d041
2 changed files with 85 additions and 97 deletions
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>().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 })
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue