Skip to content

Command

Command<I, F> is the universal envelope through which all state mutations flow. Every dispatch() call receives a Command, and every async effect completion returns one.

crates/core/src/template/command.rs [10:13]
pub enum Command<I, F = ()> {
Intent(I),
Feedback(F),
}

The two type parameters are:

  • I — the intent type. Externally-initiated mutations. Session dispatches Command::Intent(i) only.
  • F — the feedback type (defaults to ()). Internal async results such as effect completions, streaming tokens, or export results. Never exposed externally.
IntentFeedback
OriginExternal caller (Dart, AI tool, test)Internal async effect (fx.spawn)
Entry pointsession.dispatch(Command::Intent(i))sender.send(Command::Feedback(fb))
VisibilityPublic API surfaceInternal to the reduce cycle
TimingImmediate — reduced in the current dispatch()Deferred — arrives on cmd_rx, drained at the top of the next dispatch()
flowchart LR
A[Dart / AI / Test] -->|"Command::Intent(i)"| B[dispatch]
B --> C[reduce]
C -->|"fx.spawn"| D[async effect]
D -->|"sender.send(Command::Feedback(fb))"| E[cmd_rx]
E -->|next dispatch| B

Previously, each modality defined its own command enum (WhiteboardCommand, WorksheetCommand, LessonPlanCommand). These are deleted. Command<Self::Intent, Self::Feedback> replaces them all.

The intent and feedback types are associated types on the Reducer trait:

trait Reducer: Send + Sync + 'static {
type Intent: Send + 'static;
type Feedback: Send + 'static;
// ...
fn reduce(
cmd: Command<Self::Intent, Self::Feedback>,
state: &mut Self::State,
fx: &mut Effects<Self::Services, Command<Self::Intent, Self::Feedback>>,
);
}

For a modality like Whiteboard, the concrete command type is Command<WhiteboardIntent, WhiteboardFeedback>. For a Session<Whiteboard, SessionIntent<WhiteboardIntent>>, the command type is Command<SessionIntent<WhiteboardIntent>, SessionFeedback>.

Both Effects::send and Effects::spawn work with the full Command<I, F> type:

// Synchronous -- reduced in the same dispatch() call
fx.send(Command::Intent(SomeIntent::DoSomething));
// Async -- feedback arrives on the next dispatch()
fx.spawn(|svc, sender| {
let result = svc.backend.load("id");
sender.send(Command::Feedback(SomeFeedback::Loaded(result)));
});
  • Reducer — uses Command as the input to reduce() and dispatch()
  • Effectssend() and spawn() produce Command values