Inside the Harness
Models propose. Harnesses coordinate.
Download the complete guide ↓Evidence & scope ↗
Learn / Field guide

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

Scroll sideways to explore the diagram →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.

This is a teaching abstraction, not a claim that all three implementations use 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. Their chapters show the actual implementations.

Use these terms throughout the guide:

TermMeaning here
Model requestOne request to a model provider, possibly streamed.
Tool callA model-produced name, arguments, and identifier that the harness resolves to executable behavior.
Tool resultThe outcome associated with that identifier, including failure or denial.
RunWork initiated by user input, potentially containing many model requests and tool calls.
SessionConversation state and metadata that can outlive a run.
ContextThe portion and representation of state sent in the next model request.
HarnessThe 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.

StepModel or user behaviorHarness responsibility
1User asks for a parser fix.Select instructions, tools, model, and conversation context.
2Model requests read_file("parser.ts").Resolve the tool, validate its arguments, check access, execute, and associate its result with the call ID.
3Model requests an edit based on the file contents.Check the edit's preconditions and execute the mutation under the configured policy.
4Model requests a test command.Manage a process, collect bounded output, propagate cancellation, and report success or failure accurately.
5Test fails and the model proposes a second edit.Continue with the observed failure in context. A failed command need not be a failed run.
6Tests 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 to study a clearly exposed loop and its hooks. Then read Goose to see ordered operations and effects express control flow. Read Codex for task lifecycles, tool scheduling, policy enforcement, and client coordination. This is an educational reading order, not a product ranking.

Then use the comparison and advanced topics to follow one concern through all three. The builder's lab turns the comparison into design exercises.

Learn / Field guide

Three ways to organize the same feedback loop

All three 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.

The short descriptions below are an interpretation of the inspected architecture, not a claim about which product is best. See the methodology 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

HarnessCenter of the designWhat a builder can learn
CodexSession 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.
piA reusable agent loop exposes hooks; the coding session adds application behavior around it.How to separate provider normalization, loop scheduling, and product-level state.
GooseAn 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.

Sources: Codex turn preparation, pi loop, Goose state machine.

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.

Compare behavior, not feature counts

ConcernCodexpiGoose
Concurrent toolsParallel-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.
Context managementClient-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.
SteeringActive-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.
PersistenceA 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.
Multi-player foundationsMultiple 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.
Multi-agent supportChild-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.
MCP and code modeDeferred 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.

Each row compresses important details. Follow the evidence matrices in Codex, pi, and Goose, 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. Both 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, pi scheduling, Goose execution.

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, Codex turn processing, Goose steering.

A conversation is too large

All three can continue from a reduced representation, but they store and adopt that representation 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. “Compaction supported” therefore says little about retained raw history, replay, adoption races, or context reconstruction. The advanced chapter compares those contracts.

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.

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.

Learn / Field guide

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, pi, and Goose chapters contain the detailed evidence and reading paths.

Explore the topics

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

  • Compaction — Understand when a summary replaces model context, what survives, and how to adopt it without losing new input.

  • Steering & cancellation — Distinguish input admission, follow-up queues, and cancellation at model and tool boundaries.

  • Multi-player — Explore shared observation, competing input, approval authority, and reconnection across humans and clients.

  • Multi-agent delegation — Define ownership, workspace isolation, cancellation, and result contracts when a parent delegates work.

  • MCP — Follow tool discovery, catalog changes, authorization, and connection lifetimes across server boundaries.

  • Code mode for MCP — Compare model-written tool orchestration, nested-call policy, execution limits, and result shapes.

  • Persistence & recovery — Separate recorded history from durable effects and handle uncertainty after a crash.

  • Observability — Trace the boundaries that explain latency, completion, and the meaning of tool timing.

Put the ideas into practice

Use the builder's lab to exercise these boundaries with deterministic scenarios. The design comparison connects individual topics to each harness's overall design.

Advanced topics / Field guide

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?

HarnessTrigger and transformationState consequence
CodexThresholds 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.
piEstimated 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.
GooseToken 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 and background adoption, pi compaction and session projection, Goose compaction operation and storage update.

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.

Advanced topics / Field guide

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, Codex start/steer, Goose steering.

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.

Advanced topics / Field guide

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?

CapabilityCodexpiGoose
Shared observationApp-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 arbitrationTurn 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 workspaceNot established by these primitives.Not established in the inspected coding-agent SDK/RPC.Not established by these paths.

