Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
128 lines
4.5 KiB
Rust
128 lines
4.5 KiB
Rust
//! The baseline: the fixed prefix of every request in an epoch — the system prompt, the memory
|
|
//! block 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 bxcore::{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<ToolSchema>,
|
|
}
|
|
|
|
#[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, with no memory: for tests and the self-test.
|
|
pub fn assemble(cfg: &Config, registry: &Registry) -> Result<Baseline, BaselineError> {
|
|
Baseline::assemble_with(cfg, registry, "")
|
|
}
|
|
|
|
/// The system prompt trimmed, then a blank line and the memory block `memoryd` rendered (M5a
|
|
/// spec, section 4), when there is one; the core tool schemas.
|
|
pub fn assemble_with(
|
|
cfg: &Config,
|
|
registry: &Registry,
|
|
memory: &str,
|
|
) -> Result<Baseline, BaselineError> {
|
|
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 memory = memory.trim();
|
|
if !memory.is_empty() {
|
|
system.push_str("\n\n");
|
|
system.push_str(memory);
|
|
}
|
|
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<String, serde_json::Error> {
|
|
serde_json::to_string(self)
|
|
}
|
|
|
|
pub fn from_json(text: &str) -> Result<Baseline, serde_json::Error> {
|
|
serde_json::from_str(text)
|
|
}
|
|
|
|
pub fn load(path: &Path) -> Result<Baseline, BaselineError> {
|
|
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<Hash32, BaselineError> {
|
|
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<ChatMessage> {
|
|
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
|
|
}
|