loopd: compact summary on the background slot

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-03 00:14:03 -07:00
co-authored by ornith-1.5-35b-a3b
parent 32e81b51fb
commit c8b0a01c41
4 changed files with 245 additions and 1 deletions
+61 -1
View File
@@ -1,8 +1,34 @@
//! Compaction: when a session starts a new epoch, the summary call, and writing the next files
//! (M5d spec, section 3). Later tasks fill the call and the write.
//! (M5d spec, section 3). Task 07 writes the next files; task 08 calls this after a turn.
use bxcore::sanitize::neutralise;
use bxcore::{LogRecord, Usage};
use crate::baseline::messages_with_carry;
use crate::config::Config;
use crate::llama::{ChatMessage, ChatRequest, Client};
use crate::session::Session;
use crate::turn::is_context_full;
/// Why a summary call or a next-epoch write failed. Every variant's `Display` ends with the
/// runbook pointer `session-compact-failed`.
#[derive(Debug)]
pub enum CompactError {
Failed { detail: String },
}
impl std::fmt::Display for CompactError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompactError::Failed { detail } => {
write!(f, "{detail}; see docs/runbook.md#session-compact-failed")
}
}
}
}
impl std::error::Error for CompactError {}
/// True when `cache_n + prompt_n` is at or above `threshold * n_ctx`.
pub fn due(usage: &Usage, threshold: f64, n_ctx: u64) -> bool {
let total = usage.cache_n.saturating_add(usage.prompt_n) as f64;
@@ -30,3 +56,37 @@ pub fn fallback_summary(records: &[LogRecord], summary_chars: usize) -> String {
pub(crate) fn cap_chars(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}
/// One completion on the background slot: no tools, thinking on, last user `compact.md`.
/// Context-full uses `fallback_summary`. A missing prompt, or any other infer error, is
/// `CompactError::Failed` with the runbook pointer. No fallback for a missing prompt.
pub fn summarise(cfg: &Config, client: &Client, session: &Session) -> Result<String, CompactError> {
let path = cfg.compact_prompt();
let prompt = std::fs::read_to_string(&path).map_err(|e| CompactError::Failed {
detail: format!("{}: {e}", path.display()),
})?;
let mut messages = messages_with_carry(session.baseline(), session.records(), session.carry());
messages.push(ChatMessage::User {
content: prompt.trim_end().to_string(),
});
let request = ChatRequest {
slot: cfg.slots.background,
messages,
tools: Vec::new(),
thinking: true,
};
match client.chat_with_retry(&request, &mut |_| {}) {
Ok(completion) => {
let text = completion.content.unwrap_or_default();
let text = neutralise(&text);
Ok(cap_chars(text.as_ref(), cfg.compact.summary_chars))
}
Err(e) if is_context_full(&e) => Ok(fallback_summary(
session.records(),
cfg.compact.summary_chars,
)),
Err(e) => Err(CompactError::Failed {
detail: e.to_string(),
}),
}
}