Sources: Codex subscriptions and turn checks, pi RPC and durable views, Goose shared registry and notification forwarding.

“Not established” is a scope limit, not an absence claim. These 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.

Advanced topics / Field guide

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, pi example and durable example, Goose subagents.

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.

Advanced topics / Field guide

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 and search, pi MCP runtime, Goose lease.

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.

Advanced topics / Field guide

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:

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.

HarnessRuntime and discoveryNested-call boundary
CodexFresh 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.
piSelectable 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.
GooseFeature-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 and nested dispatch, pi nested execution, Goose code execution and registration.

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, pi MCP tool adapter.

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.

Advanced topics / Field guide

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, pi events and durable recovery, Goose effect application and tool 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.

Advanced topics / Field guide

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, pi nested-call timing.

The codebase chapters identify concrete events and tests for each implementation. The builder's lab supplies deterministic scenarios for these boundaries. Together they provide a more useful starting point than a checklist of feature names.

Practice / Field guide

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

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, Codex, and Goose.

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.”

Reference / Field guide

Evidence and scope

This resource compares the source checkouts available on 18 October 2026. It explains implementation choices; it does not rank products or measure task-solving quality.

HarnessRepositoryInspected commit
Codexopenai/codex8c076942cb70fba8e5c4732c23f1faabc4aed484
piearendil-works/pi482c1381a00d8d00bc81697e4a980c593db6803e
Gooseaaif-goose/goose34a222e3f2b95d8b4a43482b53c2cc957ecce26d

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

One research agent investigated each repository using its local source, documentation, and test definitions. 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 three projects.

How to read support labels

LabelInterpretation
CoreImplemented in the primary runtime or documented baseline under discussion. This does not mean enabled in every configuration.
OptionalA flag, setting, extension, build feature, or deployment choice is needed.
LibraryA separate reusable package provides primitives; the default application may not use them.
ExampleSupplied as an example or pattern, rather than a built-in workflow.
PartialThe inspected implementation covers part of the capability; read the qualification.
UnknownNot 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 three agents 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.

Reference / Field guide

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

FileRole
registry.jsonHarness identities, repositories, snapshot metadata, common comparison axes, and guide navigation.
Each snapshot's chapterHuman-readable explanation with evidence and a source reading path.
Each snapshot's facts JSONOne 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.mdCommon research assignment and chapter structure.
website/Renderer and interactive learning interface. Generated output lives in website/dist/.
scripts/validate.mjsStructural 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:

{
  "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:

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). 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.

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 three 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.

← Comparison explorer
Codex / Source reading
8c076942cb70 ↗Reviewed 2026-10-18

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.

Scroll sideways to explore the diagram →Codex architecture: a RegularTask captures each sampling step, routes tools, collects ordered results, and checkpoints thread state.

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:

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

An illustrative read → edit → test → finish trace

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

StageModel and harness behavior
InputUser asks to fix an off-by-one error in src/range.py. The harness records input and builds the first step.
ReadModel requests exec_command with cat src/range.py. The harness parses arguments, applies execution policy, runs the command, and records the call/result pair.
EditThe 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.
TestA 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 finishA 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

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

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

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.

IDSource-backed claimPrimary code entry point
C1Core/frontend seam; exec starts an in-process app-server clientcore README; exec startup
C2A task can repeat run_turn; each run can repeat samplingRegularTask::run
C3Inputs and coherent step state are prepared/admitted at loop boundariesrun_turn
C4Follow-up, pending input, and Stop hooks influence completionrun_turn continuation logic
C5Patch has a freeform grammar and verified execution pathcreate_apply_patch_freeform_tool; handle_call
C6Router separates exposure and payload formsToolRouter
C7Registry hooks surround execution; dynamic tools await client outputdispatch_any_with_state; dynamic handler
C8Parallel tools use shared admission; others exclusivehandle_tool_call_with_source
C9MCP parallel metadata, binding snapshot, and readinessMcpHandler
C10Concurrent results are drained in insertion orderdrain_in_flight
C11Approval/sandbox attempt and escalation are policy-sensitiveToolOrchestrator::run
C12Platform sandbox support differscore sandbox documentation
C13Automatic thresholds and full context limits remain distinctcontext-window status; model-switch compaction
C14Client compaction summarizes and reconstructs retained contextrun_compact_task_inner_impl; replacement history
C15Provider capability selects remote compactionrun_auto_compact
C16Token-budget rollover does not invoke summarizationtoken-budget compaction
C17Background compaction is feature/capability gated and adopted laterasync compaction lifecycle
C18Start may steer; explicit steering checks expected turn IDstart_or_steer_turn caller; steer checks
C19Multiple client subscriptions and ordered attach/history mechanismlistener command; ThreadStateManager
C20Spawned child input has a separate ownership constraintdirect-input guard
C21Deferred lexical search, cache, and Code Mode policy filteringToolSearchHandler
C22Fresh V8 cell, controlled globals, rejected importsruntime; globals; module loader
C23Nested Code Mode calls reenter normal routing with origin contextnested dispatch; cell delegate
C24Persistence contexts distinguish enqueue from durability barriersPersistContext and ThreadStore
C25Resume and compaction-aware reconstruction have explicit contractsresume_thread contract; reconstruction
C26Cancellation can preserve completed/finish-on-cancel outcomestool cancellation branch
C27Stream retry limits, optional unbounded path, and fallback differhandle_response_stream_error
C28Provider interface and capability boundariesModelProvider; capabilities
C29Skills/plugins and lifecycle contributors extend turn preparationbuild_skills_and_plugins; task lifecycle
C30Child thread tools, inheritance, and spawn cleanuptool surface; spawn; spawn guard; interrupt
C31Runtime timing and tool-exposure registration metrics existtiming guard; registration metrics
C32Contract tests exist for cancellation and search cachingcancellation tests; search tests

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.

← Comparison explorer
pi / Source reading
482c1381a00d ↗Reviewed 2026-10-18

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

Scroll sideways to explore the diagram →Pi architecture: AgentSession wraps the reusable Agent loop, provider boundary, tool batches, and ordered results, with separate JSONL session storage.

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]

LayerResponsibilityWhat a harness builder can reuse
packages/ai (pi-ai)Common messages, model metadata, provider authentication and streamingOne provider boundary instead of provider-specific logic throughout the loop
packages/agent (pi-agent-core)In-memory state, model/tool loop, queues, lifecycle eventsThe execution loop without the terminal application
packages/coding-agentCoding tools, resources, extensions, compaction, retry and sessionsA complete application or its AgentSession SDK
packages/tuiTerminal components consumed by coding-agentPresentation separate from agent decisions
packages/durableA separate, experimental durable harnessPersisted tasks and explicit crash-recovery semantics
packages/chord, codemode, envApplication state/RPC; restricted JavaScript execution; remote execution environmentsAdditional 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.

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.

IDClaim and owning implementationPinned evidence
P1createAgentSession composes resources, model, Agent and AgentSession; default tool selection and filteringsdk.ts L196–487
P2prompt processes input, validates startup, and queues active-run inputagent-session.ts L2050–2144
P3runLoop scheduling, stop conditions, continuation hooks and queue boundariesagent-loop.ts L159–321
P4prepareToolCall, runToolCall, execution and finalization pipelineagent-loop.ts L675–906
P5Batch sequencing versus parallel preflight/execution/result orderagent-loop.ts L498–650
P6createEditToolDefinition and withFileMutationQueueedit.ts L143–220, queue L1–61
P7Local bash spawning, cancellation and output handling; bounded readbash.ts L105–189, bash results L340–414, read.ts L86–224
P8declareToolChanges records tool deltas in system messagesagent-loop.ts L323–378
P9Stream contract and transform/convert boundarytypes.ts L19–37, streamAssistantResponse L380–454
P10Length-truncated calls fail; arguments are cloned, normalized and validatedagent-loop.ts L456–496, validation.ts L317–353
P11Agent defaults, queues, cancellation and idle contractagent.ts L236–350
P12Session message persistence, post-run recovery and retryevents L1104–1200, post-run L1837–2008, retry L3735–3834
P13Optional approval and OS sandbox examplespermission gate L1–34, sandbox L221–253
P14Built-in extension list; project-trust bootstrapextensions/index.ts L1–14, resource-loader.ts L500–529
P15Documented code-mode VM capability boundarycodemode README L1–63
P16Compaction defaults, threshold, retained-tail preparation, summary validationcompaction.ts L120–130, threshold L264–270, preparation L789–922, summary check L581–593
P17buildContextEntries, buildSessionProjection, appendCompactionprojection L483–589, compaction record L1261–1292
P18JSONL persistence, branch/fork and reopen operationspersistence L1156–1221, branching/reopen L1577–1817
P19Provider owns auth/catalog/streaming; normalized dispatchmodels.ts L155–224, dispatch L907–922
P20Experimental durable harness, its task model and storage; adjacent packagesdurable README L1–117, tool recovery L122–210, env manifest L1–5
P21MCP transports, reconnection and schema/result adaptationruntime.ts L99–122, client behavior L239–308, tool adapter L312–354
P22Coding subagent example launches child processes; install/use is separatesubagent index L270–348, example README L1–110
P23Durable foreground child ownership and request-ID deduplicationforeground example L42–82
P24Faux-provider regression coverage for abort and message orderingpreflight test L1–72, ordering test L1–151
P25Code-mode activation, deferred discovery, nested policy pipeline and script stateactivation L1–49, catalog L118–278, execution L428–557
P26RPC is subprocess transport; Chord and durable supply replicated-view primitivesRPC L1–35, Chord L1–29, ConversationViews L77–177
P27MCP OAuth selection, capability negotiation and tool-list refreshauth L83–97, connection/refresh L345–446

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.

