Skip to content

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.

crates/core/src/template/reducer.rs
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 */ }
}
TypeBoundPurpose
StatenoneThe state reduce() mutates. Free generic — not tied to Synced/Ephemeral.
IntentSend + 'staticExternal mutations. Relaxed bound at the trait level.
FeedbackFrom<Self::Error> + Send + 'staticAsync effect completions. Must accept errors via From.
ErrorError + Clone + Send + Sync + 'staticTyped error for this reducer. Flows through feedback via From.
ServicesSend + Sync + 'staticInjected I/O services (backend, agent, domain).

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.

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.

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()
}
}
}

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.

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

  • Effect — the return type of reduce()
  • Command — the envelope reduce() and dispatch() operate on
  • Modality — pins Reducer::State to State<Synced, Ephemeral>