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,10 +1,10 @@
|
||||
//! Read/write helpers for the `llm_sessions` LWW-map collection, including the
|
||||
//! atomic `assemble_and_advance_session` helper used by the Matrix bot.
|
||||
//! atomic `assemble_and_advance_session` helper used by every chat transport.
|
||||
//!
|
||||
//! LLM sessions are keyed by `session_id` (typically a Matrix room ID) and track
|
||||
//! per-sled high-water marks so that `assemble_and_advance_session` can inject
|
||||
//! only events the LLM has not yet seen and advance the marks atomically within
|
||||
//! a single CRDT lock acquisition.
|
||||
//! LLM sessions are keyed by **persona name** (e.g. `"timmy"` for the
|
||||
//! gateway-level bot) and track per-sled high-water marks so that
|
||||
//! `assemble_and_advance_session` can inject only events the LLM has not yet
|
||||
//! seen and advance the marks atomically within a single CRDT lock acquisition.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -16,16 +16,15 @@ use super::super::state::{apply_and_persist, get_crdt, rebuild_llm_session_index
|
||||
use super::super::types::{LlmSessionCrdt, LlmSessionView, ScopeFilter};
|
||||
use super::event_log::GAP_PIPELINE_EVENT;
|
||||
|
||||
/// Write or upsert an LLM session entry keyed by `session_id`.
|
||||
/// Write or upsert an LLM session entry keyed by `persona`.
|
||||
///
|
||||
/// Creates a new entry if `session_id` is not yet present; updates
|
||||
/// `persona_name` and `scope` on an existing entry. The `high_water`
|
||||
/// register is not touched by this function — use `assemble_and_advance_session`
|
||||
/// to advance it atomically.
|
||||
/// Creates a new entry if `persona` is not yet present; updates `scope` on an
|
||||
/// existing entry. The `high_water` register is not touched by this function —
|
||||
/// use `assemble_and_advance_session` to advance it atomically.
|
||||
///
|
||||
/// The `scope` string must be in wire form: `"all"` for [`ScopeFilter::All`]
|
||||
/// or `"sleds:hex1,hex2"` for [`ScopeFilter::Sleds`].
|
||||
pub fn write_llm_session(session_id: &str, persona_name: &str, scope: &str) {
|
||||
pub fn write_llm_session(persona: &str, scope: &str) {
|
||||
let Some(state_mutex) = get_crdt() else {
|
||||
return;
|
||||
};
|
||||
@@ -33,19 +32,19 @@ pub fn write_llm_session(session_id: &str, persona_name: &str, scope: &str) {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(&idx) = state.llm_session_index.get(session_id) {
|
||||
if let Some(&idx) = state.llm_session_index.get(persona) {
|
||||
apply_and_persist(&mut state, |s| {
|
||||
s.crdt.doc.llm_sessions[idx]
|
||||
.persona_name
|
||||
.set(persona_name.to_string())
|
||||
.set(persona.to_string())
|
||||
});
|
||||
apply_and_persist(&mut state, |s| {
|
||||
s.crdt.doc.llm_sessions[idx].scope.set(scope.to_string())
|
||||
});
|
||||
} else {
|
||||
let entry: JsonValue = json!({
|
||||
"session_id": session_id,
|
||||
"persona_name": persona_name,
|
||||
"session_id": persona,
|
||||
"persona_name": persona,
|
||||
"scope": scope,
|
||||
"high_water": "{}",
|
||||
})
|
||||
@@ -57,19 +56,19 @@ pub fn write_llm_session(session_id: &str, persona_name: &str, scope: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a single LLM session entry by `session_id`.
|
||||
pub fn read_llm_session(session_id: &str) -> Option<LlmSessionView> {
|
||||
/// Read a single LLM session entry by persona name.
|
||||
pub fn read_llm_session(persona: &str) -> Option<LlmSessionView> {
|
||||
let state_mutex = get_crdt()?;
|
||||
let state = state_mutex.lock().ok()?;
|
||||
let &idx = state.llm_session_index.get(session_id)?;
|
||||
let &idx = state.llm_session_index.get(persona)?;
|
||||
extract_llm_session_view(&state.crdt.doc.llm_sessions[idx])
|
||||
}
|
||||
|
||||
/// Atomically read new event-log entries for `session_id` past the stored
|
||||
/// Atomically read new event-log entries for `persona` past the stored
|
||||
/// high-water marks, render them as a block of audit lines, and advance the
|
||||
/// marks to prevent double-injection on the next call.
|
||||
///
|
||||
/// The set of sleds whose events are collected is determined by the session's
|
||||
/// The set of sleds whose events are collected is determined by the persona's
|
||||
/// [`ScopeFilter`]:
|
||||
/// - [`ScopeFilter::All`]: events from every sled present in the event log are
|
||||
/// included — this is the gateway-level persona default that gives a full
|
||||
@@ -81,7 +80,7 @@ pub fn read_llm_session(session_id: &str) -> Option<LlmSessionView> {
|
||||
///
|
||||
/// Returns an empty `Vec` when there are no new events or the CRDT is not
|
||||
/// initialised.
|
||||
pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
|
||||
pub fn assemble_and_advance_session(persona: &str) -> Vec<String> {
|
||||
let local_sled_id = crate::crdt_state::our_node_id().unwrap_or_default();
|
||||
|
||||
let Some(state_mutex) = get_crdt() else {
|
||||
@@ -91,9 +90,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
// Determine the session's scope filter and current high-water map.
|
||||
let (scope_filter, current_high_water) = match state.llm_session_index.get(session_id).copied()
|
||||
{
|
||||
// Determine the persona's scope filter and current high-water map.
|
||||
let (scope_filter, current_high_water) = match state.llm_session_index.get(persona).copied() {
|
||||
Some(idx) => {
|
||||
let filter = parse_scope(&state.crdt.doc.llm_sessions[idx], &local_sled_id);
|
||||
let hw = parse_high_water(&state.crdt.doc.llm_sessions[idx]);
|
||||
@@ -168,8 +166,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
|
||||
}
|
||||
let new_hw_json = serde_json::to_string(&new_high_water).unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
// Upsert the session entry with the new high-water value.
|
||||
let idx_opt = state.llm_session_index.get(session_id).copied();
|
||||
// Upsert the persona entry with the new high-water value.
|
||||
let idx_opt = state.llm_session_index.get(persona).copied();
|
||||
if let Some(idx) = idx_opt {
|
||||
apply_and_persist(&mut state, |s| {
|
||||
s.crdt.doc.llm_sessions[idx]
|
||||
@@ -179,8 +177,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
|
||||
} else {
|
||||
let scope_str = scope_filter.to_scope_str();
|
||||
let entry: JsonValue = json!({
|
||||
"session_id": session_id,
|
||||
"persona_name": "",
|
||||
"session_id": persona,
|
||||
"persona_name": persona,
|
||||
"scope": scope_str,
|
||||
"high_water": new_hw_json,
|
||||
})
|
||||
@@ -191,8 +189,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
|
||||
state.llm_session_index = rebuild_llm_session_index(&state.crdt);
|
||||
}
|
||||
|
||||
// Observability: log event-log size and gap count across the session's
|
||||
// target sleds (the scope actually assembled for this session).
|
||||
// Observability: log event-log size and gap count across the persona's
|
||||
// target sleds (the scope actually assembled for this persona).
|
||||
let total_entries = state
|
||||
.crdt
|
||||
.doc
|
||||
@@ -211,7 +209,7 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
|
||||
})
|
||||
.count();
|
||||
crate::slog!(
|
||||
"[event-log] assemble session={session_id} sled_entries={total_entries} gap_count={gap_count}"
|
||||
"[event-log] assemble persona={persona} sled_entries={total_entries} gap_count={gap_count}"
|
||||
);
|
||||
|
||||
// Render each new event as a compact audit line; gap sentinels get a
|
||||
|
||||
Reference in New Issue
Block a user