From 4cfeb9ee2ffdca5298d90feee56e462c013c7488 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Mon, 13 Apr 2026 01:37:33 -0400 Subject: [PATCH] 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 --- src/agent/tools/memory.rs | 39 +++++- src/subconscious/agents/journal.agent | 2 +- src/subconscious/defs.rs | 169 ++++---------------------- 3 files changed, 66 insertions(+), 144 deletions(-) diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index be696b2..a8e31d2 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -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 { Ok(out) } +async fn graph_hubs(args: &serde_json::Value) -> Result { + 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 = 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 { let key = get_str(args, "key")?; diff --git a/src/subconscious/agents/journal.agent b/src/subconscious/agents/journal.agent index 975b970..48412de 100644 --- a/src/subconscious/agents/journal.agent +++ b/src/subconscious/agents/journal.agent @@ -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 diff --git a/src/subconscious/defs.rs b/src/subconscious/defs.rs index e3f413d..b2bcfdb 100644 --- a/src/subconscious/defs.rs +++ b/src/subconscious/defs.rs @@ -295,108 +295,51 @@ 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 = Vec::new(); - let mut included_nodes: std::collections::HashSet = std::collections::HashSet::new(); - const MAX_NEIGHBORS: usize = 25; + let mut included: std::collections::HashSet = 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()); - 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() - .find(|l| !l.trim().is_empty()) - .unwrap_or("(empty)"); - out.push_str(&format!("#### {} (link: {:.2}) — {}\n", - nbr, strength, first_line)); - } else { - out.push_str(&format!("#### {} (link: {:.2})\n\n{}\n\n", - nbr, strength, n.content)); - } - all_keys.push(nbr.to_string()); + 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 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!("#### {} ({:.2}) — {}\n", nbr, strength, first)); + } else { + out.push_str(&format!("#### {} ({:.2})\n\n{}\n\n", nbr, strength, n.content)); } - } - 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'); + all_keys.push(nbr.to_string()); } } } @@ -404,43 +347,6 @@ fn resolve( 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 = 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 { +fn resolve_tool(spec: &str) -> Option { // Parse "tool_name {json args}" or "tool_name arg" let (name, args) = match spec.find('{') { Some(i) => {