# Inside the Harness

A source-grounded guide to agent harnesses. Reviewed 2026-10-19.

# The harness around the model

A model can propose an action. An agent harness decides what that proposal means, whether it may run, what state to save, and when to ask the model again.

Suppose a developer asks: “Fix the parser bug and run its tests.” The model might request a file read. Your program executes the read and sends the result back. The model then requests an edit and a test command. Those requests are data until the harness dispatches them to real code.

The small loop is easy to recognize. Most of the engineering lives around it.

## One task, several model requests

![Agent loop: user input leads to context preparation and a model response. Tool calls are validated and authorized; permitted tools run, and their outcomes return to context. Invalid or denied calls also become results. Without tool calls, the harness checks pending input and stop rules, then continues or finishes.](guide/assets/agent-loop.svg)

This is a teaching abstraction, not a claim that every implementation uses this exact control structure. Codex organizes work around session tasks and sampling steps; pi exposes a reusable loop with lifecycle hooks; Goose composes ordered state-machine operations; Sidekick coordinates a native loop or external runtime through events, durable turns, and workflow workers. Their chapters show the actual implementations.

Use these terms throughout the guide:

| Term | Meaning here |
| --- | --- |
| Model request | One request to a model provider, possibly streamed. |
| Tool call | A model-produced name, arguments, and identifier that the harness resolves to executable behavior. |
| Tool result | The outcome associated with that identifier, including failure or denial. |
| Run | Work initiated by user input, potentially containing many model requests and tool calls. |
| Session | Conversation state and metadata that can outlive a run. |
| Context | The portion and representation of state sent in the next model request. |
| Harness | The runtime that coordinates model requests, tools, state, policies, and external clients. |

“Turn” is deliberately not a universal unit here. Each project uses it differently. Compare lifecycle boundaries before comparing metrics such as turns per task.

## Walk through a repair

This trace is illustrative. It was not executed as a benchmark.

| Step | Model or user behavior | Harness responsibility |
| --- | --- | --- |
| 1 | User asks for a parser fix. | Select instructions, tools, model, and conversation context. |
| 2 | Model requests `read_file("parser.ts")`. | Resolve the tool, validate its arguments, check access, execute, and associate its result with the call ID. |
| 3 | Model requests an edit based on the file contents. | Check the edit's preconditions and execute the mutation under the configured policy. |
| 4 | Model requests a test command. | Manage a process, collect bounded output, propagate cancellation, and report success or failure accurately. |
| 5 | Test fails and the model proposes a second edit. | Continue with the observed failure in context. A failed command need not be a failed run. |
| 6 | Tests pass and the model writes an explanation. | Apply stop rules, account for queued input, finalize state, and notify clients. |

The harness does not know that the fix is correct just because the model says it is finished. The test result is evidence; the model's conclusion is an interpretation of that evidence. Evaluation of task quality is separate from the runtime's ability to execute a valid loop.

## Five boundaries worth learning

### Proposed action to executed action

A schema describes what arguments are valid. It does not grant permission. An approval decision authorizes an action under a policy. A sandbox limits what the process can actually access. These are three separate mechanisms.

For example, a shell command may be valid JSON, approved by a user, and still fail because the operating system denies access to a path. Conversely, a custom extension can bypass an application-level check if it holds direct host access. Always locate the enforcement point.

### Live events to saved state

A UI can display a tool result before the persistence layer has committed it. A process crash in that gap may leave the UI and resumed conversation disagreeing. Saving a conversation also does not prove that an interrupted external action is safe to repeat.

For an email send, the process might crash after delivery but before recording success. Replaying the tool may send a second email. Idempotency means that repeating an operation with the same identity does not repeat its effect; it needs support at the effect boundary, not just a transcript file.

### Full history to model context

A session can contain more information than fits in the next request. Compaction replaces some history with a shorter representation; truncation simply removes or bounds content. Both influence what the model knows. They must preserve enough tool-call/result structure for the provider to accept the request.

The saved original history, the current model context, and the user-visible conversation can therefore be different views. Ask which one a resume, fork, or compaction operation updates.

### Concurrent work to ordered history

Two reads can finish in the opposite order from their calls. A UI may want to display each result immediately, while the transcript may need a deterministic order. Two edits to the same file introduce another problem: they may invalidate one another's assumptions.

Concurrency is a scheduling decision. It requires reasoning about effects, not just wrapping tool calls in `Promise.all` or its equivalent.

### One run to many participants

Steering adds new input while work is active. Follow-up queues defer input until a later boundary. Cancellation requests that active work stop. Multi-agent delegation creates additional agent runs. Multi-player collaboration lets multiple humans or clients participate in shared work. These solve different problems.

A server accepting two connections is a useful primitive for multi-player support. It does not establish user identity, ownership of approvals, conflict resolution, or a shared editing experience. The advanced chapter evaluates those separately.

## Read the implementations in a useful order

Start with [pi](#pi-a-small-agent-loop-inside-a-configurable-coding-application) to study a clearly exposed loop and its hooks. Then read [Goose](#goose-a-persisted-ordered-operation-pipeline) to see ordered operations and effects express control flow. Read [Codex](#codex-a-turn-engine-with-explicit-execution-boundaries) for task lifecycles, tool scheduling, policy enforcement, and client coordination. Read [Sidekick](#sidekick-event-driven-orchestration-around-a-durable-agent-loop) to see event-driven orchestration across a shared service, workers, and external runtimes. This is an educational reading order, not a product ranking.

Then use the [comparison](#four-ways-to-organize-the-same-feedback-loop) and [advanced topics](#advanced-topics) to follow one concern through all four. The [builder's lab](#build-a-harness-by-making-its-contracts-explicit) turns the comparison into design exercises.


---

# Four ways to organize the same feedback loop

All four harnesses turn model output into tool execution and feed the results back. The useful comparison is where each places decisions about state, authority, concurrency, and continuation. Sidekick also wraps external harnesses; its native execution path is the comparison target here.

The short descriptions below are an interpretation of the inspected architecture, not a claim about which product is best. See the [methodology](#evidence-and-scope) for pinned revisions and scope. The website's comparison explorer exposes all 15 dimensions with per-cell qualifications and direct source links.

## The architectural center

| Harness | Center of the design | What a builder can learn |
| --- | --- | --- |
| Codex | Session tasks contain repeated sampling steps; tools execute against a captured step context through routing and policy machinery. | How to preserve coherent execution settings while tools, clients, and context change. |
| pi | A reusable agent loop exposes hooks; the coding session adds application behavior around it. | How to separate provider normalization, loop scheduling, and product-level state. |
| Goose | An ordered set of operations inspects session state; the first applicable operation emits effects, then the machine scans again. | How to make continuation decisions reconstructible from persisted state. |
| Sidekick | Durable turns and worker attempts coordinate an iterative model/tool loop through workflows, message reducers, and recorded tool intents. | How to distinguish admitted work from a worker's lifetime and recover without assuming an uncertain tool never ran. |

Sources: [Codex turn preparation][c-loop], [pi loop][p-loop], [Goose state machine][g-loop], [Sidekick native loop][s-loop] and [attempt ownership][s-attempt]. Sidekick's repository is private; its pinned source links require repository access.

These are different decompositions, not mutually exclusive strategies. Pi's coding application still has persistence. Codex still has a repeated sampling loop. Goose still needs a provider adapter and executable tools. Sidekick still iterates within its conversation workflow.

## Compare behavior, not feature counts

| Concern | Codex | pi | Goose | Sidekick |
| --- | --- | --- | --- | --- |
| Concurrent tools | Parallel-capable calls share an execution lock; exclusive calls take a write lock. Results drain into history in call order. | Parallel by default after ordered preflight; any sequential tool can serialize its batch. Results are recorded in call order. | An ordered operation dispatches tool work, then polls concurrent streams. Results are recorded as observed and associated by request ID. | Tool workflows may overlap; correlated results return in model call order. [Source][s-order] |
| Context management | Client-managed or provider-supported summarization, optional background compaction, and a distinct token-budget rollover path. | Compaction entries summarize history while preserving raw JSONL records and a recent projected tail. | Compaction changes model visibility and adds summary records while retaining conversation history. | Optional trimming replaces old large tool outputs in request context; native full-history summary compaction was not established. [Source][s-context] |
| Steering | Active-turn input admission, expected-turn checks, and some feature-controlled sampling preemption. | Input arriving during a response/tool batch is consumed after that batch; additional polls occur before requests. Follow-up waits for natural quiescence. | Queued guidance consumed between model/tool phases; shared run IDs support cross-connection steering. | Durable steers enter at the main loop's next iteration; answered settlement admits waiting follow-ups as a new turn. [Placement][s-loop], [follow-up][s-followup] |
| Persistence | A storage interface distinguishes append contexts and explicit durability barriers. | Coding sessions use append-only JSONL trees. A separate experimental durable runtime has stronger recovery contracts. | SQLite session state and operation notes participate directly in state-machine decisions. | Turn, attempt, and tool-intent records distinguish committed outcomes from uncertain execution. [Source][s-recovery] |
| Multi-player foundations | Multiple subscribed connections and attach/history ordering. | Replicated view primitives in Chord/durable; coding RPC is a separate subprocess interface. | Shared active-run arbitration and steering across ACP connections. | Shared conversations have view/edit/manage access; sending requires edit and admission is serialized. [Source][s-sharing] |
| Multi-agent support | Child-thread lifecycle tools and control rules. | Optional coding-agent example; separate durable child-conversation examples. | Foreground child sessions with progress, result delivery, and approval forwarding. | Owned child turns and peer panels use distinct coordination rules. [Delegation][s-child], [panels][s-panel] |
| MCP and code mode | Deferred tool search and V8 orchestration; nested calls reenter the tool runtime. | Built-in MCP and selectable QuickJS code mode; nested calls retain tool hooks. | MCP-based extension layer and optional Boa code mode; nested callbacks have a different approval boundary from direct calls. | Neither native MCP integration nor MCP code mode was established; external adapters are a separate scope. [Review](#context-collaboration-and-tool-authority) |

Each row compresses important details. Follow the evidence in [Codex](#evidence-matrix), [pi](#pi-a-small-agent-loop-inside-a-configurable-coding-application), [Goose](#goose-a-persisted-ordered-operation-pipeline), and [Sidekick](#sidekick-event-driven-orchestration-around-a-durable-agent-loop), or expand the corresponding website cells. A “core” or “optional” label is a scope marker, not a quality score.

## Follow one problem across the designs

### A tool takes longer than its neighbor

The model requests reads A and B; B finishes first. Codex and pi can report progress while retaining call order in the final transcript. Goose records observed results with IDs. Sidekick submits separate tool workflows, collects results by correlation ID, and returns the batch in call order. These approaches can represent the exchange correctly. The tradeoff is between a predictable source-order transcript and immediate append of the observed outcome; client code must understand which contract it consumes. [Codex ordered drain][c-order], [pi scheduling][p-order], [Goose execution][g-order], [Sidekick collection][s-order].

Now replace read B with “test the file after edit A.” These are dependent actions. None of these scheduling descriptions proves the harness infers that dependency from natural-language tool arguments. Put dependent work in separate model steps or explicitly sequence it in a supported orchestration program.

### The user changes their mind during execution

“Steer” can mean “queue this for the next safe boundary,” not “stop the current tool.” Pi explicitly finishes the current batch before consuming steering. Codex has active-turn admission and feature-dependent preemption paths. Goose consumes queued input through a state-machine operation after the relevant model/tool phase. A client should distinguish queued, admitted, executed, and cancelled states. [Pi loop][p-loop], [Codex turn processing][c-loop], [Goose steering][g-steer].

Sidekick makes the queue itself durable. Its main bound-agent loop places steers before rebuilding context, commits them, then broadcasts their placement. Follow-up admission occurs after an answered settlement. Those are separate boundaries from cancellation and from child or panel execution. [Sidekick iteration boundary][s-loop], [next-turn admission][s-followup].

### A conversation is too large

Codex, pi, and Goose can continue from summary-based representations, but they store and adopt them differently. Codex has multiple compaction/rollover paths. Pi projects an append-only session tree using compaction checkpoints. Goose uses conversation visibility plus inserted summary records. Sidekick's inspected native pipeline instead establishes optional trimming of older, large tool outputs; this is not evidence of equivalent full-history summary compaction. [Sidekick context factory][s-context]. “Compaction supported” therefore says little about retained raw history, replay, adoption races, or context reconstruction. The [advanced chapter](#advanced-topics) compares those contracts.

### A worker disappears while a tool is running

Sidekick separates the user's turn from the worker attempt processing it. A replacement attempt can take ownership, then inspect recorded tool intents before another model request. A settled intent supplies its saved result. An uncertain running call may rerun only when stored and current replay policy allow it; an unsafe call receives an interrupted-result explanation instead. Ownership prevents a superseded worker from settling the turn, but cannot undo an external effect. [Attempt ownership][s-attempt], [intent recovery][s-recovery].

This illustrates event-driven orchestration: admission, completion, and recovery change durable state and determine which work may execute next. Panel completion provides another example: the last completed or failed grant in a batch makes a moderator wake eligible. The native model/tool continuation remains an ordinary loop. The session reducer is not a complete event-sourced history, and this comparison does not establish Sidekick as the only event-driven harness. [Panel completion][s-panel], [native iteration][s-loop], [dispatch and storage boundaries][s-dispatch].

The [event-driven execution topic](#event-driven-execution) follows Sidekick's orchestration boundaries and compares their costs with the other designs.

## What to borrow first

For a small custom agent, study pi's explicit loop and hooks before adopting its surrounding application. For a runtime reconstructed from stored state, study Goose's operation/effect boundary. For a long-lived coding system with changing tools and multiple clients, study Codex's step snapshots, execution scheduling, and persistence barriers. For a shared service where work can outlive a worker, study Sidekick's turn/attempt distinction and tool-intent recovery.

These are educational suggestions grounded in the architectures, not deployment recommendations or measured rankings. Copy the invariant you need before copying the surrounding machinery. A script with one trusted tool may need only a loop and cancellation. A shared service with remote effects needs identities, policies, durable state, and recovery semantics even if its model loop stays small.

[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
[g-loop]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/machine.rs#L118-L292
[c-order]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L2568-L2695
[p-order]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L498-L650
[g-order]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_toolcalling.rs#L664-L1106
[g-steer]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_steer.rs#L46-L98
[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-order]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_executor.py#L238-L393
[s-context]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/context_pipeline/factory.py#L34-L117
[s-followup]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/message_queue.py#L603-L679
[s-recovery]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L475-L651
[s-sharing]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/routers/conversations.py#L2010-L2045
[s-child]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_executor.py#L46-L126
[s-panel]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/panel_session_service.py#L433-L508
[s-dispatch]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_dispatch.py#L171-L219


---

# Advanced topics

The basic loop asks a model what to do next. Advanced harness engineering asks what happens when the world changes while that loop is running: new user input arrives, a tool catalog refreshes, context fills up, a client disconnects, or a process crashes after an external action.

These topics compare the inspected snapshots. They separate implemented behavior from the design implications we draw from it. The [Codex](#codex-a-turn-engine-with-explicit-execution-boundaries), [pi](#pi-a-small-agent-loop-inside-a-configurable-coding-application), [Goose](#goose-a-persisted-ordered-operation-pipeline), and [Sidekick](#sidekick-event-driven-orchestration-around-a-durable-agent-loop) chapters contain the detailed evidence and reading paths. Sidekick's source repository is private; its citations require access.

## Explore the topics

Each topic has its own page. Read them in order to follow execution, state, coordination, tool integration, and recovery, or choose the boundary you are designing now.

- **[Event-driven execution](#event-driven-execution)** — Use Sidekick to trace durable work across workers and compare the tradeoffs with loops, session tasks, and operation-based state machines.

- **[Compaction](#compaction-is-a-state-transition)** — Understand when a summary replaces model context, what survives, and how to adopt it without losing new input.

- **[Steering & cancellation](#steering-follow-up-and-cancellation-are-separate-controls)** — Distinguish input admission, follow-up queues, and cancellation at model and tool boundaries.

- **[Multi-player](#multi-player-is-more-than-multi-agent)** — Explore shared observation, competing input, approval authority, and reconnection across humans and clients.

- **[Multi-agent delegation](#multi-agent-delegation-introduces-ownership)** — Define ownership, workspace isolation, cancellation, and result contracts when a parent delegates work.

- **[MCP](#mcp-separates-capability-providers-from-the-loop)** — Follow tool discovery, catalog changes, authorization, and connection lifetimes across server boundaries.

- **[Code mode for MCP](#code-mode-for-mcp-changes-the-orchestration-boundary)** — Compare model-written tool orchestration, nested-call policy, execution limits, and result shapes.

- **[Tool safety classification](#tool-safety-classification-risk-authorization-and-execution)** — Separate action risk from user authorization, and understand where model-based review fits into tool execution policy.

- **[Skills](#skills-load-procedures-when-the-task-needs-them)** — Follow discovery, selection, and loading of reusable instructions, including their context cost and trust boundaries.

- **[Next user prompt suggestions](#next-user-prompt-suggestions)** — Understand how a harness predicts a useful next message, presents it, and waits for the user to accept it.

- **[Persistence & recovery](#persistence-recovery-and-replay)** — Separate recorded history from durable effects and handle uncertainty after a crash.

- **[Observability](#observability-should-describe-these-boundaries)** — Trace the boundaries that explain latency, completion, and the meaning of tool timing.

## Put the ideas into practice

Use the [builder's lab](#build-a-harness-by-making-its-contracts-explicit) to exercise these boundaries with deterministic scenarios. The [design comparison](#four-ways-to-organize-the-same-feedback-loop) connects individual topics to each harness's overall design.


---

# 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-follow-up-and-cancellation-are-separate-controls) topic examines when new user input becomes eligible for the next request. [Observability](#observability-should-describe-these-boundaries) 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](#sidekick-event-driven-orchestration-around-a-durable-agent-loop) 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-recovery-and-replay).

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


---

# Compaction is a state transition

Suppose a model has just read a large log. The next request exceeds its context window. Deleting arbitrary old messages may discard the user's correction or leave a tool result without its call. Summarization can retain intent, but it changes the representation from which the agent continues.

Ask four questions: **what triggers compaction, what survives, when the result is adopted, and what remains available for replay?**

| Harness | Trigger and transformation | State consequence |
| --- | --- | --- |
| Codex | Thresholds and full-context limits; model/capability changes can matter. Client-managed and provider-supported remote summaries are separate paths. Token-budget rollover can occur without summarization. | Replacement history and context-window metadata affect reconstruction. Optional background summaries are adopted at defined checkpoints after compatibility checks. |
| pi | Estimated context use approaches the window minus reserved tokens; overflow recovery is another path. Summaries retain a recent tail and handle split turns. | Compaction entries project the next model context without erasing the raw append-only session history. |
| Goose | Token thresholds or context errors can trigger compaction; providers managing their own context are a separate case. | Older messages become hidden from the model; summary/continuation records are inserted. Storage updates visibility and summary records transactionally. |

Sources: [Codex context checks][c-context] and [background adoption][c-background], [pi compaction][p-context] and [session projection][p-projection], [Goose compaction operation][g-context] and [storage update][g-compaction-store].

**Design implication:** a background summary is computed against an older history. If the user changes the task while it is running, blindly replacing current context can lose that correction. Capture the input boundary, check compatibility when adopting, and preserve subsequent input. Do not confuse a summary successfully generated with a summary safely installed.

For an experienced builder, test compaction alongside tool-call/result pairing, model switches, user corrections, forks, cancellation, failed summaries, and overflow retries. A good summary of a static transcript does not test these transitions.

## Sidekick: trimming is not summary compaction

Sidekick’s native loop rebuilds request context from stored messages each iteration. Its optional pipeline strategy replaces old, large tool outputs with placeholders while preserving tool-result identifiers. That changes the request projection, not the saved history. The inspected factory does not establish full-history summary checkpoints or automatic summary-compaction recovery. Configuration fields naming a summarization model do not establish those behaviors. An external Outpost runtime may manage its own context. [Pipeline][s-context], [trimming strategy][s-trim] (private source).

[c-context]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/context_window.rs#L1-L130
[c-background]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/async_compaction.rs#L40-L214
[p-context]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/compaction/compaction.ts#L789-L922
[p-projection]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/session-manager.ts#L483-L589
[g-context]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_compaction.rs#L184-L295
[g-compaction-store]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/session/session_manager.rs#L2198-L2255

[s-context]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/context_pipeline/factory.py#L34-L117
[s-trim]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/context_pipeline/processors/limits.py#L13-L157


---

# Steering, follow-up, and cancellation are separate controls

Imagine a model requests three tools and the user types “Stop editing; just explain.” There are at least three possible contracts: cancel active work, let the current call finish but skip the rest, or finish the batch and apply the instruction to the next request. A control named “interrupt” does not tell you which one is implemented.

For input arriving during a response/tool batch, pi's core consumes steering after that batch completes; it also polls initially and after next-turn preparation when needed. Follow-up waits until work would otherwise stop. Codex admits input to an active turn with expected-turn guards and has feature-controlled paths that can preempt sampling/retry waits. Goose drains a session's steering queue between model/tool phases; its ACP interface can check an expected active run ID. [Pi scheduling boundaries][p-steer], [Codex start/steer][c-steer], [Goose steering][g-steer].

Cancellation propagates to cooperating work. It does not undo a successful file write or remote action. Nor does a queued steering message prove the model has seen it. A client should expose the distinction between receiving input and admitting it into the next model context.

**Design implication:** specify the admission boundary and test it under a slow model, slow tool, parallel batch, pending approval, and run completion. A stale run ID should not accidentally steer a later task.

## Sidekick: input placement is durable

Sidekick records queued input with its author and requested mode. At the next native main-loop iteration it checks cancellation, places steers, commits their message rows, and rebuilds context. Child, panel-peer, and deliberation loops do not drain that main-thread queue. After an answered turn, settlement can admit waiting follow-ups and unconsumed steers as a new turn; an unanswered settlement leaves them queued. Outpost adapters without steering support downgrade busy steering to follow-up. The arrival of an event and the admission of its input are separate steps. [Iteration boundary][s-steer], [next-turn admission][s-followup], [adapter capabilities][s-adapter] (private source).

[p-steer]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L159-L321
[c-steer]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/request_processors/turn_processor.rs#L1045-L1150
[g-steer]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_steer.rs#L46-L98

[s-steer]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L397-L488
[s-followup]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/message_queue.py#L603-L679
[s-adapter]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/message_queue.py#L721-L813


---

# Multi-player is more than multi-agent

Here **multi-player** means multiple humans or clients participating in shared work. **Multi-agent** means several model-driven workers. They can coexist, but adding workers does not solve shared-human authority.

Consider two clients attached to one session. Alice starts a change; Bob submits a conflicting instruction while Alice is approving a command. Which run receives Bob's input? Who may approve? Will both clients see the same event history? What happens if the approving client disconnects?

| Capability | Codex | pi | Goose |
| --- | --- | --- | --- |
| Shared observation | App-server tracks multiple subscribed connection IDs per thread and orders history attachment with notifications. | Chord and the experimental durable runtime provide snapshot/delta and conversation-view primitives. Coding-agent subprocess RPC is a separate surface. | The inspected ACP stream forwards updates to the prompting connection; generic fan-out was not established. |
| Input arbitration | Turn start can admit input to active work; explicit steering checks the expected turn. | The coding loop defines steering/follow-up queues. The adjacent replication libraries do not by themselves define a shared-human coding application. | A shared active-run registry rejects competing prompts and can route steering to the active owner. |
| Full multi-human workspace | Not established by these primitives. | Not established in the inspected coding-agent SDK/RPC. | Not established by these paths. |

Sources: [Codex subscriptions][c-clients] and [turn checks][c-steer], [pi RPC][p-rpc] and [durable views][p-views], [Goose shared registry][g-clients] and [notification forwarding][g-forward].

“Not established” is a scope limit, not an absence claim. Those three sets of code paths do not prove per-human identity, room membership, role-based approval, presence, collaborative file editing, or conflict resolution. A connection ID is not automatically a human identity.

**Design implication:** separate the session log from its audiences and writers. Define who can append input, control a run, grant permission, and receive tool output. Reconnection needs an ordering contract between the historical snapshot and live events; otherwise updates can be missed or duplicated.

## Sidekick: shared humans are an application boundary

Sidekick establishes a concrete shared-conversation surface. Access checks distinguish view, edit, and manage rights, including direct and group sharing. Sending requires edit access; submissions retain author identity and client-message IDs. Conversation locking decides whether input is placed or queued. These are application-level collaboration contracts beyond accepting multiple connections. [Access checks][s-access], [send admission][s-send] (private source).

Concurrent message writers obtain sequence numbers atomically. Reconnecting clients receive a state cursor and a queue high-water mark to reconcile snapshots with later updates. Persisted messages, transient streaming events, and authoritative turn rows still have different lifetimes. This supports shared observation and controlled writing; it does not establish collaborative file editing or one transactional event log. [Sequence allocation][s-seq], [queue reconciliation][s-queue], [state cursor][s-cursor].

[c-steer]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/request_processors/turn_processor.rs#L1045-L1150
[c-clients]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/thread_state.rs#L320-L687
[p-rpc]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/docs/rpc.md#L1-L35
[p-views]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/src/harness/view.ts#L77-L177
[g-clients]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/acp/server_factory.rs#L85-L233
[g-forward]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/acp/server.rs#L2103-L2241

[s-access]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/dependencies.py#L128-L187
[s-send]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/routers/conversations.py#L2010-L2045
[s-seq]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_dispatch.py#L92-L168
[s-queue]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/message_queue.py#L420-L448
[s-cursor]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/routers/conversations.py#L1278-L1285


---

# Multi-agent delegation introduces ownership

Codex exposes child-thread lifecycle controls, including spawning, messaging, waiting, and interruption. Pi's coding-agent supplies an optional example that launches child processes; its separate durable package demonstrates owned child conversations. Goose runs foreground subagents in separate persisted child sessions and returns their results and approval requests through the parent workflow. [Codex controls][c-agents], [pi example][p-agents] and [durable example][p-durable-agents], [Goose subagents][g-agents].

Separate conversation state does not prove separate filesystem access. Two agents can have independent histories and still overwrite the same file. The delegation contract should identify task ownership, workspace isolation, cancellation propagation, approval routing, budgets, and what the parent receives.

**Design implication:** delegate independent work with a bounded result contract. For shared mutations, provide explicit ownership or conflict handling rather than assuming a coordinator can repair every race afterward.

## Sidekick: owned child turns and peer panels

Native delegation admits an owned child turn only while its parent attempt still owns the running turn. The parent tool intent records the child identity in the same transaction. The child then has its own attempt, scoped context, intent recovery, cancellation checks, and settlement. A retry can reason about that existing child instead of blindly spawning another worker. [Child linkage][s-child], [child execution][s-execution] (private source).

Panels use a different topology: peers receive persisted permission to speak, called floor grants. When the last statement in a locked batch completes or fails, finalization produces a directive to wake the moderator. This is event-driven coordination with durable state determining which work becomes eligible next. It coexists with an explicit model/tool loop inside each native worker. [Panel completion][s-panel].

[c-agents]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/multi_agents_v2.rs#L1-L66
[p-agents]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/subagent/README.md#L1-L110
[p-durable-agents]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/test/examples/22-subagent-foreground.ts#L42-L82
[g-agents]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/subagent.rs#L57-L290

[s-child]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/tool_executor.py#L46-L126
[s-execution]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/workflows/agent_delegation_workflow.py#L803-L893
[s-panel]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/panel_session_service.py#L433-L508


---

# MCP separates capability providers from the loop

MCP provides a standard way to discover tools and call them through a server boundary. The harness still decides which tools become visible, when schemas refresh, how calls are authorized, how long they run, and what to do if a connection fails after an effect may have occurred.

Codex captures tool bindings and output-policy state for admitted MCP calls and can expose deferred tools through lexical search. Pi's built-in MCP integration handles configured connections and tool-list changes, adapting tools into the agent pipeline. Goose routes names through session-scoped extension leases. [Codex MCP handler][c-mcp] and [search][c-search], [pi MCP runtime][p-mcp], [Goose lease][g-mcp].

A tool catalog can change while work runs. If a model requested tool `lookup` under catalog A, refreshing to catalog B should not silently change what that already-admitted call means. Catalog lifetime and binding ownership are therefore correctness concerns, not only caching details.

Read-only annotations can guide scheduling or retry policy. They are metadata supplied by an integration, not proof that an arbitrary remote server has no effects. Similarly, an MCP connection standard does not imply that all servers are inside the same sandbox.

## Sidekick: hosting an agent does not establish native MCP

The inspected native executor routes registered system tools, configured CLI tools, and stored Python tools. No native MCP discovery/client path was established in the backend and Outpost review. The ACP adapter supplies an empty MCP-server list when creating or loading a session. ACP carries an agent session; MCP exposes capabilities such as tools. An independently configured wrapped harness may use MCP, but that is not evidence of a Sidekick-owned MCP client. [Native routing][s-routing], [ACP configuration][s-acp] (private source).

[c-mcp]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/mcp.rs#L135-L325
[c-search]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/tool_search.rs#L66-L358
[p-mcp]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/runtime.ts#L278-L446
[g-mcp]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/extension_manager/lease.rs#L573-L729

[s-routing]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/unified_executor.py#L88-L127
[s-acp]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/outpost/src/agent_runtime/adapters/acp/adapter.py#L213-L242


---

# Code mode for MCP changes the orchestration boundary

With direct tool calling, the model may need a request for “search,” another for “fetch each result,” and another to combine them. With code mode, the model writes a small program that performs those calls and returns the useful subset.

This conceptual example is pseudocode; each harness exposes different names and result shapes:

```text
matches = await tools.search(query)
details = await concurrently(fetch(match) for match in matches)
emit only the fields needed for the answer
```

This can reduce model round trips and intermediate context. It also moves control flow into model-written code. You now need execution limits, cancellation, observable nested calls, explicit result/error shapes, and a clear policy boundary around the capabilities exposed to that code.

| Harness | Runtime and discovery | Nested-call boundary |
| --- | --- | --- |
| Codex | Fresh V8 cells, controlled globals, explicit cross-cell storage, yield/wait handling; deferred search is scoped to the originating step's tool catalog. | Nested calls reenter `ToolCallRuntime` with that step context. This is distinct from a persistent Node REPL. |
| pi | Selectable QuickJS code mode, deferred tool discovery, explicit state handling, MCP structured results. | `ctx.executeTool` preserves tool validation and extension hooks. Structured MCP errors may resolve with `isError`, so a script must inspect the result. |
| Goose | Feature-gated, default-disabled Boa extension with discovery tools and `tools.call`; some tools remain directly visible. | The outer code tool follows normal approval/execution. The nested callback calls an `ExtensionLease` directly; it does not independently rerun the outer approval operation or pre-tool hooks. |

Sources: [Codex runtime][c-code-runtime] and [nested dispatch][c-code], [pi nested execution][p-code], [Goose code execution][g-code] and [registration][g-code-gate].

The Goose distinction is an architectural scope finding, not a claim that nested tools have no checks anywhere. Its catalog restriction, lease routing, server-side behavior, cancellation, and outer policy still matter. Conversely, routing through a hook in Codex or pi only enforces the policy actually configured there.

Result shapes also differ. Goose's nested callback returns assistant-visible text, parsed as JSON where possible; it excludes arbitrary structured content, images, and hidden text from that adapter. Pi can expose declared MCP structured output to scripts. A program that assumes every tool returns the same JSON structure can fail even when both harnesses support the same server. [Goose result adapter][g-code-result], [pi MCP tool adapter][p-mcp-result].

**Design implication:** test a denied tool both directly and inside code mode. Do not assume an interpreter with no filesystem or network APIs is harmless: its injected tools can carry filesystem, network, and credential authority. Review the capability boundary as carefully as the JavaScript runtime.

## Sidekick: configured Python tools are a different mechanism

Sidekick can execute configured Python tool implementations and dispatch a skill’s declared built-in tool through `UseSkillTool`. Neither inspected mechanism is a model-authored program that discovers and composes MCP tools. Native MCP code mode was not established in this review; a wrapped external harness may bring its own facilities. Distinguish execution of an installed implementation from generation of an orchestration program during a run. [Configured Python execution][s-python], [skill dispatch][s-skill] (private source).

[c-code-runtime]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/code-mode-runtime/src/runtime/mod.rs#L73-L260
[c-code]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/code_mode/mod.rs#L350-L474
[p-code]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/codemode/execute.ts#L428-L557
[g-code]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/code_execution.rs#L22-L335
[g-code-gate]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/mod.rs#L154-L172
[g-code-result]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/code_execution.rs#L240-L381
[p-mcp-result]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/tools.ts#L312-L354

[s-python]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/tool_execution_service.py#L325-L369
[s-skill]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/skill_executor.py#L349-L412


---

# Tool safety classification: risk, authorization, and execution

A model requests a Bash command. Before executing it, the harness can ask a second model to assess its effects and whether the user authorized them. This is **model-based tool review**. It can reduce approval interruptions, but “auto mode” is not a standardized contract: one implementation may invoke a reviewer, another may simply permit execution.

Suppose the user asks for a parser explanation and the agent proposes `touch /tmp/unrelated-note.txt`. Creating that empty file is low risk, but unrelated to the request. Conversely, deleting a production database may be explicitly requested and still prohibited by policy. “Safe,” “authorized,” and “allowed” answer different questions.

## Four decisions that should stay separate

| Question | What it establishes |
| --- | --- |
| What could this action do? | **Risk:** data exposure, state changes, reversibility, and scope of possible damage. |
| Did the user request these effects? | **Authorization:** evidence covering the target, payload, destination, and side effects. |
| Should this action proceed? | **Policy decision:** how risk, authorization, and explicit rules combine. |
| What can the process actually access? | **Sandbox enforcement:** operating-system or executor restrictions on files, processes, and network access. |

An allow decision is not proof that all four answers were favorable. Codex's bundled reviewer policy, for example, generally allows low- and medium-risk actions regardless of authorization, with explicit-policy and malicious-injection exceptions. Its high-risk threshold depends on authorization. Deployments can supply policy configuration, so read the effective rules rather than interpreting `allow` as “the user explicitly approved this.” [Bundled decision policy][c-policy].

## Model review, hooks, and permission judges

These findings describe the pinned source snapshots used throughout this guide, not every hosted product or provider integration.

| Harness | Inspected mechanism | What the classification means |
| --- | --- | --- |
| Codex | Guardian review uses a model session and a structured assessment containing risk, authorization, outcome, and rationale. | Contextual review of a particular action; low-risk output can contain only an allow outcome, with parser defaults filling the remaining fields. [Review session][c-session], [assessment contract][c-schema]. |
| pi | The core provides a blocking `beforeToolCall` hook; the coding agent exposes `tool_call` extension handlers. Its permission-gate example uses regular expressions and a confirmation UI. | An application can implement model review here. The inspected core path and example do not supply a built-in risk/authorization classifier. [Execution boundary][p-loop], [extension dispatch][p-extension], [example][p-example]. |
| Goose | `SmartApprove` checks user permissions and read-only annotations, then sends eligible requests to an LLM permission judge. `Auto` allows at the permission-inspector layer. | The judge identifies read-only request IDs. It receives request names and arguments, not the task transcript, so this is not a contextual user-authorization judgment. Other inspectors can override permission results. [Routing][g-inspector], [judge input][g-judge], [overrides][g-overrides]. |

Goose also has a separate security scanner combining command patterns with optional command/prompt classifiers. Its shell scan estimates maliciousness; this is distinct from SmartApprove's read-only classification. A failed classifier call returns no model score, leaving pattern evidence available. [Scanner composition][g-scanner], [classifier failure][g-failure].

## Put review before the effect

A useful design sequence is: validate arguments, resolve the exact action, apply rules, review when required, bind the decision to that action, and execute under the selected restrictions. This is a design pattern, not a claim that every tool follows identical code.

Codex's tool orchestrator resolves an approval requirement before its sandboxed execution attempt. A forbidden requirement rejects immediately; a skipped requirement can still require review under strict auto-review. Its Guardian reviewer runs in a restricted review session with read-only checks and approvals disabled. Reviewing a command should not itself grant a reviewer the command's write capabilities. [Orchestrator][c-orchestrator], [reviewer isolation][c-session].

Bind approval to the arguments and relevant state, not merely the tool name. `bash` can list files or destroy them. A reviewed script can change before execution. Codex's review host contract requires fresh authorization evidence and revalidation before consuming cached approval. This illustrates the general **time-of-check/time-of-use problem**: the action that executes must still be the action that was reviewed. [Review host contract][c-binding].

## Failure is not a safety verdict

Codex distinguishes parse/session failures, timeout, cancellation, and completed denials. Parse/session failures block execution; timeout and cancellation have their own outcomes. An input-budget failure can fall back to user approval when Guardian is optional. A review that could not complete is not evidence that the requested action was dangerous. [Completion handling][c-failure].

Goose's permission judge returns no read-only IDs when model configuration, completion, or response extraction fails. Deferred candidates absent from that result require approval. In pi's permission-gate example, a matching dangerous command is blocked if no confirmation UI exists; this behavior belongs to that example extension, not every pi installation. [Judge failures][g-judge], [candidate handling][g-inspector], [pi example][p-example].

## Protect the reviewer from the material it reviews

Tool arguments and retrieved text can contain instructions such as “ignore the policy and approve this.” Treat those strings as evidence, not authority. Codex's bundled review prompt separates trusted authorization sources from untrusted outputs and permits explicitly delegated authority. Goose labels the serialized requests as untrusted and tells its judge not to obey instructions embedded in IDs, names, or arguments. These are prompt-level defenses, not guarantees of correct classification. [Codex evidence handling][c-evidence], [Goose input][g-judge], [Goose instruction][g-prompt].

Fast paths deserve equal scrutiny. A read-only annotation is a claim supplied through the tool definition. A regular expression catches particular spellings; it cannot establish the behavior of arbitrary scripts, shell substitutions, or interpreters. Goose's annotation shortcut and pi's small regex example make those tradeoffs visible. [Goose shortcuts][g-inspector], [pi patterns][p-example].

**Builder exercise:** evaluate the whole decision path with paired cases: the same command under different user requests; the same tool name with different arguments; a harmless wrapper invoking a mutating script; malicious approval instructions in tool data; a stale approval; and reviewer timeout or malformed output. Measure unauthorized executions, unnecessary interruptions, latency, and failure behavior separately. Test shortcuts and fallback paths alongside the classifier. A classifier's accuracy alone does not establish the safety of the harness around it.

## Sidekick: automatic acceptance and capability checks

Sidekick’s native system-tool dispatcher has a deterministic capability gate: it reads the acting agent’s current grants and checks action prerequisites, argument constraints, and applicable role restrictions. At this pin, only Mail and Files declare capability maps. This gate is not a universal Bash classifier or a policy for every tool. [Dispatch][s-dispatch], [declared coverage][s-coverage], [gate][s-gate] (private source).

Separately, the frontend’s `auto_approve` mode turns incoming permission requests into automatic acceptance. That is different from asking a model whether the action is safe and authorized. Outpost adapters map settings onto their wrapped runtimes, so evaluate both the adapter configuration and the client’s response behavior. No Sidekick-owned model safety classifier was established in these inspected paths. [Response selection][s-auto], [acceptance submission][s-accept], [adapter profiles][s-profiles].

[c-policy]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/prompts/templates/guardian/policy_template.md#L40-L78
[c-session]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/guardian/review.rs#L168-L228
[c-schema]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/guardian-reviewer/src/assessment.rs#L11-L115
[c-orchestrator]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/orchestrator.rs#L164-L235
[c-binding]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/guardian-reviewer/src/review.rs#L30-L69
[c-failure]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/guardian-reviewer/src/completion.rs#L83-L158
[c-evidence]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/prompts/templates/guardian/policy_template.md#L1-L25
[p-loop]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L724-L775
[p-extension]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/extensions/runner.ts#L1249-L1267
[p-example]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/permission-gate.ts#L1-L34
[g-inspector]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/permission/permission_inspector.rs#L144-L270
[g-judge]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/permission/permission_judge.rs#L91-L184
[g-overrides]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/permission/permission_inspector.rs#L87-L130
[g-scanner]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/security/scanner.rs#L132-L218
[g-failure]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/security/scanner.rs#L290-L307
[g-prompt]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/prompts/permission_judge.md#L1-L1

[s-dispatch]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/system_tool_registry.py#L764-L817
[s-coverage]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/capability_map.py#L348-L353
[s-gate]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/capability_gate.py#L603-L738
[s-auto]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/frontend/stores/conversation-view/reducer.ts#L1456-L1469
[s-accept]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/frontend/stores/conversation-view/store.ts#L108-L123
[s-profiles]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/outpost/src/agent_runtime/adapters/acp/profiles.py#L187-L287


---

# Skills: load procedures when the task needs them

A skill packages instructions for a kind of work, usually in `SKILL.md`, alongside optional references, scripts, and templates. A tool schema describes an executable operation; a skill explains how to carry out a workflow using available operations. An extension can add executable behavior or lifecycle hooks. A skill can arrive inside an extension or plugin without becoming executable merely because it was discovered.

Consider a harness with fifty specialist workflows. Putting every procedure into every request consumes context and makes irrelevant instructions compete with the task. **Progressive disclosure** means advertising compact descriptions first, then loading a selected procedure and its relevant resources. Codex, pi, and Goose support this separation through the loading paths below; Sidekick adds a versioned binding model described later. [Codex catalog guidance][c-prompt], [pi prompt formatting][p-format], [Goose skill operation][g-operation].

## Follow one skill through the loop

Suppose the user asks: “Extract the invoice totals from these PDFs.” A teaching trace is:

1. **Discover:** the harness finds `invoice-extraction`, its description, and its source identity. It advertises this metadata to the model.
2. **Select:** the model recognizes a matching description, or the user explicitly invokes the skill. These are separate entry points; an implicit match is not guaranteed.
3. **Load:** the harness injects the procedure, or the model requests its contents through a reader. The procedure directs it to a format reference and an extraction script.
4. **Resolve resources:** the agent loads that reference relative to the skill's location. A remote package needs its provider's reader, not an invented local path.
5. **Act:** the agent requests a command to execute the script. Tool authorization and sandbox enforcement apply at this execution boundary.
6. **Continue:** results return to the ordinary agent loop. Loading a skill did not create a separate autonomous worker.

This trace is a design abstraction. The pinned implementations below do not all produce the same message roles or tool calls.

## File and provider catalog loading

| Harness | Discovery and model selection | Explicit selection and loading |
|---|---|---|
| Codex | Catalogs describe skills with source locators. Prompt guidance directs the model to read a matching skill and relevant resources. Sources can be host files or environment/provider-owned packages. | Structured selections and textual mentions are resolved by the skill subsystem. The extension path reads selected main prompts and adds instruction fragments to turn context. |
| pi | Prompt entries contain name, description, and file location. The model uses an available file-reading mechanism when a description matches. `disable-model-invocation` excludes a skill from this catalog. | `/skill:name arguments` reads the file, strips frontmatter, and expands the input with its body, base directory, and supplied arguments. |
| Goose | The native state-machine operation advertises names and descriptions and exposes `load_skill`, including access to supporting files. | Skill slash commands expand into model-visible user content. Built-in commands and existing recipe commands take precedence over skill command resolution. |

Sources: [Codex catalog instructions][c-prompt] and [explicit injection][c-injection]; [pi catalog filtering][p-format] and [command expansion][p-expand]; [Goose loading][g-operation], [command effects][g-command-effects], and [command precedence][g-commands]. These describe the pinned paths, not every UI or deployment configuration.

## Identity and precedence are behavioral contracts

Two packages can both contain a skill named `review`. Preserve the selected source identity, expose collisions, and make resolution predictable. Display names alone are insufficient for reproducibility.

Codex illustrates why subsystem scope matters: its host selection helper requires an unambiguous plain name, while the extension catalog selector takes the first enabled plain-name match and deduplicates by authority plus package. Both support explicit source selection. Do not describe these as one universal collision rule. [Host selection][c-host-selection], [extension selection][c-selection].

Pi keeps the first discovered name and emits a collision diagnostic containing winner and loser paths; it also deduplicates canonical file paths. Goose's discovery visits roots in order and keeps the first parsed name. Its project roots precede global roots, but duplicate winners *within one root* are unspecified because traversal is unsorted. [Pi loader][p-load], [Goose root order][g-roots] and [discovery contract][g-discovery].

## A loaded procedure is not an execution grant

Distinguish **discovery precedence**—which file wins—from **instruction authority**—which instructions the agent must obey. Finding a project skill does not establish who authored it or authorize every effect it requests. A procedure saying “publish the report” still needs the user's applicable authorization and the harness's execution policy.

Keep tool permissions in an enforced policy layer. Treat skill metadata as declarations unless that layer explicitly validates and interprets them. In pi's inspected loader, `disable-model-invocation` affects catalog visibility; that mechanism is not a command sandbox. A bundled script remains code that executes through a tool. [Pi loader and formatter][p-format].

File discovery and safe file access are also distinct. Goose's discovery contract explicitly says traversal can follow symlinks outside a search root. Its supporting-file reader separately rejects parent-directory paths and uses platform-specific confined opens; the native `load_skill` path also runs pre/post tool hooks. A successful discovery scan is therefore not evidence that every later read is safe. [Discovery boundary][g-discovery], [supporting-file access][g-files], [tool lifecycle][g-hooks].

## Budget context and define refresh behavior

Progressive disclosure reduces initial prompt size; loaded procedures and references still consume context. Codex budgets catalog metadata, can shorten descriptions or omit entries, and warns when an explicitly injected main prompt exceeds its limit. A shortened routing description may change selection quality, so record truncation as observable behavior. [Metadata allocation][c-budget], [main-prompt limit][c-injection].

Refresh metadata and content deliberately. Pi's explicit command reads the current file using the already-loaded catalog entry; `/reload` refreshes resources. Goose discovers skills when building its prompt contribution and again when handling a load. A file changed between discovery and execution can therefore differ from the version originally described. For reproducible runs, record the selected identity and content digest, and define what happens after [compaction](#compaction-is-a-state-transition) removes earlier instructions. [Pi expansion][p-expand] and [reload][p-reload], [Goose operation][g-operation].

## Builder checks

- Install two identically named skills and verify the selected source and collision report.
- Hide a skill from automatic selection and verify explicit invocation separately.
- Change or delete a file after discovery; verify refresh and missing-file behavior.
- Test oversized bodies, paginated reads, relative resources, and symlink escapes.
- Request an action explicitly forbidden by execution policy and verify the skill cannot override that denial.
- Compact after loading a procedure and verify the agent can recover required instructions without silently substituting a newer version.

## Sidekick: versioned bindings and a stable tool interface

Sidekick stores skills as drafts and published versions, with saved agent bindings resolving a version or channel. A metadata catalog enters the system prompt; `Skill` and `ReadSkillReference` load instructions and references on demand as tool results. This makes saved bindings, rather than filesystem discovery order, the runtime selection boundary. Draft access follows the saved binding rather than the conversation user’s skill-edit permissions. [Binding resolution][s-binding], [catalog assembly][s-catalog] (private source).

`UseSkillTool` checks that a skill is attached and that the requested tool is declared, then invokes the normal system dispatcher with the acting agent’s context. This preserves its applicable capability checks. Only built-in system tools are supported through that path at the pin. “Load the skill first” is model guidance; the dispatcher checks the binding on invocation rather than requiring proof of an earlier load event. A skill’s instructions, its binding, and its executable capabilities therefore remain distinct contracts. [Invocation boundary][s-use].

[c-prompt]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/skills/src/catalog_prompt.rs#L3-L40
[c-injection]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/skills/src/extension.rs#L403-L525
[c-host-selection]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/skills/src/selection.rs#L33-L200
[c-selection]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/skills/src/selection.rs#L21-L140
[c-budget]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/ext/skills/src/render.rs#L331-L371
[p-format]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/skills.ts#L331-L390
[p-expand]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L2172-L2200
[p-load]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/skills.ts#L414-L460
[p-reload]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L3690-L3728
[g-operation]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_skills.rs#L47-L122
[g-command-effects]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_skills.rs#L276-L318
[g-commands]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/slash_commands/skill_slash_command.rs#L45-L65
[g-roots]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/skills/mod.rs#L317-L388
[g-discovery]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/skills/discovery.rs#L215-L247
[g-files]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/skills/supporting_files.rs#L181-L265
[g-hooks]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_skills.rs#L365-L425

[s-binding]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/skill_service.py#L606-L737
[s-catalog]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/context_builder_service.py#L838-L868
[s-use]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/skill_executor.py#L349-L412


---

# Next user prompt suggestions

A next user prompt suggestion is a proposed message the user might send after an agent finishes. After “The parser fix passes its tests,” the interface might offer “Explain why the old parser failed.” The user can accept, edit, or ignore it. Generating that text is a separate inference task from answering the original request.

The interesting engineering problem is ownership: the model may predict an instruction, but only the user can adopt it. A suggestion must not silently enter the conversation as something the user said.

## Separate prediction from nearby features

| Feature | Example | What the harness does |
| --- | --- | --- |
| Next user prompt prediction | “Add a regression test for empty input.” | Generates a candidate user message from conversation context. |
| Command or path autocomplete | Completing `/skill:review` or `src/parser.ts` | Matches known entries against text being typed. |
| Assistant follow-up question | “Which input format should I support?” | Emits an assistant message asking the user for information. |
| Queued follow-up or automatic continuation | Processing an instruction already queued by the user | Schedules another agent turn. |

These features can share an editor or event system, but their contracts differ. See [steering and cancellation](#steering-follow-up-and-cancellation-are-separate-controls) for input that has already been submitted.

## What the pinned sources establish

| Harness | Evidence in this snapshot | Scope of the conclusion |
| --- | --- | --- |
| Codex | Prediction request/result types and an experimental prediction fork exist. The dedicated prediction request handler returns “not implemented yet.” | Infrastructure is visible; an end-to-end next-prompt generator is not established by this code. |
| pi | Interactive autocomplete combines commands, prompt templates, extensions, and skills. An example adds GitHub issue completion. | Extensible input assistance is implemented; model-generated next-message prediction was not found in the inspected surfaces. |
| Goose | Desktop autocomplete maps available commands and agent mentions into selectable entries. | Command and mention completion is implemented; model-generated next-message prediction was not found in the inspected surfaces. |

Codex's request carries `thread_id` and `source_turn_id`. Its notification repeats those identifiers and distinguishes `Completed { text: Option<String> }` from `Failed`. That contract can represent both a successful prediction and a successful decision to suggest nothing. The handler's explicit rejection prevents treating the types alone as a working feature. [Prediction types][c-types], [request handler][c-handler].

Codex also supports `experimentalPredictionMode` on thread forks. It requires an ephemeral fork of a loaded parent, inherits parent state, disables automatic compaction, and enables reasoning-effort override. The protocol comment identifies prompt-cache reuse as the purpose of inheriting configuration, tools, environments, and history. This is useful supporting infrastructure, but it does not establish the prediction prompt, trigger, filtering rules, or acceptance UI. [Fork contract][c-fork-contract], [fork implementation][c-fork].

pi's built-in completion catalog and optional GitHub issue example have concrete data sources; the latter fetches open issues and filters them locally. Goose's desktop adapter obtains commands and mentions through its ACP client. These are useful extension points, not evidence that either harness predicts a new user message. [Pi catalog][p-catalog], [issue example][p-issues], [Goose adapter][g-autocomplete].

**Search boundary:** this review checked Codex core, app-server, protocol, and TUI sources; pi agent, coding-agent, TUI, web-UI sources and extension examples; and Goose agent/server and desktop sources. Searches covered prompt suggestions, predictions, autocomplete, and follow-up paths at the registered commits. Codex rendering fixtures contain references to `next_prompt_suggestion.rs`; those fixture strings do not establish an implementation. The conclusions concern these source snapshots, not every client, external extension, or subsequent release.

## A design for an optional suggestion worker

The following is a proposed design, not a claim that the compared harnesses implement it.

1. **Wait for the run to settle.** Start after the final response and after queued work is exhausted. A low-level completion event may be too early: pi's notification example uses `agent_settled` because `agent_end` can precede retry, compaction, or queued follow-up work. [Pi settled event example][p-settled].
2. **Capture a bounded context snapshot.** Include recent user intent, the final answer, and relevant outcomes. Attach the session, completed-turn identifier, and composer revision. Exclude pending tool calls and unrelated private material.
3. **Request text or abstention.** Use a separate instruction such as “Suggest one concise message this user is likely to send next; return no suggestion when uncertain.” Require a small validated output, with no tool execution. Keep this synthetic instruction outside normal conversation history.
4. **Reject stale results.** Give inference a short time budget. Cancel when typing, a new turn, a branch switch, or a session change invalidates the snapshot. Recheck identifiers when the result arrives: cancellation alone may lose a race.
5. **Present an editable draft.** Acceptance can fill the composer; submission follows the ordinary user-message path. A displayed or accepted draft grants no permission to execute its contents.

For example, turn 12 ends and prediction starts. The user immediately types “Actually, revert that fix.” A late prediction saying “Commit the fix” belongs to turn 12's old composer state and must be discarded. It must never overwrite the user's correction or queue a commit.

pi's autocomplete editor demonstrates a reusable part of this design: it passes an abort signal, snapshots text and cursor position, and checks request identity plus the unchanged editor state before applying a result. This is evidence for handling asynchronous editor results, not a prediction-model implementation. [Editor freshness checks][p-freshness].

## Cost, privacy, and evaluation

**Design implications:** prediction adds model calls even when nobody accepts the result. Budget tokens and latency separately from the main agent, permit abstention, and avoid delaying the final response. Reusing full conversation context can improve relevance while exposing more data to the selected provider; use deliberate context selection and make any provider change explicit.

Evaluate acceptance together with edits, dismissals, stale-result rejection, and whether suggestions remain within the user's intent. Acceptance alone can reward intrusive suggestions. Include cases with a completed task, unanswered clarification, sensitive tool output, a newly typed correction, and multiple clients. The essential invariant is that prediction produces optional text; execution still requires a submitted user instruction and the harness's normal policy checks.

## Sidekick: clarification is not prediction

No dedicated next-user-message prediction path was located in the inspected backend, frontend conversation state, or chat composer. The native `AskUserQuestion` tool asks clarifying questions and suspends execution until answers resume it. That is an assistant requesting missing input, not a model proposing the user’s next message. This is a bounded finding about the reviewed sources, not a claim that an external client or wrapped agent cannot provide suggestions. [Question contract][s-question] (private source).

[c-types]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server-protocol/src/protocol/v2/thread_prediction.rs#L8-L35
[c-handler]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/message_processor.rs#L1460-L1462
[c-fork-contract]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server-protocol/src/protocol/v2/thread.rs#L544-L554
[c-fork]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/request_processors/thread_processor.rs#L5109-L5186
[p-catalog]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/modes/interactive/interactive-mode.ts#L786-L822
[p-issues]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/github-issue-autocomplete.ts#L71-L184
[p-settled]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/notify.ts#L51-L57
[p-freshness]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/tui/src/components/editor.ts#L2318-L2429
[g-autocomplete]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/ui/desktop/src/acp/autocomplete.ts#L27-L76

[s-question]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/tools/ask_user_question_tools.py#L1-L27


---

# Persistence, recovery, and replay

A saved conversation is evidence of what was recorded. It does not guarantee that an action was never executed twice or that every event a user saw reached storage.

Codex's `ThreadStore` distinguishes standard persistence from contexts allowed to enqueue background writes, with explicit flush/shutdown barriers. Pi's coding-agent emits message events before persistence; the separate experimental durable runtime makes stronger tool-intent and replay-policy guarantees. Goose's machine persists effects and reloads session state; live streaming may precede persistence, and pending approval execution depends on retaining a valid extension lease. [Codex storage contract][c-store], [pi events][p-events] and [durable recovery][p-recovery], [Goose effect application][g-store] and [tool execution][g-execution].

**Design implication:** model recovery around uncertainty. If a tool charged a card and the process crashed before recording success, blindly rerunning it is wrong. Use an externally enforced idempotency key, reconcile status, or surface an interrupted result. Avoid claiming exactly-once effects from the existence of a database or append-only log.

## Sidekick: recover the turn, not just the worker

Sidekick's native path separates a durable turn from the worker attempt advancing it. A heartbeat and ownership checks govern replacement attempts. A superseded attempt cannot write a valid final settlement, although this does not stop an external operation it already started. Outpost dispatch attempts, which represent separately hosted execution, are not automatically taken over and resent. [Attempt ownership][s-attempt]. Sidekick's source is private; these pinned links require repository access.

The tool boundary is equally explicit. The loop commits tool intents beside the assistant request before dispatching them. Suppose a tool changes a remote system and its worker disappears before the result enters the transcript. Recovery first inspects the intent: a stored result can be redelivered; an unclaimed call can be claimed; a running call has an uncertain outcome. After waiting for an existing result, the native recovery path reruns a running call only when both stored and current replay policy allow it. Otherwise it returns an interrupted-result explanation. Replay permission is separate from user authorization. [Intent commit][s-intent], [recovery decisions][s-recovery], [policy intersection][s-policy].

This is a stronger contract than replaying the last transcript entry, but it is not exactly-once execution. A replay-safe operation may run more than once. Post-commit hooks and completion projections also remain separate work: the sweeper logs failures rather than promising durable retries for every downstream notification. The [Sidekick chapter](#tool-recovery-records-what-is-known) traces these failure windows. [Post-commit limits][s-finish].

[c-store]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/thread-store/src/store.rs#L100-L250
[p-events]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L1104-L1200
[p-recovery]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/src/harness/tool.ts#L122-L210
[g-store]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/session.rs#L40-L153
[g-execution]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_toolcalling.rs#L664-L1106
[s-attempt]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L900-L1027
[s-intent]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/services/agent_loop.py#L675-L694
[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-finish]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_sweeper.py#L627-L708


---

# Observability should describe these boundaries

Useful traces distinguish model latency, admission wait, tool execution, approval wait, compaction, persistence, and user-input admission. A single run-duration counter cannot explain which layer was slow or whether “done” means model completion, tool completion, session settlement, or committed storage.

Pi provides a concrete example: an ordinary tool's duration measures `tool.execute`, excluding preflight and post-tool hooks. Its code-mode nested-call timer surrounds `ctx.executeTool`, which includes the nested pipeline. The numbers describe different intervals even within one harness. Compare like intervals before attributing a difference to performance. [Pi tool timing][p-timing], [pi nested-call timing][p-code-timing].

## Sidekick: visible does not always mean committed

Sidekick separates ephemeral text/progress events, persisted conversation messages, and durable turn outcomes. Its session reducer describes effects; the executor writes message rows into the caller's database transaction and can then broadcast them before that transaction commits. A user may therefore see an update that a later rollback removes from durable history. Atomic sequence numbers coordinate concurrent writers, but receiving a number is not proof of commit. Explicit deferred-broadcast paths provide a stronger acknowledgement boundary. [Effect execution][s-effects], [dispatch][s-dispatch]. Sidekick's source is private; these pinned links require repository access.

Native turn completion has a different contract: a completion event projects an already committed settlement. It identifies the turn and outcome; delegated-child completion also carries parent identity, so it does not stand in for the parent's completion. Clients can reconstruct the settlement metadata from durable state if the live notification is missed. This contract belongs to Sidekick's orchestration layer; a wrapped external harness's own stream events should not automatically receive the same meaning. [Completion projection][s-completion].

For example, a worker can emit text, fail, and be replaced while the same turn remains active. Monitoring should correlate the conversation, turn, worker attempt, and tool call separately. Record whether an observation means “streamed,” “committed,” or “settled”; a single “agent finished” timestamp would hide the recovery boundary. [Attempt replacement][s-attempt].

The codebase chapters identify concrete events and tests for each implementation. The [builder's lab](#build-a-harness-by-making-its-contracts-explicit) supplies deterministic scenarios for these boundaries. Together they provide a more useful starting point than a checklist of feature names.

[p-timing]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L820-L910
[p-code-timing]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/codemode/execute.ts#L472-L502
[s-effects]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_effect_executor.py#L41-L105
[s-dispatch]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/session_dispatch.py#L92-L219
[s-completion]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_lifecycle.py#L67-L205
[s-attempt]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/turn_record.py#L900-L1027


---

# Build a harness by making its contracts explicit

The goal of this lab is a small runtime whose behavior you can explain. The pseudocode below is a proposed design, not an extracted implementation or production-ready library. The codebase chapters supply concrete implementations of the tradeoffs.

## Start with the smallest observable loop

```text
run(input, session, cancellation):
    record input
    repeat while run budget remains:
        stop if cancelled
        context = prepare_context(session)
        response = request_model(context, tool_schemas, cancellation)
        record completed response

        if response failed:
            return a classified failure

        if response contains tool calls:
            for call in response.tool_calls:
                resolve name and validate arguments
                decision = authorize(call, current_policy)
                result = execute_or_record_denial(call, decision, cancellation)
                record result with call.id

        input = take_steering_at_defined_boundary()
        if input exists:
            record input
            continue

        if response has tool calls:
            continue

        input = take_follow_up()
        if input exists:
            record input
            continue

        return completed

    return budget_exhausted
```

Use a fake model and fake tools first. A fixed sequence of model responses is enough to test the runtime. It isolates loop bugs from provider behavior and costs no inference tokens.

### Exercise 1: maintain call/result identity

Have the fake model request two tools, one with an unknown name. Record the successful result and the unknown-tool error against their original IDs. The next request should contain a valid representation of both outcomes.

**Completion criterion:** no orphan tool calls; a tool error reaches the model as a tool outcome; malformed requests cannot dispatch an unintended function.

**Further reading:** the tool validation and execution sections in [pi](#pi-a-small-agent-loop-inside-a-configurable-coding-application), [Codex](#codex-a-turn-engine-with-explicit-execution-boundaries), and [Goose](#goose-a-persisted-ordered-operation-pipeline).

## Add policies before expanding the tool surface

Expose a read-only in-memory file tool first. Add file writes only after defining which paths are permitted. Add process execution after defining environment, working directory, output limits, timeout, and cancellation behavior.

### Exercise 2: deny a valid request

Send a structurally valid write request for a disallowed path. The tool executor must never run. Feed the denial back as a result the model can respond to.

**Completion criterion:** validation and authorization have separate tests; a denied action is observable; a tool's “read-only” label is not accepted as operating-system enforcement.

The necessary part is an actual boundary around effects. A classifier, remembered approvals, or a policy language is optional complexity until your application needs it.

## Define interruption before adding parallelism

Cancellation requests that active work stop and must reach cooperating tools. It does not undo completed effects. Steering supplies new instructions. They should not share an ambiguous “interrupt” operation.

### Exercise 3: steer during a slow tool

Run a tool that waits for a test-controlled signal. Queue “Use the other file instead.” Verify whether the current tool completes, whether other calls in its batch start, and when the next model request sees the new message.

**Completion criterion:** tests assert the chosen boundary; the UI describes that behavior accurately; an explicit cancel reaches the tool independently of steering.

### Exercise 4: reverse tool completion order

Let the model request reads A and B. Complete B first. Decide whether events and persisted results use completion order or request order, then assert both independently. Repeat with two writes to the same file.

**Completion criterion:** results retain IDs; transcript order is intentional; dependent mutations are serialized or rejected. A faster happy path is not enough to validate a scheduler.

## Make context preparation a visible operation

### Exercise 5: compact around a tool boundary

Create a transcript close to your context budget. Include a tool request, its result, and a later instruction correcting the task. Produce a summary and retain a recent tail.

**Completion criterion:** the next provider request is structurally valid; the correction survives; the original history remains inspectable according to your storage policy; failure to summarize has an explicit outcome. Test a summary that grows larger than expected.

Treat context estimates as estimates. Reserve space for the model's response and tool results. When the provider reports overflow, make recovery bounded; repeatedly summarizing an already tiny context can otherwise loop forever.

## Separate resume from safe replay

### Exercise 6: crash after an external effect

Use a fake counter service. Execute an increment, then simulate a crash before saving its result. Resume the session.

**Completion criterion:** the runtime either knows the operation is safe to retry, reconciles its external status, or reports an interrupted/uncertain outcome. It must not claim exactly-once behavior merely because the transcript is durable.

Record operation IDs, statuses, and replay policy before needing recovery. For an application with external side effects, this is more consequential than adding another model provider.

## Extend the harness without bypassing its rules

### Exercise 7: put the same tool behind MCP and code mode

Model Context Protocol (MCP) lets a server advertise tools and execute calls over a defined interface. Start with one fake server exposing a read and a write. Then expose a code execution tool whose program calls those tools.

**Completion criterion:** direct calls and code-generated calls pass through the same policy checks, tracing, cancellation, and result handling. A denied write stays denied when wrapped in a script. Tool discovery is scoped to the current configuration and access rights.

Test errors at three layers: the script fails, an MCP call fails, and the connection fails after the server may have acted. These are not interchangeable retry cases.

## Add another participant deliberately

### Exercise 8: two clients, one active run

Connect clients A and B. A starts a run; B submits input while A's tool is active. Have both try to approve the same pending action. Disconnect A, then reconnect with an event cursor.

**Completion criterion:** the system defines who can steer and approve, detects stale run IDs, avoids duplicate decisions, and reconciles history with live events. Presence indicators and shared cursors are optional UI; authority and consistency are runtime concerns.

### Exercise 9: a child agent with a bounded task

Delegate one independent read-only investigation. Give the child its own context and an explicit result contract. Cancel the parent while the child is active.

**Completion criterion:** you know whether cancellation propagates, whether the child can mutate shared files, how its tools are authorized, and how much of its output returns to the parent. More agents do not remove the need for ownership of shared state.

## Evaluate behavior, then task quality

Keep two test collections. Runtime tests check invariants using fake providers, clocks, tools, and storage failures. Task evaluations measure whether a real model solves representative problems under the chosen harness configuration.

For fair task comparisons, record model and version, prompts, tool schemas, permissions, initial files, context limits, retries, budgets, and result criteria. Repeat runs and report variability. This resource performs source analysis; it does not provide task-quality, cost, or speed benchmarks.

A useful first milestone is not “the agent solved one task.” It is “the runtime reliably represents completion, denial, failure, cancellation, and uncertain recovery, and I can inspect why it chose each state.”


---

# Evidence and scope

This resource compares commit-pinned source checkouts. Codex, pi, and Goose were inspected on **18 October 2026**; Sidekick was added on **19 October 2026**. It explains implementation choices; it does not rank products or measure task-solving quality.

| Harness | Repository | Inspected commit |
| --- | --- | --- |
| Codex | [openai/codex](https://github.com/openai/codex) | `8c076942cb70fba8e5c4732c23f1faabc4aed484` |
| pi | [earendil-works/pi](https://github.com/earendil-works/pi) | `482c1381a00d8d00bc81697e4a980c593db6803e` |
| Goose | [aaif-goose/goose](https://github.com/aaif-goose/goose) | `34a222e3f2b95d8b4a43482b53c2cc957ecce26d` |
| Sidekick | [datex/sidekick](https://github.com/datex/sidekick) — private | `463d653124e1c2cda46a3f14f01f3d6f384326b1` |

Sidekick was reviewed from the supplied local checkout. Its source links require repository access; public readers may receive a GitHub 404. This guide publishes explanations and citations, not a copy of its private source. Its native model/tool runtime and its Outpost adapters for external agents are assessed separately. Hosting another harness does not establish that harness's capabilities as native Sidekick behavior.

These are source snapshots, not an assertion about the latest published release. A feature present in a checkout may be unreleased, optional, experimental, or used only by a particular frontend.

## How the research was done

Research agents investigated the repositories using local source, documentation, and test definitions. Sidekick's review was divided across execution, coordination, and tool/provider boundaries. The coordinating agent synthesized a common vocabulary and compared the findings. Each chapter contains an evidence matrix and a source reading path. Structured feature records power the website's comparison table.

Implementation claims link to immutable GitHub commit URLs, usually with line anchors and named symbols. Repository documentation explains intended behavior; implementation code establishes the paths inspected; test code provides evidence of intended invariants. Reading a test is not the same as running it.

The example repair traces and interactive walkthrough are educational simulations. They do not call the harnesses, execute tools, or report observed performance. The builder's exercises describe a proposed runtime, not a single design shared by all four projects.

## How to read support labels

| Label | Interpretation |
| --- | --- |
| Core | Implemented in the primary runtime or documented baseline under discussion. This does not mean enabled in every configuration. |
| Optional | A flag, setting, extension, build feature, or deployment choice is needed. |
| Library | A separate reusable package provides primitives; the default application may not use them. |
| Example | Supplied as an example or pattern, rather than a built-in workflow. |
| Partial | The inspected implementation covers part of the capability; read the qualification. |
| Unknown | Not established in the inspected scope. This is not proof of absence. |

These labels are descriptions, not scores. A cell can span several layers, so its text and citations take precedence over its compact label. For example, a library can support replicated state without its terminal application providing a collaborative workspace.

## Fair comparison rules

Compare the same boundary: model request with model request, persisted session with persisted session, and application-level policy with application-level policy. Do not equate permission prompts with operating-system isolation, MCP support with arbitrary MCP server safety, or saved history with exactly-once execution.

Separate the reusable runtime from its application shell. Codex's public repository does not establish every proprietary desktop or hosted-service behavior. Pi's newer packages do not automatically describe the coding-agent's default runtime. Goose's shared state machine and application-specific operations are different layers.

Negative findings are bounded to the inspected code paths. “No full collaborative room established” means the research found specific sharing primitives without establishing the complete product; it does not claim such a product cannot be built.

## Limits

The research did not run the harnesses against live providers or execute their full test suites. It did not audit every tool implementation, platform-specific sandbox, extension, or deployment. It provides no latency, cost, reliability, or model-quality rankings. Website checks are separate from harness validation and are recorded in the project's verification report.

Source behavior can depend on flags, policy, model capabilities, frontend, and environment. For decisions that depend on one setting or deployment, follow the linked code and add a focused runtime test in that environment.

## Revisit with new evidence

Keep old snapshots intact. Add a new commit-pinned chapter and fact set, check changes along the same comparison axes, and document whether a difference is a real behavior change, a newly discovered fact, or a correction to this guide. See [the maintenance workflow](#add-a-harness-or-revisit-a-release).


---

# Add a harness or revisit a release

The content is the project. The website is a generated reading surface over that content. There are no harness-specific branches in the comparison renderer.

## Files and ownership

| File | Role |
| --- | --- |
| `registry.json` | Harness identities, repositories, snapshot metadata, common comparison axes, and guide navigation. |
| Each snapshot's chapter | Human-readable explanation with evidence and a source reading path. |
| Each snapshot's facts JSON | One qualified, cited record per comparison axis. Powers the website's comparison explorer. |
| `guide/` | Cross-harness teaching material. Update its synthesis when the selected snapshots change. |
| `templates/chapter.md` | Common research assignment and chapter structure. |
| `website/` | Renderer and interactive learning interface. Generated output lives in `website/dist/`. |
| `scripts/validate.mjs` | Structural checks, citation pin checks, and optional local source-range verification. |

## Add a harness

1. Identify the repository, commit, release tag if known, and precise runtime/application scope. Do not use a moving branch URL as evidence.
2. Create a new directory under `snapshots/<harness-id>/<short-sha>/`. Copy the chapter template and write `chapter.md` plus `facts.json` there.
3. Research the same axes as the existing harnesses. Use `unknown` with a bounded explanation when evidence is missing. Do not copy a competitor's behavior into a blank cell.
4. Add a harness record to `registry.json`, set `current` to its snapshot ID, and add the snapshot's commit, review date, label, chapter path, and facts path. Keep IDs lowercase and stable.
5. Review claims and source links. Update the guide where the new harness changes a comparison or teaches a distinct design.
6. Run validation and render the site. The new harness appears in navigation, snapshot selectors, and comparison columns from the registry.

A fact record has this shape:

```json
{
  "context": {
    "summary": "A short description of the observed behavior.",
    "detail": "Trigger, state transition, scope, and important qualification.",
    "availability": "core",
    "evidence": [
      {
        "label": "Source symbol and path",
        "url": "https://github.com/OWNER/REPO/blob/FULL_COMMIT/path/to/file#L10-L40"
      }
    ]
  }
}
```

Include every axis from the registry. Allowed availability labels are `core`, `optional`, `library`, `example`, `partial`, and `unknown`. Established claims need evidence. An unknown finding may have an empty evidence array only when its text explains the bounded inspection scope. Cite the source path that owns the behavior, not just a marketing description.

## Revisit a new release

Create a new snapshot; preserve the previous files and metadata. Resolve the release tag to a full commit and record both. A review date is not a release date.

Start with a diff between the old and new commit for the source paths in the evidence records. Then read the changed owning functions and their callers in full. A path that did not change can still behave differently because a caller, default, feature gate, or provider adapter changed.

For each axis, answer:

- Did runtime behavior change, or only an implementation detail?
- Did the default or feature gate change?
- Did responsibility move between the core, application, extension, and provider?
- Do cancellation, context, approval, and persistence still compose the same way?
- Is a formerly missing capability now established, or was the old comparison incomplete?

Write `changes.md` beside the new snapshot with four categories: **behavior changes**, **newly established evidence**, **documentation corrections**, and **still unresolved**. Include old/new source references. Add the new entry to `snapshots` and update `current` only after review. The website keeps older snapshots selectable; its release comparison shows record changes for a selected harness. Text differences are a review aid, not proof of a semantic behavior change.

## Keep the comparison fair

Do not give one project credit for an optional SDK while evaluating another only by its default terminal application. Use the detail field to identify layers. A source checkout with a feature is not proof that a released binary enables it.

Avoid boolean cells for nuanced behavior. “MCP supported” is less useful than the discovery path, transport, tool catalog lifetime, nested-call policy boundary, and error handling. “Multi-player” should separately evaluate client attachment, authorship, active-run arbitration, event distribution, and approval authority.

## Add a guide topic

Give each topic a Markdown file and a page record in `registry.json`. Set `parent` to the overview page's ID to place it in that page's collapsible submenu. Keep child pages immediately after their overview in registry order so the complete guide follows the same reading order. Use a short `title` for navigation and a descriptive heading in the Markdown file.

The overview uses `layout: "topic-overview"`; its top-level Markdown list renders as linked topic cards. Add the new topic's link and description there. Routes, downloads, and submenu entries are generated from the registry. A topic route automatically expands its parent's submenu; the parent's title opens the overview independently of the disclosure button.

## Verify and regenerate

From the comparison project directory:

```sh
cd website
npm install --ignore-scripts
cd ..
node scripts/validate.mjs
node scripts/validate.mjs --source-root /absolute/path/to/parent-of-checkouts
node --test scripts/validate.test.mjs website/render.test.mjs
node website/render.mjs
python3 -m http.server 4173 --directory website/dist --bind 127.0.0.1
```

The second validator command assumes checkout directories match harness IDs (`codex`, `pi`, `goose`, `sidekick`). When repositories live under different parents, create a temporary directory with one symlink per harness ID pointing to its checkout, and pass that directory as `--source-root`. It uses `git show` against the pinned commit, so a changed checkout branch does not silently change the evidence being checked. Missing repositories or objects fail source validation; obtain the intended source explicitly before retrying. Private source citations require an authorized local checkout; a public GitHub 404 does not establish that a private citation is invalid.

The generated site opens directly from `dist/index.html` as well as through a static server. It uses no live provider calls or runtime server state. Check navigation, matrix filters, snapshot selection, the learning walkthrough, deep links, and a narrow viewport after UI changes. Re-render after content changes so the guide and website agree.

Keep package versions exact and refresh the website's own lockfile with dependency changes. Do not install or build the compared harnesses merely to regenerate this resource. The host repository's required checks remain applicable to code edits.

## Suggested research delegation

Assign one researcher per harness, each owning its new snapshot directory. Ask a second researcher to challenge claims involving authority, replay, concurrency, and default settings. The coordinator owns cross-project vocabulary and synthesis. Verify citations mechanically and review semantics manually: a valid line number alone does not establish a claim.


---

# Codex: a turn engine with explicit execution boundaries

Snapshot: **2026-10-18**, local repository `https://github.com/openai/codex.git`, commit **`8c076942cb70fba8e5c4732c23f1faabc4aed484`**. This is a source-based account of that revision, not a claim about every Codex release or the proprietary desktop/cloud implementation. The checkout was clean when inspected. No builds, tests, model calls, or example commands were executed for this research.

Codex is useful to study when a small “call model, execute tools, repeat” loop has grown into a system that must survive interruption, permissions, changing tool catalogs, long histories, and multiple clients. Its distinctive lesson is how much state belongs around that loop: a coherent request snapshot, execution policy, event delivery, and persistence checkpoints.

## First distinguish the layers and clocks

The Rust **core** implements conversation and execution behavior. Frontends present that behavior; the **app-server** accepts thread/turn requests and distributes events. At this revision even the noninteractive `codex exec` path starts an `InProcessAppServerClient`. That is a concrete integration seam, not evidence that every frontend uses an identical transport. [C1]

Three units of work matter:

- A **thread** supplies the continuing conversation identity and resumable history.
- A **user turn** is the running task that handles input and emits lifecycle events. `RegularTask::run` can invoke `run_turn` again if input remains pending.
- A **sampling request** asks the model for one response stream. `run_turn` can make many such requests as tools return, steering arrives, hooks continue work, or context changes. [C2]

Consequently, counting API requests is not counting turns. A single visible “fix this bug” turn might contain reading, editing, testing, another edit, and a final answer.

![Codex architecture: a RegularTask captures each sampling step, routes tools, collects ordered results, and checkpoints thread state.](guide/assets/codex-architecture.svg)

The diagram is a reading map, not a complete dependency graph. Runtime policy differs by tool: command and patch execution use approval/sandbox orchestration; remote MCP execution is not implied to share the local process sandbox.

## The turn lifecycle

`run_turn` prepares context before the first request: it handles pending asynchronous hook output, compaction, relevant MCP readiness, skills/plugins, context changes, and accepted input. Each sampling step captures a `StepContext`, so tools execute against the tools, model settings, and environment associated with that request. [C3]

The following pseudocode omits lifecycle bookkeeping but preserves the important loop boundaries:

```text
RegularTask:
  emit TurnStarted; run startup contributors
  repeat:
    run_turn:
      prepare/compact context; record accepted input
      repeat:
        admit pending input at an allowed boundary
        capture coherent step; construct model prompt
        stream one model response, with transport retries
        dispatch completed tool calls as they arrive
        collect tool results into history in call order
        process asynchronous hook output
        if continuation is needed: compact/roll context if required
        otherwise run Stop hooks
        if Stop hooks request more work: continue
        otherwise finish this run_turn
    stop on terminal error
  until no pending input remains
```

“No more tool calls” is insufficient as a stop rule. Pending input can require continuation; a response can explicitly request a follow-up; Stop hooks can contribute new work. Conversely, an error can terminate the task without replaying queued input. The model stream must reach its completed event; a stream ending early is an error. [C2][C4]

### An illustrative read → edit → test → finish trace

This example was **not executed**. Tool availability, approvals, names, and command choice depend on configuration.

| Stage | Model and harness behavior |
|---|---|
| Input | User asks to fix an off-by-one error in `src/range.py`. The harness records input and builds the first step. |
| Read | Model requests `exec_command` with `cat src/range.py`. The harness parses arguments, applies execution policy, runs the command, and records the call/result pair. |
| Edit | The next model response requests `apply_patch`. Its freeform patch syntax is grammar-described; the handler verifies the patch against the selected filesystem and sends the verified action through the patch runtime/orchestrator. |
| Test | A later response requests `exec_command` with `python -m unittest tests.test_range`. If the process outlives the initial wait, terminal controls can collect more output. |
| Repair or finish | A failing result becomes input to another model request. A passing result may lead to a final explanation. Stop hooks and pending input still participate in determining completion. |

The harness supplies execution and feedback; it does not mechanically insert a test after every patch. That sequence is the model's chosen workflow in this illustration. Patch verification and the freeform specification are explicit code paths. [C5]

## Tools: exposure, execution, and ordering

`ToolRouter` distinguishes tools registered internally from tools actually exposed in the current step. Exposure can be direct, deferred through search, or available to Code Mode. It normalizes function, custom/freeform, and search response items into invocations. Unknown tools can produce model-visible errors rather than crashing the whole turn; incompatible internal payloads can be fatal. [C6]

The registry provides hooks around execution. A pre-tool hook can block or rewrite a request. Post-tool hooks run after successful execution and may reject the result. They cannot reverse an external side effect that already happened. Client-provided dynamic tools form another extension boundary: the handler sends a request outward and waits for the client's result. [C7]

Concurrency is deliberately selective. `ToolCallRuntime` waits for readiness before taking an execution lock. Parallel-capable tools acquire a shared read lock; other tools acquire the exclusive write lock. Tasks can start while the model stream continues. MCP tools opt into parallel execution through an explicit flag or their `readOnlyHint`; this is metadata-based admission, not a proof of purity. [C8][C9]

Execution completion order and conversation order are different. Sampling collects futures in `FuturesOrdered`, so history receives results in call order even when later calls finish earlier. Readiness notification can happen earlier than that ordered collection. This design preserves a predictable transcript while permitting useful overlap, at the cost of waiting behind an earlier slow call before later results enter ordered history. [C10]

## Approval is different from a sandbox

An **approval policy** decides whether an action may proceed or must be reviewed. An **OS sandbox** constrains what the resulting process can actually access. The tool orchestrator evaluates approval requirements, constructs sandbox attempts, and handles certain sandbox-denial retries. It does not simply rerun every failed command with greater authority. Escalation depends on policy and approval conditions; a Guardian-reviewed authority scope may require a fresh review for escalation. [C11]

Platform enforcement differs. The core documentation describes macOS Seatbelt, Linux sandbox selection, and Windows support constraints. Unsupported policy combinations can fail closed. These are implementation-specific capabilities, so a comparison should preserve the distinction between requested policy and supported enforcement rather than publish one universal “sandbox: yes” cell. [C12]

For a harness builder, the essential separation is authorization before execution, bounded execution, then explicit interpretation of failure. Tool descriptions alone do not enforce any of those boundaries.

## Long conversations: compaction has several meanings

Codex does not have only one summarization switch. The context-window code considers both an automatic-compaction threshold and the full model context limit; a threshold measured after a reusable prefix does not remove the full-context cap. Model changes can also require compaction, including a move to a smaller window or a changed compaction compatibility hash. [C13]

There are three important paths:

1. **Client-managed summarization.** `compact.rs` makes a model request with a compacting prompt. “Local” here means client-managed orchestration, not offline summarization. On context overflow it can remove older items and retry. The replacement retains selected recent user text plus a summary and reconstructs canonical context; it does not preserve every old item verbatim. [C14]
2. **Provider-supported remote compaction.** `run_auto_compact` selects the remote path when the provider advertises `RemoteCompactionSupport::V2`; otherwise it can use the client-managed path. Provider support is explicit capability information. [C15]
3. **Token-budget context rollover.** The token-budget path installs a fresh context window without running model/server summarization. Calling every window transition “summarization” would misdescribe this behavior. [C16]

Background compaction is another dimension, not a fourth summary format. It requires its feature and remote-provider capability and is disabled for token-budget mode. A background attempt captures history/model/auth compatibility information. Ready results are candidates adopted at defined checkpoints; they are not allowed to mutate arbitrary current history whenever the request finishes. Shutdown cancels pending work and waits for accounting. [C17]

These mechanisms protect different invariants: request size, coherent initial instructions, compatible model state, and continuity across history replacement. They also create a learning tradeoff: the model continues from a deliberately lossy representation, so a robust harness must identify what information survives and what must be recoverable from tools or storage.

## Steering and multiple clients

**Steering** means adding direction to an active task. At the app-server boundary, `turn/start` calls `start_or_steer_turn`: the result may be a newly started turn or input admitted to the existing turn. The explicit `turn/steer` route requires a nonempty expected turn ID and can reject stale or incompatible state. Within the loop, pending input is admitted at controlled boundaries; the initial request is protected from being overtaken by later queued input. Some feature-controlled paths can preempt sampling/retry waits to process newly available input. [C18][C3]

That means “steering supported” should not be read as “every tool stops instantly.” Cancellation, pending-input admission, model-stream preemption, and a completed tool's external effects are different events.

**Multiple clients** are supported as an app-server primitive. `ThreadStateManager` maintains sets of subscribed connection IDs per thread and reverse connection/thread mappings. One disconnected client can be removed without deleting the other subscriptions. A thread listener serializes resume-response/history work with subscription so attaching a client does not introduce a gap between the history snapshot and subsequent notifications. [C19]

These are useful foundations for shared sessions, but a connection is not necessarily a distinct human. In the inspected thread/turn processors, a complete multi-human product—identity attribution, role-based collaboration policy, presence, edit conflict resolution, or ownership transfer—was **not established**. `expectedTurnId` is a stale-input guard, not a collaborative document merge algorithm. Multi-agent child threads also have a separate ownership rule: direct app-server user input is disallowed for the relevant spawned-subagent surface. [C20]

## MCP, lazy discovery, and Code Mode

**MCP** supplies tool catalogs and remote tool execution. Codex translates MCP schemas into its own tool specifications, waits for server readiness, and captures the executed binding and output-policy snapshot so a refreshed catalog cannot silently reinterpret an already admitted call. Search text includes tool descriptions and useful schema/namespace metadata. [C9]

**Lazy discovery** reduces how much of that catalog must be exposed initially. `ToolSearchHandler` indexes deferred entries using BM25, a lexical ranking method. Search returns loadable tool specifications, with namespace coalescing. Its cache is tied to runtime identity and dynamic metadata; it can rebuild when the registry changes. The Code Mode search path filters candidates against that exact step's allowed nested-tool catalog before applying the result limit. [C21]

**Code Mode** lets a model express orchestration as JavaScript: call several tools, combine/filter their results, and emit selected output. It is distinct from running a shell script or using a persistent Node REPL. The inspected runtime creates a fresh V8 isolate/context for a cell, rejects static and dynamic imports, and exposes controlled globals such as `tools`, `ALL_TOOLS`, `text`, `store`, `load`, and yield/notification helpers. Cross-cell state is explicit through storage rather than arbitrary surviving JavaScript variables. Node filesystem/import APIs were not present in this inspected global surface. [C22]

For example, conceptually, a cell could `await Promise.all(...)` for two independent MCP lookups, then emit only a comparison. This reduces model round trips and intermediate text. It does **not** bypass normal tool handling: nested calls are translated back into typed tool payloads and routed through `ToolCallRuntime` with the originating `StepContext`. The delegate retains that snapshot across a yielded cell; recursive invocation of the execution tool is rejected. Long-running cells can yield, be waited on, or be terminated. [C23]

This architecture separates two costs: catalog selection determines what the model sees, while programmatic orchestration determines how many model turns are needed to combine tool work. Either can be useful independently; feature/configuration gates determine availability.

## Persistence, cancellation, and recovery

`ThreadStore` is an explicit storage-neutral boundary. An append is not automatically a universal durability guarantee. `Standard` persistence and `flush_thread` must make queued items durable/readable before returning. `TurnStart`, `SteeredUserInput`, and `SubagentSpawn` permit background persistence subject to later barriers; acknowledging a spawned child requires the documented standard barrier. `shutdown_thread` flushes; `discard_thread` may drop pending in-memory writes. [C24]

Resume also has an ordering contract: the store returns authoritative replay context after acquiring the writer, including writes committed before acquisition. Core reconstruction finds the latest usable compaction checkpoint and replays its surviving suffix, respecting rollback and restoring context/window metadata. A saved UI transcript and the next model prompt are therefore not interchangeable. This review does not establish implementation-level `fsync`, crash atomicity, or exactly-once external side effects. [C25]

Tool cancellation preserves already completed lifecycle outcomes. Runtime behavior can wait for handlers designated to finish on cancellation; otherwise it can abort the task and synthesize an aborted tool result. That is bookkeeping for interrupted work, not rollback of filesystem or remote effects. [C26]

Model transport recovery is separate. Response-stream errors use provider retry limits, retry delays, and supported fallback behavior. WebSocket-to-HTTPS fallback can reset the retry counter. An optional feature permits unbounded connection-failure retries for eligible paths, so “bounded retries” is not universally true across configurations. Context-window and usage-limit failures are handled differently from retryable transport failures. [C27]

## Providers, extensions, and agent teams

The provider interface covers metadata, authentication/recovery, model catalog/routing, and capabilities. The factory supports configured providers and a dedicated Bedrock path; capability declarations decide features such as remote compaction. This is an abstraction boundary with capability differences, not a promise that every provider implements every behavior. [C28]

Extensibility operates at several levels: Rust tool runtimes, dynamic client tools, MCP tools, skills/plugins contributing instructions or tools, and lifecycle/tool hooks. These extension points have different trust and execution boundaries. Adding a skill is not the same operation as registering a callable runtime. [C7][C29]

The multi-agent v2 tool surface includes spawn, send-message, follow-up, list, interrupt, and wait. Spawning can inherit history or start without it; spawn cleanup uses a guard to shut down/discard a provisional child on failure. The control graph enforces distinctions such as not interrupting the root/self through the child-control path. These are agent-thread orchestration primitives. They do not, by themselves, prove separate filesystem/worktree isolation for each child. [C30]

## What to test, and what the code already observes

The execution runtime records dispatch wait, handler duration, and total time, with care around nested Code Mode accounting. Registration telemetry distinguishes exposed and hidden/deferred tools. This makes an execution queue delay distinguishable from a slow handler. [C31]

Inspected tests cover cancellation before admission, preserving a completed lifecycle during cancellation, metadata-sensitive search-cache rebuilding, and coalescing mixed search results. They are useful executable descriptions of contracts; they were read, **not run**, for this chapter. [C32]

Three design lessons follow:

1. **Capture request state deliberately.** A tool must execute under the same relevant settings and catalog used to generate its request. Snapshots and bindings add machinery but prevent time-of-check/time-of-use confusion when input or catalogs change.
2. **Separate parallel work from ordered history.** Shared/exclusive execution admission plus ordered result insertion provides concurrency without an unpredictable replay transcript. The price is potential waiting behind earlier calls.
3. **Name your barriers.** Authorization, sandbox enforcement, turn completion, model completion, compaction adoption, and durable persistence are separate boundaries. Explicit contracts make recovery possible; collapsing them into a single “done” flag hides failure modes.

## Evidence matrix

All links below are immutable links to the inspected commit. IDs are stable within this research snapshot and can be reused by the comparison matrix.

| ID | Source-backed claim | Primary code entry point |
|---|---|---|
| C1 | Core/frontend seam; exec starts an in-process app-server client | [core README][C1a]; [exec startup][C1] |
| C2 | A task can repeat `run_turn`; each run can repeat sampling | [`RegularTask::run`][C2] |
| C3 | Inputs and coherent step state are prepared/admitted at loop boundaries | [`run_turn`][C3] |
| C4 | Follow-up, pending input, and Stop hooks influence completion | [`run_turn` continuation logic][C4] |
| C5 | Patch has a freeform grammar and verified execution path | [`create_apply_patch_freeform_tool`][C5a]; [`handle_call`][C5] |
| C6 | Router separates exposure and payload forms | [`ToolRouter`][C6] |
| C7 | Registry hooks surround execution; dynamic tools await client output | [`dispatch_any_with_state`][C7]; [dynamic handler][C7a] |
| C8 | Parallel tools use shared admission; others exclusive | [`handle_tool_call_with_source`][C8] |
| C9 | MCP parallel metadata, binding snapshot, and readiness | [`McpHandler`][C9] |
| C10 | Concurrent results are drained in insertion order | [`drain_in_flight`][C10] |
| C11 | Approval/sandbox attempt and escalation are policy-sensitive | [`ToolOrchestrator::run`][C11] |
| C12 | Platform sandbox support differs | [core sandbox documentation][C12] |
| C13 | Automatic thresholds and full context limits remain distinct | [context-window status][C13]; [model-switch compaction][C13a] |
| C14 | Client compaction summarizes and reconstructs retained context | [`run_compact_task_inner_impl`][C14]; [replacement history][C14a] |
| C15 | Provider capability selects remote compaction | [`run_auto_compact`][C15] |
| C16 | Token-budget rollover does not invoke summarization | [token-budget compaction][C16] |
| C17 | Background compaction is feature/capability gated and adopted later | [async compaction lifecycle][C17] |
| C18 | Start may steer; explicit steering checks expected turn ID | [`start_or_steer_turn` caller][C18]; [steer checks][C18a] |
| C19 | Multiple client subscriptions and ordered attach/history mechanism | [listener command][C19a]; [`ThreadStateManager`][C19] |
| C20 | Spawned child input has a separate ownership constraint | [direct-input guard][C20] |
| C21 | Deferred lexical search, cache, and Code Mode policy filtering | [`ToolSearchHandler`][C21] |
| C22 | Fresh V8 cell, controlled globals, rejected imports | [runtime][C22]; [globals][C22a]; [module loader][C22b] |
| C23 | Nested Code Mode calls reenter normal routing with origin context | [nested dispatch][C23]; [cell delegate][C23a] |
| C24 | Persistence contexts distinguish enqueue from durability barriers | [`PersistContext` and `ThreadStore`][C24] |
| C25 | Resume and compaction-aware reconstruction have explicit contracts | [`resume_thread` contract][C25]; [reconstruction][C25a] |
| C26 | Cancellation can preserve completed/finish-on-cancel outcomes | [tool cancellation branch][C26] |
| C27 | Stream retry limits, optional unbounded path, and fallback differ | [`handle_response_stream_error`][C27] |
| C28 | Provider interface and capability boundaries | [`ModelProvider`][C28]; [capabilities][C28a] |
| C29 | Skills/plugins and lifecycle contributors extend turn preparation | [`build_skills_and_plugins`][C29]; [task lifecycle][C29a] |
| C30 | Child thread tools, inheritance, and spawn cleanup | [tool surface][C30]; [spawn][C30a]; [spawn guard][C30b]; [interrupt][C30c] |
| C31 | Runtime timing and tool-exposure registration metrics exist | [timing guard][C31]; [registration metrics][C31a] |
| C32 | Contract tests exist for cancellation and search caching | [cancellation tests][C32]; [search tests][C32a] |

### Suggested source-reading path and limits

Start with `tasks/regular.rs`, then `session/turn.rs`. Follow one tool through `tools/router.rs`, `tools/registry.rs`, `tools/parallel.rs`, and its handler. Then read the orchestrator, compaction modules, and `thread-store/src/store.rs`. Finally trace app-server steering/subscriptions and Code Mode's nested dispatcher. This order keeps the basic feedback loop visible before introducing concurrency and recovery.

This is a focused source review, not a security audit or performance benchmark. Defaults, feature gates, provider capabilities, and frontend configuration affect which paths run. The chapter does not infer proprietary product behavior, remote MCP-server isolation, full multi-human collaboration, or crash-level durability from these interfaces. Revisiting a release should recheck each C-ID against the new pinned commit, especially loop entry points, persistence contracts, tool-exposure rules, and feature-gated compaction/Code Mode behavior.

[C1]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/exec/src/lib.rs#L1021-L1040
[C1a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/README.md#L1-L12
[C2]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tasks/regular.rs#L40-L126
[C3]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L166-L570
[C4]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L570-L800
[C5]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/apply_patch.rs#L307-L589
[C5a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/apply_patch_spec.rs#L9-L32
[C6]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/router.rs#L185-L311
[C7]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/registry.rs#L615-L910
[C7a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/dynamic.rs#L100-L282
[C8]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/parallel.rs#L126-L260
[C9]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/mcp.rs#L135-L325
[C10]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L2568-L2695
[C11]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/orchestrator.rs#L125-L580
[C12]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/README.md#L13-L98
[C13]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/context_window.rs#L1-L130
[C13a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L1416-L1506
[C14]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/compact.rs#L264-L610
[C14a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/compact.rs#L651-L794
[C15]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L1507-L1613
[C16]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/compact_token_budget.rs#L1-L69
[C17]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/async_compaction.rs#L40-L214
[C18]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/request_processors/turn_processor.rs#L649-L737
[C18a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/request_processors/turn_processor.rs#L1045-L1150
[C19]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/thread_state.rs#L320-L687
[C19a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/thread_state.rs#L62-L103
[C20]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/app-server/src/request_processors/thread_input.rs#L1-L37
[C21]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/tool_search.rs#L66-L358
[C22]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/code-mode-runtime/src/runtime/mod.rs#L73-L260
[C22a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/code-mode-runtime/src/runtime/globals.rs#L15-L98
[C22b]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/code-mode-runtime/src/runtime/module_loader.rs#L142-L217
[C23]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/code_mode/mod.rs#L350-L474
[C23a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/code_mode/delegate.rs#L1-L250
[C24]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/thread-store/src/store.rs#L100-L250
[C25]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/thread-store/src/store.rs#L177-L205
[C25a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/rollout_reconstruction.rs#L65-L340
[C26]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/parallel.rs#L260-L335
[C27]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/responses_retry.rs#L57-L210
[C28]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/model-provider/src/provider.rs#L112-L420
[C28a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/model-provider/src/capabilities.rs#L1-L57
[C29]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L1099-L1320
[C29a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tasks/lifecycle.rs#L1-L122
[C30]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/multi_agents_v2.rs#L1-L66
[C30a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs#L103-L260
[C30b]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/agent/control/spawn_guard.rs#L1-L105
[C30c]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/agent/control/interrupt.rs#L1-L52
[C31]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/parallel.rs#L399-L540
[C31a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/router.rs#L398-L440
[C32]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/parallel.rs#L604-L883
[C32a]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/handlers/tool_search.rs#L360-L620


---

# Pi: a small agent loop inside a configurable coding application

Pi is useful for learning because its core loop is separate from the application around it. A model proposes tool calls; ordinary program code validates and executes them; results become messages; the model runs again. The terminal interface, session history, compaction, and extensions sit around that loop. The important boundary is between **deciding what to do**, which the model does, and **making it happen**, which the harness does.

This chapter describes the local `earendil-works/pi` checkout at commit `482c1381a00d8d00bc81697e4a980c593db6803e`, inspected on October 18, 2026. The package manifests identify version 1.3.0. All source links below are pinned to that commit. This is source analysis, not a performance benchmark or a claim about every installed Pi version. The inspected checkout includes built-in MCP integration and parallel tool execution.

## The layers and their responsibilities

![Pi architecture: AgentSession wraps the reusable Agent loop, provider boundary, tool batches, and ordered results, with separate JSONL session storage.](guide/assets/pi-architecture.svg)

*Simplified coding-agent reading map. The separate experimental pi-durable runtime is not part of this loop.*

The coding application's main composition point is `createAgentSession()` in `sdk.ts`. It restores session messages, selects the model and thinking level, constructs `Agent`, supplies its streaming function, and wraps it in `AgentSession`. [P1]

| Layer | Responsibility | What a harness builder can reuse |
| --- | --- | --- |
| `packages/ai` (`pi-ai`) | Common messages, model metadata, provider authentication and streaming | One provider boundary instead of provider-specific logic throughout the loop |
| `packages/agent` (`pi-agent-core`) | In-memory state, model/tool loop, queues, lifecycle events | The execution loop without the terminal application |
| `packages/coding-agent` | Coding tools, resources, extensions, compaction, retry and sessions | A complete application or its `AgentSession` SDK |
| `packages/tui` | Terminal components consumed by coding-agent | Presentation separate from agent decisions |
| `packages/durable` | A separate, experimental durable harness | Persisted tasks and explicit crash-recovery semantics |
| `packages/chord`, `codemode`, `env` | Application state/RPC; restricted JavaScript execution; remote execution environments | Additional infrastructure, with separate responsibilities |

Do not merge the two harnesses into one architecture diagram: the coding-agent SDK instantiates `Agent`, while `pi-durable` builds on `pi-ai` and Chord. The latter is explicitly experimental. Its capabilities do not automatically describe the coding CLI's persistence guarantees. [P1, P20]

## Follow one request through the loop

Consider an illustrative request: “Fix the empty-input case in `src/parser.ts`, run the focused parser test, and explain the change.” The following is a possible model trajectory, not a recorded run and not a hardcoded workflow.

1. `AgentSession.prompt()` processes extension commands and input hooks, expands skills/templates, checks model/auth availability, and starts a run. `Agent.prompt()` rejects overlapping prompts; an already-running session requires an explicit steering or follow-up choice. [P2]
2. The first provider response requests `read({path: "src/parser.ts"})`. The loop resolves the tool, prepares and validates its arguments, runs the pre-execution hook, then calls its implementation. The returned file text becomes a `toolResult` associated with the original call ID. [P3, P4]
3. Another provider request sees that result. The model proposes `edit({path: "src/parser.ts", edits: [{oldText: "...", newText: "..."}]})`. The edit implementation reads the file, applies replacements against its original content, writes the result, and returns success text plus a diff for the UI. [P6]
4. The model requests `bash({command: "node --test test/parser.test.ts"})` in this hypothetical repository. Bash streams progress and returns the exit status/output. A nonzero exit is an error tool result, so the next model response can investigate and try another edit. [P7]
5. Once the model responds without tool calls, the loop checks queued work and continuation hooks. If none request more work, it emits `agent_end`; the session may still perform retry or settlement processing before `agent_settled`. [P3, P12]

The loop does not know what a parser fix is, and a final answer does not itself prove that the tests passed. Correctness comes from the actual tool evidence and the model's interpretation of it.

```text
append prompt and changed tool declarations
emit agent_start, turn_start, prompt message events
repeat:
    before subsequent turns: refresh context/model/tools, possibly compact
    append pending steering or follow-up messages
    prepare request
    transform application messages -> provider-compatible messages
    stream assistant response and emit message events
    if response is error or aborted: finish turn and end this core run
    if output hit length limit: fail its tool calls without executing them
    else: validate, authorize through hooks, execute, finalize tool batch
    append tool-result messages
    apply finishTurn decision and emit turn_end
    if finishTurn says end: stop
    if tool continuation or steering exists: repeat
    if follow-up exists: repeat
    if finishTurn requested continuation: repeat once with current context
    otherwise stop
emit agent_end
```

This condenses `runLoop()` rather than reproducing every error branch. One core “turn” is one assistant response and its tools; a user request can require many such turns. Tool declarations and prompt changes are recorded in system messages. `declareToolChanges()` reconciles the executable tool set with the transcript before requests. [P3, P8]

## Tools: schemas, concurrency, and useful failure results

A tool has a name, description, parameter schema, and executable function. Tool implementations can also provide display details, structured output for programmatic callers, progress updates, argument preparation, and execution-mode constraints. The loop's provider boundary converts application messages only when it requests another response. UI-only or custom information need not be sent to the model. [P4, P9]

`prepareToolCall()` performs lookup, optional argument repair, schema validation, then `beforeToolCall`. Unknown names, invalid arguments, blocked calls, and thrown tool errors become error results the model can observe. Validation can coerce values; it is not merely checking that the original JSON already has the correct types. Truncated assistant output is handled more conservatively: all its tool calls fail without execution because even parseable arguments may be incomplete. [P4, P10]

**Parallel execution is the default in this checkout.** The runtime first prepares calls sequentially, then starts permitted executions concurrently. Completion events can arrive in completion order, but final transcript results are appended in the assistant's original call order. A globally sequential setting, or any tool marked `executionMode: "sequential"` in that batch, serializes the whole batch. [P5]

For example, two independent reads can overlap while the conversation remains deterministic about their order. Two edits to the same file require another safeguard: `withFileMutationQueue()` serializes mutations by resolved file identity, while different files can proceed independently. This is a process-local coordination mechanism, not a lock against unrelated editors or processes. A bash command touching the same file does not automatically participate in the edit queue. [P6]

The default coding tool selection is `read`, `bash`, `edit`, and `write`. Other built-in tools and extension tools exist, and the SDK supports allowlists and denylists. Read output is bounded and supplies continuation offsets; shell output is bounded and points to a temporary file when truncated. This is context management at the tool boundary, before conversation summarization is necessary. [P1, P7]

## Steering, follow-up, and cancellation are different operations

`steer()` means “include this before the next model response after the current assistant turn's tools finish.” It does **not** cancel the running request or skip the rest of its tool batch. `followUp()` means “wait until the agent would otherwise stop.” Both queues default to one message at a time and support draining all queued messages instead. [P11]

Suppose the model already requested two edits when the user types “Only change the parser.” Steering will influence the next model request; those already-issued tools still complete. If the user needs execution to stop, cancellation is the relevant operation. `Agent.abort()` signals the active `AbortController`; tool implementations must cooperate. Bash kills its process tree on cancellation or timeout. File edits check cancellation after awaited operations and retain their mutation lock until those operations settle. Cancellation does not undo a write that has already happened. [P6, P7, P11]

`AgentSession` distinguishes a core run ending from the broader activity settling. Its post-run path can retry, compact and continue, or process extension-generated work. This matters for clients deciding when to enable another input or declare a task finished. [P12]

## Approval is policy; sandboxing is containment

The inspected default coding tool path executes local filesystem and shell operations. An always-on per-call approval policy or OS sandbox was not found in that path. The core supplies blocking hooks, and the repository includes a **permission-gate example** that asks about selected shell patterns. That is a policy decision before execution, not a guarantee about what an approved process can access. [P4, P7, P13]

A separate **sandbox example** replaces bash operations with `@anthropic-ai/sandbox-runtime`, applying filesystem/network restrictions to shell execution. Its scope is the overridden bash path; it should not be presented as confinement of every local file tool. Project trust is another boundary: it governs loading project configuration/resources, rather than making individual tools safe. [P13, P14]

Code mode introduces a third boundary. `pi-codemode` runs JavaScript inside a QuickJS/WebAssembly VM with injected tool functions. The script lacks direct host facilities such as `process`, modules, and `fetch`; an injected bash tool still has whatever authority its host implementation gives it. Restricted orchestration code and restricted operating-system commands solve different problems. [P15]

## Context and persistence: preserve history, project a smaller request

Long sessions must fit inside a model's context window. Pi estimates current usage from provider token accounting plus newer messages, with message-size estimates when necessary. Automatic compaction triggers when usage exceeds the context window minus reserved tokens; the baseline defaults reserve 16,384 tokens and aim to keep roughly 20,000 recent tokens. Settings and model-specific choices can alter effective behavior. [P16]

Compaction selects a retained tail without cutting at a tool result, summarizes older material, and can separately summarize the prefix of a turn that crosses the cut. It carries file-operation information and rejects summaries that hit the output length limit. The session appends a compaction entry containing the summary, the first retained entry ID, and the current system/tool state. The raw historical entries remain available. [P16, P17]

`SessionManager` stores an append-only JSONL tree: every entry has an ID and parent ID, and a current leaf chooses the active path. `buildSessionProjection()` turns that path into the effective messages, applying compaction and later context edits. A context edit can omit an earlier failed response from future model input without erasing the raw failure record. This separates “what happened” from “what the next request should see.” [P17]

Resuming opens a session and reconstructs its context. Branching moves the leaf so new entries grow from an earlier point; creating a branched session copies a selected path into another session file. Optional tree-navigation summaries preserve useful information from the abandoned path. These are conversation operations; the inspected session code does not restore filesystem contents to their historical state. [P18]

The coding session persists completed messages at `message_end`, after extension/public event dispatch. It is therefore inappropriate to describe its UI stream as a transactional crash-recovery log. The first user/assistant message causes initial file creation, and subsequent records append synchronously. [P12, P18]

## Providers, retries, and extensions

The agent accepts a `StreamFn`; the provider must produce a common event stream and encode request failures as final assistant messages with `error` or `aborted` stop reasons. `pi-ai`'s `Provider` interface owns authentication, model enumeration, and streaming; `Models.streamSimple()` normalizes the transcript, resolves authentication, and dispatches to the provider. This keeps provider-specific request formation outside the loop. [P9, P19]

Transient provider failures receive bounded exponential-backoff retries at the coding-session layer. The failed attempt stays in raw history but is omitted from subsequent model context. Context overflow takes a separate compact-and-retry path, with a guard against repeated recovery; it is not treated as ordinary throttling. Tool failures generally return to the model rather than automatically rerunning potentially side-effecting operations. [P10, P12]

Extensions can register tools and commands, intercept input/tool calls/results, transform context, participate in turn/settlement boundaries, and supply compaction. Built-in extensions include MCP, tool search, code mode, and llama.cpp integration. MCP uses a standalone `pi-mcp` client with stdio and Streamable HTTP transports, tool discovery, resources, and authentication handling. MCP tool definitions adapt server schemas and results into the same tool pipeline. [P14, P21]

Subagents require precise labeling. Coding-agent includes an **example extension**, installed separately, which launches child `pi --mode json -p --no-session` processes. It supports single, parallel, and chained work, with its own concurrency limits. This is not a default subagent tool in the four-tool coding set. Separately, `pi-durable` provides owned conversations and tasks, with foreground/background subagent examples. [P22, P23]

## MCP through code mode

Code mode lets the model write a short program that searches for tools, calls several, and sends only useful results back to the conversation. For example, a script can discover an MCP search tool, fetch three documents concurrently, then emit just their titles and relevant excerpts. The tool description budgets inline declarations; deferred tools remain discoverable through `searchTools`, `describeTool`, `describeNamespace`, and `ALL_TOOLS`. This reduces repeated model round trips and oversized tool-result messages, while making the script responsible for choosing what evidence to retain. [P25]

The coding-agent extension is registered but inactive until selected; MCP configuration can activate it when required. Each nested tool call goes through `ctx.executeTool`, preserving argument validation and `tool_call`/`tool_result` hooks. Code mode is therefore another caller of the tool pipeline, not a permission bypass. Tools with output schemas return structured content to scripts. MCP error results can resolve as structured objects, so scripts must inspect `isError`; other failures reject. Successful script store writes become branch-local session entries. The worker has a 256 MiB VM memory limit; cancellation and optional timeouts stop the script, without undoing completed external effects. [P25]

MCP authentication and transport stay on the host side. Connections negotiate capabilities and discover tools; tool-list changes refresh registrations. Resource read/list operations opt into one retry after a transient HTTP failure. `callTool` does not opt into that retry, regardless of a tool's read-only annotations, because the operation may already have run. An expired-session response has a separate reconnect-and-retry path because the server did not execute the request. These transport decisions are distinct from approval hooks and server-side authorization. [P21, P27]

## The newer durable harness changes the recovery problem

Saving a conversation is insufficient when a process crashes after a tool changes an external system but before its result is saved. Replaying that call could duplicate the change.

`pi-durable` records tool intent before execution. Recovery reruns a call only when **both the stored and current tool replay policies say safe**; otherwise it settles an interrupted result explaining that the operation may have partially run. Durable subagent examples use ownership plus stable request IDs to reconnect to an existing child/submission instead of creating duplicates. This is explicit recovery design, not a blanket exactly-once guarantee for arbitrary external effects. [P20, P23]

Its documented storage options include memory, JSONL, SQLite, and Cloudflare Durable Object storage. Chord supplies application/document state infrastructure, while `pi-env` supplies remote execution environments. These are valuable advanced reading paths, but a first harness can start with the smaller `Agent` loop. [P20]

## Multiplayer means shared humans or clients, not subagents

The coding-agent RPC interface is a long-lived subprocess speaking JSONL over stdin/stdout. It correlates command responses and streams session events for custom clients, but the inspected SDK/RPC path does not establish a turnkey authenticated multi-user session service. Merely sharing a transcript file would not solve concurrent input ownership or conflicting filesystem edits. [P1, P26]

The adjacent libraries offer useful building blocks: Chord exposes objects across workers, browser tabs, or servers, with per-principal variants and observable snapshots/deltas. The experimental durable harness's `ConversationViews` publishes committed conversation state to multiple observers and exposes a replicated state view. Initial snapshots and observer attachment share the session's serialized read boundary, preventing a missed commit between them. These are replication primitives; applications must still define membership, input arbitration, and permission policy. They should be labeled library support for multiplayer, not advertised as a finished coding-agent multiplayer UI. [P26]

## Tests, observation, and three design lessons

Core lifecycle events expose assistant deltas, tool progress, final results, duration, and turn boundaries. Session events add compaction, retry, queue, and settlement state; session statistics aggregate usage across stored history. The repository's faux provider makes ordering and failure cases testable without real model calls. For example, regression tests check that a later preflight abort prevents earlier prepared tools from starting, and that injected custom messages never split a tool-call/result pair. These tests were inspected, not executed for this research. [P12, P24]

1. **Keep the loop small, but define the boundaries rigorously.** Provider streaming, application context projection, tool execution, and presentation are independently replaceable. The cost is more contracts: when callbacks run, whether failures throw, and which event means truly idle all matter.
2. **Concurrency needs transcript rules as well as locks.** Completion-order progress gives responsive UI; source-order results preserve a predictable transcript. Same-file queues solve a narrower mutation problem. None of these mechanisms makes dependent tools safe to run concurrently by itself.
3. **Choose the persistence guarantee deliberately.** An append-only conversation tree supports inspection, compaction and branching. Durable task intent and replay policies address interrupted execution. Adopting the latter adds real complexity and is justified when work must survive process loss.

## Evidence matrix and reading path

Begin with P1 → P3 → P4/P5 → P12 → P17. Then explore provider adaptation, extensions, and the separate durable runtime. These claim IDs can be reused in the cross-harness comparison.

| ID | Claim and owning implementation | Pinned evidence |
| --- | --- | --- |
| P1 | `createAgentSession` composes resources, model, Agent and AgentSession; default tool selection and filtering | [sdk.ts L196–487](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/sdk.ts#L196-L487) |
| P2 | `prompt` processes input, validates startup, and queues active-run input | [agent-session.ts L2050–2144](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L2050-L2144) |
| P3 | `runLoop` scheduling, stop conditions, continuation hooks and queue boundaries | [agent-loop.ts L159–321](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L159-L321) |
| P4 | `prepareToolCall`, `runToolCall`, execution and finalization pipeline | [agent-loop.ts L675–906](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L675-L906) |
| P5 | Batch sequencing versus parallel preflight/execution/result order | [agent-loop.ts L498–650](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L498-L650) |
| P6 | `createEditToolDefinition` and `withFileMutationQueue` | [edit.ts L143–220](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/tools/edit.ts#L143-L220), [queue L1–61](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/tools/file-mutation-queue.ts#L1-L61) |
| P7 | Local bash spawning, cancellation and output handling; bounded read | [bash.ts L105–189](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/tools/bash.ts#L105-L189), [bash results L340–414](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/tools/bash.ts#L340-L414), [read.ts L86–224](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/tools/read.ts#L86-L224) |
| P8 | `declareToolChanges` records tool deltas in system messages | [agent-loop.ts L323–378](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L323-L378) |
| P9 | Stream contract and transform/convert boundary | [types.ts L19–37](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/types.ts#L19-L37), [streamAssistantResponse L380–454](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L380-L454) |
| P10 | Length-truncated calls fail; arguments are cloned, normalized and validated | [agent-loop.ts L456–496](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L456-L496), [validation.ts L317–353](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/utils/validation.ts#L317-L353) |
| P11 | Agent defaults, queues, cancellation and idle contract | [agent.ts L236–350](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent.ts#L236-L350) |
| P12 | Session message persistence, post-run recovery and retry | [events L1104–1200](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L1104-L1200), [post-run L1837–2008](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L1837-L2008), [retry L3735–3834](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L3735-L3834) |
| P13 | Optional approval and OS sandbox examples | [permission gate L1–34](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/permission-gate.ts#L1-L34), [sandbox L221–253](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/sandbox/index.ts#L221-L253) |
| P14 | Built-in extension list; project-trust bootstrap | [extensions/index.ts L1–14](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/index.ts#L1-L14), [resource-loader.ts L500–529](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/resource-loader.ts#L500-L529) |
| P15 | Documented code-mode VM capability boundary | [codemode README L1–63](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/codemode/README.md#L1-L63) |
| P16 | Compaction defaults, threshold, retained-tail preparation, summary validation | [compaction.ts L120–130](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/compaction/compaction.ts#L120-L130), [threshold L264–270](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/compaction/compaction.ts#L264-L270), [preparation L789–922](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/compaction/compaction.ts#L789-L922), [summary check L581–593](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/compaction/compaction.ts#L581-L593) |
| P17 | `buildContextEntries`, `buildSessionProjection`, `appendCompaction` | [projection L483–589](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/session-manager.ts#L483-L589), [compaction record L1261–1292](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/session-manager.ts#L1261-L1292) |
| P18 | JSONL persistence, branch/fork and reopen operations | [persistence L1156–1221](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/session-manager.ts#L1156-L1221), [branching/reopen L1577–1817](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/session-manager.ts#L1577-L1817) |
| P19 | Provider owns auth/catalog/streaming; normalized dispatch | [models.ts L155–224](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/models.ts#L155-L224), [dispatch L907–922](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/ai/src/models.ts#L907-L922) |
| P20 | Experimental durable harness, its task model and storage; adjacent packages | [durable README L1–117](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/README.md#L1-L117), [tool recovery L122–210](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/src/harness/tool.ts#L122-L210), [env manifest L1–5](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/env/package.json#L1-L5) |
| P21 | MCP transports, reconnection and schema/result adaptation | [runtime.ts L99–122](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/runtime.ts#L99-L122), [client behavior L239–308](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/runtime.ts#L239-L308), [tool adapter L312–354](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/tools.ts#L312-L354) |
| P22 | Coding subagent example launches child processes; install/use is separate | [subagent index L270–348](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/subagent/index.ts#L270-L348), [example README L1–110](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/examples/extensions/subagent/README.md#L1-L110) |
| P23 | Durable foreground child ownership and request-ID deduplication | [foreground example L42–82](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/test/examples/22-subagent-foreground.ts#L42-L82) |
| P24 | Faux-provider regression coverage for abort and message ordering | [preflight test L1–72](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/test/suite/regressions/8935-parallel-preflight-abort.test.ts#L1-L72), [ordering test L1–151](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/test/suite/regressions/8537-custom-message-tool-result-ordering.test.ts#L1-L151) |
| P25 | Code-mode activation, deferred discovery, nested policy pipeline and script state | [activation L1–49](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/codemode/index.ts#L1-L49), [catalog L118–278](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/codemode/tool.ts#L118-L278), [execution L428–557](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/codemode/execute.ts#L428-L557) |
| P26 | RPC is subprocess transport; Chord and durable supply replicated-view primitives | [RPC L1–35](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/docs/rpc.md#L1-L35), [Chord L1–29](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/chord/README.md#L1-L29), [ConversationViews L77–177](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/src/harness/view.ts#L77-L177) |
| P27 | MCP OAuth selection, capability negotiation and tool-list refresh | [auth L83–97](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/runtime.ts#L83-L97), [connection/refresh L345–446](https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/extensions/mcp/runtime.ts#L345-L446) |

Scope limits: the core loop, coding-session orchestration, session projection, compaction and principal coding tools received the deepest inspection. Provider-specific wire formats, the full durable scheduler, all extensions, and OS sandbox enforcement were not audited exhaustively. Extra-package descriptions combine manifest/documentation evidence with the explicitly linked implementation/example paths. No dependencies were installed, real providers called, tests run, or source files modified. Negative findings apply only to the inspected default paths; extensions or host applications can change the behavior.


---

# Goose: a persisted, ordered operation pipeline

Goose is useful for learning how an agent harness grows beyond “call the model, execute tools, repeat.” Its central design is an ordered pipeline of operations over a saved conversation. Approval, tool execution, compaction, retry policy, and model inference each get their own operation. After an operation changes state, Goose saves its effects and starts scanning the pipeline again. This makes control flow explicit, while moving some complexity into message metadata and operation ordering. [G1], [G2]

**Snapshot:** local `goose` checkout, clean tracked worktree, commit `34a222e3f2b95d8b4a43482b53c2cc957ecce26d`; origin `https://github.com/aaif-goose/goose.git`. Inspected October 18, 2026. All source links below are pinned to that commit. This chapter analyzes implementation, not benchmark performance. No Goose sessions, builds, or tests were executed.

## Start with four layers

![Goose architecture: the Agent assembles a state machine that loads session state, scans operations, persists effects from the first applicable operation, then reloads and scans again.](guide/assets/goose-architecture.svg)

*Simplified Goose-managed execution map. Providers that own their own loop follow a different path.*

1. **Clients:** the CLI and Electron desktop provide input, rendering, and permission responses. Desktop sends prompt/cancel/steer requests using Agent Client Protocol (ACP). CLI definitions expose `acp` over stdio and, with the `acp-http` feature, `serve` over HTTP/WebSocket. These are delivery surfaces, not separate model algorithms. [G3], [G4]
2. **Goose application:** `Agent` assembles provider, session manager, extensions, hooks, inspection, and steering. Its `reply` method returns a stream of `AgentEvent`s. [G5]
3. **Reusable engine:** `goose-agent` supplies `StateMachine`, `Operation`, and `InferenceRunner`. The Goose application selects and configures operations. An *effect* is a requested state change, such as appending a message or changing message visibility. [G1], [G6]
4. **Capabilities and storage:** providers translate model requests; extension clients implement tools; `SessionManager` stores conversations and session metadata in SQLite. [G7], [G8], [G9]

Do not equate every provider with a remote language-model API. The interface also allows providers that own their conversation context. The Goose builder skips its own full compaction for those providers. `ProviderDef::RUNS_OWN_TOOL_LOOP` separately identifies integrations handed MCP servers to run their own tool loop. This chapter's detailed trace describes Goose-managed execution. [G2], [G7], [G10]

## One user request, several model requests

A *user turn* here means the work initiated by a user message until the harness yields or stops. A model request is one inference within that work. Goose's `max_turns` mechanism counts contiguous assistant blocks since the kickoff message; it is not a universal measure of user turns, tokens, or elapsed time. Streaming chunks in one assistant block do not individually consume that count. [G6], [G11]

`reply_with_state_machine_inner` acquires a per-session active-turn guard and saves the user message. `stream_state_machine_session` resolves provider/model/context limit, builds the machine, and forwards its events. Inside the reusable machine, the first operation that applies wins that iteration. [G1], [G5]

```text
persist incoming message
build the ordered pipeline
repeat:
    session = load persisted session and conversation
    for step in pipeline order:
        stop if cancelled
        if step is inference:
            prepare a session/extension snapshot
            gather every operation's tools and prompt contributions
        result = run step
        if result applies:
            ensure message IDs
            persist result.effects
            break the scan
    if nothing applied, or result yields/cancels: stop
finalize cancellation and repair unanswered tool requests if needed
```

The concrete order begins with slash-command dispatch, approval, skills, recipes, extension tool execution, remaining-tool handling, subagents, steering, and bang-shell handling. Then come context operations and completion controls, ending with inference. Every restart begins at the front; an operation must recognize from persisted state that its work is already done. `Operation::set_message_meta` records past actions for this purpose. This is reconstructible control flow, not a claim that external side effects execute exactly once after arbitrary crashes. [G1], [G2], [G6]

## A read → edit → test → finish trace

This illustrative Unix trace assumes the developer extension and an ordinary model provider. It does **not** execute commands. At this revision, developer tools are flat names such as `shell`, `edit`, `write`, `tree`, and `read_image`; its instructions use shell commands to read text files. A fictional `read_file` call would misrepresent this particular tool surface. [G12]

1. User: “Fix the parser's empty-input case and verify it.” The message is saved; inference advertises the currently leased tool definitions.
2. Model: `shell({command: "cat src/parser.ts"})`. The assistant request gets an ID and an advertised-tool list. The next pipeline scan inspects permission, dispatches the tool, and persists its response under the same request ID. [G5], [G13]
3. The model sees the file and requests `edit` with path and exact replacement text. The tool description requires an exact, unique match. If approval is required, Goose records an action-required message and marks execution unavailable pending a decision. The outer agent stream waits for confirmation answers before rerunning the machine. [G12], [G14]
4. After editing, the next model request asks `shell` to run the appropriate focused test. A failed tool result is information returned to the model; it can choose another edit. The harness does not inherently know that this particular test proves the original request correct. [G13]
5. The model returns a plain final answer. If no recipe/goal/stop-hook operation asks for more work, inference no longer applies and the pipeline yields. An optional recipe success check can reject apparent completion and initiate another attempt. [G6], [G15]

The trace uses separate model requests because the edit depends on reading, and testing depends on the edit. Emitting dependent calls together would undermine that ordering.

## Tools: contracts, routing, concurrency

Tools use MCP's `Tool` representation, including JSON input schemas and annotations. The developer extension generates schemas from Rust parameter types and deserializes arguments into those types at execution. The inference adapter canonicalizes recoverable tool names, coerces arguments using the advertised schema, and records advertised tool names on assistant messages. Execution checks that model-originated requests were advertised. [G12], [G16], [G13]

An extension lease is the selected set of extension instances and capabilities for a session snapshot. Its catalog maps a public tool name to its extension and server-side name. A call resolves that entry, builds a context containing session, working directory, and cancellation, and calls the extension's MCP client. Platform extensions run in the agent process; configuration also supports bundled, stdio, and Streamable HTTP extensions. MCP standardizes the contract, not the trustworthiness of the implementation. [G8], [G17]

Concurrency has two stages. `ToolExecutionOperation::run` prepares dispatches in a loop, then uses `select_all` to poll their result and notification streams together. Thus tool futures can progress concurrently; this is not a sequential “await each complete result” loop. `ToolBatch::record` adds responses as observed, deduplicating by request ID. There is no request-order sorting in this operation: result order can differ from request order. Notifications can reach the client before the batch finishes; the final response message is emitted and persisted after the combined stream finishes. Cancellation finalization can preserve results already recorded in the batch. [G13]

The engineering implication is specific: independent reads can overlap, but the model or caller must preserve dependencies between writes and subsequent tests. The generic execution operation does not infer a filesystem dependency graph. That is a bounded observation about this operation, not a statement about every extension's internal scheduling.

## Approval is policy; isolation is execution environment

Permission inspection distinguishes `Chat`, `Auto`, `Approve`, and `SmartApprove`. In the permission inspector, Auto allows calls; Approve and SmartApprove consult explicit tool permissions. SmartApprove additionally uses read-only annotations and model-based classification for candidates. Other security inspectors can override the permission baseline. A read-only hint or model judgment is a policy input, not an operating-system guarantee. [G18], [G14]

Approval requests and responses are saved conversation records. The wrapper can reconnect to pending confirmations or apply a saved answer, but the original extension lease still matters: when an executable request has no valid retained lease, tool execution returns an “approval expired” response asking for a new request. Therefore “the conversation is persistent” must not be shortened to “every pending authorized action can be replayed after restart.” [G5], [G13]

For execution isolation, the inspected stdio connector supports explicitly running an extension using `docker exec` when supplied a container; without it, it starts the configured local process. The developer platform extension's ordinary shell builder launches a host shell with the selected working directory. Neither path shown establishes a universal filesystem/network sandbox for all Goose tools. Container configuration is a separate deployment choice, and the stdio container branch should not be advertised as enclosing all in-process platform tools or remote MCP servers. [G19], [G20]

## Context and persistence are different views of history

Goose records user visibility and agent visibility separately. The provider receives a projection of the saved conversation, not necessarily the entire transcript. Inference removes unanswered old tool requests from prior turns, repairs provider-facing conversation structure, and merges compatible messages. [G21]

Compaction can run before inference when usage exceeds the configured threshold, or reactively after a context-length error. The operation bounds repeated reactive attempts; providers owning context bypass Goose's full compaction. `compact_messages` summarizes the agent-visible history, makes original messages agent-invisible, adds an agent-only summary and continuation instructions, and preserves the current text request for automatic compaction. The user-visible history can therefore remain longer than the model context. Optional tool-pair summarization is separately configured and defaults off in the inspected configuration helper. [G22], [G23]

`SessionManager` persists messages and session configuration in `sessions.db`, using SQLite WAL mode. Effects include append/replace, request-metadata patches, visibility changes, recipe/extension state, and usage. Compaction saving updates existing records' agent visibility and inserts new summary records in a transaction rather than blindly deleting concurrent messages. Session persistence supports reconstruction, while streaming output may be visible before its operation's effects have been stored. Approval requests deliberately publish after persistence so an immediate answer can find them. [G9], [G24], [G33]

## Steering and multiple clients

Steering is guidance submitted while a run is active. `Agent::steer` adds a message to a session's in-memory queue. `SteerOperation` drains that queue after tool results or a completed assistant response, emits the messages, invokes prompt-submit hooks, and returns persistence effects. It does not splice instructions into an in-flight model request. Undrained steers are not yet durable transcript entries; the ACP wrapper discards pending steers when clearing the run. [G34], [G35]

For **multiple humans or clients**, the ACP server shares an `ActiveRunRegistry` across connection-specific agents. It admits only one prompt run per session; a competing prompt gets an error directing the caller to steer. A second connection can steer the agent owning the active run, supplying `expectedRunId` to reject stale targeting. The factory contains a test specifically for this cross-connection routing. [G35], [G36]

This establishes shared input arbitration, not a complete multiplayer editing experience. The inspected `forward_agent_stream` sends notifications through the prompting connection; loading a session replays stored history. These paths do not establish a generic live broadcast subscription, per-human authorship model, or conflict-free shared editor. Treat those as unverified product capabilities, separately from the demonstrated multi-client steering and the separate subagent mechanism. [G37], [G38]

## Code mode for MCP: a second tool-calling shape

Goose's optional Code Mode is compiled behind the `code-mode` feature and registered with `default_enabled: false`. It exposes `list_functions`, `get_function_details`, and `execute_javascript`. Discovery lists exact callable names and short descriptions; details return original input/output schemas. The model writes `async function run()` and invokes `await tools.call(name, args)`, using `Promise.all` for independent calls. The callback catalog is taken from the dispatching extension lease; it excludes app-only tools, MCP resource tools, the code-mode extension itself, and Summon's delegate tool. [G39], [G40]

The model-facing tool list is reduced, but first-class platform tools and MCP resource tools remain directly visible. Thus code mode is not “all tools replaced by one interpreter.” Its practical advantage is letting JavaScript combine/filter results before returning text to the model, reducing intermediate context traffic; this is a design rationale, not a measured speed or token claim. [G41]

The interpreter uses Boa with an idle module loader and injected `tools`/`console`, rather than a Node.js environment. It limits instructions, loop iterations, recursion, output, worker slots, and execution time. Its own documented interface has no ambient filesystem, shell, network, packages, or environment access; external effects go through callable tools. These interpreter restrictions do not confine the external tools themselves. [G40], [G42]

There is an important policy boundary: the outer `execute_javascript` request follows normal tool approval/execution, while nested `dispatch_tool` invokes the lease directly. The inspected nested path does not re-enter `ToolApprovalOperation` or `run_pre_tool_hooks` for each callback. Therefore do not assume approving one script provides the same per-call review as separate model-issued tool requests; underlying tools may also enforce their own policy. Nested tool errors reject the JavaScript call and can be caught, and cancellation propagates to dispatch tasks. [G13], [G14], [G40]

## Cancellation, errors, and three distinct retries

The machine races each step against a cancellation token. On cancellation, it asks operations to retain partial output and creates interrupted responses for unanswered tool calls. Inference retains accumulated chunks; tool execution retains completed batch results. The developer shell handles cancellation by killing and waiting for its child, but this does not establish rollback of file edits or cancellation guarantees for every remote tool. [G1], [G13], [G20], [G21]

Keep these mechanisms separate:

- **Transport retry:** Goose's provider-stream adapter retries qualifying transient first-item failures before accepting stream output, with backoff and provider retry policy. This branch skips providers owning context. It is not a general replay of partially emitted output. [G25]
- **Empty-response retry:** `InferenceRunner` can retry an empty response up to three times, with special handling after successful tool responses. Exhaustion leaves a marker that completion operations can handle. [G21]
- **Task retry:** configured recipe checks can reject a completed answer, run an optional failure command, reset history to the kickoff, and try again within `max_retries`. Resetting conversation history does not undo files or external side effects. [G15]

Unrecovered errors eventually reach `ExitOnErrorOperation`, which yields. The operation pipeline therefore expresses both continuation policy and exit policy; neither should be inferred merely from whether the model emitted text. [G26]

## Extensions, recipes, and subagents

Recipes package instructions/prompt, extensions, provider/model settings, parameters, optional output schema, sub-recipes, and retry configuration. They describe reusable task setup and completion contracts; they are not a second implementation of the base state machine. Skills and hook operations add prompt material or lifecycle behavior around the same pipeline. [G2], [G27]

Foreground subagents have separate persisted sessions linked to the parent. Their saved recipe, provider/model, extension selection, and turn budget are used to prepare execution. The reusable `SubagentOperation` tracks multiple child update streams with `SelectAll`, persists delivered outcomes as agent-only parent messages, and records cancelled children so reconstruction does not relaunch them. A child can forward approval or elicitation requests to the parent. [G28], [G29]

The inspected Goose runner refuses to discover new subagents when the parent is already a `SubAgent`, and validates that a child belongs to the requesting parent. This is a bounded nesting restriction in this runner. These children are foreground work managed by the parent turn; the source explicitly distinguishes this implementation from older background-delegation history. [G30]

## What a builder should borrow

**Separate a decision from its durable effect.** A permission answer can arrive immediately. Persisting its request before emitting it prevents a race; recording operation notes lets reconstructed control flow recognize completed work. The cost is that metadata becomes part of the state-machine contract and needs migration/reconstruction tests. [G24], [G6]

**Separate transcript, model context, and live events.** A learner might delete old messages to save tokens, inadvertently destroying the audit trail. Goose's visibility projection and summary records preserve both views. The cost is more invariants: hidden messages, request/result pairing, and compaction must all agree. [G21], [G23]

**Make capability ownership explicit.** A provider may own context; an extension owns execution; the client owns interaction. This allows many integrations, but means “supports MCP,” “has approvals,” and “runs safely in a container” are three different claims. A new harness should expose those boundaries before adding more integration options. [G7], [G10], [G17], [G19]

For verification, the repository includes a dummy-provider pipeline test that reconstructs after every applied step and checks a two-inference tool exchange. Observability includes streamed message/usage/MCP events and tracing spans for inference, tool execution, and agent invocation. These are source-level capabilities; their presence is not evidence that tests passed here or that a telemetry exporter is enabled in every installation. [G31], [G32]

## Evidence matrix and reading path

Read G1 → G6 → G2 first, then G5 for the outer wrapper. Follow G13/G14/G8 for execution, and G21/G23/G24 for model context and durable state. Each row is a bounded implementation claim.

| ID | Claim | Source symbol |
|---|---|---|
| G1 | First applicable operation; save/reload; cancellation repair | [StateMachine::step/run/finalize][G1] |
| G2 | Concrete operation order and optional compaction | [build_state_machine][G2] |
| G3 | Desktop uses ACP prompt/cancel/steer | [acpPromptSession][G3] |
| G4 | CLI ACP stdio and feature-gated HTTP server | [Command::Acp/Serve][G4] |
| G5 | Persist kickoff, stream events, wait/resume approvals | [Agent::reply_with_state_machine_inner][G5] |
| G6 | Operation notes, kickoff and assistant-block counting | [Operation; assistant_turn_count][G6] |
| G7 | Provider stream/resume/context-ownership contract | [Provider][G7] |
| G8 | Lease resolves tools and builds execution context | [ExtensionLease::call_resolved][G8] |
| G9 | SQLite file and WAL storage configuration | [SessionStorage::create_pool/new][G9] |
| G10 | Some providers run their own tool loop | [ProviderDef][G10] |
| G11 | Autonomous assistant-block budget | [MaxTurnsOperation][G11] |
| G12 | Flat developer tools and typed schemas | [DeveloperClient::get_tools/call_tool][G12] |
| G13 | Advertised-tool check, lease expiry, concurrent streams | [ToolExecutionOperation::run][G13] |
| G14 | Durable approval effects and metadata | [ToolApprovalOperation::run][G14] |
| G15 | Recipe success checks and bounded retries | [RetryOperation::run][G15] |
| G16 | Tool-name recovery and argument coercion | [prepare_tool_requests][G16] |
| G17 | In-process, bundled, stdio, HTTP extension types | [ExtensionConfig][G17] |
| G18 | Modes, explicit permissions, read-only classification | [PermissionInspector::inspect][G18] |
| G19 | Optional Docker branch for stdio subprocesses | [connect; docker_exec][G19] |
| G20 | Host-shell creation, timeout and child cancellation | [run_command; build_shell_command][G20] |
| G21 | Provider projection, partial output, empty retries | [InferenceRunner::infer][G21] |
| G22 | Proactive/reactive compaction policy | [CompactionOperation::run][G22] |
| G23 | Visibility-based summarization and default-off pair summaries | [compact_messages][G23] |
| G24 | Durable effects and post-persistence approval emission | [SessionManager::apply_effects][G24] |
| G25 | Transient retry before first accepted stream item | [stream_response_from_provider][G25] |
| G26 | Unhandled trailing errors stop execution | [ExitOnErrorOperation::run][G26] |
| G27 | Recipe task/settings/output/retry fields | [Recipe][G27] |
| G28 | Child-stream concurrency and durable delivery notes | [SubagentOperation::run][G28] |
| G29 | Saved child configuration and forwarded decisions | [prepare_subagent; ForegroundSubagentRunner][G29] |
| G30 | Parent ownership and bounded nesting restriction | [SubagentRunner implementation][G30] |
| G31 | Reconstruction after every applied step | [tool_turn_reconstructs_after_every_applied_step][G31] |
| G32 | Client-visible event variants | [AgentEvent][G32] |
| G33 | Compaction transaction preserves existing rows | [save_compacted_conversation][G33] |
| G34 | Steering is drained between model/tool phases | [SteerOperation::run][G34] |
| G35 | Single active prompt; run-ID-checked steering | [start_active_run; require_active_run][G35] |
| G36 | Connections share ownership registry | [AcpServer::create_agent_with_session_cwd][G36] |
| G37 | Stream forwarding targets the current connection | [forward_agent_stream][G37] |
| G38 | Loading replays persisted conversation | [handle_load_session][G38] |
| G39 | Code Mode is optional and disabled by default | [PLATFORM_EXTENSIONS registration][G39] |
| G40 | Discovery, leased catalog, nested callback dispatch | [CodeExecutionClient; dispatch_tool][G40] |
| G41 | Some direct tools remain in code mode | [prepare_inference_tools][G41] |
| G42 | Restricted Boa context and bounded workers | [evaluate; execute_with_slots][G42] |

Limitations: the detailed path is Goose-managed inference and extension execution at this commit. Provider-specific internal loops, every remote MCP server, every platform-specific process behavior, and full crash consistency were not exhaustively audited. Code-mode restrictions and multiplayer conclusions are bounded to the cited paths. No runtime benchmark or security certification is implied.

[G1]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/machine.rs#L118-L292
[G2]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine_builder.rs#L43-L136
[G3]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/ui/desktop/src/acp/prompt.ts#L9-L35
[G4]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-cli/src/cli.rs#L832-L859
[G5]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/agent.rs#L639-L1040
[G6]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/operation.rs#L32-L203
[G7]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/base.rs#L520-L697
[G8]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/extension_manager/lease.rs#L573-L729
[G9]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/session/session_manager.rs#L1056-L1078
[G10]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/providers/base.rs#L31-L69
[G11]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_maxturns.rs#L35-L68
[G12]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/developer/mod.rs#L41-L255
[G13]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_toolcalling.rs#L251-L1106
[G14]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_tool_approval.rs#L39-L164
[G15]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_retry.rs#L170-L279
[G16]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_llm.rs#L75-L200
[G17]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/extension.rs#L158-L234
[G18]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/permission/permission_inspector.rs#L134-L283
[G19]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/extension_manager/stdio.rs#L14-L53
[G20]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/developer/shell.rs#L414-L593
[G21]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/inference.rs#L225-L560
[G22]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_compaction.rs#L184-L295
[G23]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/context_mgmt/mod.rs#L29-L225
[G24]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/session.rs#L40-L370
[G25]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/reply_parts.rs#L245-L375
[G26]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_exit_on_error.rs#L15-L33
[G27]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/recipe/mod.rs#L43-L130
[G28]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/subagent.rs#L57-L290
[G29]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/subagent_handler.rs#L36-L287
[G30]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_subagent.rs#L15-L148
[G31]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/tests/reconstruction_isolation_lifecycle.rs#L18-L38
[G32]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-agent/src/events.rs#L9-L18
[G33]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/session/session_manager.rs#L2198-L2255
[G34]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_steer.rs#L46-L98
[G35]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/acp/server.rs#L1876-L1957
[G36]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/acp/server_factory.rs#L85-L233
[G37]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/acp/server.rs#L2103-L2241
[G38]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/acp/server/load_session.rs#L381-L429
[G39]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/mod.rs#L154-L172
[G40]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/code_execution.rs#L22-L335
[G41]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/reply_parts.rs#L184-L207
[G42]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/platform_extensions/javascript.rs#L27-L429


---

# 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](#event-driven-execution), 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

