loopd: [compact] threshold, summary_chars and summarise
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -175,6 +175,8 @@ pub struct Config {
|
||||
pub broker: Broker,
|
||||
#[serde(default)]
|
||||
pub memory: Memory,
|
||||
#[serde(default)]
|
||||
pub compact: Compact,
|
||||
}
|
||||
|
||||
/// `[memory]`: `memoryd`'s socket for `loopd` (M5a spec, section 9). An empty path means the
|
||||
@@ -185,6 +187,26 @@ pub struct Memory {
|
||||
pub socket: std::path::PathBuf,
|
||||
}
|
||||
|
||||
/// `[compact]`: when a session starts a new epoch, and how long a summary may be (M5d spec,
|
||||
/// section 5). `summarise` is stored here and unused until M5d-2.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct Compact {
|
||||
pub threshold: f64,
|
||||
pub summary_chars: usize,
|
||||
pub summarise: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Compact {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold: 0.85,
|
||||
summary_chars: 4_000,
|
||||
summarise: vec!["http_fetch".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
Read(std::path::PathBuf, std::io::Error),
|
||||
@@ -232,4 +254,9 @@ impl Config {
|
||||
self.channel.socket.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// `compact.md` beside the system prompt: the summary call's last user message.
|
||||
pub fn compact_prompt(&self) -> std::path::PathBuf {
|
||||
self.baseline.system.with_file_name("compact.md")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//! Tests for `[compact]` on `loopd.toml`. Do not edit.
|
||||
|
||||
use loopd::config::Config;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn fixture(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/config")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn minimal() -> String {
|
||||
std::fs::read_to_string(fixture("minimal.toml")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_table_gets_the_documented_defaults() {
|
||||
let c = Config::load(&fixture("minimal.toml")).unwrap();
|
||||
assert_eq!(c.compact.threshold, 0.85);
|
||||
assert_eq!(c.compact.summary_chars, 4_000);
|
||||
assert_eq!(c.compact.summarise, vec!["http_fetch".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_table_overrides_every_default() {
|
||||
let text = format!(
|
||||
"{}\n[compact]\nthreshold = 0.5\nsummary_chars = 100\nsummarise = [\"clock\"]\n",
|
||||
minimal()
|
||||
);
|
||||
let c = Config::parse(&text).unwrap();
|
||||
assert_eq!(c.compact.threshold, 0.5);
|
||||
assert_eq!(c.compact.summary_chars, 100);
|
||||
assert_eq!(c.compact.summarise, vec!["clock".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_partial_table_keeps_the_other_defaults() {
|
||||
let text = format!("{}\n[compact]\nthreshold = 0.9\n", minimal());
|
||||
let c = Config::parse(&text).unwrap();
|
||||
assert_eq!(c.compact.threshold, 0.9);
|
||||
assert_eq!(c.compact.summary_chars, 4_000);
|
||||
assert_eq!(c.compact.summarise, vec!["http_fetch".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_keys_are_errors() {
|
||||
let text = minimal();
|
||||
let bad = format!("{text}\n[compact]\nzz_unknown = 1\n");
|
||||
assert!(
|
||||
Config::parse(&bad).is_err(),
|
||||
"[compact] accepted an unknown key"
|
||||
);
|
||||
let misspelt = format!("{text}\n[compact]\nsummary_char = 10\n");
|
||||
assert!(
|
||||
Config::parse(&misspelt).is_err(),
|
||||
"[compact] accepted a misspelt key"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_summarise_is_allowed() {
|
||||
let text = format!("{}\n[compact]\nsummarise = []\n", minimal());
|
||||
let c = Config::parse(&text).unwrap();
|
||||
assert!(c.compact.summarise.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_prompt_is_compact_md_beside_system_md() {
|
||||
let c = Config::load(&fixture("minimal.toml")).unwrap();
|
||||
assert_eq!(c.compact_prompt(), fixture("compact.md"));
|
||||
let home = Scratch::new();
|
||||
let cfg_path = home.dir.join("loopd.toml");
|
||||
std::fs::write(&cfg_path, minimal()).unwrap();
|
||||
std::fs::write(home.dir.join("system.md"), "sys\n").unwrap();
|
||||
let c = Config::load(&cfg_path).unwrap();
|
||||
assert_eq!(c.compact_prompt(), home.dir.join("compact.md"));
|
||||
}
|
||||
|
||||
struct Scratch {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Scratch {
|
||||
fn new() -> Scratch {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"loopd-compact-cfg-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
Scratch { dir }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Scratch {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_prompt_follows_an_overridden_system_path() {
|
||||
let text = std::fs::read_to_string(fixture("m2b.toml")).unwrap();
|
||||
let c = Config::load(&fixture("m2b.toml")).unwrap();
|
||||
assert!(
|
||||
text.contains("prompts/agent.md"),
|
||||
"m2b fixture names a prompt in a subdirectory"
|
||||
);
|
||||
assert_eq!(c.compact_prompt(), fixture("prompts/compact.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_match_the_struct_default() {
|
||||
let c = Config::load(&fixture("minimal.toml")).unwrap();
|
||||
assert_eq!(c.compact, loopd::config::Compact::default());
|
||||
}
|
||||
Reference in New Issue
Block a user