Effect
Effect<Svc, I, F> is the value returned from every reduce() call. It describes what should happen after state mutation. The dispatch loop interprets effects; reduce itself is pure.
Enum Definition
Section titled “Enum Definition”pub enum Effect<Svc, I, F> { /// No effect — only state was mutated. None, /// Synchronous follow-up command. Reduced in the same dispatch cycle /// with depth limiting (max 64) to prevent infinite loops. Send(Command<I, F>), /// Synchronous service access. Runs inline on the current thread. Task(Box<dyn FnOnce(&Svc) + Send>), /// Async work on a new thread. Feedback via Tx. Spawn(Box<dyn FnOnce(Arc<Svc>, Tx<I, F>) + Send + 'static>), /// Multiple effects executed sequentially. Batch(Vec<Effect<Svc, I, F>>),}Reduce functions return an Effect describing what should happen. The dispatch()/execute() loop interprets it. This replaces the old imperative Effects struct.
Five Variants
Section titled “Five Variants”Effect::None — state-only mutation
Section titled “Effect::None — state-only mutation”The reduce arm mutated state directly. No side effects needed.
Command::Intent(MyIntent::SetName { name }) => { state.synced.name = name; Effect::none()}Effect::Send(cmd) — synchronous follow-up
Section titled “Effect::Send(cmd) — synchronous follow-up”Queues a command for reduction in the same dispatch() call. Sends can produce more sends — execute() recurses with a depth limit of 64.
Command::Intent(MyIntent::Transition) => { state.synced.phase = Phase::Next; Effect::intent(MyIntent::RefreshView)}Effect::Task(f) — synchronous service access
Section titled “Effect::Task(f) — synchronous service access”Runs a closure inline on the current thread with &Svc. Use for agent tool output and lightweight service calls.
Command::Intent(MyIntent::ListItems) => { let items = state.synced.items.clone(); Effect::task(move |svc| { svc.agent.tool_output(json!({ "items": items })); })}Effect::Spawn(f) — async work
Section titled “Effect::Spawn(f) — async work”Runs a closure on a new thread. The closure receives Arc<Svc> and a Tx for sending feedback. Feedback is processed on the next dispatch() call.
Command::Intent(MyIntent::LoadData { id }) => { state.ephemeral.loading = true; Effect::spawn(move |svc, tx| { let bytes = svc.backend.load(&id).unwrap(); tx.feedback(MyFeedback::DataLoaded(bytes)); })}The default Reducer::execute() handles Spawn via std::thread::spawn + ensure_spawn_loop() for automatic feedback drain.
Effect::Batch(vec) — multiple effects
Section titled “Effect::Batch(vec) — multiple effects”Execute multiple effects sequentially. Built via Effect::batch() or the .and() combinator.
Command::Intent(MyIntent::Reset) => { state.synced = Default::default(); Effect::task(|svc| svc.agent.tool_output(json!({"ok": true}))) .and(Effect::intent(MyIntent::RefreshView))}Constructors
Section titled “Constructors”| Constructor | Returns |
|---|---|
Effect::none() | None |
Effect::intent(i) | Send(Command::Intent(i)) |
Effect::feedback(f) | Send(Command::Feedback(f)) |
Effect::error(e) | Send(Command::Feedback(e.into())) — requires E: Into<F> (satisfied by Reducer::Error) |
Effect::task(f) | Task(Box::new(f)) |
Effect::spawn(f) | Spawn(Box::new(f)) |
Effect::batch(vec) | Batch(vec) |
a.and(b) | Combines two effects, flattening None and nested Batch |
Tx — typed feedback sender
Section titled “Tx — typed feedback sender”Tx<I, F> wraps std::sync::mpsc::Sender<Command<I, F>>. Used inside Spawn closures to send feedback commands back to the reducer.
pub struct Tx<I, F>(std::sync::mpsc::Sender<Command<I, F>>);
impl<I, F> Tx<I, F> { pub fn intent(&self, i: I); pub fn feedback(&self, f: F); pub fn error<E: Into<F>>(&self, err: E); // sends err.into() as feedback pub fn send(&self, cmd: Command<I, F>);}Tx implements Clone. Feedback sent via Tx arrives on the spawn channel and is drained at the top of the next dispatch() call.
Error sugar
Section titled “Error sugar”Effect::error(e) and Tx::error(e) convert a typed error into feedback via the From<Reducer::Error> bound on Feedback. This lets reduce functions return errors without manually constructing Feedback::Error(...):
Command::Intent(MyIntent::Remove { id }) => { match state.synced.items.remove(&id) { Some(_) => Effect::none(), None => Effect::error(MyError::NotFound(id)), }}AgentService Channel
Section titled “AgentService Channel”Tool output from reduce does not flow through Effect fields. Instead, it flows through the AgentService channel, accessed via Effect::task():
Effect::task(|svc| { svc.agent.tool_output(json!({ "elements": [...] }));})AgentService is a write-only bridge with its own mpsc::Sender<AgentEvent>. Session owns the matching receiver and drains events via drain_agent_events() after dispatch.
pub enum AgentEvent { ToolOutput(serde_json::Value), ToolError(String),}
pub struct AgentService { tx: mpsc::Sender<AgentEvent>,}ModalityServices
Section titled “ModalityServices”ModalityServices is the default services type for simple modalities. It wraps Arc<AgentService>. Modalities with domain-specific services define their own type.