# Event-driven execution

An agent can spend seconds generating a response, minutes waiting for a tool, and hours waiting for a human. If its work must survive a worker disappearing, a browser disconnecting, or several participants acting at once, execution needs an identity and lifecycle beyond one function call.

**Event-driven execution** means that incoming facts or commands cause state transitions that make further work eligible. Sidekick is the main example here: its orchestration uses durable turns, worker attempts, message reducers, and tool workflows. Its native worker still runs an iterative model/tool loop. The useful distinction is where continuation and ownership are decided, not whether the implementation contains a loop. [Iteration boundary][s-loop], [attempt ownership][s-attempt].

## Events, commands, and effects

These concepts describe different responsibilities, even when a protocol transports them in similar envelopes:

| Concept | Example | Question it answers |
| --- | --- | --- |
| Command | “Start this admitted turn” or “cancel this turn” | What is being requested? It may be refused. |
| Event | “The tool produced a result” or “the user answered” | What happened? What may now become eligible? |
| State | A turn is awaiting input and has no running attempt | What does the system currently consider true? |
| Effect | Persist a message, broadcast progress, submit a workflow | What input/output work should follow this decision? |

Sidekick's session reducer accepts messages, commands, and signals and returns new state plus effects. A separate executor handles persistence and broadcasting. That makes the decision testable apart from database and network operations, but it does not make every effect durable or atomic. [Reducer][s-reducer], [effect executor][s-effects].

**Event sourcing** is a stronger storage choice: an authoritative sequence of events reconstructs state. Merely publishing events does not establish it. Sidekick also relies on turn, attempt, and tool-intent rows; its session stream is not a complete authoritative execution history. Likewise, token streaming is observability unless the events also drive execution decisions. [Dispatch][s-dispatch], [settlement][s-settle].

## Two levels of continuation in Sidekick

![Sidekick's native architecture: durable turn orchestration, model/tool iteration, and session reducer effects](../guide/assets/sidekick-architecture.svg)

At the **inner level**, the native loop rebuilds context, streams a model response, dispatches tools, records their results, and repeats. At the **outer level**, admission and ownership determine which worker may advance the turn; waiting, recovery, and settlement determine what happens after an attempt ends. The reducer translates messages into session effects. These mechanisms cooperate rather than replace one another. [Native iteration][s-loop], [tool dispatch][s-tools], [attempt ownership][s-attempt].

The [steering and cancellation](steering.md) topic examines when new user input becomes eligible for the next request. [Observability](observability.md) covers what clients can infer from lifecycle events.

Scope matters: Sidekick can also wrap an external harness through an Outpost. That path delegates model execution and has different retry behavior. The examples below concern the native cluster path. Sidekick's repository is private; its pinned citations require access. The [Sidekick chapter](../../snapshots/sidekick/463d653124e1/chapter.md) provides the full reviewed scope. [Execution routing][s-routing].

## Trace a wait and a worker failure

Consider a request to investigate an issue, ask the user which remedy to apply, and then perform the selected action. This is an illustrative trace, not a recorded benchmark.

1. **Admit and claim.** The request receives a pending turn. A worker claims an attempt on it. Conversation admission and current-attempt checks coordinate competing producers. [Admission][s-admit], [claiming][s-attempt].
2. **Reach a human wait.** A sole native `AskUserQuestion` call suspends the turn and ends the worker run. The turn remains awaiting input rather than completed. The answer returns the same turn to pending for a later attempt; one process need not wait for the human's entire absence. [Question suspension][s-question], [resume admission][s-resume].
3. **Record tool intent.** After resumption, a model requests an action. The loop commits the assistant request and tool intent before dispatch. The tool executor must claim that intent before acting. [Intent commit][s-intent], [execution gate][s-gate].
4. **Lose the worker.** Suppose an action starts but its result never reaches the transcript. A replacement attempt may take ownership of the turn. Ownership does not prove the action failed or stop a remote effect already underway. [Attempt ownership][s-attempt].
5. **Recover from evidence.** A stored outcome is redelivered. An unclaimed intent can execute. A running call waits for an existing result, then reruns only if both stored and current replay policy permit it; otherwise recovery reports an interrupted, uncertain outcome. The next model request receives that result. [Recovery][s-recovery], [policy intersection][s-policy].

This separates **retrying the worker** from **repeating the action**. A replay-safe operation may execute more than once. An uncertain mutation needs reconciliation, an externally enforced idempotency key, or an explicit unknown result—not an assumption that silence means nothing happened. See [persistence and recovery](persistence.md).

Events can also create new work without a crash: Sidekick panel completion updates durable grants, and the last completed or failed statement can make a moderator wake eligible. The important feature is that completion changes scheduling eligibility, not simply that a notification was emitted. [Panel completion][s-panel].

## Compare the four decompositions

These designs overlap. The table compares their inspected architectural centers, not exclusive categories or measured rankings.

| Harness | Where continuation is organized | What this makes explicit | Boundary to keep in mind |
| --- | --- | --- | --- |
| Codex | Session turns and repeated sampling steps with captured execution context | Which configuration and tool context a step uses | Session tasks and events do not alone imply distributed turn recovery. [Source][c-loop] |
| pi | A reusable loop, callbacks, steering/follow-up queues, and a surrounding coding session | Model/tool ordering and application hooks | Coding-session persistence differs from the separate experimental durable runtime. [Loop][p-loop], [durable tool recovery][p-durable] |
| Goose | Ordered operations inspect loaded state; the first applicable operation emits effects, then the machine reloads and scans again | State-dependent continuation and effect application | A state machine is not automatically a distributed workflow scheduler. [Source][g-loop] |
| Sidekick | Durable turn/attempt orchestration around a native loop, plus separately dispatched tool workflows | Admission, ownership, waiting, and uncertain-result recovery | Durable rows do not guarantee exactly-once external effects or atomic notifications. [Claiming][s-attempt], [recovery][s-recovery] |

The tradeoff for a builder follows from those boundaries. Pi's reusable loop gives a compact place to understand model/tool ordering, while the application must choose its durable recovery contract. Codex's captured sampling context makes a step's settings explicit, while tracing a complete request requires following its session and execution layers. Goose makes the next action depend on loaded state, while adding an operation requires understanding its position in the ordered scan. Sidekick makes cross-worker ownership explicit, while adding storage, dispatch, and recovery protocols around the loop. These are design implications of the cited structures, not measured complexity or performance scores.

The useful question is not “Which one has events?” Ask which facts determine the next action, where those facts live, and what a replacement process can safely infer.

## Costs that move into the harness

The following are design implications, not measured performance results.

- **Ordering:** independent producers can finish out of order. Correlation IDs associate results with requests; sequence allocation orders presentation. Neither establishes that dependent effects ran in the required order. Sidekick returns a tool batch in original call order after collecting correlated results. [Tool collection][s-tools].
- **Duplicate delivery:** a workflow retry can encounter work already claimed or completed. Stable identities and conditional claims must distinguish redelivery from a new request. A boolean “busy” flag is insufficient when its owner can die. [Attempt claiming][s-attempt], [intent gate][s-gate].
- **Fencing:** the current attempt identifies the producer permitted to change durable state. Rejecting an obsolete worker's settlement does not revoke authority already exercised against an outside service. [Settlement][s-settle].
- **Consistency:** database commits and broadcasts have different boundaries. Sidekick's ordinary dispatch can broadcast after a flush but before the caller commits; deferred acknowledgements and settled-turn projections use stronger boundaries. Clients need a recovery path to authoritative state. [Dispatch][s-dispatch], [completion projection][s-completion].
- **Latency:** workflow dispatch, coordination, and additional commits introduce waits. They can enable overlap and release a run during human input, but do not inherently make a single model/tool exchange faster.
- **Operations:** workers, database state, result channels, timeouts, and recovery policies create more failure modes to monitor. Post-commit hooks can still fail after settlement; the inspected sweeper explicitly logs lost completion work. [Post-commit limits][s-finish].

## When this model earns its complexity

Choose durable orchestration when accepted work must survive process loss, waits outlive individual runs, several workers share responsibility, or completion must unlock another participant's work. Make admission, ownership, and retry policy explicit before distributing execution.

For a small agent owned by one process, start with an understandable loop, tool-call identities, and cancellation. A pure reducer can improve testability without adding a broker. Add durable attempts when you can name a failure they must survive; add cross-worker scheduling when it solves an actual execution or coordination need.

## Builder exercise: expose the crash windows

Build a tiny simulated runner with a scripted model, a read tool, and a mutation tool that increments an external counter. Give the turn, attempt, and tool intent distinct IDs. No paid model or real external service is needed.

Inject failure before tool claim, after the counter changes, after its result is stored, and before completion is broadcast. Redeliver the same work twice. Then let an obsolete attempt finish after a replacement has claimed ownership.

Assert separate properties: one current owner, no stale settlement, stored results redelivered without another effect, uncertain mutations surfaced honestly, and clients able to recover completion from durable state. Also suspend for a human answer and resume the same turn under a new attempt. Sidekick's inspected crash-window tests provide a concrete reference; they were reviewed as source, not executed for this guide. [Recovery tests][s-tests].

[c-loop]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L166-L570
[p-loop]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L159-L321
[p-durable]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/src/harness/tool.ts#L122-L210
[g-loop]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/machine.rs#L118-L292
[s-loop]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L397-L513
[s-attempt]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L900-L1027
[s-reducer]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_reducer.py#L31-L101
[s-effects]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_effect_executor.py#L26-L112
[s-dispatch]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_dispatch.py#L171-L219
[s-settle]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L1386-L1539
[s-tools]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_executor.py#L238-L393
[s-routing]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_routing.py#L25-L44
[s-admit]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L693-L818
[s-question]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L699-L774
[s-resume]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_admission.py#L54-L61
[s-intent]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L675-L694
[s-gate]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_intents.py#L750-L808
[s-recovery]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L475-L651
[s-policy]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L290-L299
[s-panel]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/panel_session_service.py#L433-L508
[s-completion]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_lifecycle.py#L170-L205
[s-finish]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_sweeper.py#L627-L708
[s-tests]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/tests/integration/test_turn_contract_intents.py#L1000-L1089