← Comparison explorer
Goose / Source reading
34a222e3f2b9 ↗Reviewed 2026-10-18

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

Scroll sideways to explore the diagram →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.

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 AgentEvents. 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

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.

IDClaimSource symbol
G1First applicable operation; save/reload; cancellation repairStateMachine::step/run/finalize
G2Concrete operation order and optional compactionbuild_state_machine
G3Desktop uses ACP prompt/cancel/steeracpPromptSession
G4CLI ACP stdio and feature-gated HTTP serverCommand::Acp/Serve
G5Persist kickoff, stream events, wait/resume approvalsAgent::reply_with_state_machine_inner
G6Operation notes, kickoff and assistant-block countingOperation; assistant_turn_count
G7Provider stream/resume/context-ownership contractProvider
G8Lease resolves tools and builds execution contextExtensionLease::call_resolved
G9SQLite file and WAL storage configurationSessionStorage::create_pool/new
G10Some providers run their own tool loopProviderDef
G11Autonomous assistant-block budgetMaxTurnsOperation
G12Flat developer tools and typed schemasDeveloperClient::get_tools/call_tool
G13Advertised-tool check, lease expiry, concurrent streamsToolExecutionOperation::run
G14Durable approval effects and metadataToolApprovalOperation::run
G15Recipe success checks and bounded retriesRetryOperation::run
G16Tool-name recovery and argument coercionprepare_tool_requests
G17In-process, bundled, stdio, HTTP extension typesExtensionConfig
G18Modes, explicit permissions, read-only classificationPermissionInspector::inspect
G19Optional Docker branch for stdio subprocessesconnect; docker_exec
G20Host-shell creation, timeout and child cancellationrun_command; build_shell_command
G21Provider projection, partial output, empty retriesInferenceRunner::infer
G22Proactive/reactive compaction policyCompactionOperation::run
G23Visibility-based summarization and default-off pair summariescompact_messages
G24Durable effects and post-persistence approval emissionSessionManager::apply_effects
G25Transient retry before first accepted stream itemstream_response_from_provider
G26Unhandled trailing errors stop executionExitOnErrorOperation::run
G27Recipe task/settings/output/retry fieldsRecipe
G28Child-stream concurrency and durable delivery notesSubagentOperation::run
G29Saved child configuration and forwarded decisionsprepare_subagent; ForegroundSubagentRunner
G30Parent ownership and bounded nesting restrictionSubagentRunner implementation
G31Reconstruction after every applied steptool_turn_reconstructs_after_every_applied_step
G32Client-visible event variantsAgentEvent
G33Compaction transaction preserves existing rowssave_compacted_conversation
G34Steering is drained between model/tool phasesSteerOperation::run
G35Single active prompt; run-ID-checked steeringstart_active_run; require_active_run
G36Connections share ownership registryAcpServer::create_agent_with_session_cwd
G37Stream forwarding targets the current connectionforward_agent_stream
G38Loading replays persisted conversationhandle_load_session
G39Code Mode is optional and disabled by defaultPLATFORM_EXTENSIONS registration
G40Discovery, leased catalog, nested callback dispatchCodeExecutionClient; dispatch_tool
G41Some direct tools remain in code modeprepare_inference_tools
G42Restricted Boa context and bounded workersevaluate; execute_with_slots

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.

Page not found

Choose a chapter from the contents or start here.