Reducer
Reducer is the core state management trait. It follows the Genvoy pattern: reduce() is a static function (no &self) that mutates state and returns an Effect. The struct holds state and services. dispatch() and execute() are instance methods that drive the reduce-then-execute loop.
Trait Definition
Section titled “Trait Definition”pub trait Reducer: Sized + Send + Sync + 'static { type State; type Intent: Send + 'static; type Feedback: From<Self::Error> + Send + 'static; type Error: std::error::Error + Clone + Send + Sync + 'static; type Services: Send + Sync + 'static;
// Required (2) — Runtime container accessors fn runtime(&self) -> &Runtime<Self>; fn runtime_mut(&mut self) -> &mut Runtime<Self>;
// Provided (3) — delegate to Runtime fn state(&self) -> &Self::State { &self.runtime().state } fn state_mut(&mut self) -> &mut Self::State { &mut self.runtime_mut().state } fn services(&self) -> &Arc<Self::Services> { &self.runtime().services }
fn post_init(&mut self, _weak: Weak<Mutex<Self>>) {}
fn reduce( cmd: Command<Self::Intent, Self::Feedback>, state: &mut Self::State, ) -> Effect<Self::Services, Self::Intent, Self::Feedback>;
fn dispatch(&mut self, cmd: Command<Self::Intent, Self::Feedback>) { /* default */ } fn execute(&mut self, effect: Effect<...>, depth: usize) { /* default */ }}Associated Types
Section titled “Associated Types”| Type | Bound | Purpose |
|---|---|---|
State | none | The state reduce() mutates. Free generic — not tied to Synced/Ephemeral. |
Intent | Send + 'static | External mutations. Relaxed bound at the trait level. |
Feedback | From<Self::Error> + Send + 'static | Async effect completions. Must accept errors via From. |
Error | Error + Clone + Send + Sync + 'static | Typed error for this reducer. Flows through feedback via From. |
Services | Send + Sync + 'static | Injected I/O services (backend, agent, domain). |
State is a free generic
Section titled “State is a free generic”Reducer does not know about Synced or Ephemeral — that split is a Modality concern. This lets non-modality types implement Reducer with arbitrary state. Session, for example, uses type State = Self, giving reduce() full &mut Session<M, I> access.
Intent bound is relaxed
Section titled “Intent bound is relaxed”The trait requires only Send + 'static on Intent. Modality intents additionally implement CommandTool (via #[derive(CommandTool)]), making them available as AI tools. Session intents (SessionIntent<I>) do not implement CommandTool — they wrap AI/Export/Modality routing, not individual tools.
The Genvoy Pattern
Section titled “The Genvoy Pattern”reduce() is static — it receives &mut State, not &mut self. Side effects are described by the returned Effect, not performed imperatively. This makes reduce logic easy to test:
fn reduce( cmd: Command<Intent, Feedback>, state: &mut State,) -> Effect<Services, Intent, Feedback> { match cmd { Command::Intent(MyIntent::Add { item }) => { state.items.push(item); Effect::task(|svc| svc.agent.tool_output(json!({"ok": true}))) } Command::Feedback(MyFeedback::Loaded(data)) => { state.data = data; Effect::none() } }}Two Required Accessors
Section titled “Two Required Accessors”Every Reducer struct embeds a Runtime<Self> and must provide:
runtime()/runtime_mut()— access the shared Runtime container (state + services + channels)
Three convenience accessors are provided by default: state(), state_mut(), services() — all delegate to the Runtime.
Default dispatch()
Section titled “Default dispatch()”fn dispatch(&mut self, cmd: Command<I, F>) { let effect = Self::reduce(cmd, self.state_mut()); self.execute(effect, 0);}Calls reduce, then executes the returned effect. Override for:
- Spawn support — the default handles spawns via
std::thread::spawn+ensure_spawn_loop - Custom lifecycle — Session adds recompile → commit → emit
- Feedback drain — Session drains pending async feedback before reduce
Default execute()
Section titled “Default execute()”fn execute(&mut self, effect: Effect<Svc, I, F>, depth: usize) { const MAX_SEND_DEPTH: usize = 64; match effect { Effect::None => {} Effect::Send(cmd) => { if depth < MAX_SEND_DEPTH { let e = Self::reduce(cmd, self.state_mut()); self.execute(e, depth + 1); } } Effect::Task(f) => { let svc = self.services().clone(); f(&svc); } Effect::Spawn(f) => { let svc = self.runtime().services.clone(); let tx = self.runtime().spawn_tx(); std::thread::spawn(move || f(svc, tx)); self.runtime().ensure_spawn_loop(); } Effect::Batch(effects) => { for e in effects { self.execute(e, depth); } } }}Send recurses with a depth limit of 64. Task runs inline. Spawn starts a new OS thread and feeds back via Tx. Batch iterates sequentially.
Session overrides dispatch()
Section titled “Session overrides dispatch()”Session<M, I> overrides both dispatch() and execute() to add lifecycle and spawn support:
flowchart LR A[drain feedback] --> B[reduce + execute] --> C[recompile] --> D[commit] --> E[version++] --> F[emit] --> G[fire_on_change]Session’s execute() handles Spawn by cloning services and spawn_tx, then calling std::thread::spawn. Feedback arrives on the spawn channel and is drained at the top of the next dispatch() call. Session also handles deferred agent spawn (pending_prompt) and cache invalidation for dirty children.