ReduceIntent
ReduceIntent<M> is the trait that bridges Session and intent types. It defines how a given intent enum reduces when dispatched through a Session<M, I>. The intent type I is a generic on Session — not an associated type on Modality — so the same modality can be wrapped with different intent types depending on whether it is a leaf or a child of a parent session.
Trait Definition
Section titled “Trait Definition”pub trait ReduceIntent<M: Modality>: Send + 'static { type Error: std::error::Error + Clone + Send + Sync + 'static; type Feedback: From<Self::Error> + Send + 'static;
fn wrap_modality(intent: M::Intent) -> Self; fn wrap_ai(ai: AiIntent) -> Self; fn wrap_ai_feedback(fb: AiFeedback) -> Self::Feedback;
fn reduce( self, state: &mut SessionState<M>, ) -> Effect<SessionServices, Self, Self::Feedback> where Self: Sized;
fn handle_feedback( fb: Self::Feedback, state: &mut SessionState<M>, ) -> Effect<SessionServices, Self, Self::Feedback> where Self: Sized;
fn spawn_generate( self_ref: &Option<Weak<Mutex<Session<M, Self>>>>, spawn_tx: Tx<Self, Self::Feedback>, agent_config: Option<&AgentConfig>, prompt: Option<String>, ) where Self: Sized;}Key methods:
wrap_modality(intent)— wraps a rawM::Intentinto this intent type. Used by the bridge socreate_tools()works generically.wrap_ai(ai)— wraps anAiIntentfor dispatch through Session.wrap_ai_feedback(fb)— wraps anAiFeedbackinto this type’sFeedback.reduce(self, state)— called whenCommand::Intent(intent)arrives. Consumes the intent. Receives&mut SessionState<M>— NOT&mut Session. Returns anEffect.handle_feedback(fb, state)— called whenCommand::Feedback(fb)arrives from an async effect. Returns anEffect.spawn_generate(self_ref, spawn_tx, config, prompt)— deferred agent spawn. Called by Session’s dispatch after reduce setspending_prompt. Needsself_ref(not available in reduce).
Purity Guarantees
Section titled “Purity Guarantees”There are two levels of purity in the reduce path:
| Level | Gets | Purity |
|---|---|---|
M::reduce() (modality) | &mut State<S, E> | Type-enforced — cannot access Doc, sinks, cache, or session services |
ReduceIntent::reduce() (session) | &mut SessionState<M> | Type-enforced — can access modality, doc, ephemeral, agent_tx, agent_config. Cannot access sinks, cache, callbacks, or self_ref. |
This is an improvement over the previous design where ReduceIntent::reduce received &mut Session<M, I> — full access to infrastructure it shouldn’t touch. SessionState<M> enforces the boundary at the type level.
Deferred Patterns
Section titled “Deferred Patterns”Two operations can’t happen inside reduce (they need self_ref or cache, which live on Session). Instead, reduce sets flags that Session’s dispatch() handles:
state.ephemeral.ai.pending_prompt = Some(prompt)— dispatch callsI::spawn_generate(...)after reduce completesstate.invalidated_children.push(id)— dispatch drains and clears cache entries before recompile
Leaf Impl: SessionIntent<I>
Section titled “Leaf Impl: SessionIntent<I>”For leaf modalities (Whiteboard, Worksheet), the intent type is SessionIntent<M::Intent>:
pub enum SessionIntent<I> { Modality(I), Ai(AiIntent), Export(OutputFormat), Undo, Redo,}The implementation returns Effect from each arm:
impl<M: Modality> ReduceIntent<M> for SessionIntent<M::Intent> { type Error = SessionError<M::Error>; type Feedback = SessionFeedback<M::Error>;
fn reduce(self, state: &mut SessionState<M>) -> Effect<...> { match self { Modality(i) => { state.modality.dispatch(Command::Intent(i)); Effect::none() } Ai(AiIntent::Generate { prompt }) => { state.ephemeral.ai.pending_prompt = Some(prompt.unwrap_or_default()); Effect::none() // deferred — dispatch handles the actual spawn } Export(fmt) => { Effect::spawn(move |svc, tx| { let bytes = /* export */; tx.feedback(SessionFeedback::ExportComplete(Ok(bytes))); }) } Undo => { state.undo(); Effect::none() } Redo => { state.redo(); Effect::none() } } }}The key arms:
Modality(i)— delegates toM::dispatch(). The modality’s own reduce runs and executes effects independently. ReturnsEffect::none().Ai(Generate { prompt })— setspending_prompton SessionState. Session’s dispatch callsspawn_generate()after reduce completes (needsself_ref).Export(fmt)— returnsEffect::spawn(). The feedback returns asSessionFeedback::ExportComplete.
Parent Impl: ParentSessionIntent<I, CI>
Section titled “Parent Impl: ParentSessionIntent<I, CI>”For parent modalities (LessonPlan), the intent type adds child routing:
pub enum ParentSessionIntent<I, CI> { Base(SessionIntent<I>), Child { id: Option<String>, intent: CI }, WarmChild { id: String },}The implementation adds two new arms plus undo timeline tracking:
Base(Modality(i))— delegates toM::dispatch(), then pushesUndoSource::Parentto the undo timeline.Child { id, intent }— routes to a Hot child session. IfidisNone, usesselected_child_id(). After dispatch, if the child’s version changed, pushesUndoSource::Child(id)to the undo timeline. Dispatching to a Cold slot is an error.WarmChild { id }— returnsEffect::spawn()for a backend load. TheChildWarmedfeedback creates the Hot session viainto_arc.Base(Undo)/Base(Redo)— consultsstate.undo_timelineto determine which session (parent or child) to undo/redo. Exhausted entries are automatically popped.
How Session::reduce Delegates
Section titled “How Session::reduce Delegates”Session’s Reducer impl is a thin dispatcher:
fn reduce(cmd, 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), }}All routing logic lives in the ReduceIntent impls, not in Session itself.
Feedback Enums
Section titled “Feedback Enums”pub enum SessionFeedback<E> { ExportComplete(Result<Vec<u8>, String>), Ai(AiFeedback), Error(SessionError<E>),}
pub enum ParentSessionFeedback<E, ChildE> { Base(SessionFeedback<E>), ChildWarmed { id: String, bytes: Vec<u8> }, Error(ParentSessionError<E, ChildE>),}Feedback re-enters Session via dispatch() — async feedback arrives on the Runtime’s spawn channel and is drained at the top of the next dispatch call.
Related Pages
Section titled “Related Pages”- Session Overview — the Session struct and ownership model
- Dispatch Lifecycle — how
dispatch()drives the reduce-execute loop - Children —
HasChildren,ParentSessionIntent, and the Hot/Cold lifecycle - Effect — the return type of
reduce()andhandle_feedback()