# Agent runtime and tools

> The Code Mode agent loop: the tool set (`readFile`, `writeFile`, `editFile`, `executeCode`, `describeBinding`, `setGadgetBinding`, `createGadget`, `listBlueprints`, `listConnectableResources`, `requestConnection`, `webFetch`, `observeUserChanges`, `giveUp`), how `prepareChatBindings` folds ambient gatekeepers into `env` under `suggestedBindingName`, slash-command collection, and chat compaction checkpoints.

- Repository: cloudflare/cloudflare-os
- GitHub: https://github.com/cloudflare/cloudflare-os
- Human docs: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd
- Complete Markdown: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd/llms-full.txt

## Source Files

- `packages/workshop-backend/src/agent.ts`
- `packages/workshop-backend/src/agent-catalog.ts`
- `packages/workshop-backend/src/agent-compaction.ts`
- `packages/workshop-backend/src/slash-commands.ts`
- `packages/workshop-backend/src/overseer.ts`
- `packages/workshop-backend/src/web-fetch.ts`

---

---
title: "Agent runtime and tools"
description: "The Code Mode agent loop: the tool set (`readFile`, `writeFile`, `editFile`, `executeCode`, `describeBinding`, `setGadgetBinding`, `createGadget`, `listBlueprints`, `listConnectableResources`, `requestConnection`, `webFetch`, `observeUserChanges`, `giveUp`), how `prepareChatBindings` folds ambient gatekeepers into `env` under `suggestedBindingName`, slash-command collection, and chat compaction checkpoints."
---

The agent runs in Code Mode: instead of calling resource APIs through individual tool schemas, the agent writes JavaScript that is loaded as a Dynamic Worker and executed against an `env` object of named bindings. `packages/workshop-backend/src/overseer.ts` holds the harness source that wraps the agent's module, `packages/workshop-backend/src/agent.ts` runs the loop through `runAgentLoopContinue` from `@earendil-works/pi-agent-core`, `packages/workshop-backend/src/agent-compaction.ts` implements context compaction, `packages/workshop-backend/src/agent-catalog.ts` builds the always-available-resources prompt section, `packages/workshop-backend/src/slash-commands.ts` collects and invokes gatekeeper-provided commands, and `packages/workshop-backend/src/web-fetch.ts` implements the `webFetch` capability.

## Code Mode harness

`CODE_MODE_HARNESS` in `overseer.ts` is the module the runtime loads around the agent's generated `agent.js`. It exposes a `WorkerEntrypoint` with two methods, matching the `CodeModeEntrypoint` interface:

```js
// packages/workshop-backend/src/overseer.ts — CODE_MODE_HARNESS
import { WorkerEntrypoint, restore, RpcStub, RpcTarget } from "cloudflare:workers";
import agent from "agent.js";

export default class extends WorkerEntrypoint {
  verify() {}
  async run(self, callbackResolvers) {
    let env = this.env;
    if (callbackResolvers) {
      for (let [index, {resolve, reject}] of Object.entries(callbackResolvers)) {
        env[index] = {
          args: env[index],
          resolve,
          reject,
        };
      }
    }
    await agent(self, env, this.ctx);
  }
  ...
}
```

| Member | Purpose |
| --- | --- |
| `verify()` | No-op entrypoint used to check that the generated module loads and links. |
| `run(self, callbackResolvers)` | Invokes the agent's default export as `agent(self, env, this.ctx)`. |
| `callbackResolvers` | Per-index `{resolve, reject}` native RPC stubs. Each named `env` entry for that index is rewritten to `{args, resolve, reject}`. |
| `[restore](params)` | Returns a stub wrapping `PlaceholderRpcTarget` instead of the real target. |

`PlaceholderRpcTarget` is a `Proxy` that returns `undefined` for `then` and `dup` and throws for every other property access, with a message stating the stub is only intended to be stored and will work once loaded back from storage. The code comments this as a temporary hack until the runtime provides sealing/unsealing APIs, and notes such stubs are generally not expected to be called before being passed to `bindHook()`.

