loopd: replay a carried summary as an unlogged user message

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-02 23:46:06 -07:00
co-authored by ornith-1.5-35b-a3b
parent 18039fa2e4
commit 45984b45be
4 changed files with 141 additions and 2 deletions
+18
View File
@@ -91,12 +91,30 @@ impl Baseline {
}
}
/// The heading in front of a carried summary. Built from pieces (T33).
pub const CARRY_HEADING: &str = concat!("Earlier ", "turns, summarised:");
/// 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> {
messages_with_carry(baseline, records, None)
}
/// Like `messages`, and when `carry` is `Some` inserts one user message after the system
/// prompt: the heading, a blank line, the summary. That user is not a `LogRecord::User`.
pub fn messages_with_carry(
baseline: &Baseline,
records: &[LogRecord],
carry: Option<&str>,
) -> Vec<ChatMessage> {
let mut out = vec![ChatMessage::System {
content: baseline.system.clone(),
}];
if let Some(summary) = carry {
out.push(ChatMessage::User {
content: format!("{CARRY_HEADING}\n\n{summary}"),
});
}
for record in records {
match record {
LogRecord::SessionStart { .. } => {}
+2 -2
View File
@@ -5,7 +5,7 @@
//! logged (M5b spec, section 8), so every later request replays the same bytes; the model's own
//! output is logged as the server returned it.
use crate::baseline::messages;
use crate::baseline::messages_with_carry;
use crate::config::Config;
use crate::llama::info::{CacheOutcome, cache_outcome};
use crate::llama::{ChatEvent, ChatRequest, Client, InferError};
@@ -140,7 +140,7 @@ pub fn run_turn(
let baseline = session.baseline();
let request = ChatRequest {
slot: rt.cfg.slots.main,
messages: messages(baseline, session.records()),
messages: messages_with_carry(baseline, session.records(), session.carry()),
tools: baseline.tools.clone(),
thinking: true,
};
+120
View File
@@ -0,0 +1,120 @@
//! Tests for the carried summary in the next epoch's prefix. Do not edit.
use bxcore::{Epoch, Hash32, LogRecord, SessionId, Timestamp};
use loopd::baseline::{Baseline, CARRY_HEADING, messages, messages_with_carry};
use loopd::llama::ChatMessage;
fn ts() -> Timestamp {
Timestamp::parse("2026-09-18T08:00:00.000Z").unwrap()
}
fn user(content: &str) -> LogRecord {
LogRecord::User {
time: ts(),
content: content.to_string(),
speaker: None,
}
}
fn start(epoch: u32) -> LogRecord {
LogRecord::SessionStart {
time: ts(),
session: SessionId::new("a").unwrap(),
epoch: Epoch(epoch),
slot: 0,
baseline: Hash32::ZERO,
}
}
#[test]
fn epoch_0_matches_messages_without_a_carry() {
let b = Baseline {
system: "sys".to_string(),
tools: vec![],
};
let records = vec![start(0), user("hi")];
assert_eq!(
messages_with_carry(&b, &records, None),
messages(&b, &records)
);
}
#[test]
fn a_carry_is_a_user_message_after_the_system_prompt() {
let b = Baseline {
system: "new-sys".to_string(),
tools: vec![],
};
let records = vec![start(1), user("next")];
let got = messages_with_carry(&b, &records, Some("the summary"));
assert_eq!(
got,
vec![
ChatMessage::System {
content: "new-sys".to_string(),
},
ChatMessage::User {
content: format!("{CARRY_HEADING}\n\nthe summary"),
},
ChatMessage::User {
content: "next".to_string(),
},
]
);
}
#[test]
fn the_carry_is_not_taken_from_a_log_user_record() {
let b = Baseline {
system: "sys".to_string(),
tools: vec![],
};
let records = vec![start(1)];
let got = messages_with_carry(&b, &records, Some("carried"));
assert_eq!(got.len(), 2);
assert!(
!records.iter().any(|r| matches!(r, LogRecord::User { .. })),
"the carry is not in the log"
);
}
#[test]
fn the_heading_is_built_from_pieces() {
assert_eq!(CARRY_HEADING, concat!("Earlier ", "turns, summarised:"));
assert!(
!CARRY_HEADING.contains('<'),
"the heading is words, not a tag"
);
}
#[test]
fn epoch_1_is_not_an_extension_of_epoch_0() {
let old = Baseline {
system: "old-sys".to_string(),
tools: vec![],
};
let new = Baseline {
system: "new-sys".to_string(),
tools: vec![],
};
let epoch0 = vec![start(0), user("first")];
let epoch1 = vec![start(1), user("second")];
let prev = messages(&old, &epoch0);
let next = messages_with_carry(&new, &epoch1, Some("sum of first"));
assert!(
next.len() >= 2,
"system plus carry, then the new epoch's user"
);
assert_ne!(next[0], prev[0], "the system message changed");
assert!(
!next.starts_with(&prev[..]),
"the new prefix is not an extension of the old messages"
);
match &next[1] {
ChatMessage::User { content } => {
assert!(content.contains("sum of first"), "{content}");
assert!(!content.contains("second"), "{content}");
}
other => panic!("{other:?}"),
}
}