Skip to content

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.

crates/session/src/intent.rs
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 raw M::Intent into this intent type. Used by the bridge so create_tools() works generically.
  • wrap_ai(ai) — wraps an AiIntent for dispatch through Session.
  • wrap_ai_feedback(fb) — wraps an AiFeedback into this type’s Feedback.
  • reduce(self, state) — called when Command::Intent(intent) arrives. Consumes the intent. Receives &mut SessionState<M> — NOT &mut Session. Returns an Effect.
  • handle_feedback(fb, state) — called when Command::Feedback(fb) arrives from an async effect. Returns an Effect.
  • spawn_generate(self_ref, spawn_tx, config, prompt) — deferred agent spawn. Called by Session’s dispatch after reduce sets pending_prompt. Needs self_ref (not available in reduce).

There are two levels of purity in the reduce path:

LevelGetsPurity
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.

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 calls I::spawn_generate(...) after reduce completes
  • state.invalidated_children.push(id) — dispatch drains and clears cache entries before recompile

For leaf modalities (Whiteboard, Worksheet), the intent type is SessionIntent<M::Intent>:

crates/session/src/intent.rs
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:

  1. Modality(i) — delegates to M::dispatch(). The modality’s own reduce runs and executes effects independently. Returns Effect::none().
  2. Ai(Generate { prompt }) — sets pending_prompt on SessionState. Session’s dispatch calls spawn_generate() after reduce completes (needs self_ref).
  3. Export(fmt) — returns Effect::spawn(). The feedback returns as SessionFeedback::ExportComplete.

For parent modalities (LessonPlan), the intent type adds child routing:

crates/session/src/intent.rs
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 to M::dispatch(), then pushes UndoSource::Parent to the undo timeline.
  • Child { id, intent } — routes to a Hot child session. If id is None, uses selected_child_id(). After dispatch, if the child’s version changed, pushes UndoSource::Child(id) to the undo timeline. Dispatching to a Cold slot is an error.
  • WarmChild { id } — returns Effect::spawn() for a backend load. The ChildWarmed feedback creates the Hot session via into_arc.
  • Base(Undo)/Base(Redo) — consults state.undo_timeline to determine which session (parent or child) to undo/redo. Exhausted entries are automatically popped.

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.

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.

  • Session Overview — the Session struct and ownership model
  • Dispatch Lifecycle — how dispatch() drives the reduce-execute loop
  • ChildrenHasChildren, ParentSessionIntent, and the Hot/Cold lifecycle
  • Effect — the return type of reduce() and handle_feedback()