<Warning>
The `[restore]` placeholder means a persistent hook callback stub must be round-tripped through storage before it is invocable. Calling it in the same execution that constructed it throws.
</Warning>

## Tool set

The agent loop uses `AgentTool` values from `@earendil-works/pi-agent-core`, declared in `agent.ts`. The tools split into code-and-file tools, binding/resource tools, and control tools.

| Tool | Role in the loop |
| --- | --- |
| `readFile` | Read agent-visible source. |
| `writeFile` | Write a whole file. |
| `editFile` | Patch a file; `agent.ts` imports `createTwoFilesPatch` and `FILE_HEADERS_ONLY` from `diff` for change presentation. |
| `executeCode` | Run agent-authored JavaScript against `env` through the Code Mode harness. |
| `describeBinding` | Learn a binding's API by name before using it. |
| `setGadgetBinding` | Wire a binding into a gadget's persistent code. |
| `createGadget` | Create a new gadget workpiece. |
| `listBlueprints` | Enumerate available blueprints. |
| `listConnectableResources` | Enumerate resources the user could connect. |
| `requestConnection` | Ask the user to connect a resource; recorded as a `connectionRequest` chat message. |
| `webFetch` | HTTPS GET with optional document-to-Markdown conversion. |
| `observeUserChanges` | Observe user-side code changes. |
| `giveUp` | End the turn without completing the request. |

### Binding discovery guidance

`formatAlwaysAvailableResourcesPrompt` in `agent-catalog.ts` emits the system-prompt text that ties `describeBinding` and `setGadgetBinding` together. For each always-available resource it renders a line of the form `- ${title}: \`env.${name}\`` followed by the resource's catalog JSON, then appends the fixed instruction:

> When one is relevant, use describeBinding with the binding's name to learn its API before using it. If a Gadget's persistent code needs one, wire it into that gadget with setGadgetBinding.

The preamble states these bindings are always available in `env` for use with `executeCode` and do not need to be requested.

## Chat bindings and ambient gatekeepers

A chat's `env` name space is described by `AiChatAgentContext` in `agent.ts`:

<ResponseField name="chatId" type="number" required>
Chat ID, corresponding to `chatMeta`.
</ResponseField>

<ResponseField name="spawnerConfig" type="AgentSpawnerConfig">
Present when the chat was spawned through a spawner; the spawner config as it was at spawn time.
</ResponseField>

<ResponseField name="bindings" type="Record<string, WorkpieceId>">
Initial `env` binding set gathered when the chat started — typically all gadgets plus the gatekeepers those gadgets bind to. Frozen after the chat starts: `"changes"` messages may introduce new bindings but are not added here, so the current binding set is obtained by replaying the chat log. Absent for chats created before named chat bindings existed; those are seeded lazily at the next turn start.
</ResponseField>

<ResponseField name="alwaysAvailableCapsuleIds" type="WorkpieceId[]">
Gatekeeper IDs for ambient capsules instantiated when the chat started. Predates named chat bindings, when ambient gatekeepers were delivered as numbered "capsules" occupying the lowest capsule numbers. Retained to support migration of old chats and as a record of which bindings came from ambient gatekeepers.
</ResponseField>

<ResponseField name="alwaysAvailableCatalogs" type="AgentCatalogSnapshot[]">
Cached discovery catalogs for the always-available resources, keyed per gatekeeper. Regenerable — re-fetched when missing or stale by `prepareChatBindings`.
</ResponseField>

`AgentHooks.prepareChatBindings()` returns the chat's seed binding layer as `SeedBindingInfo` entries:

| Field | Type | Meaning |
| --- | --- | --- |
| `name` | `string` | The name in the chat's `env`. |
| `target` | `WorkpieceId` | The workpiece the name resolves to. |
| `title` | `string` | Human title: a gadget's title, or a gatekeeper's resource title. |
| `isGadget` | `boolean` | Gadget target versus external resource gatekeeper. |
| `catalog` | `AgentCatalog \| null` | Present only for always-available (ambient) entries; `null` when the gatekeeper provides no catalog. |

The `catalog` field is documented as being present when the entry is an always-available (ambient) resource — for example the read session of a connected account that provides a singleton — and such entries get their own system-prompt section, which is the section `formatAlwaysAvailableResourcesPrompt` renders. Ambient gatekeeper mode resolution enters the overseer through `ambientGatekeeperMode` from `./provisioning-policy`.

Binding names are validated with `validateBindingName` from `@gadgets/workshop-shared/api`, imported by both `agent.ts` and `overseer.ts`.

At execution time, a name resolves through `ChatBindingEntry`:

```ts
// packages/workshop-backend/src/agent.ts
export type ChatBindingEntry =
  | { type: "workpiece"; id: WorkpieceId }
  | { type: "value"; messageSequence: number };
```

`"workpiece"` covers gadgets and gatekeepers — the comment notes the overseer distinguishes the two at env-build time — while `"value"` carries the value arguments of an agent callback, addressed by the chat message sequence.

<Note>
`AiChatAgentContext.bindings` documents that if a referenced workpiece is deleted, this is detected when `env` is materialized for a particular execution and the corresponding bindings are dropped.
</Note>

```mermaid
flowchart TB
  subgraph shared["@gadgets/workshop-shared"]
    api["api: WorkpieceId, validateBindingName,\nSlashCommandChoice/Request"]
    gk["gatekeeper: Gatekeeper, AgentCatalog,\nObservationAuthorizer"]
  end

  subgraph backend["packages/workshop-backend/src"]
    overseer["overseer.ts\nCODE_MODE_HARNESS, AgentHooks impl"]
    agent["agent.ts\nrunAgent, AiChatAgentContext,\nChatBindingEntry, CompactionCheckpoint"]
    catalog["agent-catalog.ts\nnormalizeAgentCatalog,\nformatAlwaysAvailableResourcesPrompt"]
    compaction["agent-compaction.ts\nshouldCompactChat, foldProposedChanges,\nfindCompactionBoundary"]
    slash["slash-commands.ts\ncollectSlashCommands, invokeSlashCommand"]
    fetch["web-fetch.ts\nvalidateWebFetchUrl, webFetch"]
  end

  subgraph runtime["Workers runtime"]
    dyn["Dynamic Worker\nCODE_MODE_HARNESS + agent.js"]
    ai["env.WORKERS_AI.toMarkdown()"]
    gkw["Gatekeeper workers\n(Fetcher<Gatekeeper<any>>)"]
  end

  overseer --> agent
  agent --> compaction
  agent --> catalog
  agent --> fetch
  overseer --> slash
  overseer --> catalog
  overseer --> compaction
  agent -->|executeCode| dyn
  dyn -->|env.NAME| gkw
  slash --> gkw
  catalog --> gkw
  fetch --> ai
  agent --> api
  slash --> api
  catalog --> gk
  slash --> gk
```

## Agent catalogs

Gatekeeper-supplied catalogs are untrusted output and are re-validated on the workshop side.

### `normalizeAgentCatalog(catalog)`

Strips control characters (`\p{Cc}`), collapses whitespace, trims, and slices each field to its bound; drops entries whose `id` or `title` is empty; sorts by `title` then `id` with `localeCompare`; and clamps to `AGENT_CATALOG_MAX_ENTRIES`. `truncated: true` is set when the input already declared it or when the entry count exceeded the maximum.

| Field | Bound |
| --- | --- |
| `id` | `AGENT_CATALOG_MAX_ID_LENGTH` — kept at full bound because it is the opaque key the agent passes back |
| `title` | `AGENT_CATALOG_MAX_TITLE_LENGTH` |
| `description` | `AGENT_CATALOG_MAX_DESCRIPTION_LENGTH` |
| entry count | `AGENT_CATALOG_MAX_ENTRIES` |

