huskies: merge 1143 story Decouple LLM environmental awareness from chat transport — persona-keyed sessions and a real-time event subscription
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
//! LLM session management — CRDT-backed context assembly for bot prompts.
|
||||
//!
|
||||
//! The central export is [`assemble_prompt_context`], which reads new pipeline
|
||||
//! transition events from the CRDT event log past the session's stored high-water
|
||||
//! transition events from the CRDT event log past the persona's stored high-water
|
||||
//! marks, wraps them in a `<system-reminder>` block for injection at the head of
|
||||
//! the next LLM prompt, and atomically advances the marks so a mid-turn crash
|
||||
//! cannot double-inject the same events.
|
||||
|
||||
/// Assemble a `<system-reminder>` block containing new pipeline-transition events
|
||||
/// for `session_id` and atomically advance the high-water marks.
|
||||
/// for `persona` and atomically advance the high-water marks.
|
||||
///
|
||||
/// Reads events from the local sled's CRDT event log that have not yet been
|
||||
/// injected into this session (tracked via per-sled high-water marks stored in
|
||||
/// the `LlmSessionCrdt` entity). Returns an empty string when there are no new
|
||||
/// events or the CRDT is not yet initialised.
|
||||
pub fn assemble_prompt_context(session_id: &str) -> String {
|
||||
let lines = crate::crdt_state::assemble_and_advance_session(session_id);
|
||||
/// All chat transports call this with the same persona name (e.g. `"timmy"`)
|
||||
/// so that events are visible to whichever transport handles the next turn,
|
||||
/// regardless of transport-specific session identifiers. Returns an empty
|
||||
/// string when there are no new events or the CRDT is not yet initialised.
|
||||
pub fn assemble_prompt_context(persona: &str) -> String {
|
||||
let lines = crate::crdt_state::assemble_and_advance_session(persona);
|
||||
let event_count = lines.len();
|
||||
crate::slog!(
|
||||
"[llm-session] assemble_prompt_context session={session_id} new_events={event_count}"
|
||||
"[llm-session] assemble_prompt_context persona={persona} new_events={event_count}"
|
||||
);
|
||||
if lines.is_empty() {
|
||||
return String::new();
|
||||
@@ -187,14 +187,14 @@ mod tests {
|
||||
"AgentCompleted",
|
||||
);
|
||||
|
||||
// Set up a session scoped to ALL sleds.
|
||||
crate::crdt_state::write_llm_session("room-scope-all", "Timmy", "all");
|
||||
// Set up a session scoped to sled-A only.
|
||||
// Set up a persona scoped to ALL sleds.
|
||||
crate::crdt_state::write_llm_session("timmy", "all");
|
||||
// Set up a persona scoped to sled-A only.
|
||||
let sled_a_scope = format!("sleds:{sled_a}");
|
||||
crate::crdt_state::write_llm_session("room-scope-sled-a", "Sally", &sled_a_scope);
|
||||
crate::crdt_state::write_llm_session("sally", &sled_a_scope);
|
||||
|
||||
// All-scope session: both events must appear.
|
||||
let ctx_all = assemble_prompt_context("room-scope-all");
|
||||
// All-scope persona: both events must appear.
|
||||
let ctx_all = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
ctx_all.contains("10_story_alpha"),
|
||||
"All scope must contain sled-A event; got: {ctx_all}"
|
||||
@@ -204,8 +204,8 @@ mod tests {
|
||||
"All scope must contain sled-B event; got: {ctx_all}"
|
||||
);
|
||||
|
||||
// Sled-A-only session: only sled-A's event visible.
|
||||
let ctx_a = assemble_prompt_context("room-scope-sled-a");
|
||||
// Sled-A-only persona: only sled-A's event visible.
|
||||
let ctx_a = assemble_prompt_context("sally");
|
||||
assert!(
|
||||
ctx_a.contains("10_story_alpha"),
|
||||
"Sleds filter must contain sled-A event; got: {ctx_a}"
|
||||
@@ -215,19 +215,73 @@ mod tests {
|
||||
"Sleds filter must NOT contain sled-B event; got: {ctx_a}"
|
||||
);
|
||||
|
||||
// Second call on both sessions: nothing new (high-water advanced).
|
||||
let ctx_all2 = assemble_prompt_context("room-scope-all");
|
||||
// Second call on both personas: nothing new (high-water advanced).
|
||||
let ctx_all2 = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
ctx_all2.is_empty(),
|
||||
"All scope second call must be empty; got: {ctx_all2}"
|
||||
);
|
||||
let ctx_a2 = assemble_prompt_context("room-scope-sled-a");
|
||||
let ctx_a2 = assemble_prompt_context("sally");
|
||||
assert!(
|
||||
ctx_a2.is_empty(),
|
||||
"Sleds filter second call must be empty; got: {ctx_a2}"
|
||||
);
|
||||
}
|
||||
|
||||
/// AC 5 e2e: fire a pipeline transition, then verify that calling
|
||||
/// `assemble_prompt_context` with the same persona key from any "transport"
|
||||
/// (simulated by different caller labels) sees the event. The persona is
|
||||
/// transport-agnostic; subsequent transports sharing the persona see their
|
||||
/// own new events independently via independent calls (each drains a fresh
|
||||
/// batch).
|
||||
#[test]
|
||||
fn persona_key_is_transport_agnostic() {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::crdt_state::write_llm_session("timmy", "all");
|
||||
|
||||
// Fire event 1.
|
||||
crate::event_log::log_transition_event(&make_fired("e2e_story_1"));
|
||||
|
||||
// Matrix turn: see event 1.
|
||||
let matrix_ctx = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
matrix_ctx.contains("e2e_story_1"),
|
||||
"Matrix turn must see event 1; got: {matrix_ctx}"
|
||||
);
|
||||
|
||||
// Fire event 2.
|
||||
crate::event_log::log_transition_event(&make_fired("e2e_story_2"));
|
||||
|
||||
// Web-UI turn (same persona): see event 2 only (event 1 high-water already advanced).
|
||||
let web_ui_ctx = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
web_ui_ctx.contains("e2e_story_2"),
|
||||
"Web-UI turn must see event 2; got: {web_ui_ctx}"
|
||||
);
|
||||
assert!(
|
||||
!web_ui_ctx.contains("e2e_story_1"),
|
||||
"Web-UI turn must NOT re-see event 1; got: {web_ui_ctx}"
|
||||
);
|
||||
|
||||
// Fire event 3.
|
||||
crate::event_log::log_transition_event(&make_fired("e2e_story_3"));
|
||||
|
||||
// CLI turn (same persona): see event 3 only.
|
||||
let cli_ctx = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
cli_ctx.contains("e2e_story_3"),
|
||||
"CLI turn must see event 3; got: {cli_ctx}"
|
||||
);
|
||||
assert!(
|
||||
!cli_ctx.contains("e2e_story_1"),
|
||||
"CLI turn must NOT re-see event 1; got: {cli_ctx}"
|
||||
);
|
||||
assert!(
|
||||
!cli_ctx.contains("e2e_story_2"),
|
||||
"CLI turn must NOT re-see event 2; got: {cli_ctx}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Newly-added sled events appear in an All-scope session without
|
||||
/// restarting (AC 5 runtime pickup).
|
||||
#[test]
|
||||
@@ -246,9 +300,9 @@ mod tests {
|
||||
"2_current",
|
||||
"DepsMet",
|
||||
);
|
||||
crate::crdt_state::write_llm_session("room-runtime-pickup", "Timmy", "all");
|
||||
crate::crdt_state::write_llm_session("timmy", "all");
|
||||
|
||||
let ctx1 = assemble_prompt_context("room-runtime-pickup");
|
||||
let ctx1 = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
ctx1.contains("30_story_first"),
|
||||
"first event must appear; got: {ctx1}"
|
||||
@@ -264,7 +318,7 @@ mod tests {
|
||||
"AgentCompleted",
|
||||
);
|
||||
|
||||
let ctx2 = assemble_prompt_context("room-runtime-pickup");
|
||||
let ctx2 = assemble_prompt_context("timmy");
|
||||
assert!(
|
||||
ctx2.contains("40_story_second"),
|
||||
"newly adopted sled event must appear; got: {ctx2}"
|
||||
|
||||
Reference in New Issue
Block a user