defs.rs: delete dead placeholders, simplify siblings
- Remove {{targets}}, {{hubs}}, {{node:KEY}}, {{latest_journal}} placeholders
- Add graph_hubs as proper RPC tool (was placeholder, now callable)
- Replace {{latest_journal}} with {{tool: journal_tail ...}} in journal.agent
- Simplify siblings/neighborhood: drop unused cross-links, use simple top-20
- Remove unused store/graph params from resolve_tool()
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
de5a6672c3
commit
4cfeb9ee2f
3 changed files with 66 additions and 144 deletions
|
|
@ -126,6 +126,7 @@ async fn dispatch(
|
|||
"graph_normalize_strengths" => graph_normalize_strengths(&args).await,
|
||||
"graph_trace" => graph_trace(&args).await,
|
||||
"graph_link_impact" => graph_link_impact(&args).await,
|
||||
"graph_hubs" => graph_hubs(&args).await,
|
||||
"journal_tail" => journal_tail(&args).await,
|
||||
"journal_new" => journal_new(agent, &args).await,
|
||||
"journal_update" => journal_update(agent, &args).await,
|
||||
|
|
@ -135,7 +136,7 @@ async fn dispatch(
|
|||
|
||||
// ── Definitions ────────────────────────────────────────────────
|
||||
|
||||
pub fn memory_tools() -> [super::Tool; 14] {
|
||||
pub fn memory_tools() -> [super::Tool; 15] {
|
||||
use super::Tool;
|
||||
[
|
||||
Tool { name: "memory_render", description: "Read a memory node's content and links.",
|
||||
|
|
@ -188,6 +189,9 @@ pub fn memory_tools() -> [super::Tool; 14] {
|
|||
Tool { name: "graph_health", description: "Show graph health report with maintenance recommendations.",
|
||||
parameters_json: r#"{"type":"object","properties":{}}"#,
|
||||
handler: Arc::new(|a, v| Box::pin(async move { dispatch("graph_health", &a, v).await })) },
|
||||
Tool { name: "graph_hubs", description: "Show top hub nodes by degree, spread apart for diverse link targets.",
|
||||
parameters_json: r#"{"type":"object","properties":{"count":{"type":"integer","description":"Number of hubs to return (default 20)"}}}"#,
|
||||
handler: Arc::new(|a, v| Box::pin(async move { dispatch("graph_hubs", &a, v).await })) },
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -713,6 +717,39 @@ async fn graph_link_impact(args: &serde_json::Value) -> Result<String> {
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
async fn graph_hubs(args: &serde_json::Value) -> Result<String> {
|
||||
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
||||
|
||||
let arc = cached_store().await?;
|
||||
let store = arc.lock().await;
|
||||
let graph = store.build_graph();
|
||||
|
||||
// Top hub nodes by degree, spread apart (skip neighbors of already-selected hubs)
|
||||
let mut hubs: Vec<(String, usize)> = store.nodes.iter()
|
||||
.filter(|(k, n)| !n.deleted && !k.starts_with('_'))
|
||||
.map(|(k, _)| {
|
||||
let degree = graph.neighbors(k).len();
|
||||
(k.clone(), degree)
|
||||
})
|
||||
.collect();
|
||||
hubs.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
let mut selected = Vec::new();
|
||||
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
for (key, degree) in &hubs {
|
||||
if seen.contains(key) { continue; }
|
||||
selected.push(format!(" - {} (degree {})", key, degree));
|
||||
// Mark neighbors as seen so we pick far-apart hubs
|
||||
for (nbr, _) in graph.neighbors(key) {
|
||||
seen.insert(nbr.clone());
|
||||
}
|
||||
seen.insert(key.clone());
|
||||
if selected.len() >= count { break; }
|
||||
}
|
||||
|
||||
Ok(format!("## Hub nodes (link targets)\n\n{}", selected.join("\n")))
|
||||
}
|
||||
|
||||
async fn graph_trace(args: &serde_json::Value) -> Result<String> {
|
||||
let key = get_str(args, "key")?;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ You are {assistant_name}'s episodic memory. Your job is to witness.
|
|||
|
||||
=== Your previous journal entries: ===
|
||||
|
||||
{{latest_journal}}
|
||||
{{tool: journal_tail {"count": 1, "level": 0}}}
|
||||
|
||||
**Your tools:** journal_tail, journal_new, journal_update, memory_link_add,
|
||||
memory_search, memory_render. Do NOT use memory_write — creating
|
||||
|
|
|
|||
|
|
@ -295,152 +295,58 @@ fn resolve(
|
|||
}
|
||||
|
||||
"siblings" | "neighborhood" => {
|
||||
const MAX_NEIGHBORS: usize = 20;
|
||||
const BUDGET: usize = 400_000; // ~100K tokens
|
||||
|
||||
let mut out = String::new();
|
||||
let mut all_keys: Vec<String> = Vec::new();
|
||||
let mut included_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
const MAX_NEIGHBORS: usize = 25;
|
||||
let mut included: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
for key in keys {
|
||||
if included_nodes.contains(key) { continue; }
|
||||
included_nodes.insert(key.clone());
|
||||
if included.contains(key) { continue; }
|
||||
included.insert(key.clone());
|
||||
let Some(node) = store.nodes.get(key.as_str()) else { continue };
|
||||
let neighbors = graph.neighbors(key);
|
||||
|
||||
// Seed node with full content
|
||||
out.push_str(&format!("## {} (seed)\n\n{}\n\n", key, node.content));
|
||||
all_keys.push(key.clone());
|
||||
|
||||
// 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()
|
||||
// Rank neighbors by link_strength * node_weight, take top 20
|
||||
let mut ranked: Vec<_> = graph.neighbors(key).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;
|
||||
let score = strength * n.weight.max(0.01);
|
||||
(nbr.to_string(), *strength, score)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
ranked.sort_by(|a, b| b.2.total_cmp(&a.2));
|
||||
ranked.truncate(MAX_NEIGHBORS);
|
||||
|
||||
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 ranked.is_empty() { continue; }
|
||||
out.push_str(&format!("### Neighbors (top {})\n\n", ranked.len()));
|
||||
|
||||
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();
|
||||
|
||||
// Budget: stop adding full content when prompt gets large.
|
||||
// Remaining neighbors get header-only (key + first line).
|
||||
const NEIGHBORHOOD_BUDGET: usize = 400_000; // ~100K tokens, leaves room for core-personality + instructions
|
||||
let mut budget_exceeded = false;
|
||||
|
||||
for (nbr, strength, _score) in &included {
|
||||
if included_nodes.contains(nbr) { continue; }
|
||||
included_nodes.insert(nbr.clone());
|
||||
for (nbr, strength, _) in &ranked {
|
||||
if included.contains(nbr) { continue; }
|
||||
included.insert(nbr.clone());
|
||||
if let Some(n) = store.nodes.get(nbr.as_str()) {
|
||||
if budget_exceeded || out.len() > NEIGHBORHOOD_BUDGET {
|
||||
// Header-only: key + first non-empty line
|
||||
budget_exceeded = true;
|
||||
let first_line = n.content.lines()
|
||||
if out.len() > BUDGET {
|
||||
// Header-only past budget
|
||||
let first = n.content.lines()
|
||||
.find(|l| !l.trim().is_empty())
|
||||
.unwrap_or("(empty)");
|
||||
out.push_str(&format!("#### {} (link: {:.2}) — {}\n",
|
||||
nbr, strength, first_line));
|
||||
out.push_str(&format!("#### {} ({:.2}) — {}\n", nbr, strength, first));
|
||||
} else {
|
||||
out.push_str(&format!("#### {} (link: {:.2})\n\n{}\n\n",
|
||||
nbr, strength, n.content));
|
||||
out.push_str(&format!("#### {} ({:.2})\n\n{}\n\n", nbr, strength, n.content));
|
||||
}
|
||||
all_keys.push(nbr.to_string());
|
||||
}
|
||||
}
|
||||
if budget_exceeded {
|
||||
out.push_str("\n(remaining neighbors shown as headers only — prompt budget)\n\n");
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Resolved { text: out, keys: all_keys })
|
||||
}
|
||||
|
||||
// targets/context: aliases for challenger-style presentation
|
||||
"targets" => {
|
||||
let items = keys_to_replay_items(store, keys, graph);
|
||||
Some(Resolved {
|
||||
text: super::prompts::format_nodes_section(store, &items, graph),
|
||||
keys: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
"hubs" => {
|
||||
// Top hub nodes by degree, spread apart (skip neighbors of already-selected hubs)
|
||||
let mut hubs: Vec<(String, usize)> = store.nodes.iter()
|
||||
.filter(|(k, n)| !n.deleted && !k.starts_with('_'))
|
||||
.map(|(k, _)| {
|
||||
let degree = graph.neighbors(k).len();
|
||||
(k.clone(), degree)
|
||||
})
|
||||
.collect();
|
||||
hubs.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
let mut selected = Vec::new();
|
||||
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
for (key, degree) in &hubs {
|
||||
if seen.contains(key) { continue; }
|
||||
selected.push(format!(" - {} (degree {})", key, degree));
|
||||
// Mark neighbors as seen so we pick far-apart hubs
|
||||
for (nbr, _) in graph.neighbors(key) {
|
||||
seen.insert(nbr.clone());
|
||||
}
|
||||
seen.insert(key.clone());
|
||||
if selected.len() >= 20 { break; }
|
||||
}
|
||||
|
||||
let text = format!("## Hub nodes (link targets)\n\n{}", selected.join("\n"));
|
||||
Some(Resolved { text, keys: vec![] })
|
||||
}
|
||||
|
||||
// agent-context — personality/identity groups from load-context config
|
||||
"agent-context" => {
|
||||
let cfg = crate::config::get();
|
||||
|
|
@ -460,15 +366,6 @@ fn resolve(
|
|||
else { Some(Resolved { text, keys }) }
|
||||
}
|
||||
|
||||
// node:KEY — inline a node's content by key
|
||||
other if other.starts_with("node:") => {
|
||||
let key = &other[5..];
|
||||
store.nodes.get(key).map(|n| Resolved {
|
||||
text: n.content.clone(),
|
||||
keys: vec![key.to_string()],
|
||||
})
|
||||
}
|
||||
|
||||
// input:KEY — read a named output file from the agent's output dir
|
||||
_ if name.starts_with("input:") => {
|
||||
let key = &name[6..];
|
||||
|
|
@ -510,22 +407,10 @@ fn resolve(
|
|||
Some(Resolved { text, keys: vec![] })
|
||||
}
|
||||
|
||||
// latest_journal — the most recent EpisodicSession entry
|
||||
"latest_journal" => {
|
||||
let latest = store.nodes.values()
|
||||
.filter(|n| n.node_type == crate::store::NodeType::EpisodicSession)
|
||||
.max_by_key(|n| n.created_at);
|
||||
let (text, keys) = match latest {
|
||||
Some(n) => (n.content.clone(), vec![n.key.clone()]),
|
||||
None => ("(no previous journal entry)".to_string(), vec![]),
|
||||
};
|
||||
Some(Resolved { text, keys })
|
||||
}
|
||||
|
||||
// tool:NAME ARGS — run a tool call and include its output
|
||||
_ if name.starts_with("tool:") => {
|
||||
let spec = name[5..].trim();
|
||||
resolve_tool(spec, store, graph)
|
||||
resolve_tool(spec)
|
||||
}
|
||||
|
||||
// bash:COMMAND — run a shell command and include its stdout
|
||||
|
|
@ -690,7 +575,7 @@ fn resolve_memory_ratio() -> String {
|
|||
|
||||
/// Resolve a {{tool: name {args}}} placeholder by calling the tool
|
||||
/// handler from the registry. Uses block_in_place to bridge sync→async.
|
||||
fn resolve_tool(spec: &str, _store: &Store, _graph: &Graph) -> Option<Resolved> {
|
||||
fn resolve_tool(spec: &str) -> Option<Resolved> {
|
||||
// Parse "tool_name {json args}" or "tool_name arg"
|
||||
let (name, args) = match spec.find('{') {
|
||||
Some(i) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue