loopd: compact after a full turn and retry SessionFull once
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
//! Tests for compacting after a turn and retrying a full context. Do not edit.
|
||||
|
||||
mod support;
|
||||
|
||||
use bxcore::{Epoch, LogRecord, SessionId, TurnEvent};
|
||||
use loopd::baseline::{Baseline, CARRY_HEADING};
|
||||
use loopd::compact;
|
||||
use loopd::llama::Client;
|
||||
use loopd::memory::NoMemory;
|
||||
use loopd::session::Session;
|
||||
use loopd::tools::Registry;
|
||||
use loopd::turn::{Runtime, TurnError, run_turn};
|
||||
use support::{FakeServer, Home, Reply};
|
||||
|
||||
const CHAT: &str = "/v1/chat/completions";
|
||||
const PROMPT: &str = "Summarise this conversation for the next epoch.\n";
|
||||
|
||||
fn sse(content: &str, cache_n: u64, prompt_n: u64) -> Reply {
|
||||
let content_json = serde_json::to_string(content).unwrap();
|
||||
let data1 = format!(
|
||||
r#"data: {{"choices":[{{"finish_reason":null,"index":0,"delta":{{"content":{content_json}}}}}],"id":"t","timings":{{"cache_n":{cache_n},"prompt_n":{prompt_n},"predicted_n":1}}}}"#
|
||||
);
|
||||
let data2 = format!(
|
||||
r#"data: {{"choices":[{{"finish_reason":"stop","index":0,"delta":{{}}}}],"id":"t","timings":{{"cache_n":{cache_n},"prompt_n":{prompt_n},"predicted_n":1}}}}"#
|
||||
);
|
||||
let body = format!("{data1}\n\n{data2}\n\ndata: [DONE]\n\n");
|
||||
Reply::raw(format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
||||
body.len()
|
||||
))
|
||||
}
|
||||
|
||||
struct World {
|
||||
home: Home,
|
||||
server: FakeServer,
|
||||
cfg: loopd::config::Config,
|
||||
client: Client,
|
||||
port: support::ScriptedPort,
|
||||
registry: Registry,
|
||||
}
|
||||
|
||||
impl World {
|
||||
fn new() -> World {
|
||||
let home = Home::new();
|
||||
home.write("compact.md", PROMPT);
|
||||
let server = FakeServer::start();
|
||||
let mut cfg = home.config(&server.socket);
|
||||
cfg.expect.n_ctx = 1_000;
|
||||
cfg.compact.threshold = 0.85;
|
||||
let client = Client::new(cfg.clone());
|
||||
World {
|
||||
home,
|
||||
server,
|
||||
cfg,
|
||||
client,
|
||||
port: support::ScriptedPort::new(vec![]),
|
||||
registry: Registry::m2b(),
|
||||
}
|
||||
}
|
||||
|
||||
fn session(&self) -> Session {
|
||||
let baseline = Baseline::assemble(&self.cfg, &self.registry).unwrap();
|
||||
Session::create(
|
||||
&self.home.dir,
|
||||
SessionId::new("a").unwrap(),
|
||||
baseline,
|
||||
self.cfg.slots.main,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn runtime(&self) -> Runtime<'_> {
|
||||
Runtime {
|
||||
cfg: &self.cfg,
|
||||
client: &self.client,
|
||||
port: &self.port,
|
||||
registry: &self.registry,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(
|
||||
&self,
|
||||
session: &mut Session,
|
||||
text: &str,
|
||||
outcome: Result<loopd::turn::TurnOutcome, TurnError>,
|
||||
) -> Result<loopd::turn::TurnOutcome, TurnError> {
|
||||
let mut events = Vec::new();
|
||||
compact::finish_turn(
|
||||
session,
|
||||
&self.runtime(),
|
||||
&NoMemory,
|
||||
text,
|
||||
None,
|
||||
&mut |e: &TurnEvent| events.push(e.clone()),
|
||||
outcome,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_under_the_threshold_leaves_the_epoch() {
|
||||
let w = World::new();
|
||||
w.server.route(CHAT, vec![sse("ok", 400, 449)]);
|
||||
let mut s = w.session();
|
||||
let mut events = Vec::new();
|
||||
let outcome = run_turn(&mut s, &w.runtime(), "hi", None, &mut |e| {
|
||||
events.push(e.clone())
|
||||
});
|
||||
let outcome = w.finish(&mut s, "hi", outcome).unwrap();
|
||||
assert_eq!(outcome.content, "ok");
|
||||
assert_eq!(s.epoch(), Epoch(0));
|
||||
assert_eq!(w.server.requests_to(CHAT).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_at_the_threshold_compacts_before_the_turn_returns() {
|
||||
let w = World::new();
|
||||
w.server
|
||||
.route(CHAT, vec![sse("ok", 400, 450), sse("the sum", 0, 10)]);
|
||||
let mut s = w.session();
|
||||
let mut events = Vec::new();
|
||||
let outcome = run_turn(&mut s, &w.runtime(), "hi", None, &mut |e| {
|
||||
events.push(e.clone())
|
||||
});
|
||||
w.finish(&mut s, "hi", outcome).unwrap();
|
||||
assert_eq!(s.epoch(), Epoch(1));
|
||||
assert_eq!(s.carry(), Some("the sum"));
|
||||
w.server.route(CHAT, vec![sse("next", 0, 20)]);
|
||||
let mut events = Vec::new();
|
||||
run_turn(&mut s, &w.runtime(), "again", None, &mut |e| {
|
||||
events.push(e.clone())
|
||||
})
|
||||
.unwrap();
|
||||
let sent = w.server.requests_to(CHAT);
|
||||
assert_eq!(sent.len(), 3);
|
||||
assert_eq!(sent[0].json()["id_slot"], 0);
|
||||
assert_eq!(sent[1].json()["id_slot"], 1);
|
||||
assert!(sent[1].json().get("tools").is_none());
|
||||
let live = sent[2].json();
|
||||
assert_eq!(live["id_slot"], 0);
|
||||
let contents: Vec<&str> = live["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|m| m["content"].as_str().unwrap_or(""))
|
||||
.collect();
|
||||
assert!(
|
||||
contents
|
||||
.iter()
|
||||
.any(|c| c.contains(CARRY_HEADING) && c.contains("the sum")),
|
||||
"{contents:?}"
|
||||
);
|
||||
let old = w.home.records("a");
|
||||
assert!(matches!(
|
||||
old.last(),
|
||||
Some(LogRecord::EpochEnd { next, .. }) if *next == Epoch(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_full_compacts_and_retries_the_user_message_once() {
|
||||
let w = World::new();
|
||||
w.server.route(
|
||||
CHAT,
|
||||
vec![
|
||||
Reply::fixture("context_full"),
|
||||
sse("the sum", 0, 10),
|
||||
sse("retried", 0, 20),
|
||||
],
|
||||
);
|
||||
let mut s = w.session();
|
||||
let mut events = Vec::new();
|
||||
let outcome = run_turn(&mut s, &w.runtime(), "please fit", None, &mut |e| {
|
||||
events.push(e.clone())
|
||||
});
|
||||
assert!(matches!(outcome, Err(TurnError::SessionFull)));
|
||||
let done = w.finish(&mut s, "please fit", outcome).unwrap();
|
||||
assert_eq!(done.content, "retried");
|
||||
assert_eq!(s.epoch(), Epoch(1));
|
||||
let users: Vec<_> = s
|
||||
.records()
|
||||
.iter()
|
||||
.filter_map(|r| match r {
|
||||
LogRecord::User { content, .. } => Some(content.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(users, ["please fit"]);
|
||||
let old_users = w
|
||||
.home
|
||||
.records("a")
|
||||
.iter()
|
||||
.filter(|r| matches!(r, LogRecord::User { .. }))
|
||||
.count();
|
||||
assert_eq!(old_users, 1);
|
||||
assert_eq!(s.turns(), 2);
|
||||
let sent = w.server.requests_to(CHAT);
|
||||
assert_eq!(sent.len(), 3);
|
||||
assert_eq!(sent[1].json()["id_slot"], 1);
|
||||
assert_eq!(sent[2].json()["id_slot"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_full_stays_session_full_when_compact_fails() {
|
||||
let w = World::new();
|
||||
w.server.route(
|
||||
CHAT,
|
||||
vec![
|
||||
Reply::fixture("context_full"),
|
||||
Reply::fixture("bad_request"),
|
||||
],
|
||||
);
|
||||
let mut s = w.session();
|
||||
let mut events = Vec::new();
|
||||
let outcome = run_turn(&mut s, &w.runtime(), "x", None, &mut |e| {
|
||||
events.push(e.clone())
|
||||
});
|
||||
let err = w.finish(&mut s, "x", outcome).unwrap_err();
|
||||
assert!(matches!(err, TurnError::SessionFull), "{err:?}");
|
||||
assert_eq!(s.epoch(), Epoch(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_successful_turn_stays_successful_when_compact_fails() {
|
||||
let home = Home::new();
|
||||
let server = FakeServer::start();
|
||||
let mut cfg = home.config(&server.socket);
|
||||
cfg.expect.n_ctx = 1_000;
|
||||
cfg.compact.threshold = 0.85;
|
||||
let client = Client::new(cfg.clone());
|
||||
server.route(CHAT, vec![sse("ok", 400, 450)]);
|
||||
let baseline = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
|
||||
let mut s = Session::create(
|
||||
&home.dir,
|
||||
SessionId::new("a").unwrap(),
|
||||
baseline,
|
||||
cfg.slots.main,
|
||||
)
|
||||
.unwrap();
|
||||
let port = support::ScriptedPort::new(vec![]);
|
||||
let registry = Registry::m2b();
|
||||
let rt = Runtime {
|
||||
cfg: &cfg,
|
||||
client: &client,
|
||||
port: &port,
|
||||
registry: ®istry,
|
||||
};
|
||||
let mut events = Vec::new();
|
||||
let outcome = run_turn(&mut s, &rt, "hi", None, &mut |e| events.push(e.clone()));
|
||||
let done = compact::finish_turn(
|
||||
&mut s,
|
||||
&rt,
|
||||
&NoMemory,
|
||||
"hi",
|
||||
None,
|
||||
&mut |e| events.push(e.clone()),
|
||||
outcome,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(done.content, "ok");
|
||||
assert_eq!(s.epoch(), Epoch(0), "compact.md is missing, so no epoch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_turn_does_not_compact_an_error_other_than_session_full() {
|
||||
let w = World::new();
|
||||
w.server.route(CHAT, vec![Reply::fixture("bad_request")]);
|
||||
let mut s = w.session();
|
||||
let mut events = Vec::new();
|
||||
let outcome = run_turn(&mut s, &w.runtime(), "x", None, &mut |e| {
|
||||
events.push(e.clone())
|
||||
});
|
||||
let err = w.finish(&mut s, "x", outcome).unwrap_err();
|
||||
assert!(matches!(err, TurnError::Infer(_)), "{err:?}");
|
||||
assert_eq!(s.epoch(), Epoch(0));
|
||||
assert_eq!(w.server.requests_to(CHAT).len(), 1);
|
||||
}
|
||||
Reference in New Issue
Block a user