show suggested link targets in agent prompts

Agents were flying blind — they could see nodes to review and the
topology header, but had no way to discover what targets to link to.
Now each node shows its top 8 text-similar semantic nodes that aren't
already neighbors, giving agents a search-like capability.

Also added section-level targeting guidance to linker.md, transfer.md,
and replay.md prompts: always target the most specific section, not
the file-level node.

Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-03-01 00:37:03 -05:00
parent 59cfa2959f
commit 3e883b7ba7
4 changed files with 59 additions and 0 deletions

View file

@ -91,6 +91,21 @@ Observations about patterns across episodes that aren't yet captured anywhere.
If a node is from weeks ago and already has good connections, it doesn't If a node is from weeks ago and already has good connections, it doesn't
need more. Focus your energy on recent, under-linked episodes. need more. Focus your energy on recent, under-linked episodes.
- **Prefer lateral links over hub links.** Connecting two peripheral nodes
to each other is more valuable than connecting both to a hub like
`identity.md`. Lateral links build web topology; hub links build star
topology.
- **Target sections, not files.** When linking to a topic file, always
target the most specific section: use `identity.md#boundaries` not
`identity.md`, use `kernel-patterns.md#restart-handling` not
`kernel-patterns.md`. The suggested link targets show available sections.
- **Use the suggested targets.** Each node shows text-similar targets not
yet linked. Start from these — they're computed by content similarity and
filtered to exclude existing neighbors. You can propose links beyond the
suggestions, but the suggestions are usually the best starting point.
{{TOPOLOGY}} {{TOPOLOGY}}
## Nodes to review ## Nodes to review

View file

@ -85,6 +85,12 @@ for the human to review.
- **Trust the decay.** If a node is genuinely unimportant, you don't need - **Trust the decay.** If a node is genuinely unimportant, you don't need
to actively prune it. Just don't link it, and it'll decay below threshold to actively prune it. Just don't link it, and it'll decay below threshold
on its own. on its own.
- **Target sections, not files.** When linking to a topic file, always
target the most specific section: use `identity.md#boundaries` not
`identity.md`. The suggested link targets show available sections.
- **Use the suggested targets.** Each node shows text-similar semantic nodes
not yet linked. These are computed by content similarity and are usually
the best starting point for new links.
{{TOPOLOGY}} {{TOPOLOGY}}

View file

@ -128,6 +128,13 @@ Meta-observations about patterns in the consolidation process itself.
symmetric lock ordering when the hot path is asymmetric" is conceptual. symmetric lock ordering when the hot path is asymmetric" is conceptual.
Extract the conceptual version. Extract the conceptual version.
- **Target sections, not files.** When linking to a topic file, always
target the most specific section: use `reflections.md#emotional-patterns`
not `reflections.md`. The suggested link targets show available sections.
- **Use the suggested targets.** Each episode shows text-similar semantic
nodes not yet linked. Start from these when proposing LINK actions.
{{TOPOLOGY}} {{TOPOLOGY}}
## Episodes to process ## Episodes to process

View file

@ -284,6 +284,37 @@ fn format_nodes_section(store: &Store, items: &[ReplayItem], graph: &Graph) -> S
out.push_str(")\n"); out.push_str(")\n");
} }
} }
// Suggested link targets: text-similar semantic nodes not already neighbors
let neighbor_keys: std::collections::HashSet<&str> = neighbors.iter()
.map(|(k, _)| k.as_str()).collect();
let mut candidates: Vec<(&str, f32)> = store.nodes.iter()
.filter(|(k, _)| {
// Only semantic/topic file nodes, not episodic
!k.starts_with("journal.") && !k.starts_with("deep-index.")
&& !k.starts_with("daily-") && !k.starts_with("weekly-")
&& !k.starts_with("monthly-") && !k.starts_with("session-")
&& *k != &item.key
&& !neighbor_keys.contains(k.as_str())
})
.map(|(k, n)| {
let sim = similarity::cosine_similarity(content, &n.content);
(k.as_str(), sim)
})
.filter(|(_, sim)| *sim > 0.1)
.collect();
candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
candidates.truncate(8);
if !candidates.is_empty() {
out.push_str("\nSuggested link targets (by text similarity, not yet linked):\n");
for (k, sim) in &candidates {
let is_hub = graph.degree(k) >= hub_thresh;
out.push_str(&format!(" - {} (sim={:.3}{})\n",
k, sim, if is_hub { ", HUB" } else { "" }));
}
}
out.push_str("\n---\n\n"); out.push_str("\n---\n\n");
} }
out out