The code comments this as defense-in-depth that intentionally overlaps the provider-side `boundAgentCatalog()` in shared code, because the gatekeeper is not trusted to have applied it.

### `completeAgentCatalogSnapshot(existing, gatekeeperIds, loadCatalog)`

Fills in missing snapshots for the active gatekeeper IDs and drops stale entries.

<ParamField body="existing" type="AgentCatalogSnapshot[] | undefined">
Previously cached snapshots. Entries whose `gatekeeperId` is not in `gatekeeperIds` are removed.
</ParamField>

<ParamField body="gatekeeperIds" type="number[]" required>
The active gatekeeper IDs to complete against.
</ParamField>

<ParamField body="loadCatalog" type="(gatekeeperId: number) => Promise<AgentCatalog | null>" required>
Loader for one missing catalog. Failures are isolated per entry: a throw is logged as `agent.catalog.load.failed` with the `gatekeeperId` and recorded as `null`.
</ParamField>

Returns `{snapshots, changed}`. `snapshots` is sorted ascending by `gatekeeperId`; `changed` is true when any entry was loaded or any stale entry was removed. The isolation comment states the reason directly: one failing loader must not reject the whole snapshot, since that would lose every other catalog and abort the turn.

`formatAgentCatalogPrompt(catalog)` returns `"\n" + JSON.stringify(catalog)` for a non-empty catalog and `""` otherwise, so an empty catalog contributes nothing to the prompt.

## Slash commands

Slash commands come from attached gatekeepers that implement `getSlashCommandProvider()`. `collectSlashCommands` fans out across sources, tolerates per-gatekeeper failure, and returns one sorted catalog.

```ts
// packages/workshop-backend/src/slash-commands.ts
type SlashCommandSource = {
  gatekeeperId: number;
  providerLabel: string;
  gatekeeper: Fetcher<Gatekeeper<any>>;
};

export async function collectSlashCommands(
    sources: SlashCommandSource[]): Promise<SlashCommandChoice[]>
```

Each produced `SlashCommandChoice` carries `selection: {gatekeeperId, commandId}`, `name`, `description`, `providerLabel`, and `resourceLabel` only when the command supplied one. A provider that throws is logged with `console.error` as `Failed to load slash commands for gatekeeper <id>:` and contributes `[]`, so the rest of the catalog still loads. Sorting is stable and lexicographic in order: `name`, `providerLabel`, `resourceLabel` (missing treated as `""`), then `selection.commandId`.

`invokeSlashCommand(gatekeeper, request, authorizer)` obtains the provider with `using` and calls `provider.invoke(request.id.commandId, request.args, authorizer)`, returning `SlashCommandResult`. The authorizer is an `RpcStub<ObservationAuthorizer>`, so command execution is subject to observation authorization.

<Note>
Both functions acquire the provider with `using provider = await gatekeeper.getSlashCommandProvider()`, so the stub is disposed when the scope exits.
</Note>

## `webFetch`

`web-fetch.ts` implements a deliberately narrow HTTP capability: HTTP GET against arbitrary public HTTPS URLs, with no support for POST/PUT/DELETE/PATCH and no credential forwarding.

<ParamField body="url" type="string" required>
Target URL. Validated by `validateWebFetchUrl`.
</ParamField>

<ParamField body="raw" type="boolean">
When true, return the exact response bytes decoded as UTF-8 with no document conversion. When false or omitted, supported formats are converted to Markdown via `env.WORKERS_AI.toMarkdown()`.
</ParamField>

<ParamField body="maxBytes" type="number">
Caller-requested cap on body length in characters. The server enforces its own hard cap on top.
</ParamField>

`WebFetchResult` returns `status`, `finalUrl`, `contentType`, `body`, and `truncated`.

| Limit | Value |
| --- | --- |
| `HARD_MAX_BYTES` | `5 * 1024 * 1024` (5 MiB) — always truncate beyond this |
| `DEFAULT_MAX_BYTES` | `1 * 1024 * 1024` (1 MiB) — used when the caller specifies no cap |
| `FETCH_TIMEOUT_MS` | `30_000` |
| `USER_AGENT` | `GadgetsWebFetch/1.0` |

