//! The baseline: the fixed prefix of every request in an epoch — the system prompt, core memory //! and core tool schemas. It is snapshotted per session so an edit to `system.md` changes nothing //! for a running session, and `messages` turns a session log back into the request's messages. use std::path::{Path, PathBuf}; use crate::config::Config; use crate::llama::{ChatMessage, ToolSchema}; use crate::tools::Registry; use proto::{Hash32, LogRecord, sha256}; use serde::Deserialize; use serde::Serialize; /// The fixed prefix of a request, and the errors assembling or reading it. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Baseline { pub system: String, pub tools: Vec, } #[derive(Debug)] pub enum BaselineError { Read(PathBuf, std::io::Error), Parse(PathBuf, serde_json::Error), Hash, } impl std::fmt::Display for BaselineError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BaselineError::Read(path, err) => write!(f, "{}: {err}", path.display()), BaselineError::Parse(path, err) => write!(f, "{}: {err}", path.display()), BaselineError::Hash => write!(f, "hashing the baseline failed"), } } } impl std::error::Error for BaselineError {} impl Baseline { /// The system prompt trimmed, plus core memory if it has content; the core tool schemas. pub fn assemble(cfg: &Config, registry: &Registry) -> Result { let system = std::fs::read_to_string(&cfg.baseline.system) .map_err(|e| BaselineError::Read(cfg.baseline.system.clone(), e))?; let mut system = system.trim_end().to_string(); let core = cfg.paths.home.join("memory/core.md"); if let Ok(text) = std::fs::read_to_string(&core) { let trimmed = text.trim(); if !trimmed.is_empty() { system.push_str("\n\n"); system.push_str(trimmed); } } Ok(Baseline { system, tools: registry.core_schemas(), }) } /// The canonical JSON: key order is fixed, so the hash is stable. pub fn to_json(&self) -> Result { serde_json::to_string(self) } pub fn from_json(text: &str) -> Result { serde_json::from_str(text) } pub fn load(path: &Path) -> Result { let text = std::fs::read_to_string(path) .map_err(|e| BaselineError::Read(path.to_path_buf(), e))?; serde_json::from_str(&text).map_err(|e| BaselineError::Parse(path.to_path_buf(), e)) } /// The sha256 of the baseline's own JSON, as written. pub fn hash(&self) -> Result { let json = self.to_json().map_err(|_| BaselineError::Hash)?; sha256(json.as_bytes()).map_err(|_| BaselineError::Hash) } } /// The system message, then each log record replayed in order. Every record variant is named, so a /// new one is a compile error rather than a silent skip. pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec { let mut out = vec![ChatMessage::System { content: baseline.system.clone(), }]; for record in records { match record { LogRecord::SessionStart { .. } => {} LogRecord::User { content, .. } => out.push(ChatMessage::User { content: content.clone(), }), LogRecord::Assistant { content, reasoning_content, tool_calls, .. } => out.push(ChatMessage::Assistant { content: content.clone(), reasoning_content: reasoning_content.clone(), tool_calls: tool_calls.clone(), }), LogRecord::ToolResult { tool_call_id, content, .. } => out.push(ChatMessage::Tool { tool_call_id: tool_call_id.clone(), content: content.clone(), }), LogRecord::Usage { .. } => {} LogRecord::CacheLoss { .. } => {} LogRecord::EpochEnd { .. } => {} } } out }