# Sidekick: event-driven orchestration around a durable agent loop

Reviewed 2026-10-19 at commit `463d653124e1c2cda46a3f14f01f3d6f384326b1`.

**Source access:** Sidekick is a private repository. This chapter publishes an explanatory review, not its implementation. The pinned GitHub citations require repository access. The findings describe the inspected checkout; they are not a claim about every deployed configuration.

Sidekick makes the execution boundary especially visible: a conversation turn can outlive the worker attempt that is currently processing it. Work is admitted into database records, dispatched to workflows, processed through explicit state transitions, and projected to connected clients. Its native worker still contains an ordinary iterative model/tool loop. The distinctive lesson is how that loop participates in a larger event-driven system, rather than the disappearance of iteration. [Native loop][S1] [Turn attempts][S2]

![Sidekick's native architecture: durable turn admission and attempt ownership surround an iterative model/tool workflow; session messages drive reducer effects and client projections.](../guide/assets/sidekick-architecture.svg)

## Two execution paths, one conversation product

Start with routing. A native Sidekick or model-API provider runs the cluster agent loop. A provider that wraps an external harness requires an Outpost, Sidekick's bridge to a separately hosted execution environment. Supplying an Outpost for a native provider can instead make local tools available while the model loop stays in the cluster. A missing required Outpost is an error, not permission to silently substitute the native loop. [Execution routing][S3]

This matters when comparing capabilities. An external harness may supply its own permissions, compaction, MCP support, or session format. Hosting it behind Sidekick does not establish that the native Sidekick runtime implements the same mechanisms. Conversely, Sidekick's admission, shared conversation, and result presentation remain useful orchestration features even when another harness owns model execution.

The native path combines four cooperating mechanisms:

| Mechanism | Responsibility | What it does not establish |
|---|---|---|
| Turn and attempt records | Track admitted work, its current producer, waiting, and final outcome | That every external effect executes exactly once |
| Workflow scheduling | Dispatch conversation and tool work across workers | That a duplicate delivery should repeat an operation |
| Session reducer and effects | Convert messages into state updates, persistence requests, and broadcasts | That every broadcast has committed to the database |
| Native agent loop | Rebuild context, request a model response, execute tools, and continue | That the request's process lifetime equals the turn's lifetime |

These are concrete layers in the inspected implementation, not four names for the same event bus. [Reducer][S4] [Effect executor][S5] [Tool scheduling][S6]

## A concrete turn trace

Imagine a user requests a search followed by a change. The trace below follows the native path; it is a teaching example, not a recorded production conversation.

1. **Admit work.** A pending turn records the request before dispatch. Admission policies run under a conversation lock so concurrent submitters do not independently decide the same conversation is idle. Policies vary for ordinary work, panel seats, and delegated children. [Admission][S7]
2. **Claim an attempt.** The worker claims that turn, records the workflow identity and attempt number, and maintains a heartbeat. A fresh competing attempt normally refuses to claim it. A permitted retry or stale-worker takeover changes which attempt owns the turn. [Attempt claiming][S2]
3. **Prepare the next request.** At an iteration boundary, the loop checks cancellation, places eligible queued steering messages, and reconstructs model context from conversation records. A steer is therefore consumed at a defined boundary, rather than spliced into an already executing model request. [Iteration boundary][S1]
4. **Process the model stream.** Text deltas update streaming state and are broadcast. Completed assistant messages produce persistence and broadcast effects. When the assistant requests tools, the loop records tool intents alongside the assistant message and commits before dispatching those tools. [Model response][S8] [Intent boundary][S9]
5. **Dispatch and collect tools.** The executor submits tool workflows with correlation identifiers. Each tool's intent must be claimed before its effect. Results may arrive independently; the executor builds the returned batch in the model's original call order. The loop records those results, commits, and reconstructs context for the next model request. [Tool scheduling][S6] [Intent gate][S10]
6. **Settle the turn.** When a terminal answer is reached, the owning attempt writes the settlement in the final commit. Post-commit processing runs hooks and publishes a completion projection. The database outcome, not merely a last token or segment result, tells the product that the turn has finished. [Final answer][S8] [Settlement][S11] [Post-commit finish][S12]

Tool workflows can overlap after submission; this is not a sequential interpreter that waits for every tool before submitting the next. It is also not an unlimited parallelism promise: workflow infrastructure, tool routing, and downstream services still constrain execution. Dependent tool calls must be separated or explicitly coordinated. [Tool scheduling][S6]

## What “event-driven” means here

The session reducer accepts backend messages, user commands, and system signals. It returns new session state plus declarative effects such as persist, broadcast, or forward to an Outpost. It performs no I/O itself. The effect executor applies those effects in order; actual Outpost sending belongs to the WebSocket handler. This separation makes message semantics inspectable without running the network or database. [Reducer][S4] [Effect executor][S5]

The native model loop dispatches its messages through the same session machinery. Tool work crosses workflow boundaries and wakes its caller through correlated results. Admission, cancellation, suspension, recovery, and settlement have explicit durable state transitions. These mechanisms justify describing Sidekick as event-driven orchestration. A streaming UI alone would not: emitting token events is observability, whereas admitting and claiming work changes who may execute it.

There are limits to the label. The model/tool continuation itself is a bounded loop inside a conversation workflow. The code does not make every model step a separately scheduled event, and the session reducer is not a complete event-sourced reconstruction of all runtime state. Durable turn rows and tool intent rows hold authority that cannot be inferred from the transient stream. Sidekick offers a useful architectural contrast within this comparison; this review does not establish that it is the only harness with event-driven execution. [Native loop][S1] [Session dispatch][S13] [Turn lifecycle][S14]

For a cross-harness treatment of these choices, read [Event-driven execution](../../../guide/advanced/event-driven.md), including the tradeoffs and a small failure-injection exercise.

## A turn is not an attempt

The turn states include pending, running, awaiting input, and settled. An attempt identifies one producer trying to advance the turn. This distinction answers a practical question: if a worker disappears, did the user's task fail, or did one process fail while work remains recoverable?

Claiming uses database locking and a conditional update against the observed state and current attempt. A current heartbeat normally prevents takeover. On the cluster path, a stale attempt can be replaced; a higher retry count for the same workflow run can also replace its earlier attempt. The latter rule distinguishes a workflow redispatch from an ordinary duplicate delivery. Ownership checks prevent a superseded attempt from recording a valid settlement for work it no longer owns. [Attempt claiming][S2] [Settlement][S11]

These checks are a form of **fencing**: a record identifies the producer currently authorized to change durable state. They do not physically stop an old process or undo a remote operation it already started. That is why tool intents and recovery policy are separate from turn ownership.

Outpost execution deliberately differs. Its dispatch attempt is not automatically taken over and re-sent, because the external harness might still be acting. A timeout or disconnected observer is not proof that the external action never began. [Attempt claiming][S2]

A native request for user input can suspend the turn and end the worker run. The response resumes the same turn through a later attempt. Asking a question alongside ordinary tools is refused for that question, allowing the other tools to produce their results; otherwise suspension would strand sibling tool calls. User cancellation is also distinct from workflow interruption: the former settles a cancelled task, while an engine interruption can end only the attempt and leave recovery possible. [Question suspension][S15] [Cancellation distinction][S1]

## Tool recovery records what is known

A durable conversation transcript does not by itself solve tool replay. Suppose a tool changes a remote system and the worker dies before writing its result into the conversation. Repeating the last model request may repeat that change. Sidekick records each intended tool call and tracks whether an executor claimed it and whether an outcome was stored. [Intent boundary][S9] [Intent gate][S10]

Recovery runs before the next model request and treats the recorded phase as evidence:

| Recorded phase | Recovery behavior |
|---|---|
| Intended, not yet claimed | Claim and execute it; a racing original executor must win the same gate |
| Running, replay-safe under both stored and current policy | Wait for its existing result window, then permit a rerun |
| Running, replay-unsafe or no longer resolvable | Wait, then report an interrupted operation with uncertain outcome |
| Settled with a stored payload | Redeliver that payload into the conversation without repeating the tool |
| Unfinished and cancellation requested | Close it with cancellation behavior; already settled results remain deliverable |

The implementation re-reads a row when a competing writer wins, so a result arriving during recovery can change the action from rerun to redelivery. Delegation adds a child-turn check and does not simply launch the child again. These are recovery decisions based on durable knowledge, not a guarantee that outside effects happen exactly once. “Replay-safe” also does not mean “authorized by the user”: retry policy and permission policy answer different questions. [Recovery decisions][S16] [Recovery policy check][S17]

The inspected integration tests make the uncertainty concrete. A simulated crash after tool effects but before result persistence causes a replay-safe search to run twice across attempts, while an unsafe operation runs once and receives an interrupted-result explanation. A different crash before the tool is claimed permits execution because the record proves it has not started. These tests are valuable specifications; this review inspected them without running Sidekick's services or test suite. [Crash-window tests][S18]

## Live streams and durable truth

The reducer persists completed assistant messages, tool results, and selected audit messages; text deltas and progress events are ephemeral. Dispatch replaces local sequence proposals with atomically allocated sequence numbers so concurrent writers can share a conversation. It also checks the persisted sequence floor when a cache counter is lost. Sequence ordering is therefore coordinated beyond one loop's memory. [Reducer][S4] [Sequence allocation][S13]

Persistence effects flush rows through the current database session. They do not commit the transaction. Ordinary dispatch can broadcast before its caller commits; explicit deferred-broadcast paths publish acknowledgements after commit. Consequently, seeing an event or sequence number does not prove that its transaction survived a crash. This is a boundary to preserve when building clients, replay tools, or monitoring. [Effect executor][S5] [Session dispatch][S13]

Turn completion has a stronger contract. Its event is a projection of the settled row, emitted after commit and reconstructible for clients from durable state. The code distinguishes a segment result from whole-turn completion and attaches parent identity so a delegated child's completion cannot masquerade as its parent's. [Turn lifecycle][S14]

Durability still has operational limits. A sweeper can settle stale producers or expired input waits, and a special recovery path can redispatch a lost queued submission. Post-settlement hooks and projections remain post-commit work; the sweeper explicitly logs cases where these steps are lost rather than claiming a durable retry for all of them. A single authoritative settlement is stronger than a UI flag, but it is not a transaction spanning every notification and downstream system. [Sweeper recovery and limits][S19]

## Context, collaboration, and tool authority

Rebuilding context is not the same as summarizing it. The inspected native context factory registers optional removal of older, large tool outputs from the request projection. That is useful context management, but it does not establish automatic full-history summary checkpoints or overflow-compaction recovery. External harness behavior remains a separate question. [Context pipeline][S21]

Human collaboration is concrete product behavior: conversation access distinguishes view, edit, and manage permissions, including direct and group shares. Sending requires edit access. Shared observation, durable input admission, and sequencing solve different parts of the collaboration problem; none should be treated as a substitute for the others. [Conversation access][S22] [Sending][S23]

Multi-agent work has two useful forms. Delegation admits an owned child turn and links it to the parent's tool intent before dispatch. Panels instead grant peer agents a floor; completion updates persisted grants, and the last completed or failed statement can make a moderator wake eligible. That latter dependency is a concrete example of events determining subsequent execution. [Child admission][S24] [Panel completion][S25]

Skills are attached, versioned instruction packages with a prompt catalog and on-demand instruction/reference tools. A stable skill-tool dispatcher checks the attached binding and declared tool, then uses the normal system dispatcher; it currently supports built-in system tools. Skill discovery does not itself grant broader tool authority. [Skill resolution][S26] [Skill invocation][S27]

The native tool path has deterministic capability checks for declared governed tools, with Mail and Files covered at this pin. That is not a general model-based Bash safety classifier. Likewise, the frontend's `auto_approve` setting automatically answers permission requests; its name does not establish a classifier that evaluates safety and user authorization. [Capability coverage][S28] [Automatic response][S29]

No native MCP client or MCP code-mode orchestration was established in this review. The inspected ACP adapter passes an empty MCP-server list, and native tool routing covers system, configured CLI, and stored Python tools. These mechanisms must not inherit capability claims from a wrapped external harness. [Tool routing][S30] [ACP session configuration][S31]

## What to borrow when building a harness

Begin with the failure you need to survive. If one local process owns a short-lived task, a clear loop and append-only transcript may be sufficient. When multiple submitters, workers, observers, and external executors participate, Sidekick's separation becomes useful: define a durable unit of admitted work, identify the current attempt, and decide what may be retried from evidence rather than from silence.

The most reusable ideas are the split between turn and attempt, declarative session effects, tool-intent gates before execution, and separate treatment of live projections and committed records. Each has a cost: more states, more transactions, more identifiers to correlate, and tests for the gaps between them. Adopt these mechanisms where your failure model needs them, rather than treating distributed orchestration as a prerequisite for the first working agent.

## Source reading path

Read [routing][S3] first to choose the native or external path. Follow [the native iteration boundary][S1] and [model/tool handling][S8], then read [the reducer][S4] and [dispatch][S13] to see what a message actually does. Move to [claiming][S2], [settlement][S11], and [intent recovery][S16] for failures. Finish with [crash-window tests][S18] and [ownership tests][S20], which expose the temporal contracts more clearly than a happy-path diagram alone.

[S1]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L397-L513
[S2]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L900-L1027
[S3]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_routing.py#L25-L44
[S4]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_reducer.py#L31-L267
[S5]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_effect_executor.py#L26-L112
[S6]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_executor.py#L238-L393
[S7]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L693-L818
[S8]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L550-L641
[S9]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L675-L694
[S10]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_intents.py#L750-L808
[S11]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L1386-L1539
[S12]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/settlement_finish.py#L193-L232
[S13]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_dispatch.py#L29-L219
[S14]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_lifecycle.py#L67-L205
[S15]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L699-L774
[S16]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L475-L651
[S17]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L290-L299
[S18]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/tests/integration/test_turn_contract_intents.py#L1000-L1089
[S19]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_sweeper.py#L588-L708
[S20]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/tests/integration/test_turn_record.py#L399-L532
[S21]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/context_pipeline/factory.py#L34-L117
[S22]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/dependencies.py#L128-L187
[S23]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/routers/conversations.py#L2010-L2045
[S24]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_executor.py#L46-L126
[S25]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/panel_session_service.py#L433-L508
[S26]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/skill_service.py#L606-L737
[S27]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/skill_executor.py#L349-L412
[S28]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/capability_map.py#L348-L353
[S29]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/frontend/stores/conversation-view/reducer.ts#L1456-L1469
[S30]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/unified_executor.py#L88-L127
[S31]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/outpost/src/agent_runtime/adapters/acp/adapter.py#L213-L242
