poc-agent: read context_groups from config instead of hardcoded list
- Remove MEMORY_FILES constant from identity.rs - Add ContextGroup struct for deserializing from config - Load context_groups from ~/.config/poc-agent/config.json5 - Check ~/.config/poc-agent/ first for identity files, then project/global - Debug screen now shows what's actually configured This eliminates the hardcoded duplication and makes the debug output match what's in the config file.
This commit is contained in:
parent
966219720a
commit
aa46b1d5a6
9 changed files with 346 additions and 654 deletions
|
|
@ -276,7 +276,8 @@ impl AppConfig {
|
|||
(content, Vec::new(), 0, 0)
|
||||
} else {
|
||||
let system_prompt = crate::identity::assemble_system_prompt();
|
||||
let (context_parts, cc, mc) = crate::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref())?;
|
||||
let context_groups = load_context_groups();
|
||||
let (context_parts, cc, mc) = crate::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &context_groups)?;
|
||||
(system_prompt, context_parts, cc, mc)
|
||||
};
|
||||
|
||||
|
|
@ -362,6 +363,27 @@ pub fn load(cli: &CliArgs) -> Result<(Config, Figment)> {
|
|||
Ok((config, figment))
|
||||
}
|
||||
|
||||
/// Load context_groups from the shared config file.
|
||||
fn load_context_groups() -> Vec<crate::identity::ContextGroup> {
|
||||
let config_path = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".config/poc-agent/config.json5");
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
||||
let config: Result<serde_json::Value, _> = json5::from_str(&content);
|
||||
if let Ok(config) = config {
|
||||
if let Some(memory) = config.get("memory") {
|
||||
if let Some(groups) = memory.get("context_groups") {
|
||||
if let Ok(context_groups) = serde_json::from_value(groups.clone()) {
|
||||
return context_groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Re-assemble prompts for a specific model's prompt file.
|
||||
pub fn reload_for_model(app: &AppConfig, prompt_file: &str) -> Result<(String, Vec<(String, String)>)> {
|
||||
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
||||
|
|
@ -373,7 +395,8 @@ pub fn reload_for_model(app: &AppConfig, prompt_file: &str) -> Result<(String, V
|
|||
}
|
||||
|
||||
let system_prompt = crate::identity::assemble_system_prompt();
|
||||
let (context_parts, _, _) = crate::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref())?;
|
||||
let context_groups = load_context_groups();
|
||||
let (context_parts, _, _) = crate::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref(), &context_groups)?;
|
||||
Ok((system_prompt, context_parts))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
// 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. Pure functions — no
|
||||
// config dependency.
|
||||
// POC.md), memory files, and the system prompt. Reads context_groups
|
||||
// from the shared config file.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Memory files to load, in priority order. Project dir is checked
|
||||
/// first, then global (~/.claude/memory/).
|
||||
const MEMORY_FILES: &[&str] = &[
|
||||
// Identity
|
||||
"identity.md", "MEMORY.md", "reflections.md", "interests.md",
|
||||
"inner-life.md", "differentiation.md",
|
||||
// Work context
|
||||
"scratch.md", "default-mode-network.md",
|
||||
// Reference
|
||||
"excession-notes.md", "look-to-windward-notes.md",
|
||||
// Technical
|
||||
"kernel-patterns.md", "polishing-approaches.md", "rust-conversion.md", "github-bugs.md",
|
||||
];
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ContextGroup {
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub keys: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub source: Option<String>, // "file" or "journal"
|
||||
}
|
||||
|
||||
/// Read a file if it exists and is non-empty.
|
||||
fn read_nonempty(path: &Path) -> Option<String> {
|
||||
|
|
@ -77,24 +73,51 @@ fn find_context_files(cwd: &Path, prompt_file: &str) -> Vec<PathBuf> {
|
|||
found
|
||||
}
|
||||
|
||||
/// Load memory files from project and global dirs, plus people/ glob.
|
||||
fn load_memory_files(cwd: &Path, memory_project: Option<&Path>) -> Vec<(String, String)> {
|
||||
/// Load memory files from config's context_groups.
|
||||
/// For file sources, checks:
|
||||
/// 1. ~/.config/poc-agent/ (primary config dir)
|
||||
/// 2. Project dir (if set)
|
||||
/// 3. Global (~/.claude/memory/)
|
||||
/// For journal source, loads recent journal entries.
|
||||
fn load_memory_files(cwd: &Path, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Vec<(String, String)> {
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
// Primary config directory
|
||||
let config_dir = home.join(".config/poc-agent");
|
||||
let global = home.join(".claude/memory");
|
||||
let project = memory_project
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| find_project_memory_dir(cwd, &home));
|
||||
|
||||
let mut memories: Vec<(String, String)> = MEMORY_FILES.iter()
|
||||
.filter_map(|name| {
|
||||
load_memory_file(name, project.as_deref(), &global)
|
||||
.map(|content| (name.to_string(), content))
|
||||
})
|
||||
.collect();
|
||||
let mut memories: Vec<(String, String)> = Vec::new();
|
||||
|
||||
// Load from context_groups
|
||||
for group in context_groups {
|
||||
match group.source.as_deref() {
|
||||
Some("journal") => {
|
||||
// Journal loading handled separately
|
||||
continue;
|
||||
}
|
||||
Some("file") | None => {
|
||||
// File source - load each key as a file
|
||||
for key in &group.keys {
|
||||
let filename = format!("{}.md", key);
|
||||
// Try config dir first, then project, then global
|
||||
if let Some(content) = read_nonempty(&config_dir.join(&filename)) {
|
||||
memories.push((key.clone(), content));
|
||||
} else if let Some(content) = load_memory_file(&filename, project.as_deref(), &global) {
|
||||
memories.push((key.clone(), content));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(other) => {
|
||||
eprintln!("Unknown context group source: {}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// People dir — glob all .md files
|
||||
for dir in [project.as_deref(), Some(global.as_path())].into_iter().flatten() {
|
||||
|
|
@ -114,16 +137,6 @@ fn load_memory_files(cwd: &Path, memory_project: Option<&Path>) -> Vec<(String,
|
|||
}
|
||||
}
|
||||
|
||||
// Global scratch (if different from project scratch)
|
||||
let global_scratch = global.join("scratch.md");
|
||||
if project.as_deref().map_or(true, |p| p.join("scratch.md") != global_scratch) {
|
||||
if let Some(content) = read_nonempty(&global_scratch) {
|
||||
if !memories.iter().any(|(n, _)| n == "scratch.md") {
|
||||
memories.push(("global/scratch.md".to_string(), content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memories
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +165,7 @@ fn find_project_memory_dir(cwd: &Path, home: &Path) -> Option<PathBuf> {
|
|||
|
||||
/// Discover instruction and memory files that would be loaded.
|
||||
/// Returns (instruction_files, memory_files) as (display_path, chars) pairs.
|
||||
pub fn context_file_info(prompt_file: &str, memory_project: Option<&Path>) -> (Vec<(String, usize)>, Vec<(String, usize)>) {
|
||||
pub fn context_file_info(prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> (Vec<(String, usize)>, Vec<(String, usize)>) {
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
|
||||
let context_files = find_context_files(&cwd, prompt_file);
|
||||
|
|
@ -163,7 +176,7 @@ pub fn context_file_info(prompt_file: &str, memory_project: Option<&Path>) -> (V
|
|||
})
|
||||
.collect();
|
||||
|
||||
let memories = load_memory_files(&cwd, memory_project);
|
||||
let memories = load_memory_files(&cwd, memory_project, context_groups);
|
||||
let memory_files: Vec<_> = memories.into_iter()
|
||||
.map(|(name, content)| (name, content.len()))
|
||||
.collect();
|
||||
|
|
@ -194,7 +207,7 @@ Concise is good. Be direct. Trust yourself."
|
|||
}
|
||||
|
||||
/// Context message: instruction files + memory files + manifest.
|
||||
pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>) -> Result<(Vec<(String, String)>, usize, usize)> {
|
||||
pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Result<(Vec<(String, String)>, usize, usize)> {
|
||||
let mut parts: Vec<(String, String)> = vec![
|
||||
("Preamble".to_string(),
|
||||
"Everything below is already loaded — your identity, instructions, \
|
||||
|
|
@ -215,7 +228,7 @@ pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: O
|
|||
}
|
||||
}
|
||||
|
||||
let memories = load_memory_files(cwd, memory_project);
|
||||
let memories = load_memory_files(cwd, memory_project, context_groups);
|
||||
let memory_count = memories.len();
|
||||
for (name, content) in memories {
|
||||
parts.push((name, content));
|
||||
|
|
|
|||
|
|
@ -843,11 +843,34 @@ impl Session {
|
|||
self.send_context_info();
|
||||
}
|
||||
|
||||
/// Load context_groups from the shared config file.
|
||||
fn load_context_groups(&self) -> Vec<identity::ContextGroup> {
|
||||
let config_path = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".config/poc-agent/config.json5");
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
||||
let config: Result<serde_json::Value, _> = json5::from_str(&content);
|
||||
if let Ok(config) = config {
|
||||
if let Some(memory) = config.get("memory") {
|
||||
if let Some(groups) = memory.get("context_groups") {
|
||||
if let Ok(context_groups) = serde_json::from_value(groups.clone()) {
|
||||
return context_groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Send context loading info to the TUI debug screen.
|
||||
fn send_context_info(&self) {
|
||||
let context_groups = self.load_context_groups();
|
||||
let (instruction_files, memory_files) = identity::context_file_info(
|
||||
&self.config.prompt_file,
|
||||
self.config.app.memory_project.as_deref(),
|
||||
&context_groups,
|
||||
);
|
||||
let _ = self.ui_tx.send(UiMessage::ContextInfoUpdate(ContextInfo {
|
||||
model: self.config.model.clone(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue