Expand description
The effect runner: dispatches Cmd values into tokio tasks.
There are exactly two places in the codebase that spawn a tokio
task: this module and tests. Everywhere else asks the
reducer to return a Cmd, and the runner handles it. That
centralization is what makes structured concurrency per turn
actually work — nothing can accidentally spawn a detached task
that outlives the turn it was started for.
Architecture:
main loop ── reducer ── Cmd ── dispatch ── EffectRunner
├── TurnScope(turn A) ── JoinSet
├── TurnScope(turn B) ── JoinSet
└── detached effects (Save, Exit, …)
↓
Msg via mpsc::Sender<Msg>
↓
main loop (next iteration)The runner dispatches every Cmd variant to a real handler —
model streaming (CallModel → ModelProvider::chat), tool
execution (ExecuteTool → ToolExecutor::execute), persistence
(SaveConversation, LoadConversation, PersistLastModel,
PersistReasoningFor), MCP lifecycle
(InitMcpServers, StopMcpServer), local side-effects
(WriteImageToTemp, OpenInSystem, PullOllamaModel,
SetTerminalTitle). Cancellation flows
through Cmd::CancelScope(TurnId) → the scope’s
CancellationToken.
Structs§
- Effect
Runner - The runner. One instance per process, constructed by
app::runand consumed when the main loop exits. - Turn
Scope - One turn’s cancellable scope. Construct once per
SubmitPrompt; abandon (drop) at the end of the turn.
Constants§
- MSG_
CHANNEL_ CAPACITY - Bounded channel capacity for the effect → reducer stream. 512 is generous — a single streaming chunk fits comfortably, and the main loop drains at ~60 Hz so backlog rarely grows. Bigger wastes RAM; smaller introduces spurious backpressure on bursty tool output.
Type Aliases§
- MsgSender
- Single channel back to the reducer.
EffectRunnerholds the sender; every spawned task clones this so it can emitMsgas work progresses. Bounded capacity applies natural backpressure — if the main loop can’t keep up, the provider’s streaming send.awaits and the whole pipeline throttles.