From a88428d642c9b89a1bf81b84707e7f2ee5f06523 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Wed, 15 Apr 2026 02:37:49 -0400 Subject: [PATCH] Simplify context config: personality_nodes and agent_nodes Replace complex context_groups (with ContextGroup struct, ContextSource enum, labels, keys arrays) with simple string lists: - personality_nodes: loaded into main session context - agent_nodes: loaded into subconscious agent context Removed ~200 lines of code. The distinction between session and agent context is now just which list you're in, not a per-group flag. Co-Authored-By: Kent Overstreet --- src/cli/node.rs | 88 ++++++++--------------- src/config.rs | 150 ++++----------------------------------- src/mind/identity.rs | 66 +++-------------- src/subconscious/defs.rs | 18 ++--- 4 files changed, 62 insertions(+), 260 deletions(-) diff --git a/src/cli/node.rs b/src/cli/node.rs index 5137791..5472505 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -165,84 +165,52 @@ pub async fn cmd_query(expr: &[String]) -> Result<()> { Ok(()) } -/// Get group content (handles daemon or local fallback) -pub async fn get_group_content(group: &crate::config::ContextGroup, cfg: &crate::config::Config) -> Vec<(String, String)> { - match group.source { - crate::config::ContextSource::Journal => { - // Query for recent journal entries - let window: i64 = cfg.journal_days as i64 * 24 * 3600; - let query = format!("all | type:episodic | age:<{} | sort:timestamp | limit:{}", - window, cfg.journal_max); - - let keys_str = match memory::memory_query(None, &query, None).await { - Ok(s) => s, - Err(_) => return vec![], - }; - - // Parse keys (one per line) and render each - let mut results = Vec::new(); - for key in keys_str.lines().filter(|k| !k.is_empty() && *k != "no results") { - if let Ok(content) = memory::memory_render(None, key, Some(true)).await { - if !content.trim().is_empty() { - results.push((key.to_string(), content)); - } - } +/// Load content for a list of node keys. +async fn load_nodes(keys: &[String]) -> Vec<(String, String)> { + let mut results = Vec::new(); + for key in keys { + if let Ok(content) = memory::memory_render(None, key, Some(true)).await { + if !content.trim().is_empty() { + results.push((key.clone(), content.trim().to_string())); } - results - } - crate::config::ContextSource::Store => { - let mut results = Vec::new(); - for key in &group.keys { - if let Ok(content) = memory::memory_render(None, key, Some(true)).await { - if !content.trim().is_empty() { - results.push((key.clone(), content.trim().to_string())); - } - } - } - results } } + results } pub async fn cmd_load_context(stats: bool) -> Result<()> { let cfg = crate::config::get(); + let personality = load_nodes(&cfg.personality_nodes).await; + let agent = load_nodes(&cfg.agent_nodes).await; + if stats { - let mut total_words = 0; - let mut total_entries = 0; + let p_words: usize = personality.iter().map(|(_, c)| c.split_whitespace().count()).sum(); + let a_words: usize = agent.iter().map(|(_, c)| c.split_whitespace().count()).sum(); + println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS"); println!("{}", "-".repeat(42)); - - for group in &cfg.context_groups { - let entries = get_group_content(group, &cfg).await; - let words: usize = entries.iter() - .map(|(_, c)| c.split_whitespace().count()) - .sum(); - let count = entries.len(); - println!("{:<25} {:>6} {:>8}", group.label, count, words); - total_words += words; - total_entries += count; - } - + println!("{:<25} {:>6} {:>8}", "personality_nodes", personality.len(), p_words); + println!("{:<25} {:>6} {:>8}", "agent_nodes", agent.len(), a_words); println!("{}", "-".repeat(42)); - println!("{:<25} {:>6} {:>8}", "TOTAL", total_entries, total_words); + println!("{:<25} {:>6} {:>8}", "TOTAL", personality.len() + agent.len(), p_words + a_words); return Ok(()); } println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name); - for group in &cfg.context_groups { - let entries = get_group_content(group, &cfg).await; - if !entries.is_empty() && group.source == crate::config::ContextSource::Journal { - println!("--- recent journal entries ({}/{}) ---", - entries.len(), cfg.journal_max); + if !personality.is_empty() { + println!("--- personality_nodes ({}) ---", personality.len()); + for (key, content) in personality { + println!("## {}", key); + println!("{}\n", content); } - for (key, content) in entries { - if group.source == crate::config::ContextSource::Journal { - println!("## {}", key); - } else { - println!("--- {} ({}) ---", key, group.label); - } + } + + if !agent.is_empty() { + println!("--- agent_nodes ({}) ---", agent.len()); + for (key, content) in agent { + println!("## {}", key); println!("{}\n", content); } } diff --git a/src/config.rs b/src/config.rs index 9fb8261..7e5eb93 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,29 +29,6 @@ pub fn config_path() -> PathBuf { static CONFIG: OnceLock>> = OnceLock::new(); -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(rename_all = "lowercase")] -#[derive(Default)] -pub enum ContextSource { - #[serde(alias = "")] - #[default] - Store, - Journal, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ContextGroup { - pub label: String, - #[serde(default)] - pub keys: Vec, - #[serde(default)] - pub source: ContextSource, - /// Include this group in agent context (default true) - #[serde(default = "default_true")] - pub agent: bool, -} - -fn default_true() -> bool { true } fn default_context_window() -> usize { 128_000 } fn default_stream_timeout() -> u64 { 60 } fn default_scoring_chunk_tokens() -> usize { 50_000 } @@ -76,13 +53,17 @@ pub struct Config { pub identity_dir: PathBuf, #[serde(deserialize_with = "deserialize_path")] pub projects_dir: PathBuf, - pub core_nodes: Vec, - /// Nodes that cannot be deleted or renamed without --force + /// Nodes that cannot be deleted or renamed #[serde(default)] pub protected_nodes: Vec, + /// Nodes loaded into main session context + #[serde(default)] + pub personality_nodes: Vec, + /// Nodes loaded into subconscious agent context + #[serde(default)] + pub agent_nodes: Vec, pub journal_days: u32, pub journal_max: usize, - pub context_groups: Vec, pub llm_concurrency: usize, pub agent_budget: usize, #[serde(deserialize_with = "deserialize_path")] @@ -147,24 +128,11 @@ impl Default for Config { data_dir: home.join(".consciousness/memory"), identity_dir: home.join(".consciousness/identity"), projects_dir: home.join(".claude/projects"), - core_nodes: vec!["identity".to_string(), "core-practices".to_string()], protected_nodes: Vec::new(), + personality_nodes: vec!["identity".into(), "core-practices".into()], + agent_nodes: vec!["identity".into(), "core-practices".into()], journal_days: 7, journal_max: 20, - context_groups: vec![ - ContextGroup { - label: "identity".into(), - keys: vec!["identity".into()], - source: ContextSource::Store, - agent: true, - }, - ContextGroup { - label: "core-practices".into(), - keys: vec!["core-practices".into()], - source: ContextSource::Store, - agent: true, - }, - ], llm_concurrency: 1, agent_budget: 1000, prompts_dir: home.join(".consciousness/prompts"), @@ -242,97 +210,9 @@ impl Config { Some(config) } - /// Load from legacy JSONL config (~/.consciousness/config.jsonl). + /// Load from legacy JSONL config — deprecated, just return defaults. fn load_legacy_jsonl() -> Self { - let path = std::env::var("POC_MEMORY_CONFIG") - .map(PathBuf::from) - .unwrap_or_else(|_| { - dirs::home_dir().unwrap_or_default() - .join(".consciousness/config.jsonl") - }); - - let mut config = Config::default(); - - let Ok(content) = std::fs::read_to_string(&path) else { - return config; - }; - - let mut context_groups: Vec = Vec::new(); - - let stream = serde_json::Deserializer::from_str(&content) - .into_iter::(); - - for result in stream { - let Ok(obj) = result else { continue }; - - if let Some(cfg) = obj.get("config") { - if let Some(s) = cfg.get("user_name").and_then(|v| v.as_str()) { - config.user_name = s.to_string(); - } - if let Some(s) = cfg.get("assistant_name").and_then(|v| v.as_str()) { - config.assistant_name = s.to_string(); - } - if let Some(s) = cfg.get("data_dir").and_then(|v| v.as_str()) { - config.data_dir = expand_home(s); - } - if let Some(s) = cfg.get("projects_dir").and_then(|v| v.as_str()) { - config.projects_dir = expand_home(s); - } - if let Some(arr) = cfg.get("core_nodes").and_then(|v| v.as_array()) { - config.core_nodes = arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - } - if let Some(d) = cfg.get("journal_days").and_then(|v| v.as_u64()) { - config.journal_days = d as u32; - } - if let Some(m) = cfg.get("journal_max").and_then(|v| v.as_u64()) { - config.journal_max = m as usize; - } - if let Some(n) = cfg.get("llm_concurrency").and_then(|v| v.as_u64()) { - config.llm_concurrency = n.max(1) as usize; - } - if let Some(n) = cfg.get("agent_budget").and_then(|v| v.as_u64()) { - config.agent_budget = n as usize; - } - if let Some(s) = cfg.get("prompts_dir").and_then(|v| v.as_str()) { - config.prompts_dir = expand_home(s); - } - if let Some(s) = cfg.get("api_base_url").and_then(|v| v.as_str()) { - config.api_base_url = Some(s.to_string()); - } - if let Some(s) = cfg.get("api_key").and_then(|v| v.as_str()) { - config.api_key = Some(s.to_string()); - } - if let Some(s) = cfg.get("api_model").and_then(|v| v.as_str()) { - config.api_model = Some(s.to_string()); - } - continue; - } - - if let Some(label) = obj.get("group").and_then(|v| v.as_str()) { - let keys = obj.get("keys") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect()) - .unwrap_or_default(); - - let source = match obj.get("source").and_then(|v| v.as_str()) { - Some("journal") => ContextSource::Journal, - _ => ContextSource::Store, - }; - - let agent = obj.get("agent").and_then(|v| v.as_bool()).unwrap_or(true); - context_groups.push(ContextGroup { label: label.to_string(), keys, source, agent }); - } - } - - if !context_groups.is_empty() { - config.context_groups = context_groups; - } - - config + Config::default() } } @@ -553,10 +433,10 @@ impl AppConfig { }; } - let context_groups = get().context_groups.clone(); + let personality_nodes = get().personality_nodes.clone(); let (context_parts, config_file_count, memory_file_count) = - crate::mind::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &context_groups).await?; + crate::mind::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &personality_nodes).await?; let session_dir = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) @@ -697,8 +577,8 @@ pub async fn load_session(cli: &crate::user::CliArgs) -> Result<(SessionConfig, /// Re-assemble context for a specific model's prompt file. pub async fn reload_for_model(app: &AppConfig, prompt_file: &str) -> Result> { let cwd = std::env::current_dir().context("Failed to get current directory")?; - let context_groups = get().context_groups.clone(); - let (context_parts, _, _) = crate::mind::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref(), &context_groups).await?; + let personality_nodes = get().personality_nodes.clone(); + let (context_parts, _, _) = crate::mind::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref(), &personality_nodes).await?; Ok(context_parts) } diff --git a/src/mind/identity.rs b/src/mind/identity.rs index ae32131..73a16c8 100644 --- a/src/mind/identity.rs +++ b/src/mind/identity.rs @@ -1,19 +1,12 @@ // identity.rs — Identity file discovery and context assembly // // Discovers and loads the agent's identity: instruction files (CLAUDE.md, -// POC.md), memory files, and the system prompt. Reads context_groups -// from the shared config file. +// POC.md), memory nodes, and the system prompt. use anyhow::Result; use std::path::{Path, PathBuf}; use crate::agent::tools::memory::memory_render; -use crate::config::{ContextGroup, ContextSource}; - -/// Read a file if it exists and is non-empty. -fn read_nonempty(path: &Path) -> Option { - std::fs::read_to_string(path).ok().filter(|s| !s.trim().is_empty()) -} /// Walk from cwd to git root collecting instruction files (CLAUDE.md / POC.md). /// @@ -60,53 +53,14 @@ fn find_context_files(cwd: &Path, prompt_file: &str) -> Vec { found } -/// Load memory files from config's context_groups. -/// Store sources load from the memory graph. Journal source loads recent entries. -async fn load_memory_files(memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Vec<(String, String)> { - let home = match dirs::home_dir() { - Some(h) => h, - None => return Vec::new(), - }; - - let global = home.join(".consciousness"); - let project = memory_project.map(PathBuf::from); - +/// Load memory nodes from the store. +async fn load_memory_nodes(keys: &[String]) -> Vec<(String, String)> { let mut memories: Vec<(String, String)> = Vec::new(); - // Load from context_groups - for group in context_groups { - match group.source { - ContextSource::Journal => { - // Journal loading handled separately - continue; - } - ContextSource::Store => { - // Load from the memory graph store via typed API - for key in &group.keys { - if let Ok(c) = memory_render(None, key, Some(true)).await { - if !c.trim().is_empty() { - memories.push((key.clone(), c)); - } - } - } - } - } - } - - // People dir — glob all .md files - for dir in [project.as_deref(), Some(global.as_path())].into_iter().flatten() { - let people_dir = dir.join("people"); - if let Ok(entries) = std::fs::read_dir(&people_dir) { - let mut paths: Vec<_> = entries.flatten() - .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) - .collect(); - paths.sort_by_key(|e| e.file_name()); - for entry in paths { - let rel = format!("people/{}", entry.file_name().to_string_lossy()); - if memories.iter().any(|(n, _)| n == &rel) { continue; } - if let Some(content) = read_nonempty(&entry.path()) { - memories.push((rel, content)); - } + for key in keys { + if let Ok(c) = memory_render(None, key, Some(true)).await { + if !c.trim().is_empty() { + memories.push((key.clone(), c)); } } } @@ -114,8 +68,8 @@ async fn load_memory_files(memory_project: Option<&Path>, context_groups: &[Cont memories } -/// Context message: instruction files + memory files + manifest. -pub async fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Result<(Vec<(String, String)>, usize, usize)> { +/// Context message: instruction files + memory nodes. +pub async fn assemble_context_message(cwd: &Path, prompt_file: &str, _memory_project: Option<&Path>, personality_nodes: &[String]) -> Result<(Vec<(String, String)>, usize, usize)> { let mut parts: Vec<(String, String)> = vec![ ("Preamble".to_string(), "Everything below is already loaded — your identity, instructions, \ @@ -136,7 +90,7 @@ pub async fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_proj } } - let memories = load_memory_files(memory_project, context_groups).await; + let memories = load_memory_nodes(personality_nodes).await; let memory_count = memories.len(); for (name, content) in memories { parts.push((name, content)); diff --git a/src/subconscious/defs.rs b/src/subconscious/defs.rs index 06f3ddc..8828043 100644 --- a/src/subconscious/defs.rs +++ b/src/subconscious/defs.rs @@ -293,19 +293,19 @@ async fn resolve( Some(Resolved { text: out, keys: all_keys }) } - // agent-context — personality/identity groups from load-context config + // agent-context — agent identity nodes from config "agent-context" => { let cfg = crate::config::get(); let mut text = String::new(); let mut keys = Vec::new(); - for group in &cfg.context_groups { - if !group.agent { continue; } - let entries = crate::cli::node::get_group_content(group, &cfg).await; - for (key, content) in entries { - use std::fmt::Write; - writeln!(text, "--- {} ({}) ---", key, group.label).ok(); - writeln!(text, "{}\n", content).ok(); - keys.push(key); + for key in &cfg.agent_nodes { + if let Ok(content) = crate::hippocampus::memory_render(None, key, Some(true)).await { + if !content.trim().is_empty() { + use std::fmt::Write; + writeln!(text, "--- {} ---", key).ok(); + writeln!(text, "{}\n", content.trim()).ok(); + keys.push(key.clone()); + } } } if text.is_empty() { None }