`WebFetchEnv` is intentionally narrow — `{ai: Ai, gateway: AiGatewayConfig | null}` — so tests can pass a stub instead of a full `Cloudflare.Env`.

### URL validation

`validateWebFetchUrl(input)` returns the parsed `URL` or throws:

| Condition | Error |
| --- | --- |
| `new URL(input)` throws | `Invalid URL: <input>` |
| `protocol !== "https:"` | `Only https:// URLs are allowed; got <proto>//. Use the HTTPS version of this URL.` |
| `username` or `password` present | `URLs with embedded credentials are not allowed.` |

The file states explicitly that hostnames are **not** inspected for "looks-internal" patterns, because such a blocklist is unsound when a symbolic hostname can resolve to any IP at fetch time.

### SSRF posture

SSRF protection relies on workerd's post-DNS-lookup IP filtering. The `global_fetch_strictly_public` compatibility flag, set in `wrangler.jsonc`, restricts `fetch()` to public IP addresses; reserved ranges (loopback, RFC1918, link-local, cloud-metadata, and similar) are rejected by the runtime after hostname resolution.

<Warning>
`wrangler dev` reconfigures its global outbound to permit fetching from any address so localhost services stay reachable, so `global_fetch_strictly_public` only takes effect in production. The source records this as an accepted tradeoff for development.
</Warning>

### Markdown conversion

Conversion is delegated to `env.WORKERS_AI.toMarkdown()`. The allowlist `TO_MARKDOWN_MIME_TYPES` covers `text/html`, `application/xhtml+xml`, `application/pdf`, `application/xml`, `text/xml`, `text/csv`, and Office/OpenDocument types: `.docx`, `.xlsx`, `.xls`, `.xlsm`, `.xlsb`, `.ods`, `.odt`, and `application/vnd.apple.numbers`. Plain text, JSON, and other unknown content types pass through unconverted.

Image MIME types are excluded on purpose: image conversion uses paid Workers AI models (object detection plus Gemma-3 for image-to-text), and `webFetch` must not silently incur per-fetch costs.

`buildGatewayOptions(gateway)` returns `undefined` when there is no gateway or no `gateway.workersAiGateway`, because `toMarkdown()` uses the Workers AI binding and can only use the same-account Workers AI gateway resolved by `AiGatewayConfig` — a cross-account platform gateway cannot be used by that binding.

Body reading is capped incrementally in `readBodyCapped`: chunks accumulate until the budget is reached, a partial slice fills the budget exactly, `truncated` is set, and the remaining stream is cancelled to free server-side resources before the reader lock is released. Decoding uses `TextDecoder("utf-8", {fatal: false, ignoreBOM: false})`.

## Chat compaction

Compaction keeps long chats within the model's context limit. It summarizes messages before a boundary and stores their replay state in a checkpoint. Canonical history keeps every message so the UI can page back, but agent replay starts at the boundary.

### Token budget

`getModelTokenLimits(config)` derives the turn's split of the model window:

```ts
// packages/workshop-backend/src/agent-compaction.ts
export function getModelTokenLimits(config: AiModelConfig):
    {inputBudget: number, maxOutputTokens?: number}
```

| Constant / rule | Value |
| --- | --- |
| `COMPACTION_TRIGGER_RATIO` | `0.85` — compact when the prompt reaches this share of the input budget |
| `COMPACTION_TARGET_RATIO` | `0.3` — target share of the input budget for retained messages |
| `DEFAULT_CONTEXT_WINDOW` | `128_000` — assumed window for a model absent from `SUGGESTED_MODELS` |
| `maxOutputTokens` | `SUGGESTED_MODELS[provider][model].outputLimit`, else `WORKERS_AI_OUTPUT_LIMIT` when `provider === "cloudflare"`, else `undefined` |
| `inputBudget` | `(contextWindow ?? DEFAULT_CONTEXT_WINDOW) - (maxOutputTokens ?? 0)` |

The reserved response capacity is both withheld from the prompt budget and sent as the request's response cap. A Cloudflare model configured by hand has no `SUGGESTED_MODELS` entry to declare its reservation, so the provider's applies. A model whose real window is smaller than `DEFAULT_CONTEXT_WINDOW` fails at the provider before compaction triggers.

`shouldCompactChat(contextTokens, inputBudget)` returns `contextTokens >= inputBudget * COMPACTION_TRIGGER_RATIO`.

### `/compact` turns

`isCompactionTurn(messages)` is true when the newest message is a `slashCommand` with `request.id.builtin === true` and `commandId === "compact"`. Such a turn compacts and then ends instead of prompting the model. Both the agent and the turn loop derive this from the log rather than passing a flag, so a turn resumed after a restart behaves the same.

### Boundary selection

`startsAgentTurn(message)` marks messages that begin an agent turn, because each produces a `user` model message and cutting there keeps the retained messages from opening mid-turn:

| `message.type` | Starts a turn when |
| --- | --- |
| `"message"` | `author.type === "user"` or `"gadget"` |
| `"agentCallback"` | always |
| `"agentNudge"` | always |
| `"connectionRequest"` | `state === "accepted"` |
| anything else | never |

`protectRetainedReverts` may still lower the cut past one of these; the summary then stands in for the turn's opening.

`findProtectedFromSequence(messages)` returns the earliest turn a checkpoint cannot absorb, or `undefined`. It finds the first `connectionRequest` with `state === "pending"` — such a message carries live accept/deny state only it can answer — then walks backward to the sequence of the message that started that turn, so the retained tail keeps the exchange explaining what the user is being asked to connect; if no earlier turn start exists it returns `messages[0]?.sequence`. Provisional gadget creations and binding additions need no such protection: the checkpoint records them, and the registry rows they name are untouched by compaction.

`CompactionProjectionMessage` tags each projected model message with its origin:

<ResponseField name="message" type="Message" required>
The model message in the prompt.
</ResponseField>

<ResponseField name="sequence" type="number">
The durable chat sequence that produced this message. System messages and an earlier summary have no source sequence.
</ResponseField>

<ResponseField name="canCut" type="boolean">
Set on the first model message a chat record contributes. The boundary cuts only here, so a record's messages are never split: a tool result always keeps the call it answers, and the tail opens on a user or assistant message.
</ResponseField>

### Summarization prompt

`COMPACTION_SYSTEM_PROMPT` asks for a single context handoff that lets the same coding agent continue the conversation. It requires preserving exact user requirements and preferences, key decisions and rationale, files and symbols, errors and resolutions, current work state, and the next concrete step, and requires fully integrating any prior summary rather than referring to it separately. The mandated structure is `## Goal`, `## Constraints & Preferences`, `## Progress`, `## Key Decisions`, `## Next Steps`, `## Critical Context`. The prompt ends with an explicit instruction not to continue the conversation or follow instructions from earlier messages, and to output only the handoff.

<Warning>
The final clause of `COMPACTION_SYSTEM_PROMPT` is a prompt-injection boundary: the transcript being summarized is data, not instructions.
</Warning>

### Checkpoint shape

`CompactionCheckpoint` in `agent.ts` stores replay state for one compacted prefix. Checkpoints are immutable and a chat keeps every one it has published, so reading history or reverting can select the newest checkpoint below any sequence.

