config: unify memory and agent config into single module
Both hippocampus/config.rs and agent/config.rs read from the same config file (~/.config/poc-agent/config.json5). Having two separate implementations was a footgun — load_context_groups() was duplicated three times across the codebase. Merged into src/config.rs: - Config (memory settings, global get()/reload()) - AppConfig (agent backend/model settings, figment-based loading) - SessionConfig (resolved agent session, renamed from agent's Config) - Single ContextGroup/ContextSource definition used everywhere Eliminated: duplicate load_context_groups(), duplicate ContextGroup definition in identity.rs, duplicate config file path constants. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
2f3fbb3353
commit
228815d807
7 changed files with 393 additions and 467 deletions
|
|
@ -5,17 +5,9 @@
|
||||||
// from the shared config file.
|
// from the shared config file.
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
use crate::config::{ContextGroup, ContextSource};
|
||||||
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.
|
/// Read a file if it exists and is non-empty.
|
||||||
fn read_nonempty(path: &Path) -> Option<String> {
|
fn read_nonempty(path: &Path) -> Option<String> {
|
||||||
|
|
@ -96,12 +88,12 @@ fn load_memory_files(cwd: &Path, memory_project: Option<&Path>, context_groups:
|
||||||
|
|
||||||
// Load from context_groups
|
// Load from context_groups
|
||||||
for group in context_groups {
|
for group in context_groups {
|
||||||
match group.source.as_deref() {
|
match group.source {
|
||||||
Some("journal") => {
|
ContextSource::Journal => {
|
||||||
// Journal loading handled separately
|
// Journal loading handled separately
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Some("file") | None => {
|
ContextSource::File | ContextSource::Store => {
|
||||||
// File source - load each key as a file
|
// File source - load each key as a file
|
||||||
for key in &group.keys {
|
for key in &group.keys {
|
||||||
let filename = format!("{}.md", key);
|
let filename = format!("{}.md", key);
|
||||||
|
|
@ -113,9 +105,7 @@ fn load_memory_files(cwd: &Path, memory_project: Option<&Path>, context_groups:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(other) => {
|
// All variants covered
|
||||||
eprintln!("Unknown context group source: {}", other);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ macro_rules! dbglog {
|
||||||
// - tools/ — tool definitions and dispatch
|
// - tools/ — tool definitions and dispatch
|
||||||
// - ui_channel — streaming UI communication
|
// - ui_channel — streaming UI communication
|
||||||
// - runner — the interactive agent loop
|
// - runner — the interactive agent loop
|
||||||
// - cli, config, context, dmn, identity, log, observe, parsing, tui
|
// - cli, context, dmn, identity, log, observe, parsing, tui
|
||||||
|
// Config moved to crate::config (unified with memory config)
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
@ -29,7 +30,6 @@ pub mod journal;
|
||||||
|
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod config;
|
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod dmn;
|
pub mod dmn;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ use poc_memory::dbglog;
|
||||||
use poc_memory::agent::*;
|
use poc_memory::agent::*;
|
||||||
use poc_memory::agent::runner::{Agent, TurnResult};
|
use poc_memory::agent::runner::{Agent, TurnResult};
|
||||||
use poc_memory::agent::api::ApiClient;
|
use poc_memory::agent::api::ApiClient;
|
||||||
use poc_memory::agent::config::{AppConfig, Config};
|
|
||||||
use poc_memory::agent::tui::HotkeyAction;
|
use poc_memory::agent::tui::HotkeyAction;
|
||||||
|
use poc_memory::config::{self, AppConfig, SessionConfig};
|
||||||
use poc_memory::agent::ui_channel::{ContextInfo, StatusInfo, StreamTarget, UiMessage};
|
use poc_memory::agent::ui_channel::{ContextInfo, StatusInfo, StreamTarget, UiMessage};
|
||||||
|
|
||||||
/// Hard compaction threshold — context is rebuilt immediately.
|
/// Hard compaction threshold — context is rebuilt immediately.
|
||||||
|
|
@ -120,7 +120,7 @@ enum Command {
|
||||||
/// and slash commands.
|
/// and slash commands.
|
||||||
struct Session {
|
struct Session {
|
||||||
agent: Arc<Mutex<Agent>>,
|
agent: Arc<Mutex<Agent>>,
|
||||||
config: Config,
|
config: SessionConfig,
|
||||||
process_tracker: tools::ProcessTracker,
|
process_tracker: tools::ProcessTracker,
|
||||||
ui_tx: ui_channel::UiSender,
|
ui_tx: ui_channel::UiSender,
|
||||||
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
||||||
|
|
@ -149,7 +149,7 @@ struct Session {
|
||||||
impl Session {
|
impl Session {
|
||||||
fn new(
|
fn new(
|
||||||
agent: Arc<Mutex<Agent>>,
|
agent: Arc<Mutex<Agent>>,
|
||||||
config: Config,
|
config: SessionConfig,
|
||||||
process_tracker: tools::ProcessTracker,
|
process_tracker: tools::ProcessTracker,
|
||||||
ui_tx: ui_channel::UiSender,
|
ui_tx: ui_channel::UiSender,
|
||||||
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
||||||
|
|
@ -817,25 +817,9 @@ impl Session {
|
||||||
self.send_context_info();
|
self.send_context_info();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load context_groups from the shared config file.
|
/// Get context_groups from the unified config.
|
||||||
fn load_context_groups(&self) -> Vec<identity::ContextGroup> {
|
fn load_context_groups(&self) -> Vec<config::ContextGroup> {
|
||||||
let config_path = dirs::home_dir()
|
config::get().context_groups.clone()
|
||||||
.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.
|
/// Send context loading info to the TUI debug screen.
|
||||||
|
|
@ -885,7 +869,7 @@ impl Session {
|
||||||
// --- Event loop ---
|
// --- Event loop ---
|
||||||
|
|
||||||
async fn run(cli: cli::CliArgs) -> Result<()> {
|
async fn run(cli: cli::CliArgs) -> Result<()> {
|
||||||
let (config, _figment) = config::load(&cli)?;
|
let (config, _figment) = config::load_session(&cli)?;
|
||||||
|
|
||||||
// Wire config.debug to the POC_DEBUG env var so all debug checks
|
// Wire config.debug to the POC_DEBUG env var so all debug checks
|
||||||
// throughout the codebase (API, SSE reader, diagnostics) see it.
|
// throughout the codebase (API, SSE reader, diagnostics) see it.
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,300 @@
|
||||||
// config.rs — Configuration and context loading
|
// config.rs — Unified configuration
|
||||||
//
|
//
|
||||||
// Loads configuration from three layers (later overrides earlier):
|
// Single config file: ~/.config/poc-agent/config.json5
|
||||||
// 1. Compiled defaults (AppConfig::default())
|
// Memory settings in the "memory" section (Config)
|
||||||
// 2. JSON5 config file (~/.config/poc-agent/config.json5)
|
// Agent/backend settings at top level (AppConfig)
|
||||||
// 3. CLI arguments
|
|
||||||
//
|
//
|
||||||
// Prompt assembly is split into two parts:
|
// Legacy fallback: ~/.config/poc-memory/config.jsonl
|
||||||
//
|
// Env override: POC_MEMORY_CONFIG
|
||||||
// - system_prompt: Short (~1K chars) — agent identity, tool instructions,
|
|
||||||
// behavioral norms. Sent as the system message with every API call.
|
|
||||||
//
|
|
||||||
// - context_message: Long — CLAUDE.md files + memory files + manifest.
|
|
||||||
// Sent as the first user message once per session. This is the identity
|
|
||||||
// layer — same files, same prompt, different model = same person.
|
|
||||||
//
|
|
||||||
// The split matters because long system prompts degrade tool-calling
|
|
||||||
// behavior on models like Qwen 3.5 (documented: >8K chars causes
|
|
||||||
// degradation). By keeping the system prompt short and putting identity
|
|
||||||
// context in a user message, we get reliable tool use AND full identity.
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, OnceLock, RwLock};
|
||||||
|
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
use figment::providers::Serialized;
|
use figment::providers::Serialized;
|
||||||
use figment::{Figment, Provider};
|
use figment::{Figment, Provider};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::agent::cli::CliArgs;
|
/// Config file path shared by all loaders.
|
||||||
|
pub fn config_path() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join(".config/poc-agent/config.json5")
|
||||||
|
}
|
||||||
|
|
||||||
// --- AppConfig types ---
|
// ============================================================
|
||||||
|
// Memory config (the "memory" section)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static CONFIG: OnceLock<RwLock<Arc<Config>>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum ContextSource {
|
||||||
|
#[serde(alias = "")]
|
||||||
|
#[default]
|
||||||
|
Store,
|
||||||
|
File,
|
||||||
|
Journal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct ContextGroup {
|
||||||
|
pub label: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keys: Vec<String>,
|
||||||
|
#[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 }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub user_name: String,
|
||||||
|
pub assistant_name: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_path")]
|
||||||
|
pub data_dir: PathBuf,
|
||||||
|
#[serde(deserialize_with = "deserialize_path")]
|
||||||
|
pub projects_dir: PathBuf,
|
||||||
|
pub core_nodes: Vec<String>,
|
||||||
|
pub journal_days: u32,
|
||||||
|
pub journal_max: usize,
|
||||||
|
pub context_groups: Vec<ContextGroup>,
|
||||||
|
pub llm_concurrency: usize,
|
||||||
|
pub agent_budget: usize,
|
||||||
|
#[serde(deserialize_with = "deserialize_path")]
|
||||||
|
pub prompts_dir: PathBuf,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_path_opt")]
|
||||||
|
pub agent_config_dir: Option<PathBuf>,
|
||||||
|
/// Resolved from agent_model → models → backend (not in config directly)
|
||||||
|
#[serde(skip)]
|
||||||
|
pub api_base_url: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub api_model: Option<String>,
|
||||||
|
/// Used to resolve API settings, not stored on Config
|
||||||
|
#[serde(default)]
|
||||||
|
agent_model: Option<String>,
|
||||||
|
pub api_reasoning: String,
|
||||||
|
pub agent_types: Vec<String>,
|
||||||
|
/// Surface agent timeout in seconds.
|
||||||
|
#[serde(default)]
|
||||||
|
pub surface_timeout_secs: Option<u32>,
|
||||||
|
/// Hook events that trigger the surface agent.
|
||||||
|
#[serde(default)]
|
||||||
|
pub surface_hooks: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
let home = PathBuf::from(std::env::var("HOME").expect("HOME not set"));
|
||||||
|
Self {
|
||||||
|
user_name: "User".to_string(),
|
||||||
|
assistant_name: "Assistant".to_string(),
|
||||||
|
data_dir: home.join(".claude/memory"),
|
||||||
|
projects_dir: home.join(".claude/projects"),
|
||||||
|
core_nodes: vec!["identity".to_string(), "core-practices".to_string()],
|
||||||
|
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("poc/consciousness/src/subconscious/prompts"),
|
||||||
|
agent_config_dir: None,
|
||||||
|
api_base_url: None,
|
||||||
|
api_key: None,
|
||||||
|
api_model: None,
|
||||||
|
agent_model: None,
|
||||||
|
api_reasoning: "high".to_string(),
|
||||||
|
agent_types: vec![
|
||||||
|
"linker".into(), "organize".into(), "distill".into(),
|
||||||
|
"separator".into(), "split".into(),
|
||||||
|
],
|
||||||
|
surface_timeout_secs: None,
|
||||||
|
surface_hooks: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn load_from_file() -> Self {
|
||||||
|
if let Some(config) = Self::try_load_shared() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
Self::load_legacy_jsonl()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load from shared config. Memory settings in the "memory" section;
|
||||||
|
/// API settings resolved from models + backend configuration.
|
||||||
|
fn try_load_shared() -> Option<Self> {
|
||||||
|
let content = std::fs::read_to_string(config_path()).ok()?;
|
||||||
|
let root: serde_json::Value = json5::from_str(&content).ok()?;
|
||||||
|
let mem_value = root.get("memory")?;
|
||||||
|
|
||||||
|
let mut config: Config = serde_json::from_value(mem_value.clone()).ok()?;
|
||||||
|
config.llm_concurrency = config.llm_concurrency.max(1);
|
||||||
|
|
||||||
|
// Resolve API settings: agent_model → models → backend
|
||||||
|
if let Some(model_name) = &config.agent_model
|
||||||
|
&& let Some(model_cfg) = root.get("models").and_then(|m| m.get(model_name.as_str())) {
|
||||||
|
let backend_name = model_cfg.get("backend").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let model_id = model_cfg.get("model_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
if let Some(backend) = root.get(backend_name) {
|
||||||
|
config.api_base_url = backend.get("base_url")
|
||||||
|
.and_then(|v| v.as_str()).map(String::from);
|
||||||
|
config.api_key = backend.get("api_key")
|
||||||
|
.and_then(|v| v.as_str()).map(String::from);
|
||||||
|
}
|
||||||
|
config.api_model = Some(model_id.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load from legacy JSONL config (~/.config/poc-memory/config.jsonl).
|
||||||
|
fn load_legacy_jsonl() -> Self {
|
||||||
|
let path = std::env::var("POC_MEMORY_CONFIG")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
PathBuf::from(std::env::var("HOME").expect("HOME not set"))
|
||||||
|
.join(".config/poc-memory/config.jsonl")
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
|
||||||
|
let Ok(content) = std::fs::read_to_string(&path) else {
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut context_groups: Vec<ContextGroup> = Vec::new();
|
||||||
|
|
||||||
|
let stream = serde_json::Deserializer::from_str(&content)
|
||||||
|
.into_iter::<serde_json::Value>();
|
||||||
|
|
||||||
|
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("agent_config_dir").and_then(|v| v.as_str()) {
|
||||||
|
config.agent_config_dir = Some(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("file") => ContextSource::File,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the global memory config (cheap Arc clone).
|
||||||
|
pub fn get() -> Arc<Config> {
|
||||||
|
CONFIG
|
||||||
|
.get_or_init(|| RwLock::new(Arc::new(Config::load_from_file())))
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload the config from disk. Returns true if changed.
|
||||||
|
pub fn reload() -> bool {
|
||||||
|
let lock = CONFIG.get_or_init(|| RwLock::new(Arc::new(Config::load_from_file())));
|
||||||
|
let new = Config::load_from_file();
|
||||||
|
let mut current = lock.write().unwrap();
|
||||||
|
let changed = format!("{:?}", **current) != format!("{:?}", new);
|
||||||
|
if changed {
|
||||||
|
*current = Arc::new(new);
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Agent config (top-level settings)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
|
|
@ -65,7 +331,8 @@ impl BackendConfig {
|
||||||
fn resolve(&self, default_base: &str) -> Result<(String, String, String)> {
|
fn resolve(&self, default_base: &str) -> Result<(String, String, String)> {
|
||||||
if self.api_key.is_empty() {
|
if self.api_key.is_empty() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"No API key. Set it in ~/.config/poc-agent/config.json5 or use --api-key"
|
"No API key. Set it in {} or use --api-key",
|
||||||
|
config_path().display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let base = self.base_url.clone()
|
let base = self.base_url.clone()
|
||||||
|
|
@ -97,11 +364,10 @@ pub struct ModelConfig {
|
||||||
pub backend: String,
|
pub backend: String,
|
||||||
/// Model identifier sent to the API
|
/// Model identifier sent to the API
|
||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
/// Instruction file ("CLAUDE.md" or "POC.md"). Falls back to
|
/// Instruction file ("CLAUDE.md" or "POC.md").
|
||||||
/// auto-detection from the model name if not specified.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prompt_file: Option<String>,
|
pub prompt_file: Option<String>,
|
||||||
/// Context window size in tokens. Auto-detected if absent.
|
/// Context window size in tokens.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub context_window: Option<usize>,
|
pub context_window: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
@ -145,66 +411,8 @@ impl Default for AppConfig {
|
||||||
|
|
||||||
fn default_model_name() -> String { String::new() }
|
fn default_model_name() -> String { String::new() }
|
||||||
|
|
||||||
// --- Json5File: figment provider ---
|
/// Resolved, ready-to-use agent session config.
|
||||||
|
pub struct SessionConfig {
|
||||||
struct Json5File(PathBuf);
|
|
||||||
|
|
||||||
impl Provider for Json5File {
|
|
||||||
fn metadata(&self) -> figment::Metadata {
|
|
||||||
figment::Metadata::named(format!("JSON5 file ({})", self.0.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn data(&self) -> figment::Result<figment::value::Map<figment::Profile, figment::value::Dict>> {
|
|
||||||
match std::fs::read_to_string(&self.0) {
|
|
||||||
Ok(content) => {
|
|
||||||
let value: figment::value::Value = json5::from_str(&content)
|
|
||||||
.map_err(|e| figment::Error::from(format!("{}: {}", self.0.display(), e)))?;
|
|
||||||
Serialized::defaults(value).data()
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(figment::value::Map::new()),
|
|
||||||
Err(e) => Err(figment::Error::from(format!("{}: {}", self.0.display(), e))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Figment construction ---
|
|
||||||
|
|
||||||
/// Merge an Option<T> into one or more figment keys.
|
|
||||||
macro_rules! merge_opt {
|
|
||||||
($fig:expr, $val:expr, $($key:expr),+) => {
|
|
||||||
if let Some(ref v) = $val {
|
|
||||||
$( $fig = $fig.merge(Serialized::default($key, v)); )+
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_figment(cli: &CliArgs) -> Figment {
|
|
||||||
let config_path = dirs::home_dir()
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
.join(".config/poc-agent/config.json5");
|
|
||||||
|
|
||||||
let mut f = Figment::from(Serialized::defaults(AppConfig::default()))
|
|
||||||
.merge(Json5File(config_path));
|
|
||||||
|
|
||||||
// CLI overrides — model/key/base go to both backends
|
|
||||||
merge_opt!(f, cli.backend, "backend");
|
|
||||||
merge_opt!(f, cli.model, "anthropic.model", "openrouter.model");
|
|
||||||
merge_opt!(f, cli.api_key, "anthropic.api_key", "openrouter.api_key");
|
|
||||||
merge_opt!(f, cli.api_base, "anthropic.base_url", "openrouter.base_url");
|
|
||||||
merge_opt!(f, cli.system_prompt_file, "system_prompt_file");
|
|
||||||
merge_opt!(f, cli.memory_project, "memory_project");
|
|
||||||
merge_opt!(f, cli.dmn_max_turns, "dmn.max_turns");
|
|
||||||
if cli.debug {
|
|
||||||
f = f.merge(Serialized::default("debug", true));
|
|
||||||
}
|
|
||||||
|
|
||||||
f
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Config loading ---
|
|
||||||
|
|
||||||
/// Resolved, ready-to-use config.
|
|
||||||
pub struct Config {
|
|
||||||
pub api_base: String,
|
pub api_base: String,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
|
|
@ -218,7 +426,7 @@ pub struct Config {
|
||||||
pub app: AppConfig,
|
pub app: AppConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl SessionConfig {
|
||||||
/// Join context parts into a single string for legacy interfaces.
|
/// Join context parts into a single string for legacy interfaces.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn context_message(&self) -> String {
|
pub fn context_message(&self) -> String {
|
||||||
|
|
@ -241,8 +449,8 @@ pub struct ResolvedModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
/// Resolve the active backend and assemble prompts into a ready-to-use Config.
|
/// Resolve the active backend and assemble prompts into a SessionConfig.
|
||||||
pub fn resolve(&self, cli: &CliArgs) -> Result<Config> {
|
pub fn resolve(&self, cli: &crate::agent::cli::CliArgs) -> Result<SessionConfig> {
|
||||||
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
||||||
|
|
||||||
let (api_base, api_key, model, prompt_file);
|
let (api_base, api_key, model, prompt_file);
|
||||||
|
|
@ -254,7 +462,6 @@ impl AppConfig {
|
||||||
model = resolved.model_id;
|
model = resolved.model_id;
|
||||||
prompt_file = resolved.prompt_file;
|
prompt_file = resolved.prompt_file;
|
||||||
} else {
|
} else {
|
||||||
// Legacy path — no models map, use backend field directly
|
|
||||||
let (base, key, mdl) = match self.backend.as_str() {
|
let (base, key, mdl) = match self.backend.as_str() {
|
||||||
"anthropic" => self.anthropic.resolve("https://api.anthropic.com"),
|
"anthropic" => self.anthropic.resolve("https://api.anthropic.com"),
|
||||||
_ => self.openrouter.resolve("https://openrouter.ai/api/v1"),
|
_ => self.openrouter.resolve("https://openrouter.ai/api/v1"),
|
||||||
|
|
@ -269,6 +476,8 @@ impl AppConfig {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let context_groups = get().context_groups.clone();
|
||||||
|
|
||||||
let (system_prompt, context_parts, config_file_count, memory_file_count) =
|
let (system_prompt, context_parts, config_file_count, memory_file_count) =
|
||||||
if let Some(ref path) = cli.system_prompt_file.as_ref().or(self.system_prompt_file.as_ref()) {
|
if let Some(ref path) = cli.system_prompt_file.as_ref().or(self.system_prompt_file.as_ref()) {
|
||||||
let content = std::fs::read_to_string(path)
|
let content = std::fs::read_to_string(path)
|
||||||
|
|
@ -276,7 +485,6 @@ impl AppConfig {
|
||||||
(content, Vec::new(), 0, 0)
|
(content, Vec::new(), 0, 0)
|
||||||
} else {
|
} else {
|
||||||
let system_prompt = crate::agent::identity::assemble_system_prompt();
|
let system_prompt = crate::agent::identity::assemble_system_prompt();
|
||||||
let context_groups = load_context_groups();
|
|
||||||
let (context_parts, cc, mc) = crate::agent::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &context_groups)?;
|
let (context_parts, cc, mc) = crate::agent::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &context_groups)?;
|
||||||
(system_prompt, context_parts, cc, mc)
|
(system_prompt, context_parts, cc, mc)
|
||||||
};
|
};
|
||||||
|
|
@ -286,7 +494,7 @@ impl AppConfig {
|
||||||
.join(".cache/poc-agent/sessions");
|
.join(".cache/poc-agent/sessions");
|
||||||
std::fs::create_dir_all(&session_dir).ok();
|
std::fs::create_dir_all(&session_dir).ok();
|
||||||
|
|
||||||
Ok(Config {
|
Ok(SessionConfig {
|
||||||
api_base, api_key, model, prompt_file,
|
api_base, api_key, model, prompt_file,
|
||||||
system_prompt, context_parts,
|
system_prompt, context_parts,
|
||||||
config_file_count, memory_file_count,
|
config_file_count, memory_file_count,
|
||||||
|
|
@ -349,41 +557,70 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Figment-based agent config loading
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
struct Json5File(PathBuf);
|
||||||
|
|
||||||
|
impl Provider for Json5File {
|
||||||
|
fn metadata(&self) -> figment::Metadata {
|
||||||
|
figment::Metadata::named(format!("JSON5 file ({})", self.0.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> figment::Result<figment::value::Map<figment::Profile, figment::value::Dict>> {
|
||||||
|
match std::fs::read_to_string(&self.0) {
|
||||||
|
Ok(content) => {
|
||||||
|
let value: figment::value::Value = json5::from_str(&content)
|
||||||
|
.map_err(|e| figment::Error::from(format!("{}: {}", self.0.display(), e)))?;
|
||||||
|
Serialized::defaults(value).data()
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(figment::value::Map::new()),
|
||||||
|
Err(e) => Err(figment::Error::from(format!("{}: {}", self.0.display(), e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! merge_opt {
|
||||||
|
($fig:expr, $val:expr, $($key:expr),+) => {
|
||||||
|
if let Some(ref v) = $val {
|
||||||
|
$( $fig = $fig.merge(Serialized::default($key, v)); )+
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_figment(cli: &crate::agent::cli::CliArgs) -> Figment {
|
||||||
|
let mut f = Figment::from(Serialized::defaults(AppConfig::default()))
|
||||||
|
.merge(Json5File(config_path()));
|
||||||
|
|
||||||
|
merge_opt!(f, cli.backend, "backend");
|
||||||
|
merge_opt!(f, cli.model, "anthropic.model", "openrouter.model");
|
||||||
|
merge_opt!(f, cli.api_key, "anthropic.api_key", "openrouter.api_key");
|
||||||
|
merge_opt!(f, cli.api_base, "anthropic.base_url", "openrouter.base_url");
|
||||||
|
merge_opt!(f, cli.system_prompt_file, "system_prompt_file");
|
||||||
|
merge_opt!(f, cli.memory_project, "memory_project");
|
||||||
|
merge_opt!(f, cli.dmn_max_turns, "dmn.max_turns");
|
||||||
|
if cli.debug {
|
||||||
|
f = f.merge(Serialized::default("debug", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
/// Load just the AppConfig — no validation, no prompt assembly.
|
/// Load just the AppConfig — no validation, no prompt assembly.
|
||||||
pub fn load_app(cli: &CliArgs) -> Result<(AppConfig, Figment)> {
|
pub fn load_app(cli: &crate::agent::cli::CliArgs) -> Result<(AppConfig, Figment)> {
|
||||||
let figment = build_figment(cli);
|
let figment = build_figment(cli);
|
||||||
let app: AppConfig = figment.extract().context("Failed to load configuration")?;
|
let app: AppConfig = figment.extract().context("Failed to load configuration")?;
|
||||||
Ok((app, figment))
|
Ok((app, figment))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the full config: figment → AppConfig → resolve backend → assemble prompts.
|
/// Load the full config: figment → AppConfig → resolve backend → assemble prompts.
|
||||||
pub fn load(cli: &CliArgs) -> Result<(Config, Figment)> {
|
pub fn load_session(cli: &crate::agent::cli::CliArgs) -> Result<(SessionConfig, Figment)> {
|
||||||
let (app, figment) = load_app(cli)?;
|
let (app, figment) = load_app(cli)?;
|
||||||
let config = app.resolve(cli)?;
|
let config = app.resolve(cli)?;
|
||||||
Ok((config, figment))
|
Ok((config, figment))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load context_groups from the shared config file.
|
|
||||||
fn load_context_groups() -> Vec<crate::agent::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.
|
/// 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)>)> {
|
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")?;
|
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
||||||
|
|
@ -395,19 +632,16 @@ pub fn reload_for_model(app: &AppConfig, prompt_file: &str) -> Result<(String, V
|
||||||
}
|
}
|
||||||
|
|
||||||
let system_prompt = crate::agent::identity::assemble_system_prompt();
|
let system_prompt = crate::agent::identity::assemble_system_prompt();
|
||||||
let context_groups = load_context_groups();
|
let context_groups = get().context_groups.clone();
|
||||||
let (context_parts, _, _) = crate::agent::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref(), &context_groups)?;
|
let (context_parts, _, _) = crate::agent::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref(), &context_groups)?;
|
||||||
Ok((system_prompt, context_parts))
|
Ok((system_prompt, context_parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn is_anthropic_model(model: &str) -> bool {
|
fn is_anthropic_model(model: &str) -> bool {
|
||||||
let m = model.to_lowercase();
|
let m = model.to_lowercase();
|
||||||
m.contains("claude") || m.contains("opus") || m.contains("sonnet")
|
m.contains("claude") || m.contains("opus") || m.contains("sonnet")
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- --show-config ---
|
|
||||||
|
|
||||||
pub fn show_config(app: &AppConfig, figment: &Figment) {
|
pub fn show_config(app: &AppConfig, figment: &Figment) {
|
||||||
fn mask(key: &str) -> String {
|
fn mask(key: &str) -> String {
|
||||||
if key.is_empty() { "(not set)".into() }
|
if key.is_empty() { "(not set)".into() }
|
||||||
|
|
@ -460,4 +694,24 @@ pub fn show_config(app: &AppConfig, figment: &Figment) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity file discovery and context assembly live in identity.rs
|
// ============================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
fn deserialize_path<'de, D: serde::Deserializer<'de>>(d: D) -> Result<PathBuf, D::Error> {
|
||||||
|
let s: String = serde::Deserialize::deserialize(d)?;
|
||||||
|
Ok(expand_home(&s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_path_opt<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<PathBuf>, D::Error> {
|
||||||
|
let s: Option<String> = serde::Deserialize::deserialize(d)?;
|
||||||
|
Ok(s.map(|s| expand_home(&s)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_home(path: &str) -> PathBuf {
|
||||||
|
if let Some(rest) = path.strip_prefix("~/") {
|
||||||
|
PathBuf::from(std::env::var("HOME").expect("HOME not set")).join(rest)
|
||||||
|
} else {
|
||||||
|
PathBuf::from(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
// Configuration for poc-memory
|
|
||||||
//
|
|
||||||
// Primary config: ~/.config/poc-agent/config.json5 (shared with poc-agent)
|
|
||||||
// Memory-specific settings live in the "memory" section.
|
|
||||||
// API backend resolved from the shared "models" + backend configs.
|
|
||||||
//
|
|
||||||
// Fallback: ~/.config/poc-memory/config.jsonl (legacy, still supported)
|
|
||||||
// Env override: POC_MEMORY_CONFIG
|
|
||||||
//
|
|
||||||
// The shared config eliminates API credential duplication between
|
|
||||||
// poc-memory and poc-agent.
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, OnceLock, RwLock};
|
|
||||||
|
|
||||||
static CONFIG: OnceLock<RwLock<Arc<Config>>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum ContextSource {
|
|
||||||
#[serde(alias = "")]
|
|
||||||
#[default]
|
|
||||||
Store,
|
|
||||||
File,
|
|
||||||
Journal,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
|
||||||
pub struct ContextGroup {
|
|
||||||
pub label: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub keys: Vec<String>,
|
|
||||||
#[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 }
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct Config {
|
|
||||||
pub user_name: String,
|
|
||||||
pub assistant_name: String,
|
|
||||||
#[serde(deserialize_with = "deserialize_path")]
|
|
||||||
pub data_dir: PathBuf,
|
|
||||||
#[serde(deserialize_with = "deserialize_path")]
|
|
||||||
pub projects_dir: PathBuf,
|
|
||||||
pub core_nodes: Vec<String>,
|
|
||||||
pub journal_days: u32,
|
|
||||||
pub journal_max: usize,
|
|
||||||
pub context_groups: Vec<ContextGroup>,
|
|
||||||
pub llm_concurrency: usize,
|
|
||||||
pub agent_budget: usize,
|
|
||||||
#[serde(deserialize_with = "deserialize_path")]
|
|
||||||
pub prompts_dir: PathBuf,
|
|
||||||
#[serde(default, deserialize_with = "deserialize_path_opt")]
|
|
||||||
pub agent_config_dir: Option<PathBuf>,
|
|
||||||
/// Resolved from agent_model → models → backend (not in config directly)
|
|
||||||
#[serde(skip)]
|
|
||||||
pub api_base_url: Option<String>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub api_key: Option<String>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub api_model: Option<String>,
|
|
||||||
/// Used to resolve API settings, not stored on Config
|
|
||||||
#[serde(default)]
|
|
||||||
agent_model: Option<String>,
|
|
||||||
pub api_reasoning: String,
|
|
||||||
pub agent_types: Vec<String>,
|
|
||||||
/// Surface agent timeout in seconds. Kill if running longer than this.
|
|
||||||
#[serde(default)]
|
|
||||||
pub surface_timeout_secs: Option<u32>,
|
|
||||||
/// Hook events that trigger the surface agent (e.g. ["UserPromptSubmit"]).
|
|
||||||
/// Empty list disables surface agent.
|
|
||||||
#[serde(default)]
|
|
||||||
pub surface_hooks: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
let home = PathBuf::from(std::env::var("HOME").expect("HOME not set"));
|
|
||||||
Self {
|
|
||||||
user_name: "User".to_string(),
|
|
||||||
assistant_name: "Assistant".to_string(),
|
|
||||||
data_dir: home.join(".claude/memory"),
|
|
||||||
projects_dir: home.join(".claude/projects"),
|
|
||||||
core_nodes: vec!["identity".to_string(), "core-practices".to_string()],
|
|
||||||
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("poc/consciousness/src/subconscious/prompts"),
|
|
||||||
agent_config_dir: None,
|
|
||||||
api_base_url: None,
|
|
||||||
api_key: None,
|
|
||||||
api_model: None,
|
|
||||||
agent_model: None,
|
|
||||||
api_reasoning: "high".to_string(),
|
|
||||||
agent_types: vec![
|
|
||||||
"linker".into(), "organize".into(), "distill".into(),
|
|
||||||
"separator".into(), "split".into(),
|
|
||||||
],
|
|
||||||
surface_timeout_secs: None,
|
|
||||||
surface_hooks: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
fn load_from_file() -> Self {
|
|
||||||
// Try shared config first, then legacy JSONL
|
|
||||||
if let Some(config) = Self::try_load_shared() {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
Self::load_legacy_jsonl()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load from shared poc-agent config (~/.config/poc-agent/config.json5).
|
|
||||||
/// Memory settings live in the "memory" section; API settings are
|
|
||||||
/// resolved from the shared model/backend configuration.
|
|
||||||
fn try_load_shared() -> Option<Self> {
|
|
||||||
let path = PathBuf::from(std::env::var("HOME").ok()?)
|
|
||||||
.join(".config/poc-agent/config.json5");
|
|
||||||
let content = std::fs::read_to_string(&path).ok()?;
|
|
||||||
let root: serde_json::Value = json5::from_str(&content).ok()?;
|
|
||||||
let mem_value = root.get("memory")?;
|
|
||||||
|
|
||||||
let mut config: Config = serde_json::from_value(mem_value.clone()).ok()?;
|
|
||||||
config.llm_concurrency = config.llm_concurrency.max(1);
|
|
||||||
|
|
||||||
// Resolve API settings: agent_model → models → backend
|
|
||||||
if let Some(model_name) = &config.agent_model
|
|
||||||
&& let Some(model_cfg) = root.get("models").and_then(|m| m.get(model_name.as_str())) {
|
|
||||||
let backend_name = model_cfg.get("backend").and_then(|v| v.as_str()).unwrap_or("");
|
|
||||||
let model_id = model_cfg.get("model_id").and_then(|v| v.as_str()).unwrap_or("");
|
|
||||||
|
|
||||||
if let Some(backend) = root.get(backend_name) {
|
|
||||||
config.api_base_url = backend.get("base_url")
|
|
||||||
.and_then(|v| v.as_str()).map(String::from);
|
|
||||||
config.api_key = backend.get("api_key")
|
|
||||||
.and_then(|v| v.as_str()).map(String::from);
|
|
||||||
}
|
|
||||||
config.api_model = Some(model_id.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load from legacy JSONL config (~/.config/poc-memory/config.jsonl).
|
|
||||||
fn load_legacy_jsonl() -> Self {
|
|
||||||
let path = std::env::var("POC_MEMORY_CONFIG")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
PathBuf::from(std::env::var("HOME").expect("HOME not set"))
|
|
||||||
.join(".config/poc-memory/config.jsonl")
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut config = Config::default();
|
|
||||||
|
|
||||||
let Ok(content) = std::fs::read_to_string(&path) else {
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut context_groups: Vec<ContextGroup> = Vec::new();
|
|
||||||
|
|
||||||
let stream = serde_json::Deserializer::from_str(&content)
|
|
||||||
.into_iter::<serde_json::Value>();
|
|
||||||
|
|
||||||
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("agent_config_dir").and_then(|v| v.as_str()) {
|
|
||||||
config.agent_config_dir = Some(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("file") => ContextSource::File,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn deserialize_path<'de, D: serde::Deserializer<'de>>(d: D) -> Result<PathBuf, D::Error> {
|
|
||||||
let s: String = serde::Deserialize::deserialize(d)?;
|
|
||||||
Ok(expand_home(&s))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_path_opt<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<PathBuf>, D::Error> {
|
|
||||||
let s: Option<String> = serde::Deserialize::deserialize(d)?;
|
|
||||||
Ok(s.map(|s| expand_home(&s)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_home(path: &str) -> PathBuf {
|
|
||||||
if let Some(rest) = path.strip_prefix("~/") {
|
|
||||||
PathBuf::from(std::env::var("HOME").expect("HOME not set")).join(rest)
|
|
||||||
} else {
|
|
||||||
PathBuf::from(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the global config (cheap Arc clone).
|
|
||||||
pub fn get() -> Arc<Config> {
|
|
||||||
CONFIG
|
|
||||||
.get_or_init(|| RwLock::new(Arc::new(Config::load_from_file())))
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload the config from disk. Returns true if changed.
|
|
||||||
pub fn reload() -> bool {
|
|
||||||
let lock = CONFIG.get_or_init(|| RwLock::new(Arc::new(Config::load_from_file())));
|
|
||||||
let new = Config::load_from_file();
|
|
||||||
let mut current = lock.write().unwrap();
|
|
||||||
let changed = format!("{:?}", **current) != format!("{:?}", new);
|
|
||||||
if changed {
|
|
||||||
*current = Arc::new(new);
|
|
||||||
}
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
|
|
@ -15,6 +15,5 @@ pub mod spectral;
|
||||||
pub mod neuro;
|
pub mod neuro;
|
||||||
pub mod counters;
|
pub mod counters;
|
||||||
pub mod migrate;
|
pub mod migrate;
|
||||||
pub mod config;
|
|
||||||
pub mod transcript;
|
pub mod transcript;
|
||||||
pub mod memory_search;
|
pub mod memory_search;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ pub mod hippocampus;
|
||||||
// Autonomous agents
|
// Autonomous agents
|
||||||
pub mod subconscious;
|
pub mod subconscious;
|
||||||
|
|
||||||
|
// Unified configuration
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
// Shared utilities
|
// Shared utilities
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
|
@ -31,7 +34,7 @@ pub mod memory_capnp {
|
||||||
pub use hippocampus::{
|
pub use hippocampus::{
|
||||||
store, graph, lookups, cursor, query,
|
store, graph, lookups, cursor, query,
|
||||||
similarity, spectral, neuro, counters,
|
similarity, spectral, neuro, counters,
|
||||||
config, transcript, memory_search, migrate,
|
transcript, memory_search, migrate,
|
||||||
};
|
};
|
||||||
pub use hippocampus::query::engine as search;
|
pub use hippocampus::query::engine as search;
|
||||||
pub use hippocampus::query::parser as query_parser;
|
pub use hippocampus::query::parser as query_parser;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue