Session Overview
Session<M, I> is the central orchestrator of the modality library. It owns domain state (via SessionState<M> inside Runtime<Self>), persistence (via Doc), reactivity (via sinks), incremental compilation (via cache), and the feedback channel for async effects. Session implements Reducer with type State = SessionState<M>, meaning reduce functions receive &mut SessionState<M> — a clean boundary that excludes sinks, cache, callbacks, and self_ref.
Struct Definition
Section titled “Struct Definition”pub struct Session<M: Modality, I: ReduceIntent<M> = SessionIntent<<M as Reducer>::Intent>> { rt: Runtime<Self>, // rt.state = SessionState<M>, rt.services = SessionServices sinks: Vec<Box<dyn Sink<M::Snapshot>>>, // reactivity (typed per-modality snapshots) on_change_callbacks: Vec<...>, // CRDT sync outbound cache: CompileCache<...>, // incremental recompilation agent_rx: Mutex<Receiver<AgentEvent>>, // AI bridge reads tool output here self_ref: Option<Weak<Mutex<Self>>>, // set by into_arc via post_init ai_undo_group_active: bool, // whether AI undo group is open (the Session Undo/Redo RFC)}The two generic parameters are:
M: Modality— the modality type (Whiteboard,Worksheet,LessonPlan).I: ReduceIntent<M>— how intents reduce inside this session. Defaults toSessionIntent<M::Intent>, so leaf sessions can be written asSession<Whiteboard>. Parent sessions override:Session<LessonPlan, ParentSessionIntent<...>>. This is a generic on Session, not an associated type on Modality.
SessionState<M> — Reduce Boundary
Section titled “SessionState<M> — Reduce Boundary”SessionState<M> is extracted from Session so that ReduceIntent::reduce gets a clean interface — no access to sinks, cache, callbacks, or self_ref:
pub struct SessionState<M: Modality> { pub modality: M, // owns State<M::Synced, M::Ephemeral> pub doc: Doc<M::Synced>, // persistence (LoroDoc for M::Synced only) pub ephemeral: SessionEphemeral, // session-level: AI + export state pub version: u64, // monotonic counter pub agent_tx: Option<Sender<String>>, // forward messages to agent thread pub agent_config: Option<AgentConfig>, // temperature, model, preamble overrides pub(crate) invalidated_children: Vec<String>, // deferred cache invalidation pub undo_timeline: UndoTimeline, // interleaved parent/child undo tracking (the Session Undo/Redo RFC)}Undo/Redo (the Session Undo/Redo RFC)
Section titled “Undo/Redo (the Session Undo/Redo RFC)”SessionState provides undo() and redo() methods that operate on the backing LoroDoc and call the modality’s on_undo()/on_redo() hooks to fix up ephemeral state. Session exposes begin_undo_group()/end_undo_group() for merging multiple changes into a single undo step (used by AI streaming).
The undo_timeline field tracks interleaved parent/child changes for parent sessions. Leaf sessions leave it empty and use Doc::can_undo/can_redo directly. Session computes can_undo/can_redo by OR-ing both sources and passes the result to M::snapshot().
Deferred patterns
Section titled “Deferred patterns”Two fields enable deferred operations — reduce sets them, Session’s dispatch() handles them:
ephemeral.ai.pending_prompt— reduce setsSome(prompt), dispatch callsI::spawn_generate(...)(needsself_ref, not available in reduce)invalidated_children— reduce pushes placement IDs, dispatch drains and clears cache entries before recompile
Ownership Split
Section titled “Ownership Split”graph TD S["Session<M, I>"] S --> RT["Runtime<Self>"] S --> SINKS["Sinks"] S --> CACHE["CompileCache"] S --> SELF["self_ref (Weak)"]
RT --> SS["SessionState<M>"] RT --> SVC["SessionServices"] RT --> CH["Spawn Channels"]
SS --> MOD["Modality (M)"] SS --> DOC["Doc<M::Synced>"] SS --> EPH["SessionEphemeral"]
MOD --> STATE["State<M::Synced, M::Ephemeral>"] DOC --> LORO["LoroDoc"] EPH --> AI["AiState"] EPH --> EXP["ExportState"] SVC --> BE["Arc<dyn StoreBackend>"]Each piece has a distinct responsibility:
| Owner | Holds | Purpose |
|---|---|---|
| Runtime<Self> | SessionState<M> + SessionServices + spawn channels | Shared Reducer infrastructure. |
| SessionState<M> | modality, doc, ephemeral, version, agent_tx, agent_config | Everything a reduce function needs. Clean boundary. |
| Modality | State<M::Synced, M::Ephemeral> | Domain state. Direct &mut access, no locks. |
| Doc<M::Synced> | LoroDoc | Pure persistence. Only M::Synced is persisted. Does not dispatch or emit. |
| SessionEphemeral | AiState, ExportState | Session-level runtime state. Not persisted. |
| Sinks | Vec<Box<dyn Sink<M::Snapshot>>> | Push typed snapshots to subscribers. Emits after reduce + recompile + commit. |
| SessionServices | Arc<dyn StoreBackend> | Session-level I/O: child warming, persistence. Separate from M::Services. |
| CompileCache | Hash-based placement cache | Incremental recompilation. Per-placement hash of (placement, child_metadata, resolved_values). |
| self_ref | Option<Weak<Mutex<Self>>> | Set by into_arc via post_init. Used for agent thread spawning. |
Session Implements Reducer
Section titled “Session Implements Reducer”Session implements Reducer with type State = SessionState<M>. The static reduce() delegates to the intent type’s own reduce method via the ReduceIntent trait:
impl<M: Modality, I: ReduceIntent<M>> Reducer for Session<M, I> { type State = SessionState<M>; type Intent = I; type Feedback = I::Feedback; type Error = I::Error; type Services = SessionServices;
fn runtime(&self) -> &Runtime<Self> { &self.rt } fn runtime_mut(&mut self) -> &mut Runtime<Self> { &mut self.rt }
fn reduce( cmd: Command<I, I::Feedback>, state: &mut SessionState<M>, ) -> Effect<SessionServices, I, I::Feedback> { match cmd { Command::Intent(intent) => I::reduce(intent, state), Command::Feedback(fb) => I::handle_feedback(fb, state), } }
fn post_init(&mut self, weak: Weak<Mutex<Self>>) { self.self_ref = Some(weak); }}Session overrides dispatch() to insert feedback drain, deferred agent spawn, cache invalidation, and a lifecycle phase (recompile, commit, emit) after all reduces complete. See Dispatch Lifecycle for the full flow.
Construction: into_arc
Section titled “Construction: into_arc”Sessions are constructed via into_arc(session), which wraps in Arc<Mutex<>> and wires the weak self-reference via post_init:
let session = Session::new(synced, services, agent_rx);let arc = into_arc(session); // post_init sets self_refarc.lock().subscribe(Box::new(sink));Session Implements Component
Section titled “Session Implements Component”Session also implements Component, enabling it to be used inside a Slot for parent-child nesting:
impl<M: Modality, I: ReduceIntent<M>> Component for Session<M, I> { type Metadata = <M as Component>::Metadata; type Output = <M as Component>::Output;
fn compile(&self, _meta, _values) -> Result<Output> { Ok(self.output().clone()) // cached -- Session already compiled during dispatch }}This is what makes Slot<Session<Whiteboard, ...>, Vec<WhiteboardElement>> work on LessonPlan. A Hot child Session returns its already-compiled output without recompilation.
SessionEphemeral
Section titled “SessionEphemeral”Runtime state that lives on SessionState, not persisted to the LoroDoc:
pub struct AiState { pub is_streaming: bool, pub messages: Vec<AiMessage>, pub current_response: String, pub thinking_content: String, pub tool_calls: Vec<AiToolCall>, pub error: Option<String>, pub pending_prompt: Option<String>, // deferred generate, consumed by dispatch}pub struct ExportState { pub bytes: Option<Vec<u8>>, pub ready: bool, pub error: Option<String>,}AI and export state are included in snapshots so Dart can render streaming indicators and export results, but they are never written to the CRDT document.
SessionServices
Section titled “SessionServices”Session-level I/O, separate from the modality’s domain services:
pub struct SessionServices { pub backend: Arc<dyn StoreBackend>,}The modality has its own M::Services (e.g., ModalityServices wrapping Arc<AgentService>). SessionServices handles persistence and child warming — concerns that live above any single modality.
Per-Modality Session Types
Section titled “Per-Modality Session Types”| Modality | Session Type | Shorthand |
|---|---|---|
| Whiteboard | Session<Whiteboard, SessionIntent<WhiteboardIntent>> | Session<Whiteboard> |
| Worksheet | Session<Worksheet, SessionIntent<WorksheetIntent>> | Session<Worksheet> |
| LessonPlan | Session<LessonPlan, ParentSessionIntent<LessonPlanIntent, SessionIntent<WhiteboardIntent>>> | (no shorthand) |
Leaf modalities use the default I = SessionIntent<M::Intent>, so Session<Whiteboard> suffices. Parent modalities explicitly specify ParentSessionIntent<I, CI> which adds child routing and warming on top of the base session intents.
Related Pages
Section titled “Related Pages”- ReduceIntent — how intent types reduce inside a Session
- Dispatch Lifecycle — the full drain-reduce-lifecycle-spawn flow
- Children — parent sessions and the Hot/Cold lifecycle
- Snapshots — per-modality snapshot DTOs and sink emission
- Persistence — Doc<S> and the CRDT persistence layer