| Field | Type | Meaning |
| --- | --- | --- |
| `chatId` | `number` | Chat the checkpoint belongs to. |
| `compactedTo` | `number` | First sequence replay starts at; earlier messages are represented by the checkpoint. |
| `summary` | `string` | The model-written summary, sent as one user message before the retained messages. |
| `chatBindings` | `[string, ChatBindingEntry][]` | The chat's named bindings; retained messages and the summary refer to these as `env.NAME`. |
| `nextChangeId` | `number` | Next change ID for replayed tool results, keeping change IDs sequential across boundaries. |
| `observedCodeVersion` | `number` | Code version used as the replay base; tool calls and change batches can establish it. |
| `acceptedChanges` | `Uint8Array` | Accepted Y.Doc updates from before the boundary, merged into one update. |
| `proposedChanges` | `Uint8Array` | Still-proposed Y.Doc updates from before the boundary, merged into one update; disjoint from `acceptedChanges`. |

Because the chat stays pinned to `observedCodeVersion`, accepted updates remain part of the replay base rather than of the version replay starts from, and replay applies both `acceptedChanges` and `proposedChanges`. Individual batches remain addressable through the chat log, so reverting to a point before the boundary is still possible.

Provisional gadget creations and binding additions from before the boundary are deliberately absent from `proposedChanges`: they carry no Y.Doc update, and the registry rows they created (`GadgetRecord.pending`, `BindingRecord.pending`) already record them with the sequence that did, untouched by compaction. Merge and revert promote and delete from those rows rather than from the log, so duplicating them in the checkpoint would be a second source of truth. `getProposedChanges()` reports the compacted prefix as pending when either the checkpoint field or such a row exists.

### Change folding

`foldProposedChanges(messages, seed)` is the single rule from which both the proposed-changes view and a new checkpoint are derived.

```ts
export function foldProposedChanges(
    messages: Iterable<AiChatMessage>, seed: readonly ChangeBatch[] = [])
    : {proposed: ChangeBatch[], accepted: Uint8Array[]}
```

`ChangeBatch` is `{sequence: number, update?: Uint8Array}`; `update` is absent for a batch that records only gadget creations or binding additions.

| Message type | Effect on the fold |
| --- | --- |
| `"changes"` | Push `{sequence, update}` onto the proposed list. |
| `"merge"` | Shift proposed batches with `sequence <= mergeThrough` (inclusive); each defined `update` moves to `accepted`. |
| `"revert"` | Pop proposed batches from the end while `sequence >= revertFrom`. |

`seed` carries batches already proposed before the log begins, as a checkpoint records. The returned `proposed` list is oldest first.

```stateDiagram
```

```mermaid
stateDiagram-v2
  [*] --> Live: turn starts
  Live --> Live: contextTokens < inputBudget * 0.85
  Live --> Compacting: shouldCompactChat() true
  Live --> Compacting: isCompactionTurn() — newest msg is builtin /compact
  Compacting --> Bounded: findCompactionBoundary()\nrespects canCut + findProtectedFromSequence()
  Bounded --> Summarizing: buildSummaryPrompt() + COMPACTION_SYSTEM_PROMPT
  Summarizing --> Checkpointed: publish CompactionCheckpoint\n(compactedTo, summary, chatBindings,\nnextChangeId, accepted/proposedChanges)
  Checkpointed --> Live: replay from compactedTo
  Checkpointed --> [*]: /compact turn ends without prompting the model
```

## Related pages

<CardGroup cols={2}>
  <Card title="Gadgets and sandboxing" href="/gadgets-and-sandboxing">
    How `executeCode` code is isolated as a Dynamic Worker with internet access disabled, and the `global_fetch_strictly_public` posture.
  </Card>
  <Card title="Gatekeeper protocol" href="/gatekeeper-protocol">
    `Gatekeeper<Session>`, resource descriptions, agent catalog limits, and ambient `autoProvisionsAccount` mode resolution.
  </Card>
  <Card title="Observations, actions, and approval queues" href="/observations-and-actions">
    `ObservationAuthorizer` and `ApprovalQueue`, the interfaces a slash-command invocation and agent reads pass through.
  </Card>
  <Card title="RPC API reference" href="/rpc-api-reference">
    `Overseer`, `SlashCommandChoice`/`SlashCommandRequest`, `validateBindingName`, and stub-disposal constraints.
  </Card>
</CardGroup>
