Streaming and runs
The Dispatch run lifecycle and how output reaches the UI.
A Dispatch run is a single execution of the agent loop, originated by some event and ended when the model stops calling tools. Runs are the unit of work in the product: every chat message turns into a run; every automation firing turns into a run; every todo dispatched for autonomous work turns into a run.
Origins
A run is invoked from one of four origins, captured on the agent context as ctx.origin:
- Chat. The user typed something in the chat surface or sent a message from a connected channel (Telegram, Slack). The agent has a present user it can clarify with.
- Triggered automation. A trigger condition (an email matching a rule, a calendar event starting, etc.) fired and the automations runtime dispatched the configured prompt. The user is not present in the loop; the agent should defer rather than ask questions.
- Scheduled task. A scheduled automation hit its cron-style trigger time. Same not-present semantics as triggered.
- Todo. A todo's runner dispatched a run scoped to that todo's prompt. Not-present.
The origin matters for the permission gate: a present user can be asked for confirmation; an absent user cannot, so the agent must defer to a proposal or a follow-up todo instead of stopping mid-task to ask.
The agent loop
Each run iterates the agent loop until the model stops calling tools or hits a stop condition. Each iteration:
- Build the system prompt from active connectors (each connector contributes a section).
- Inject
/daily-notes/today.mdand/preferences/global.mdinto context. - Filter tools through the tool-search registry: only always-on tools and tools previously unlocked this run are visible.
- Call the model. Stream content and tool calls back.
- For each tool call, route to the owning connector. Tools with a
riskannotation pass through the permission gate. Otherwise execute directly. - Append the tool result and continue the loop.
When the model stops emitting tool calls, the run ends.
Streaming to the UI
The agent's output is streamed to persistent server state in batches as the loop runs. Each batch contains a slice of model output (text, tool call, tool result) plus a sequence range that orders it in the transcript.
The client side subscribes to the active session's output stream. The subscription is reactive: as new chunks arrive, the client re-renders the transcript without re-fetching. This is the mechanism behind the chat surface's live token-by-token rendering.
Batches are coalesced to keep the write rate down (model output arrives faster than is useful to render). The exact batch granularity is a tuning knob; the default lands at small fractions of a second per batch.
Persistence
Everything a run produces is persisted server-side:
- The session record, keyed by session reference.
- Streamed output chunks, indexed by session reference and sequence range.
- Permission gate audit rows for each gated tool call that consults the validator or short-circuits to
migration-bypass. With thepermissionAgentflag off, gated calls emit only thepermission_checkanalytics event and do not write an audit row.
After a run ends the session is queryable from the run-history surface in the app and from the history connector inside the agent.
Errors and stop reasons
A run can end for several reasons:
- Normal completion. The model stopped calling tools because the work is done.
- Hard stop from the permission gate. A retry-loop guard fired (see Permission gate). The agent reports the block to the user and waits.
- Model error or rate limit. Surfaced to the transcript and to error tracking; the user sees an error chunk in place of the missing output.
- Validator unavailable. A gated tool's validator timed out or errored. The gate fails closed and the agent treats the call as denied with
failClosed: true; the agent may retry once before asking.
In all cases the session record remains stored and is reloadable, so a user can see what the agent did, what it tried to do, and where it stopped.
Authorization on streaming reads
The streaming-read query authorizes the caller against the session's organization. A non-member of the org gets back an empty list rather than a 404, which prevents probing for session refs across workspaces.
For agents
- A run is the unit of work. It is originated from one of four origins (
chat,triggered,scheduled,todo) and ends when the model stops calling tools. - The agent loop iterates: build system prompt → inject preferences and today's note → filter tools through the tool-search registry → call the model → execute tool calls (gated where annotated) → continue.
- Output streams to the server in coalesced batches; the client subscribes to the session's output stream.
- Session state and streamed chunks are persisted server-side. Permission audit rows are written only when the validator runs or a migration-bypass fires; flag-off gated calls emit telemetry but no audit row.
- Runs end on normal completion, permission hard stop, model error, or validator unavailability. The session is reloadable in all cases.
- The streaming-read query authorizes by organization membership; non-members get back an empty list to prevent cross-workspace probing.