From 7781ba68e13684b3a2e91aa6d51258b1c54540eb Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 3 Jul 2026 10:47:48 +0200 Subject: [PATCH 1/4] docs: add v2 migration guide --- docs/docs.json | 1 + docs/protocol/v2/draft/slash-commands.mdx | 2 + docs/protocol/v2/migration.mdx | 673 ++++++++++++++++++++++ docs/protocol/v2/slash-commands.mdx | 2 + 4 files changed, 678 insertions(+) create mode 100644 docs/protocol/v2/migration.mdx diff --git a/docs/docs.json b/docs/docs.json index ebcb6c02..972c931e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -119,6 +119,7 @@ "hidden": true, "pages": [ "protocol/v2/overview", + "protocol/v2/migration", "protocol/v2/initialization", "protocol/v2/authentication", "protocol/v2/session-setup", diff --git a/docs/protocol/v2/draft/slash-commands.mdx b/docs/protocol/v2/draft/slash-commands.mdx index 153c03b3..37f20df9 100644 --- a/docs/protocol/v2/draft/slash-commands.mdx +++ b/docs/protocol/v2/draft/slash-commands.mdx @@ -22,6 +22,7 @@ After creating a session, the Agent **MAY** send a list of available commands vi "name": "web", "description": "Search the web for information", "input": { + "type": "text", "hint": "query to search for" } }, @@ -33,6 +34,7 @@ After creating a session, the Agent **MAY** send a list of available commands vi "name": "plan", "description": "Create a detailed implementation plan", "input": { + "type": "text", "hint": "description of what to plan" } } diff --git a/docs/protocol/v2/migration.mdx b/docs/protocol/v2/migration.mdx new file mode 100644 index 00000000..9e94188a --- /dev/null +++ b/docs/protocol/v2/migration.mdx @@ -0,0 +1,673 @@ +--- +title: "Migrating from v1" +description: "A complete guide to ACP v2 for implementers familiar with v1: breaking changes, new features, and step-by-step migration" +--- + +ACP v2 is a consolidation release. It redesigns the prompt lifecycle around session state, unifies streaming and non-streaming updates behind upsert semantics, makes the schema forward-compatible by default, and removes protocol surface that the ecosystem had already moved away from. + +This guide covers everything an implementer familiar with v1 needs to move an Agent or Client to v2. All examples are JSON wire messages and apply to any language. + +If you only remember five things, remember these: + +1. **The `session/prompt` response no longer ends the turn.** It acknowledges acceptance; progress and completion arrive as `state_update` notifications, and the stop reason moved there too. +2. **Updates are upserts.** Messages, tool calls, and plans are patched by ID with uniform semantics: omitted field = unchanged, `null` = cleared, value = replaced, chunks append. +3. **The Client file system, terminal, and session modes APIs are gone.** Use client-provided MCP servers and session config options instead. +4. **Capabilities were reorganized.** One `capabilities` + required `info` field on both sides, session-scoped groups nested under `session`, object support markers instead of booleans, and a required baseline of session methods. +5. **Everything is extensible now.** Enums and tagged unions accept unknown values; `_`-prefixed values are yours, the rest are reserved for future ACP versions. + +The rest of this guide walks through each area: the [at-a-glance tables](#at-a-glance) for a quick diff, one section per change with before/after wire messages, and [step-by-step checklists](#migration-checklist-agents) at the end. + +## Version negotiation + +The mechanism is unchanged: the Client sends the latest protocol version it supports in `initialize`, and the Agent responds with the same version if supported, or its own latest version otherwise. To use v2, send `"protocolVersion": 2`. An Agent that only supports v1 will answer with `"protocolVersion": 1`, and the Client decides whether to continue with v1 or disconnect. + +Nothing about v2 changes the underlying JSON-RPC framing, so a single connection always speaks exactly one negotiated version after `initialize`. + +## At a glance + +### Method changes + +| v1 method | v2 | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `initialize` | Unchanged name; params and result restructured | +| `authenticate` | Renamed to `auth/login` | +| `logout` (gated by `agentCapabilities.auth.logout`) | Renamed to `auth/logout`; always required, no capability marker | +| `session/new` | Unchanged name; `mcpServers` now optional; response no longer includes `modes` | +| `session/load` | **Removed** — use `session/resume` with `"replayFrom": { "type": "start" }` | +| `session/resume` | Unchanged name; gains optional `replayFrom`; baseline (no capability marker) | +| `session/list`, `session/close` | Baseline when `capabilities.session` is present (no capability markers) | +| `session/delete` | Unchanged; still optional via `session.delete` capability | +| `session/prompt` | Unchanged shape; **response semantics redesigned** (see [Prompt lifecycle](#the-new-prompt-lifecycle)) | +| `session/cancel` | Unchanged name; completion now reported via `state_update` instead of the prompt response | +| `session/set_mode` | **Removed** — use `session/set_config_option` | +| `session/set_config_option` | Unchanged | +| `session/request_permission` | Unchanged name; params restructured (required `title`, optional `subject`) | +| `session/update` | Unchanged name; update variants changed (see below) | +| `fs/read_text_file`, `fs/write_text_file` | **Removed** | +| `terminal/create`, `terminal/output`, `terminal/release`, `terminal/wait_for_exit`, `terminal/kill` | **Removed** | +| `$/cancel_request` | Unchanged | + +### `session/update` variant changes + +| v1 `sessionUpdate` | v2 | +| --------------------------- | ----------------------------------------------------------------------------------- | +| `user_message_chunk` | Kept; `messageId` now required | +| `agent_message_chunk` | Kept; `messageId` now required | +| `agent_thought_chunk` | Kept; `messageId` now required | +| — | **New:** `user_message`, `agent_message`, `agent_thought` whole-message upserts | +| — | **New:** `state_update` (`running`, `idle`, `requires_action`) with stop reasons | +| `tool_call` | **Removed** — the first `tool_call_update` for a `toolCallId` creates the tool call | +| `tool_call_update` | Kept; now an explicit upsert with omit/`null`/value patch semantics | +| — | **New:** `tool_call_content_chunk` for streaming individual content items | +| `plan` | Replaced by `plan_update` with a `planId` and a `type` discriminator | +| `current_mode_update` | **Removed** — modes are config options; use `config_option_update` | +| `available_commands_update` | Kept; command `input` now carries a required `type` discriminator | +| `config_option_update` | Unchanged | +| `session_info_update` | Unchanged | +| `usage_update` | Unchanged | + +## Initialization + +### Role-agnostic `info` and `capabilities` + +v1 used role-specific field names: the Client sent `clientCapabilities` and optional `clientInfo`, and the Agent returned `agentCapabilities` and optional `agentInfo`. v2 uses the same two field names in both directions — `capabilities` and `info` — and `info` is now **required** on both sides. This keeps symmetric connections (such as agent-to-agent) from needing role-specific names. + + + +```json v1 request +{ + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "protocolVersion": 1, + "clientCapabilities": { + "fs": { "readTextFile": true, "writeTextFile": true }, + "terminal": true + }, + "clientInfo": { "name": "my-client", "version": "1.0.0" } + } +} +``` + +```json v2 request +{ + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "protocolVersion": 2, + "info": { + "name": "my-client", + "title": "My Client", + "version": "1.0.0" + }, + "capabilities": {} + } +} +``` + + + + + +```json v1 response +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "protocolVersion": 1, + "agentCapabilities": { + "loadSession": true, + "promptCapabilities": { "image": true, "embeddedContext": true }, + "mcpCapabilities": { "http": true, "sse": false }, + "sessionCapabilities": { + "list": {}, + "resume": {}, + "close": {} + } + }, + "authMethods": [], + "agentInfo": { "name": "my-agent", "version": "0.3.0" } + } +} +``` + +```json v2 response +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "protocolVersion": 2, + "info": { + "name": "my-agent", + "title": "My Agent", + "version": "0.3.0" + }, + "capabilities": { + "session": { + "prompt": { + "image": {}, + "embeddedContext": {} + }, + "mcp": { + "stdio": {}, + "http": {} + }, + "delete": {}, + "additionalDirectories": {} + } + }, + "authMethods": [] + } +} +``` + + + +### Support markers are objects, not booleans + +In v1, capability support was a mix of booleans (`"image": true`) and objects (`"list": {}`). In v2, every support marker is an object: supplying `{}` (or an object with fields) means supported, and omitting the key or supplying `null` means unsupported. Booleans remain appropriate only for actual data or configuration _inside_ an already-advertised capability. + +This means checks like `promptCapabilities.image === true` become presence checks: `capabilities.session.prompt.image != null`. The object shape leaves room for each capability to grow fields without another breaking change. + +### Capability reorganization + +- All session-scoped capability groups now live under `capabilities.session`. What was `agentCapabilities.promptCapabilities` is now `capabilities.session.prompt`, and `agentCapabilities.mcpCapabilities` is now `capabilities.session.mcp`. +- `capabilities.session` itself is **optional**, so agents that don't do session-based prompting (for example, agents that only serve specialized extension surfaces) can omit it entirely. +- `loadSession` is gone along with `session/load` (see [Session setup](#session-setup-and-lifecycle)). +- The individual `list`, `resume`, and `close` markers are gone: advertising `capabilities.session` at all now **requires** supporting the baseline methods `session/new`, `session/list`, `session/resume`, `session/close`, `session/prompt`, `session/cancel`, and `session/update`. Optional extras such as `session.delete`, `session.additionalDirectories`, `session.prompt`, and `session.mcp` keep their own markers. +- The v1 Client capabilities `fs` and `terminal` are removed entirely (see [Client file system and terminals](#client-file-system-and-terminals-removed)). Stable v2 currently defines no standard Client capability fields; the object remains as an extension point. + +### Auth methods + +Each entry in `authMethods` now uses `methodId` instead of `id`, and carries a required `type` discriminator so future auth flows can be introduced without breaking existing implementations: + +```json +{ + "methodId": "agent-login", + "type": "agent", + "name": "Agent login", + "description": "Sign in using the agent's login flow" +} +``` + +Stable v2 defines `type: "agent"`. Custom types **MUST** begin with `_`; unknown values without a leading `_` are reserved for future ACP versions. + +## Authentication + +The auth methods are grouped under an `auth/` prefix: + +- `authenticate` → `auth/login`. The params are unchanged (`methodId` selecting one of the advertised auth methods); the descriptor's identifier field is renamed from `id` to `methodId` to match, and its `type` discriminator is now required. +- `logout` → `auth/logout`. In v1, logout support was opt-in via a capability marker. In v2, **every Agent must implement `auth/logout`**; there is no support marker. Agents with no authentication state simply return an empty result. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "auth/login", + "params": { "methodId": "agent-login" } +} +``` + +## The new prompt lifecycle + +This is the most significant semantic change in v2. Read this section even if you skim the rest. In short, everything that v1 expressed through the pending `session/prompt` request moved into `session/update` notifications: + +| Turn signal | v1 | v2 | +| --------------------------- | ---------------------------------------------- | ---------------------------------------------------- | +| Prompt accepted | Implicit | `session/prompt` response (`{}`) | +| User message in history | Implicit (the request itself) | `user_message` update with agent-owned `messageId` | +| Turn in progress | `session/prompt` still pending | `state_update` with `state: "running"` | +| Waiting on the user | Implicit (pending permission request) | `state_update` with `state: "requires_action"` | +| Turn finished + stop reason | `session/prompt` response with `stopReason` | `state_update` with `state: "idle"` and `stopReason` | +| Cancellation confirmed | Prompt response with `stopReason: "cancelled"` | Idle `state_update` with `stopReason: "cancelled"` | + +### v1: the response is the turn + +In v1, `session/prompt` stayed pending for the entire turn. The Agent streamed `session/update` notifications while working, and the eventual response carried the `stopReason` that ended the turn: + +```json +{ "jsonrpc": "2.0", "id": 2, "result": { "stopReason": "end_turn" } } +``` + +This entangled prompt acceptance with turn completion, which made replay, multi-client sessions, background work, and queued messages awkward to express. + +### v2: the response is an acknowledgment + +In v2, the Agent **MUST** respond to `session/prompt` as soon as it has _accepted_ the prompt, with an empty result: + +```json +{ "jsonrpc": "2.0", "id": 2, "result": {} } +``` + +Everything else happens through `session/update` notifications: + +1. **User message acknowledgment.** After accepting the prompt, the Agent **MUST** report where the user message was inserted into session history, either as a `user_message` update with a full `content` array or as streamed `user_message_chunk` updates. This update is the source of truth for the agent-owned `messageId`: + + ```json + { + "jsonrpc": "2.0", + "method": "session/update", + "params": { + "sessionId": "sess_abc123", + "update": { + "sessionUpdate": "user_message", + "messageId": "msg_user_8f7a1", + "content": [{ "type": "text", "text": "Can you analyze this code?" }] + } + } + } + ``` + +2. **Running state.** When the Agent starts or resumes processing, it **MUST** send a `state_update` with `"state": "running"`. + +3. **Output.** Messages, thoughts, plans, and tool calls stream as before (with the changes described in the following sections). + +4. **Completion.** When there is no pending work, the Agent **MUST** report idle. When the idle transition completes active work, it **MUST** include the stop reason: + + ```json + { + "jsonrpc": "2.0", + "method": "session/update", + "params": { + "sessionId": "sess_abc123", + "update": { + "sessionUpdate": "state_update", + "state": "idle", + "stopReason": "end_turn" + } + } + } + ``` + +### Session states + +`state_update` is new in v2 and carries one of three stable states (extensible like all v2 enums): + +- `running` — the Agent is actively processing work in the session. +- `idle` — the Agent is not currently processing work. Carries a `stopReason` when it completes active work. +- `requires_action` — the Agent is blocked on user action. Agents **SHOULD** send this while waiting on a permission response, and another `running` update when they resume. + +The stop reasons themselves are unchanged from v1 — `end_turn`, `max_tokens`, `max_turn_requests`, `refusal`, `cancelled` — they just moved from the `session/prompt` response to the idle `state_update`. + +### Cancellation + +`session/cancel` is unchanged as a notification, but confirmation moved with the rest of the lifecycle: instead of responding to `session/prompt` with `"stopReason": "cancelled"`, the Agent **MUST** finish sending any pending updates and then send an idle `state_update` with the `cancelled` stop reason. Clients still respond to all pending `session/request_permission` requests with the `cancelled` outcome and **SHOULD** accept tool-call updates that arrive after sending `session/cancel`. + +### Why this matters beyond bookkeeping + +Because turn progress is now expressed entirely in notifications, the same message flow works for history replay on `session/resume`, for multiple clients observing one session, and for future agent-initiated or queued work — none of which fit the v1 request/response turn model. + +## Messages and message IDs + +### Message IDs are required + +Every message chunk and message update in v2 **MUST** carry a `messageId`. In v1, `messageId` was optional on chunks. Message IDs are opaque strings generated by the Agent — the Agent owns session history, so it is the single source of message identity. All chunks of one message share one `messageId`; a changed `messageId` starts a new message. + +### Whole-message upserts + +Alongside the chunk updates, v2 adds `user_message`, `agent_message`, and `agent_thought` updates that carry a full `content` **array** (chunks carry a single content block). These are upserts keyed by `messageId`, with three-state patch semantics: + +- **Omitted** `content` — leave existing content unchanged (useful for updating `_meta` or other fields without resending content). +- `content: null` **or** `content: []` — clear the message content. +- **Concrete array** — replace all content currently stored for that message, including content accumulated from earlier chunks. + +Chunks always **append** to whatever content is current. For example: `agent_message` with `content: [A]`, then `agent_message_chunk` with `B` renders as `[A, B]`. A later `agent_message` with `content: [C]` resets the message to `[C]`, and subsequent chunks append to that. + +Use chunks for streaming; use whole-message updates for replay, correction, or output that is already complete. + +## Tool calls + +### One upsert notification + +v1 had `tool_call` (create) and `tool_call_update` (modify). In practice the split bought nothing, so v2 keeps only `tool_call_update`. The first update with an unseen `toolCallId` creates the tool call; subsequent updates patch it. Only `toolCallId` is required, but the Agent **SHOULD** include `title` on the first report: + +```json +{ + "sessionUpdate": "tool_call_update", + "toolCallId": "call_001", + "title": "Reading configuration file", + "kind": "read", + "status": "pending" +} +``` + +The patch semantics are the same three-state model as messages: omitted fields stay unchanged, `null` explicitly clears a field, and concrete values replace. Array fields such as `content` and `locations` are replaced wholesale when present. The field set (`title`, `kind`, `status`, `content`, `locations`, `rawInput`, `rawOutput`) and the `kind`/`status` values are unchanged from v1, though the enums are now extensible. + +### Streaming tool-call content + +v2 adds `tool_call_content_chunk`, which **appends** a single `ToolCallContent` item to a tool call instead of resending the whole array: + +```json +{ + "sessionUpdate": "tool_call_content_chunk", + "toolCallId": "call_001", + "content": { + "type": "content", + "content": { "type": "text", "text": "Checked syntax..." } + } +} +``` + +Chunks and updates compose exactly like message chunks and updates: a `tool_call_update` that includes `content` replaces the accumulated content; later chunks append to it. + +### Terminal content removed + +The v1 `ToolCallContent` variant `{ "type": "terminal", "terminalId": ... }` is gone along with the client terminal surface. Stable v2 tool-call content is `content` (a content block) or `diff`, plus extension variants. + +## Diff content + +The v1 diff shape — a single `path` with `oldText`/`newText` — could not distinguish a deleted file from an emptied one, had no way to express renames, copies, or binary changes, and forced clients to compute renderable diffs themselves. v2 replaces it with structured changes plus optional renderable patch text: + +```json +{ + "type": "diff", + "changes": [ + { + "operation": "modify", + "path": "/home/user/project/src/config.json", + "fileType": "text", + "mimeType": "application/json" + } + ], + "patch": { + "format": "git_patch", + "diff": "diff --git a/src/config.json b/src/config.json\n--- a/src/config.json\n+++ b/src/config.json\n@@ -1,3 +1,3 @@\n {\n- \"debug\": false\n+ \"debug\": true\n }\n" + } +} +``` + +- `changes` (required) is a list of file-level operations. `add`, `delete`, and `modify` carry a `path`; `move` and `copy` carry `oldPath` and `path`. Each change may carry an optional `fileType` (`text`, `binary`, `directory`, `symlink`) and `mimeType`. Clients can build file trees and summaries from `changes` without parsing any patch text. +- `patch` (optional) carries renderable patch text with a `format` discriminator; stable v2 defines `git_patch`. Agents **SHOULD** provide `patch` whenever feasible; Clients **MUST** handle diffs where `patch` is omitted or `null` (binary changes, symlink retargets, and similar cases have no useful text rendering). + +A deleted file is now simply: + +```json +{ + "type": "diff", + "changes": [ + { + "operation": "delete", + "path": "/home/user/project/old_file.txt", + "fileType": "text" + } + ] +} +``` + +There is no mechanical mapping from v2 diffs back to `oldText`/`newText` — when migrating a v1 client, replace your diff rendering with a git-patch renderer driven by `patch`, using `changes` for headers, file trees, and non-text cases. + +## Permission requests + +`session/request_permission` no longer takes a bare `toolCall`. The prompt copy and the subject of the request are now separate concerns: + +```json +{ + "jsonrpc": "2.0", + "id": 5, + "method": "session/request_permission", + "params": { + "sessionId": "sess_abc123", + "title": "Run this script?", + "description": "The agent wants to execute scripts/setup.sh in your project.", + "subject": { + "type": "tool_call", + "toolCall": { + "toolCallId": "call_001", + "title": "Execute setup script", + "kind": "execute", + "status": "pending" + } + }, + "options": [ + { "optionId": "allow", "name": "Allow once", "kind": "allow_once" }, + { "optionId": "deny", "name": "Deny", "kind": "reject_once" } + ] + } +} +``` + +- `title` is **required** — human-readable text for the permission prompt itself. It does not update any tool call's displayed title. +- `description` is optional supporting copy. +- `subject` is an optional tagged union describing _what_ needs permission. Stable v2 defines `type: "tool_call"`, whose `toolCall` payload is the same `ToolCallUpdate` upsert shape used in session updates — so any fields it carries patch the referenced tool call's displayed state, exactly like a `tool_call_update` would. Requests may omit `subject` entirely for approvals that aren't about a tool call, and unknown subject types should be preserved when proxying and either rendered generically or declined by policy. + +In v1, agents routinely stuffed permission prompt text into the tool call's `title`, mutating displayed tool state as a side effect. In v2, put prompt copy in `title`/`description` and let `subject.toolCall` carry only genuine tool-call state. + +`options` and the response shape are unchanged: options carry `optionId`, `name`, and `kind` (`allow_once`, `allow_always`, `reject_once`, `reject_always`), and the response outcome is `{ "outcome": "selected", "optionId": ... }` or `{ "outcome": "cancelled" }`. + +While a permission request is pending, the Agent **SHOULD** report `state_update` with `requires_action`, and `running` again once resolved. + +## Plans + +The v1 `plan` update was a flat entries list with no identity and no room to grow. v2 replaces it with `plan_update`, whose payload is a tagged union keyed by a required `planId`: + + + +```json v1 +{ + "sessionUpdate": "plan", + "entries": [ + { + "content": "Check for syntax errors", + "priority": "high", + "status": "pending" + } + ] +} +``` + +```json v2 +{ + "sessionUpdate": "plan_update", + "plan": { + "type": "items", + "planId": "plan-1", + "entries": [ + { + "content": "Check for syntax errors", + "priority": "high", + "status": "pending" + } + ] + } +} +``` + + + +Stable v2 defines the `items` plan type; the entry shape (`content`, `priority`, `status`, all required) is unchanged from v1, and entry priorities and statuses are now extensible enums. Each `plan_update` for a given `planId` replaces that plan's entries, and the `planId` allows multiple plans per session and future plan content types (such as markdown or file-backed plans, which remain unstable) without another breaking change. + +## Session setup and lifecycle + +### `session/load` is gone; `session/resume` does both + +v1 had two ways to reattach to a session: `session/load` (replays full history, gated by `loadSession`) and `session/resume` (no replay, gated by `sessionCapabilities.resume`). v2 keeps only `session/resume` with an optional `replayFrom` cursor: + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "session/resume", + "params": { + "sessionId": "sess_abc123", + "cwd": "/home/user/project", + "replayFrom": { "type": "start" } + } +} +``` + +- Omit `replayFrom` (or send `null`) for v1 `resume` behavior: reattach without replay. +- `"replayFrom": { "type": "start" }` for v1 `load` behavior: the Agent replays the entire conversation as `session/update` notifications (using the same message, tool-call, and plan updates as live traffic) before responding. + +The cursor is a tagged union so future versions can add replay-from-a-point variants without new methods. + +### Consistent lifecycle requests + +`session/new` and `session/resume` take the same shape of environment parameters: a required `cwd` (absolute path), plus optional `additionalDirectories` and `mcpServers`. Note that `mcpServers` was required (even if empty) on `session/new` in v1 and is now optional. Both responses carry the session's `configOptions`; neither carries `modes` anymore. + +### Baseline methods + +As covered under [Initialization](#capability-reorganization), advertising `capabilities.session` commits the Agent to `session/new`, `session/list`, `session/resume`, `session/close`, `session/prompt`, `session/cancel`, and `session/update`. Clients can rely on listing, resuming, and closing sessions without probing individual capability markers. `session/delete` remains optional behind `session.delete`. + +## Session modes become config options + +The dedicated modes API is removed: the `modes` field on session responses, `session/set_mode`, the `current_mode_update` notification, and all `SessionMode*` types. Mode-like state — and model selection, thinking level, and similar knobs — is expressed through session config options, which already existed in v1: + +```json +{ + "configId": "mode", + "name": "Session Mode", + "category": "mode", + "type": "select", + "currentValue": "ask", + "options": [ + { "value": "ask", "name": "Ask" }, + { "value": "code", "name": "Code" } + ] +} +``` + +- The option's identifier field is renamed from `id` to `configId`, matching the parameter name used by `session/set_config_option`. +- The stable `category` values are `mode`, `model`, `model_config`, and `thought_level`; clients can use them for UX affordances (placement, shortcuts) and must tolerate unknown categories. +- Clients change values with `session/set_config_option` (`sessionId`, `configId`, `value`), and the response returns the full updated `configOptions` array, since one change can affect other options. Agent-initiated changes arrive as `config_option_update` session updates, which replace the v1 `current_mode_update`. + +## Client file system and terminals removed + +v2 removes the entire v1 Client-provided execution surface: + +- `clientCapabilities.fs` and the `fs/read_text_file` / `fs/write_text_file` methods +- `clientCapabilities.terminal` and the `terminal/create`, `terminal/output`, `terminal/release`, `terminal/wait_for_exit`, `terminal/kill` methods +- The `terminal` tool-call content type + +In practice this surface was inconsistently implemented outside of a few IDEs, and agents already needed their own file and execution handling for clients that didn't offer it. Clients that want to expose file access, unsaved editor state, or command execution to agents should do so by providing an **MCP server** to the session (via `mcpServers` on `session/new` / `session/resume`), which puts those tools on the same footing as every other tool the agent uses. Agent-side, drop the capability checks and the dual code paths; report file changes through [diff content](#diff-content) and command output through regular tool-call content. + +## MCP server configuration + +MCP transport configuration is aligned with the current MCP transport model: + +- Every server config **MUST** carry a `type` discriminator. In v1, stdio configs had no `type` field; in v2 they are `"type": "stdio"`. Unknown transport types can then be carried as extension variants instead of failing to parse. +- The deprecated HTTP+SSE transport (`"type": "sse"`) is **removed**. +- Support is advertised per transport under `capabilities.session.mcp`: `stdio` is now an explicit capability (`session.mcp.stdio`), so agents that cannot launch subprocesses can opt out, and `session.mcp.http` covers remote servers. +- Empty arrays are no longer required: `args` and `env` on stdio configs and `headers` on HTTP configs are optional in v2 (v1 required `args` and `env` even when empty). + +```json +{ + "mcpServers": [ + { + "type": "stdio", + "name": "filesystem", + "command": "/usr/local/bin/mcp-fs", + "args": ["--root", "/home/user/project"] + }, + { + "type": "http", + "name": "linear", + "url": "https://mcp.linear.app/mcp" + } + ] +} +``` + +## Content blocks + +The five content block types are unchanged: `text`, `image`, `audio`, `resource_link`, and `resource`. v2 realigns their fields with the latest MCP specification — notably, `resource_link` gains an optional `icons` array — and the `type` discriminator is extensible like every other v2 enum, so unknown block types no longer break deserialization for receivers that have a fallback. + +## Slash commands + +The advertisement model (`available_commands_update` carrying commands with `name` and `description`) is unchanged, but a command's optional `input` specification is now a tagged union. The v1 untagged unstructured input gains an explicit `type: "text"` discriminator so richer input types can be added later: + + + +```json v1 +{ + "name": "search", + "description": "Search the codebase", + "input": { "hint": "query to search for" } +} +``` + +```json v2 +{ + "name": "search", + "description": "Search the codebase", + "input": { "type": "text", "hint": "query to search for" } +} +``` + + + +As with other extensible unions, unknown input types should be preserved when proxying; a client that can't render one can fall back to plain text input or hide the command's input hint. + +## Consistent ID naming + +v2 applies one naming rule across the schema: a field that identifies a protocol entity or references one is named after its domain (`sessionId`, `messageId`, `toolCallId`, `planId`, `optionId`), never a generic `id`. The concrete renames from v1: + +| Where | v1 | v2 | +| --------------------------- | ------- | ---------- | +| Auth method descriptor | `id` | `methodId` | +| Session config option | `id` | `configId` | +| Session config select group | `group` | `groupId` | + +New entities introduced in v2 follow the same rule (`messageId` on messages, `planId` on plans). If you generate types from the schema, expect these renames to surface as compile-time breaks — a good checklist by itself. + +A few schema definition names also changed without any wire-format change, which matters only to schema-codegen consumers: the `session/update` and `session/cancel` payload types are now `UpdateSessionNotification` and `CancelSessionNotification`, and the auth request/response types follow the new method names (`LoginAuthRequest`/`LoginAuthResponse`, `LogoutAuthRequest`/`LogoutAuthResponse`). + +## Extensibility and forward compatibility + +v2 makes the whole schema forward-compatible by convention: + +- **Open enums and tagged unions.** Every enum-like string (`sessionUpdate`, tool `kind`, tool `status`, stop reasons, plan types and entry priorities/statuses, content types, content roles, diff operations and file types, permission subjects, permission option kinds and outcomes, auth method types, config option types and categories, command input types, MCP transports, …) accepts unknown values. Values beginning with `_` are reserved for implementation-specific extensions (e.g. `_mycompany_widget`); unknown values _without_ a leading `_` are reserved for future ACP versions. Receivers **SHOULD** preserve unknown values when storing, replaying, proxying, or forwarding, and either render them generically or fall back safely. Do not mint non-underscore custom values. +- **`_meta` everywhere.** As in v1, `_meta` objects carry implementation metadata. In v2's upsert-style updates, `_meta` follows the same patch semantics as other fields: omitted means unchanged, `null` clears. Chunk-level `_meta` is scoped to the chunk. +- **Custom methods** still use the `_` prefix, unchanged from v1. +- **Explicit nullability.** Throughout the v2 schema, optional fields document whether `null` is equivalent to omission; for upsert payloads they are deliberately different (omitted = unchanged, `null` = clear). + +## Transports + +stdio remains the primary transport and is unchanged except that v2 explicitly follows **JSON-RPC 2.0 batch** behavior: a newline-delimited message may be a single request, notification, response, or a batch array. Receivers may process batch entries concurrently, must not reply to notifications, and return per-entry `-32600` errors for invalid entries. Don't batch lifecycle-sensitive messages (`initialize`, `auth/login`, `session/new`, `session/resume`, `session/prompt`) — they change which later messages are valid. + +A standard remote transport (streamable HTTP with SSE streams, plus WebSocket) is being specified in its own RFD and is not part of the core v2 protocol surface; see the [transport RFD](/rfds/streamable-http-websocket-transport) for its current state. + +## Migration checklist: Agents + +1. **Initialization.** Read `params.capabilities` / `params.info`; return `capabilities` and a required `info`. Restructure your capability object: nest prompt and MCP capabilities under `session`, convert boolean markers to `{}` objects, drop `loadSession` and the `list`/`resume`/`close` markers. +2. **Implement the baseline.** If you advertise `session`, implement `session/new`, `session/list`, `session/resume`, `session/close`, `session/prompt`, `session/cancel`, and `session/update`. +3. **Auth.** Rename `authenticate` → `auth/login`; implement `auth/logout` unconditionally; add `type` and `methodId` to your auth method descriptors. +4. **Prompt lifecycle.** Respond to `session/prompt` immediately with `{}` on acceptance. Emit a `user_message` acknowledgment with an agent-generated `messageId`, a `running` state update when processing starts, and an idle `state_update` carrying the stop reason when the work completes — including `cancelled` after `session/cancel`. +5. **Message IDs.** Generate and attach a `messageId` to every message chunk and update. +6. **Tool calls.** Stop sending `tool_call`; send `tool_call_update` for creation and patches. Use `tool_call_content_chunk` to stream content. Respect omit/`null`/value patch semantics. +7. **Diffs.** Replace `oldText`/`newText` with `changes` (+ `fileType`/`mimeType` where known) and provide a `git_patch` whenever feasible. +8. **Permissions.** Put prompt copy in required `title` (+ optional `description`); wrap tool-call context in `subject: { "type": "tool_call", "toolCall": ... }`. +9. **Plans.** Send `plan_update` with `{ "type": "items", "planId": ..., "entries": [...] }`. +10. **Modes.** Drop `session/set_mode`, `modes`, and `current_mode_update`; expose the same state as config options with `category: "mode"` and emit `config_option_update`. +11. **Replay.** Handle `replayFrom` on `session/resume`; replay history as ordinary `session/update` notifications when asked. Remove `session/load`. +12. **Client surface.** Remove all `fs/*` and `terminal/*` calls and terminal tool content; rely on client-provided MCP servers for client-side access. +13. **MCP config.** Require and emit `type` discriminators; drop SSE; advertise `session.mcp.stdio` / `session.mcp.http` as appropriate; accept omitted `mcpServers`. +14. **Slash commands.** Add the `type: "text"` discriminator to command `input` specifications. +15. **Parsing.** Make enum handling open: preserve unknown variants where a safe fallback exists, and accept batch arrays on stdio. + +## Migration checklist: Clients + +1. **Initialization.** Send `protocolVersion: 2`, required `info`, and `capabilities` (drop `fs`/`terminal`). Read the Agent's capabilities from `result.capabilities` with presence checks instead of boolean checks. +2. **Rely on the baseline.** When `capabilities.session` is present, use list/resume/close/prompt/cancel without probing markers; keep checking `session.delete` and other optional extras. +3. **Prompt lifecycle.** Don't treat the `session/prompt` response as turn completion. Drive UI from `state_update` (`running` / `idle` / `requires_action`), take the stop reason from the idle update, and render the user's message from the `user_message` acknowledgment (which carries its canonical `messageId`). +4. **Messages.** Track messages by required `messageId`. Implement upsert semantics: whole-message updates replace content (or clear it with `null`/`[]`), chunks append. +5. **Tool calls.** Create tool calls on first-seen `toolCallId` in `tool_call_update`; apply omit/`null`/value patches; append `tool_call_content_chunk` items; replace accumulated content when an update carries `content`. +6. **Diffs.** Render `patch.diff` (git patch format) when present; drive file trees and summaries from `changes`; handle patch-less diffs (binary, symlink, directory). +7. **Permissions.** Render `title`/`description`; when `subject.type` is `tool_call`, apply its `toolCall` payload to the referenced tool call like any other update; show a generic prompt for absent or unknown subjects. +8. **Plans.** Handle `plan_update` keyed by `planId`; each update replaces that plan's entries. +9. **Modes.** Drop `session/set_mode` and `current_mode_update` handling; drive mode/model UI from `configOptions` (using `category`) via `session/set_config_option` and `config_option_update`. +10. **Resume.** Replace `session/load` with `session/resume` + `replayFrom: { "type": "start" }`, and plain reconnects with `session/resume` without `replayFrom`. +11. **Client surface.** Remove `fs/*` and `terminal/*` implementations for v2 connections; expose that functionality through MCP servers passed in `mcpServers` if you want agents to use it. +12. **Cancellation.** After `session/cancel`, keep accepting updates and wait for the idle `state_update` with `stopReason: "cancelled"` as confirmation. +13. **Parsing.** Preserve unknown enum variants, tolerate unknown `sessionUpdate` types, read command `input` as a tagged union, and accept batch arrays on stdio. + +## Supporting v1 and v2 side by side + +Version negotiation gives you one protocol version per connection, so the cleanest approach is to keep two thin protocol surfaces behind shared application logic and select one after `initialize`. Design your internal model around v2 concepts — upserts, message IDs, session states, structured diffs — and downgrade at the edge for v1 peers, because the reverse direction doesn't round-trip: + +- v1 has no way to express message-content replacement or clearing, metadata-only message updates, appended tool-call content chunks, structured diff operations (moves, copies, deletes, binary changes), or permission prompt copy separate from tool-call state. +- v2 deliberately drops v1's client file system and terminal surface and the modes API, so v1 peers relying on those need genuine fallback behavior, not just field mapping. +- The prompt lifecycles differ architecturally: emulating v1 on top of a v2 core means holding the `session/prompt` response until your v2-style idle state fires — simple in that direction, impossible in the other. + +If a message can't be expressed for a v1 peer, degrade deliberately (drop the refinement, not the message) rather than relying on any automatic translation to decide for you. diff --git a/docs/protocol/v2/slash-commands.mdx b/docs/protocol/v2/slash-commands.mdx index 66ff40b6..6074aef2 100644 --- a/docs/protocol/v2/slash-commands.mdx +++ b/docs/protocol/v2/slash-commands.mdx @@ -22,6 +22,7 @@ After creating a session, the Agent **MAY** send a list of available commands vi "name": "web", "description": "Search the web for information", "input": { + "type": "text", "hint": "query to search for" } }, @@ -33,6 +34,7 @@ After creating a session, the Agent **MAY** send a list of available commands vi "name": "plan", "description": "Create a detailed implementation plan", "input": { + "type": "text", "hint": "description of what to plan" } } From dd27518c8a86105fd549cb7328bb40aeb6d71bda Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 6 Jul 2026 15:05:12 +0200 Subject: [PATCH 2/4] docs(unstable-v2): expand v2 migration guide --- docs/protocol/v2/migration.mdx | 70 +++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/docs/protocol/v2/migration.mdx b/docs/protocol/v2/migration.mdx index 9e94188a..454d17a2 100644 --- a/docs/protocol/v2/migration.mdx +++ b/docs/protocol/v2/migration.mdx @@ -15,7 +15,23 @@ If you only remember five things, remember these: 4. **Capabilities were reorganized.** One `capabilities` + required `info` field on both sides, session-scoped groups nested under `session`, object support markers instead of booleans, and a required baseline of session methods. 5. **Everything is extensible now.** Enums and tagged unions accept unknown values; `_`-prefixed values are yours, the rest are reserved for future ACP versions. -The rest of this guide walks through each area: the [at-a-glance tables](#at-a-glance) for a quick diff, one section per change with before/after wire messages, and [step-by-step checklists](#migration-checklist-agents) at the end. +The rest of this guide walks through each area: the [at-a-glance tables](#at-a-glance) for a quick diff, one section per change with before/after wire messages, and [step-by-step checklists](#migration-checklist-agents) and [common mistakes](#common-migration-mistakes) at the end. + +## Scope: stable v2 and draft surfaces + +This guide describes the stable v2 baseline (`schema/v2/schema.json`). The v2 protocol surface as a whole is still labeled draft, so gate v2 support behind explicit version negotiation and feature flags until it stabilizes. + +The unstable schema (`schema/v2/schema.unstable.json`) layers opt-in draft features on top of that baseline. Negotiating `protocolVersion: 2` does **not** imply any of them — gate each behind its own capability or feature flag, and keep stable-v2 and unstable-v2 test fixtures separate. Draft-only surfaces currently include: + +- Provider configuration (`providers/list`, `providers/set`, `providers/disable`) +- Session forking (`session/fork` and `session.fork`) +- ACP-tunneled MCP servers (`session.mcp.acp` with `mcp/connect`, `mcp/message`, `mcp/disconnect`) +- Client elicitation (`elicitation/create`, `elicitation/complete`) +- Next Edit Suggestions and document sync (`nes/*`, `document/*`, and position-encoding negotiation) +- The `env_var` and `terminal` auth method types and the Client auth capabilities backing them +- Boolean session config options +- `file` and `markdown` plan content types +- Token `usage` on idle `state_update` notifications ## Version negotiation @@ -66,6 +82,16 @@ Nothing about v2 changes the underlying JSON-RPC framing, so a single connection | `session_info_update` | Unchanged | | `usage_update` | Unchanged | +## Suggested migration order + +1. Add `protocolVersion: 2` negotiation and route each connection to a separate v1 or v2 protocol surface after `initialize`. +2. Update `initialize` params/results and all capability checks. +3. Remove v1-only surfaces from v2 code paths: `fs/*`, `terminal/*`, session modes, `session/load`. +4. Rework prompt handling around `state_update` instead of the `session/prompt` response. +5. Convert message, tool-call, and plan handling to keyed upserts. +6. Update permissions, diffs, MCP configs, and slash-command input. +7. Make parsing forward-compatible (open enums, batch arrays) and add v2 fixtures covering the full prompt lifecycle, replay, cancellation, permissions, and diff rendering. + ## Initialization ### Role-agnostic `info` and `capabilities` @@ -373,7 +399,7 @@ The v1 diff shape — a single `path` with `oldText`/`newText` — could not dis ], "patch": { "format": "git_patch", - "diff": "diff --git a/src/config.json b/src/config.json\n--- a/src/config.json\n+++ b/src/config.json\n@@ -1,3 +1,3 @@\n {\n- \"debug\": false\n+ \"debug\": true\n }\n" + "diff": "diff --git /home/user/project/src/config.json /home/user/project/src/config.json\n--- /home/user/project/src/config.json\n+++ /home/user/project/src/config.json\n@@ -1,3 +1,3 @@\n {\n- \"debug\": false\n+ \"debug\": true\n }\n" } } ``` @@ -434,7 +460,7 @@ There is no mechanical mapping from v2 diffs back to `oldText`/`newText` — whe In v1, agents routinely stuffed permission prompt text into the tool call's `title`, mutating displayed tool state as a side effect. In v2, put prompt copy in `title`/`description` and let `subject.toolCall` carry only genuine tool-call state. -`options` and the response shape are unchanged: options carry `optionId`, `name`, and `kind` (`allow_once`, `allow_always`, `reject_once`, `reject_always`), and the response outcome is `{ "outcome": "selected", "optionId": ... }` or `{ "outcome": "cancelled" }`. +`options` and the response shape are unchanged: options carry `optionId`, `name`, and `kind` (`allow_once`, `allow_always`, `reject_once`, `reject_always`), and the response outcome is `{ "outcome": "selected", "optionId": ... }` or `{ "outcome": "cancelled" }`. Both unions are extensible in v2, with one safety rule: an Agent that receives an outcome it does not understand **MUST NOT** treat it as approval. While a permission request is pending, the Agent **SHOULD** report `state_update` with `requires_action`, and `running` again once resolved. @@ -504,7 +530,9 @@ The cursor is a tagged union so future versions can add replay-from-a-point vari ### Consistent lifecycle requests -`session/new` and `session/resume` take the same shape of environment parameters: a required `cwd` (absolute path), plus optional `additionalDirectories` and `mcpServers`. Note that `mcpServers` was required (even if empty) on `session/new` in v1 and is now optional. Both responses carry the session's `configOptions`; neither carries `modes` anymore. +`session/new` and `session/resume` take the same shape of environment parameters: a required `cwd` (absolute path), plus optional `additionalDirectories` and `mcpServers`. Note that `mcpServers` was required (even if empty) on `session/new` in v1 and is now optional; omitting it and sending `[]` are equivalent. Both responses carry the session's `configOptions`; neither carries `modes` anymore. + +`additionalDirectories` still requires the `session.additionalDirectories` capability and absolute paths, and each `session/resume` must send the full intended additional-root list — omitting the field or sending `[]` activates no additional roots rather than restoring previous ones. ### Baseline methods @@ -571,7 +599,9 @@ MCP transport configuration is aligned with the current MCP transport model: ## Content blocks -The five content block types are unchanged: `text`, `image`, `audio`, `resource_link`, and `resource`. v2 realigns their fields with the latest MCP specification — notably, `resource_link` gains an optional `icons` array — and the `type` discriminator is extensible like every other v2 enum, so unknown block types no longer break deserialization for receivers that have a fallback. +The five content block types are unchanged: `text`, `image`, `audio`, `resource_link`, and `resource`. v2 realigns their fields with the latest MCP specification — notably, `resource_link` gains an optional `icons` array (each icon has a required `src` URI plus optional `mimeType`, `sizes`, and `theme` of `light`/`dark`) — and the `type` discriminator is extensible like every other v2 enum, so unknown block types no longer break deserialization for receivers that have a fallback. + +If you maintain a validator or generated models, v2 also tightens schema-level details that v1 left loose: base64 payloads (image/audio `data`, resource `blob`) are marked `format: "byte"`, URI fields are marked `format: "uri"`, and `annotations.priority` is bounded to the inclusive range 0–1. ## Slash commands @@ -662,6 +692,30 @@ A standard remote transport (streamable HTTP with SSE streams, plus WebSocket) i 12. **Cancellation.** After `session/cancel`, keep accepting updates and wait for the idle `state_update` with `stopReason: "cancelled"` as confirmation. 13. **Parsing.** Preserve unknown enum variants, tolerate unknown `sessionUpdate` types, read command `input` as a tagged union, and accept batch arrays on stdio. +## Migration checklist: SDKs, validators, and proxies + +1. **Separate the versions.** Keep v1 and v2 schemas, generated models, and test fixtures fully separate, and gate unstable-v2 surfaces independently of `protocolVersion: 2`. +2. **Three-state fields.** Model omitted vs `null` vs concrete values distinctly wherever v2 defines patch semantics — a plain nullable/optional type erases a distinction the protocol depends on. +3. **Strict where it counts.** Reject malformed payloads for _known_ discriminator values instead of demoting them to the unknown-variant fallback; the fallback exists only for genuinely unknown values. +4. **Preserve what you don't understand.** Round-trip `_`-prefixed extension values, unknown future variants, and `_meta` when storing, replaying, proxying, or forwarding. +5. **Validation refinements.** Account for `format: "uri"`, `format: "byte"`, and the 0–1 `annotations.priority` bounds when generating validators. +6. **Fixtures.** Cover the full prompt lifecycle, resume replay, cancellation, permission flow, message replacement and clearing, tool-call content chunks, structured diffs, and JSON-RPC batches — and document anywhere your v2→v1 bridging is lossy. + +## Common migration mistakes + +- Treating the `session/prompt` response as turn completion — or treating `requires_action` as idle. +- Rendering the outgoing prompt as the canonical user message instead of the Agent's `user_message` acknowledgment. +- Appending a whole-message `content` array instead of replacing accumulated content (or replacing on chunks, which should append). +- Collapsing omitted and `null` patch fields into a single "missing" state. +- Still checking `clientCapabilities.fs` / `clientCapabilities.terminal`, or calling `session/load` or `session/set_mode`, on v2 connections. +- Assuming terminal _authentication_ was removed because terminal _execution_ was — they are separate surfaces, and only execution is gone. +- Advertising `capabilities.session` without implementing the entire baseline method set. +- Omitting the `type` discriminator on auth methods, MCP server configs, or command input because v1 had implicit defaults. +- Reading config options from `id`/`group` instead of `configId`/`groupId`. +- Minting non-underscore custom enum values, or treating an unknown non-underscore value as someone's extension when it is reserved for future ACP versions. +- Enabling unstable-v2 methods on every v2 connection instead of gating each behind its own flag. +- Treating an unknown permission outcome as approval. + ## Supporting v1 and v2 side by side Version negotiation gives you one protocol version per connection, so the cleanest approach is to keep two thin protocol surfaces behind shared application logic and select one after `initialize`. Design your internal model around v2 concepts — upserts, message IDs, session states, structured diffs — and downgrade at the edge for v1 peers, because the reverse direction doesn't round-trip: @@ -670,4 +724,8 @@ Version negotiation gives you one protocol version per connection, so the cleane - v2 deliberately drops v1's client file system and terminal surface and the modes API, so v1 peers relying on those need genuine fallback behavior, not just field mapping. - The prompt lifecycles differ architecturally: emulating v1 on top of a v2 core means holding the `session/prompt` response until your v2-style idle state fires — simple in that direction, impossible in the other. -If a message can't be expressed for a v1 peer, degrade deliberately (drop the refinement, not the message) rather than relying on any automatic translation to decide for you. +Treat the two versions as separate wire contracts with version-specific encoders and decoders — don't build a v2 implementation on generic v1↔v2 conversion helpers. If a message can't be expressed for a v1 peer, degrade deliberately (drop the refinement, not the message) rather than letting an automatic translation decide for you, and fail clearly when a v2 feature has no v1 representation at all. + +## Next steps + +Work through the v2 docs in an order that mirrors a connection's lifecycle: [Initialization](/protocol/v2/initialization), [Authentication](/protocol/v2/authentication), [Session Setup](/protocol/v2/session-setup), [Prompt Lifecycle](/protocol/v2/prompt-lifecycle), [Content](/protocol/v2/content), [Tool Calls](/protocol/v2/tool-calls), [Agent Plan](/protocol/v2/agent-plan), [Session Config Options](/protocol/v2/session-config-options), [Extensibility](/protocol/v2/extensibility), [Transports](/protocol/v2/transports), and the [Schema](/protocol/v2/schema) reference. From 37cf41ec1ccf2a8c76aaaabaa528f6a0fd3dec60 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 8 Jul 2026 16:14:10 +0200 Subject: [PATCH 3/4] Cleanups --- docs/protocol/v2/migration.mdx | 270 ++++++++++++++------------------- 1 file changed, 112 insertions(+), 158 deletions(-) diff --git a/docs/protocol/v2/migration.mdx b/docs/protocol/v2/migration.mdx index 454d17a2..5e754265 100644 --- a/docs/protocol/v2/migration.mdx +++ b/docs/protocol/v2/migration.mdx @@ -3,40 +3,30 @@ title: "Migrating from v1" description: "A complete guide to ACP v2 for implementers familiar with v1: breaking changes, new features, and step-by-step migration" --- -ACP v2 is a consolidation release. It redesigns the prompt lifecycle around session state, unifies streaming and non-streaming updates behind upsert semantics, makes the schema forward-compatible by default, and removes protocol surface that the ecosystem had already moved away from. +ACP v2 is a consolidation release. It redesigns the prompt lifecycle and allows for more flexible session state patterns, unifies streaming and non-streaming updates, makes the schema forward-compatible by default, and removes protocol surface that the ecosystem had already moved away from. -This guide covers everything an implementer familiar with v1 needs to move an Agent or Client to v2. All examples are JSON wire messages and apply to any language. +Migrating does not mean dropping v1. v1-only Agents and Clients will remain common for some time, so implementers should support both versions side by side: negotiate the version per connection, keep your v1 support working, and add v2 behind feature flags until it stabilizes. See [Supporting v1 and v2 side by side](#supporting-v1-and-v2-side-by-side) for how to structure this. If you only remember five things, remember these: -1. **The `session/prompt` response no longer ends the turn.** It acknowledges acceptance; progress and completion arrive as `state_update` notifications, and the stop reason moved there too. +1. **The `session/prompt` response no longer ends the turn.** It acknowledges acceptance. Progress and completion arrive as `state_update` notifications, and the stop reason moved there too. 2. **Updates are upserts.** Messages, tool calls, and plans are patched by ID with uniform semantics: omitted field = unchanged, `null` = cleared, value = replaced, chunks append. 3. **The Client file system, terminal, and session modes APIs are gone.** Use client-provided MCP servers and session config options instead. 4. **Capabilities were reorganized.** One `capabilities` + required `info` field on both sides, session-scoped groups nested under `session`, object support markers instead of booleans, and a required baseline of session methods. -5. **Everything is extensible now.** Enums and tagged unions accept unknown values; `_`-prefixed values are yours, the rest are reserved for future ACP versions. - -The rest of this guide walks through each area: the [at-a-glance tables](#at-a-glance) for a quick diff, one section per change with before/after wire messages, and [step-by-step checklists](#migration-checklist-agents) and [common mistakes](#common-migration-mistakes) at the end. +5. **Everything is extensible now.** Enums and tagged unions accept unknown values. `_`-prefixed values are yours, the rest are reserved for future ACP versions. ## Scope: stable v2 and draft surfaces -This guide describes the stable v2 baseline (`schema/v2/schema.json`). The v2 protocol surface as a whole is still labeled draft, so gate v2 support behind explicit version negotiation and feature flags until it stabilizes. - -The unstable schema (`schema/v2/schema.unstable.json`) layers opt-in draft features on top of that baseline. Negotiating `protocolVersion: 2` does **not** imply any of them — gate each behind its own capability or feature flag, and keep stable-v2 and unstable-v2 test fixtures separate. Draft-only surfaces currently include: +This guide describes the stable v2 baseline (`schema/v2/schema.json`). The v2 protocol surface as a whole is still labeled draft, so gate **v2 support behind explicit version negotiation and feature flags until it stabilizes.** -- Provider configuration (`providers/list`, `providers/set`, `providers/disable`) -- Session forking (`session/fork` and `session.fork`) -- ACP-tunneled MCP servers (`session.mcp.acp` with `mcp/connect`, `mcp/message`, `mcp/disconnect`) -- Client elicitation (`elicitation/create`, `elicitation/complete`) -- Next Edit Suggestions and document sync (`nes/*`, `document/*`, and position-encoding negotiation) -- The `env_var` and `terminal` auth method types and the Client auth capabilities backing them -- Boolean session config options -- `file` and `markdown` plan content types -- Token `usage` on idle `state_update` notifications +The unstable schema (`schema/v2/schema.unstable.json`) layers opt-in draft features on top of that baseline. Negotiating `protocolVersion: 2` does **not** imply any of them. Gate each behind its own capability or feature flag the same as v1. ## Version negotiation The mechanism is unchanged: the Client sends the latest protocol version it supports in `initialize`, and the Agent responds with the same version if supported, or its own latest version otherwise. To use v2, send `"protocolVersion": 2`. An Agent that only supports v1 will answer with `"protocolVersion": 1`, and the Client decides whether to continue with v1 or disconnect. +Treat v2 support as additive. Keep serving `protocolVersion: 1` peers when you add v2: an Agent that drops v1 cuts itself off from existing Clients, and a Client that drops v1 loses access to existing Agents. Each side selects its v1 or v2 surface per connection based on the negotiated version. + Nothing about v2 changes the underlying JSON-RPC framing, so a single connection always speaks exactly one negotiated version after `initialize`. ## At a glance @@ -45,58 +35,49 @@ Nothing about v2 changes the underlying JSON-RPC framing, so a single connection | v1 method | v2 | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `initialize` | Unchanged name; params and result restructured | +| `initialize` | Unchanged name. Params and result restructured | | `authenticate` | Renamed to `auth/login` | -| `logout` (gated by `agentCapabilities.auth.logout`) | Renamed to `auth/logout`; always required, no capability marker | -| `session/new` | Unchanged name; `mcpServers` now optional; response no longer includes `modes` | -| `session/load` | **Removed** — use `session/resume` with `"replayFrom": { "type": "start" }` | -| `session/resume` | Unchanged name; gains optional `replayFrom`; baseline (no capability marker) | -| `session/list`, `session/close` | Baseline when `capabilities.session` is present (no capability markers) | -| `session/delete` | Unchanged; still optional via `session.delete` capability | -| `session/prompt` | Unchanged shape; **response semantics redesigned** (see [Prompt lifecycle](#the-new-prompt-lifecycle)) | -| `session/cancel` | Unchanged name; completion now reported via `state_update` instead of the prompt response | -| `session/set_mode` | **Removed** — use `session/set_config_option` | +| `logout` (gated by `agentCapabilities.auth.logout`) | Renamed to `auth/logout`. Always required, no capability | +| `session/new` | `mcpServers` now optional. Response no longer includes `modes` | +| `session/load` | **Removed**. Use `session/resume` with `"replayFrom": { "type": "start" }` | +| `session/resume` | Gains optional `replayFrom`. Required (no capability marker) | +| `session/list` | Baseline no capability | +| `session/close` | Baseline no capability | +| `session/delete` | Unchanged. Still optional via `session.delete` capability | +| `session/prompt` | Unchanged shape. **Response semantics redesigned** (see [Prompt lifecycle](#the-new-prompt-lifecycle)) | +| `session/cancel` | Unchanged name. Completion now reported via `state_update` instead of the prompt response | +| `session/set_mode` | **Removed**. Use `session/set_config_option` | | `session/set_config_option` | Unchanged | -| `session/request_permission` | Unchanged name; params restructured (required `title`, optional `subject`) | -| `session/update` | Unchanged name; update variants changed (see below) | +| `session/request_permission` | Unchanged name. Params restructured (required `title`, optional `subject`) | +| `session/update` | Unchanged name. Update variants changed (see below) | | `fs/read_text_file`, `fs/write_text_file` | **Removed** | | `terminal/create`, `terminal/output`, `terminal/release`, `terminal/wait_for_exit`, `terminal/kill` | **Removed** | | `$/cancel_request` | Unchanged | ### `session/update` variant changes -| v1 `sessionUpdate` | v2 | -| --------------------------- | ----------------------------------------------------------------------------------- | -| `user_message_chunk` | Kept; `messageId` now required | -| `agent_message_chunk` | Kept; `messageId` now required | -| `agent_thought_chunk` | Kept; `messageId` now required | -| — | **New:** `user_message`, `agent_message`, `agent_thought` whole-message upserts | -| — | **New:** `state_update` (`running`, `idle`, `requires_action`) with stop reasons | -| `tool_call` | **Removed** — the first `tool_call_update` for a `toolCallId` creates the tool call | -| `tool_call_update` | Kept; now an explicit upsert with omit/`null`/value patch semantics | -| — | **New:** `tool_call_content_chunk` for streaming individual content items | -| `plan` | Replaced by `plan_update` with a `planId` and a `type` discriminator | -| `current_mode_update` | **Removed** — modes are config options; use `config_option_update` | -| `available_commands_update` | Kept; command `input` now carries a required `type` discriminator | -| `config_option_update` | Unchanged | -| `session_info_update` | Unchanged | -| `usage_update` | Unchanged | - -## Suggested migration order - -1. Add `protocolVersion: 2` negotiation and route each connection to a separate v1 or v2 protocol surface after `initialize`. -2. Update `initialize` params/results and all capability checks. -3. Remove v1-only surfaces from v2 code paths: `fs/*`, `terminal/*`, session modes, `session/load`. -4. Rework prompt handling around `state_update` instead of the `session/prompt` response. -5. Convert message, tool-call, and plan handling to keyed upserts. -6. Update permissions, diffs, MCP configs, and slash-command input. -7. Make parsing forward-compatible (open enums, batch arrays) and add v2 fixtures covering the full prompt lifecycle, replay, cancellation, permissions, and diff rendering. +| v1 `sessionUpdate` | v2 | +| --------------------------- | ---------------------------------------------------------------------------------- | +| `user_message_chunk` | Kept. `messageId` now required | +| `agent_message_chunk` | Kept. `messageId` now required | +| `agent_thought_chunk` | Kept. `messageId` now required | +| - | **New:** `user_message`, `agent_message`, `agent_thought` whole-message upserts | +| - | **New:** `state_update` (`running`, `idle`, `requires_action`) with stop reasons | +| `tool_call` | **Removed**. The first `tool_call_update` for a `toolCallId` creates the tool call | +| `tool_call_update` | Kept. Now an explicit upsert with omit/`null`/value patch semantics | +| - | **New:** `tool_call_content_chunk` for streaming individual content items | +| `plan` | Replaced by `plan_update` with a `planId` and a `type` discriminator | +| `current_mode_update` | **Removed**. Modes are config options. Use `config_option_update` | +| `available_commands_update` | Kept. Command `input` now carries a required `type` discriminator | +| `config_option_update` | Unchanged | +| `session_info_update` | Unchanged | +| `usage_update` | Unchanged | ## Initialization ### Role-agnostic `info` and `capabilities` -v1 used role-specific field names: the Client sent `clientCapabilities` and optional `clientInfo`, and the Agent returned `agentCapabilities` and optional `agentInfo`. v2 uses the same two field names in both directions — `capabilities` and `info` — and `info` is now **required** on both sides. This keeps symmetric connections (such as agent-to-agent) from needing role-specific names. +v1 used role-specific field names: the Client sent `clientCapabilities` and optional `clientInfo`, and the Agent returned `agentCapabilities` and optional `agentInfo`. v2 uses the same two field names in both directions (`capabilities` and `info`), and `info` is now **required** on both sides. @@ -193,39 +174,24 @@ v1 used role-specific field names: the Client sent `clientCapabilities` and opti ### Support markers are objects, not booleans -In v1, capability support was a mix of booleans (`"image": true`) and objects (`"list": {}`). In v2, every support marker is an object: supplying `{}` (or an object with fields) means supported, and omitting the key or supplying `null` means unsupported. Booleans remain appropriate only for actual data or configuration _inside_ an already-advertised capability. +In v1, capability support was a mix of booleans (`"image": true`) and objects (`"list": {}`). In v2, every support marker is an object: supplying `{}` (or an object with fields) means supported, and omitting the key or supplying `null` means unsupported. -This means checks like `promptCapabilities.image === true` become presence checks: `capabilities.session.prompt.image != null`. The object shape leaves room for each capability to grow fields without another breaking change. +This means checks like `promptCapabilities.image === true` become presence checks: `capabilities.session.prompt.image != null`. The object shape leaves room for each capability to grow fields or extend with `_meta` without another breaking change. ### Capability reorganization - All session-scoped capability groups now live under `capabilities.session`. What was `agentCapabilities.promptCapabilities` is now `capabilities.session.prompt`, and `agentCapabilities.mcpCapabilities` is now `capabilities.session.mcp`. -- `capabilities.session` itself is **optional**, so agents that don't do session-based prompting (for example, agents that only serve specialized extension surfaces) can omit it entirely. +- `capabilities.session` itself is **optional**, so agents that don't do session-based prompting (for example, agents that only serve specialized extension surfaces) can omit it entirely. Currently there isn't protocol support for this, but several experiments are in flight and we are making sure non-session components can be expressed via capabilities. - `loadSession` is gone along with `session/load` (see [Session setup](#session-setup-and-lifecycle)). - The individual `list`, `resume`, and `close` markers are gone: advertising `capabilities.session` at all now **requires** supporting the baseline methods `session/new`, `session/list`, `session/resume`, `session/close`, `session/prompt`, `session/cancel`, and `session/update`. Optional extras such as `session.delete`, `session.additionalDirectories`, `session.prompt`, and `session.mcp` keep their own markers. -- The v1 Client capabilities `fs` and `terminal` are removed entirely (see [Client file system and terminals](#client-file-system-and-terminals-removed)). Stable v2 currently defines no standard Client capability fields; the object remains as an extension point. - -### Auth methods - -Each entry in `authMethods` now uses `methodId` instead of `id`, and carries a required `type` discriminator so future auth flows can be introduced without breaking existing implementations: - -```json -{ - "methodId": "agent-login", - "type": "agent", - "name": "Agent login", - "description": "Sign in using the agent's login flow" -} -``` - -Stable v2 defines `type: "agent"`. Custom types **MUST** begin with `_`; unknown values without a leading `_` are reserved for future ACP versions. +- The v1 Client capabilities `fs` and `terminal` are removed entirely (see [Client file system and terminals](#client-file-system-and-terminals-removed)). Stable v2 currently defines no standard Client capability fields. ## Authentication The auth methods are grouped under an `auth/` prefix: -- `authenticate` → `auth/login`. The params are unchanged (`methodId` selecting one of the advertised auth methods); the descriptor's identifier field is renamed from `id` to `methodId` to match, and its `type` discriminator is now required. -- `logout` → `auth/logout`. In v1, logout support was opt-in via a capability marker. In v2, **every Agent must implement `auth/logout`**; there is no support marker. Agents with no authentication state simply return an empty result. +- `authenticate` → `auth/login`. The params are unchanged (`methodId` selecting one of the advertised auth methods). The descriptor's identifier field is renamed from `id` to `methodId` to match, and its `type` discriminator is now required. +- `logout` → `auth/logout`. In v1, logout support was opt-in via a capability marker. In v2, **every Agent must implement `auth/logout`**. There is no support marker. Agents with no authentication state simply return an empty result. ```json { @@ -236,6 +202,21 @@ The auth methods are grouped under an `auth/` prefix: } ``` +### Auth methods + +Each entry in `authMethods` now uses `methodId` instead of `id`, and carries a required `type` discriminator so future auth flows can be introduced without breaking existing implementations: + +```json +{ + "methodId": "agent-login", + "type": "agent", + "name": "Agent login", + "description": "Sign in using the agent's login flow" +} +``` + +Stable v2 defines `type: "agent"`. Custom types **MUST** begin with `_`. Unknown values without a leading `_` are reserved for future ACP versions. + ## The new prompt lifecycle This is the most significant semantic change in v2. Read this section even if you skim the rest. In short, everything that v1 expressed through the pending `session/prompt` request moved into `session/update` notifications: @@ -311,11 +292,11 @@ Everything else happens through `session/update` notifications: `state_update` is new in v2 and carries one of three stable states (extensible like all v2 enums): -- `running` — the Agent is actively processing work in the session. -- `idle` — the Agent is not currently processing work. Carries a `stopReason` when it completes active work. -- `requires_action` — the Agent is blocked on user action. Agents **SHOULD** send this while waiting on a permission response, and another `running` update when they resume. +- `running`: the Agent is actively processing work in the session. +- `idle`: the Agent is not currently processing work. Carries a `stopReason` when it completes active work. +- `requires_action`: the Agent is blocked on user action. Agents **SHOULD** send this while waiting on a permission response, and another `running` update when they resume. -The stop reasons themselves are unchanged from v1 — `end_turn`, `max_tokens`, `max_turn_requests`, `refusal`, `cancelled` — they just moved from the `session/prompt` response to the idle `state_update`. +The stop reasons themselves are unchanged from v1 (`end_turn`, `max_tokens`, `max_turn_requests`, `refusal`, `cancelled`). They just moved from the `session/prompt` response to the idle `state_update`. ### Cancellation @@ -323,31 +304,31 @@ The stop reasons themselves are unchanged from v1 — `end_turn`, `max_tokens`, ### Why this matters beyond bookkeeping -Because turn progress is now expressed entirely in notifications, the same message flow works for history replay on `session/resume`, for multiple clients observing one session, and for future agent-initiated or queued work — none of which fit the v1 request/response turn model. +Because turn progress is now expressed entirely in notifications, the same message flow works for history replay on `session/resume`, for multiple clients observing one session, and for future agent-initiated or queued work, none of which fit the v1 request/response turn model. ## Messages and message IDs ### Message IDs are required -Every message chunk and message update in v2 **MUST** carry a `messageId`. In v1, `messageId` was optional on chunks. Message IDs are opaque strings generated by the Agent — the Agent owns session history, so it is the single source of message identity. All chunks of one message share one `messageId`; a changed `messageId` starts a new message. +Every message chunk and message update in v2 **MUST** carry a `messageId`. In v1, `messageId` was optional on chunks. Message IDs are opaque strings generated by the Agent: the Agent owns session history, so it is the single source of message identity. All chunks of one message share one `messageId`. A changed `messageId` starts a new message. ### Whole-message upserts Alongside the chunk updates, v2 adds `user_message`, `agent_message`, and `agent_thought` updates that carry a full `content` **array** (chunks carry a single content block). These are upserts keyed by `messageId`, with three-state patch semantics: -- **Omitted** `content` — leave existing content unchanged (useful for updating `_meta` or other fields without resending content). -- `content: null` **or** `content: []` — clear the message content. -- **Concrete array** — replace all content currently stored for that message, including content accumulated from earlier chunks. +- **Omitted** `content` leaves existing content unchanged (useful for updating `_meta` or other fields without resending content). +- `content: null` **or** `content: []` clears the message content. +- A **concrete array** replaces all content currently stored for that message, including content accumulated from earlier chunks. Chunks always **append** to whatever content is current. For example: `agent_message` with `content: [A]`, then `agent_message_chunk` with `B` renders as `[A, B]`. A later `agent_message` with `content: [C]` resets the message to `[C]`, and subsequent chunks append to that. -Use chunks for streaming; use whole-message updates for replay, correction, or output that is already complete. +Use chunks for streaming. Use whole-message updates for replay, correction, or output that is already complete. ## Tool calls ### One upsert notification -v1 had `tool_call` (create) and `tool_call_update` (modify). In practice the split bought nothing, so v2 keeps only `tool_call_update`. The first update with an unseen `toolCallId` creates the tool call; subsequent updates patch it. Only `toolCallId` is required, but the Agent **SHOULD** include `title` on the first report: +v1 had `tool_call` (create) and `tool_call_update` (modify). In practice the split bought nothing, so v2 keeps only `tool_call_update`. The first update with an unseen `toolCallId` creates the tool call. Subsequent updates patch it. Only `toolCallId` is required, but the Agent **SHOULD** include `title` on the first report: ```json { @@ -376,7 +357,7 @@ v2 adds `tool_call_content_chunk`, which **appends** a single `ToolCallContent` } ``` -Chunks and updates compose exactly like message chunks and updates: a `tool_call_update` that includes `content` replaces the accumulated content; later chunks append to it. +Chunks and updates compose exactly like message chunks and updates: a `tool_call_update` that includes `content` replaces the accumulated content. Later chunks append to it. ### Terminal content removed @@ -384,7 +365,7 @@ The v1 `ToolCallContent` variant `{ "type": "terminal", "terminalId": ... }` is ## Diff content -The v1 diff shape — a single `path` with `oldText`/`newText` — could not distinguish a deleted file from an emptied one, had no way to express renames, copies, or binary changes, and forced clients to compute renderable diffs themselves. v2 replaces it with structured changes plus optional renderable patch text: +The v1 diff shape (a single `path` with `oldText`/`newText`) could not distinguish a deleted file from an emptied one, had no way to express renames, copies, or binary changes, and forced clients to compute renderable diffs themselves. v2 replaces it with structured changes plus optional renderable patch text: ```json { @@ -404,8 +385,8 @@ The v1 diff shape — a single `path` with `oldText`/`newText` — could not dis } ``` -- `changes` (required) is a list of file-level operations. `add`, `delete`, and `modify` carry a `path`; `move` and `copy` carry `oldPath` and `path`. Each change may carry an optional `fileType` (`text`, `binary`, `directory`, `symlink`) and `mimeType`. Clients can build file trees and summaries from `changes` without parsing any patch text. -- `patch` (optional) carries renderable patch text with a `format` discriminator; stable v2 defines `git_patch`. Agents **SHOULD** provide `patch` whenever feasible; Clients **MUST** handle diffs where `patch` is omitted or `null` (binary changes, symlink retargets, and similar cases have no useful text rendering). +- `changes` (required) is a list of file-level operations. `add`, `delete`, and `modify` carry a `path`. `move` and `copy` carry `oldPath` and `path`. Each change may carry an optional `fileType` (`text`, `binary`, `directory`, `symlink`) and `mimeType`. Clients can build file trees and summaries from `changes` without parsing any patch text. +- `patch` (optional) carries renderable patch text with a `format` discriminator. Stable v2 defines `git_patch`. Agents **SHOULD** provide `patch` whenever feasible. Clients **MUST** handle diffs where `patch` is omitted or `null` (binary changes, symlink retargets, and similar cases have no useful text rendering). A deleted file is now simply: @@ -422,7 +403,7 @@ A deleted file is now simply: } ``` -There is no mechanical mapping from v2 diffs back to `oldText`/`newText` — when migrating a v1 client, replace your diff rendering with a git-patch renderer driven by `patch`, using `changes` for headers, file trees, and non-text cases. +There is no mechanical mapping from v2 diffs back to `oldText`/`newText`. When migrating a v1 client, replace your diff rendering with a git-patch renderer driven by `patch`, using `changes` for headers, file trees, and non-text cases. ## Permission requests @@ -454,9 +435,9 @@ There is no mechanical mapping from v2 diffs back to `oldText`/`newText` — whe } ``` -- `title` is **required** — human-readable text for the permission prompt itself. It does not update any tool call's displayed title. +- `title` is **required**: human-readable text for the permission prompt itself. It does not update any tool call's displayed title. - `description` is optional supporting copy. -- `subject` is an optional tagged union describing _what_ needs permission. Stable v2 defines `type: "tool_call"`, whose `toolCall` payload is the same `ToolCallUpdate` upsert shape used in session updates — so any fields it carries patch the referenced tool call's displayed state, exactly like a `tool_call_update` would. Requests may omit `subject` entirely for approvals that aren't about a tool call, and unknown subject types should be preserved when proxying and either rendered generically or declined by policy. +- `subject` is an optional tagged union describing _what_ needs permission. Stable v2 defines `type: "tool_call"`, whose `toolCall` payload is the same `ToolCallUpdate` upsert shape used in session updates, so any fields it carries patch the referenced tool call's displayed state, exactly like a `tool_call_update` would. Requests may omit `subject` entirely for approvals that aren't about a tool call, and unknown subject types should be preserved when proxying and either rendered generically or declined by policy. In v1, agents routinely stuffed permission prompt text into the tool call's `title`, mutating displayed tool state as a side effect. In v2, put prompt copy in `title`/`description` and let `subject.toolCall` carry only genuine tool-call state. @@ -502,11 +483,11 @@ The v1 `plan` update was a flat entries list with no identity and no room to gro -Stable v2 defines the `items` plan type; the entry shape (`content`, `priority`, `status`, all required) is unchanged from v1, and entry priorities and statuses are now extensible enums. Each `plan_update` for a given `planId` replaces that plan's entries, and the `planId` allows multiple plans per session and future plan content types (such as markdown or file-backed plans, which remain unstable) without another breaking change. +Stable v2 defines the `items` plan type. The entry shape (`content`, `priority`, `status`, all required) is unchanged from v1, and entry priorities and statuses are now extensible enums. Each `plan_update` for a given `planId` replaces that plan's entries, and the `planId` allows multiple plans per session and future plan content types (such as markdown or file-backed plans, which remain unstable) without another breaking change. ## Session setup and lifecycle -### `session/load` is gone; `session/resume` does both +### `session/load` is gone. `session/resume` does both v1 had two ways to reattach to a session: `session/load` (replays full history, gated by `loadSession`) and `session/resume` (no replay, gated by `sessionCapabilities.resume`). v2 keeps only `session/resume` with an optional `replayFrom` cursor: @@ -530,9 +511,9 @@ The cursor is a tagged union so future versions can add replay-from-a-point vari ### Consistent lifecycle requests -`session/new` and `session/resume` take the same shape of environment parameters: a required `cwd` (absolute path), plus optional `additionalDirectories` and `mcpServers`. Note that `mcpServers` was required (even if empty) on `session/new` in v1 and is now optional; omitting it and sending `[]` are equivalent. Both responses carry the session's `configOptions`; neither carries `modes` anymore. +`session/new` and `session/resume` take the same shape of environment parameters: a required `cwd` (absolute path), plus optional `additionalDirectories` and `mcpServers`. Note that `mcpServers` was required (even if empty) on `session/new` in v1 and is now optional. Omitting it and sending `[]` are equivalent. Both responses carry the session's `configOptions`. Neither carries `modes` anymore. -`additionalDirectories` still requires the `session.additionalDirectories` capability and absolute paths, and each `session/resume` must send the full intended additional-root list — omitting the field or sending `[]` activates no additional roots rather than restoring previous ones. +`additionalDirectories` still requires the `session.additionalDirectories` capability and absolute paths, and each `session/resume` must send the full intended additional-root list. Omitting the field or sending `[]` activates no additional roots rather than restoring previous ones. ### Baseline methods @@ -540,7 +521,7 @@ As covered under [Initialization](#capability-reorganization), advertising `capa ## Session modes become config options -The dedicated modes API is removed: the `modes` field on session responses, `session/set_mode`, the `current_mode_update` notification, and all `SessionMode*` types. Mode-like state — and model selection, thinking level, and similar knobs — is expressed through session config options, which already existed in v1: +The dedicated modes API is removed: the `modes` field on session responses, `session/set_mode`, the `current_mode_update` notification, and all `SessionMode*` types. Mode-like state (and model selection, thinking level, and similar knobs) is expressed through session config options, which already existed in v1: ```json { @@ -557,7 +538,7 @@ The dedicated modes API is removed: the `modes` field on session responses, `ses ``` - The option's identifier field is renamed from `id` to `configId`, matching the parameter name used by `session/set_config_option`. -- The stable `category` values are `mode`, `model`, `model_config`, and `thought_level`; clients can use them for UX affordances (placement, shortcuts) and must tolerate unknown categories. +- The stable `category` values are `mode`, `model`, `model_config`, and `thought_level`. Clients can use them for UX affordances (placement, shortcuts) and must tolerate unknown categories. - Clients change values with `session/set_config_option` (`sessionId`, `configId`, `value`), and the response returns the full updated `configOptions` array, since one change can affect other options. Agent-initiated changes arrive as `config_option_update` session updates, which replace the v1 `current_mode_update`. ## Client file system and terminals removed @@ -568,13 +549,13 @@ v2 removes the entire v1 Client-provided execution surface: - `clientCapabilities.terminal` and the `terminal/create`, `terminal/output`, `terminal/release`, `terminal/wait_for_exit`, `terminal/kill` methods - The `terminal` tool-call content type -In practice this surface was inconsistently implemented outside of a few IDEs, and agents already needed their own file and execution handling for clients that didn't offer it. Clients that want to expose file access, unsaved editor state, or command execution to agents should do so by providing an **MCP server** to the session (via `mcpServers` on `session/new` / `session/resume`), which puts those tools on the same footing as every other tool the agent uses. Agent-side, drop the capability checks and the dual code paths; report file changes through [diff content](#diff-content) and command output through regular tool-call content. +In practice this surface was inconsistently implemented outside of a few IDEs, and agents already needed their own file and execution handling for clients that didn't offer it. Clients that want to expose file access, unsaved editor state, or command execution to agents should do so by providing an **MCP server** to the session (via `mcpServers` on `session/new` / `session/resume`), which puts those tools on the same footing as every other tool the agent uses. Agent-side, drop the capability checks and the dual code paths. Report file changes through [diff content](#diff-content) and command output through regular tool-call content. ## MCP server configuration MCP transport configuration is aligned with the current MCP transport model: -- Every server config **MUST** carry a `type` discriminator. In v1, stdio configs had no `type` field; in v2 they are `"type": "stdio"`. Unknown transport types can then be carried as extension variants instead of failing to parse. +- Every server config **MUST** carry a `type` discriminator. In v1, stdio configs had no `type` field. In v2 they are `"type": "stdio"`. Unknown transport types can then be carried as extension variants instead of failing to parse. - The deprecated HTTP+SSE transport (`"type": "sse"`) is **removed**. - Support is advertised per transport under `capabilities.session.mcp`: `stdio` is now an explicit capability (`session.mcp.stdio`), so agents that cannot launch subprocesses can opt out, and `session.mcp.http` covers remote servers. - Empty arrays are no longer required: `args` and `env` on stdio configs and `headers` on HTTP configs are optional in v2 (v1 required `args` and `env` even when empty). @@ -599,7 +580,7 @@ MCP transport configuration is aligned with the current MCP transport model: ## Content blocks -The five content block types are unchanged: `text`, `image`, `audio`, `resource_link`, and `resource`. v2 realigns their fields with the latest MCP specification — notably, `resource_link` gains an optional `icons` array (each icon has a required `src` URI plus optional `mimeType`, `sizes`, and `theme` of `light`/`dark`) — and the `type` discriminator is extensible like every other v2 enum, so unknown block types no longer break deserialization for receivers that have a fallback. +The five content block types are unchanged: `text`, `image`, `audio`, `resource_link`, and `resource`. v2 realigns their fields with the latest MCP specification. Notably, `resource_link` gains an optional `icons` array (each icon has a required `src` URI plus optional `mimeType`, `sizes`, and `theme` of `light`/`dark`), and the `type` discriminator is extensible like every other v2 enum, so unknown block types no longer break deserialization for receivers that have a fallback. If you maintain a validator or generated models, v2 also tightens schema-level details that v1 left loose: base64 payloads (image/audio `data`, resource `blob`) are marked `format: "byte"`, URI fields are marked `format: "uri"`, and `annotations.priority` is bounded to the inclusive range 0–1. @@ -627,7 +608,7 @@ The advertisement model (`available_commands_update` carrying commands with `nam -As with other extensible unions, unknown input types should be preserved when proxying; a client that can't render one can fall back to plain text input or hide the command's input hint. +As with other extensible unions, unknown input types should be preserved when proxying. A client that can't render one can fall back to plain text input or hide the command's input hint. ## Consistent ID naming @@ -639,7 +620,7 @@ v2 applies one naming rule across the schema: a field that identifies a protocol | Session config option | `id` | `configId` | | Session config select group | `group` | `groupId` | -New entities introduced in v2 follow the same rule (`messageId` on messages, `planId` on plans). If you generate types from the schema, expect these renames to surface as compile-time breaks — a good checklist by itself. +New entities introduced in v2 follow the same rule (`messageId` on messages, `planId` on plans). A few schema definition names also changed without any wire-format change, which matters only to schema-codegen consumers: the `session/update` and `session/cancel` payload types are now `UpdateSessionNotification` and `CancelSessionNotification`, and the auth request/response types follow the new method names (`LoginAuthRequest`/`LoginAuthResponse`, `LogoutAuthRequest`/`LogoutAuthResponse`). @@ -647,85 +628,58 @@ A few schema definition names also changed without any wire-format change, which v2 makes the whole schema forward-compatible by convention: -- **Open enums and tagged unions.** Every enum-like string (`sessionUpdate`, tool `kind`, tool `status`, stop reasons, plan types and entry priorities/statuses, content types, content roles, diff operations and file types, permission subjects, permission option kinds and outcomes, auth method types, config option types and categories, command input types, MCP transports, …) accepts unknown values. Values beginning with `_` are reserved for implementation-specific extensions (e.g. `_mycompany_widget`); unknown values _without_ a leading `_` are reserved for future ACP versions. Receivers **SHOULD** preserve unknown values when storing, replaying, proxying, or forwarding, and either render them generically or fall back safely. Do not mint non-underscore custom values. +- **Open enums and tagged unions.** Every enum-like string accepts unknown values. Values beginning with `_` are reserved for implementation-specific extensions (e.g. `_mycompany_widget`). Unknown values _without_ a leading `_` are reserved for future ACP versions. Receivers **SHOULD** preserve unknown values when storing, replaying, proxying, or forwarding, and either render them generically or fall back safely. Do not mint non-underscore custom values. - **`_meta` everywhere.** As in v1, `_meta` objects carry implementation metadata. In v2's upsert-style updates, `_meta` follows the same patch semantics as other fields: omitted means unchanged, `null` clears. Chunk-level `_meta` is scoped to the chunk. - **Custom methods** still use the `_` prefix, unchanged from v1. -- **Explicit nullability.** Throughout the v2 schema, optional fields document whether `null` is equivalent to omission; for upsert payloads they are deliberately different (omitted = unchanged, `null` = clear). ## Transports -stdio remains the primary transport and is unchanged except that v2 explicitly follows **JSON-RPC 2.0 batch** behavior: a newline-delimited message may be a single request, notification, response, or a batch array. Receivers may process batch entries concurrently, must not reply to notifications, and return per-entry `-32600` errors for invalid entries. Don't batch lifecycle-sensitive messages (`initialize`, `auth/login`, `session/new`, `session/resume`, `session/prompt`) — they change which later messages are valid. +stdio remains the primary transport and is unchanged except that v2 explicitly follows **JSON-RPC 2.0 batch** behavior: a newline-delimited message may be a single request, notification, response, or a batch array. Receivers may process batch entries concurrently, must not reply to notifications, and return per-entry `-32600` errors for invalid entries. Don't batch lifecycle-sensitive messages (`initialize`, `auth/login`, `session/new`, `session/resume`, `session/prompt`). They change which later messages are valid. -A standard remote transport (streamable HTTP with SSE streams, plus WebSocket) is being specified in its own RFD and is not part of the core v2 protocol surface; see the [transport RFD](/rfds/streamable-http-websocket-transport) for its current state. +A standard remote transport (streamable HTTP with SSE streams, plus WebSocket) is being specified in its own RFD and is not part of the core v2 protocol surface. See the [transport RFD](/rfds/streamable-http-websocket-transport) for its current state. ## Migration checklist: Agents -1. **Initialization.** Read `params.capabilities` / `params.info`; return `capabilities` and a required `info`. Restructure your capability object: nest prompt and MCP capabilities under `session`, convert boolean markers to `{}` objects, drop `loadSession` and the `list`/`resume`/`close` markers. +1. **Initialization.** Read `params.capabilities` / `params.info`. Return `capabilities` and a required `info`. Restructure your capability object: nest prompt and MCP capabilities under `session`, convert boolean markers to `{}` objects, drop `loadSession` and the `list`/`resume`/`close` markers. 2. **Implement the baseline.** If you advertise `session`, implement `session/new`, `session/list`, `session/resume`, `session/close`, `session/prompt`, `session/cancel`, and `session/update`. -3. **Auth.** Rename `authenticate` → `auth/login`; implement `auth/logout` unconditionally; add `type` and `methodId` to your auth method descriptors. -4. **Prompt lifecycle.** Respond to `session/prompt` immediately with `{}` on acceptance. Emit a `user_message` acknowledgment with an agent-generated `messageId`, a `running` state update when processing starts, and an idle `state_update` carrying the stop reason when the work completes — including `cancelled` after `session/cancel`. +3. **Auth.** Rename `authenticate` → `auth/login`. Implement `auth/logout` unconditionally. Add `type` and `methodId` to your auth method descriptors. +4. **Prompt lifecycle.** Respond to `session/prompt` immediately with `{}` on acceptance. Emit a `user_message` acknowledgment with an agent-generated `messageId`, a `running` state update when processing starts, and an idle `state_update` carrying the stop reason when the work completes, including `cancelled` after `session/cancel`. 5. **Message IDs.** Generate and attach a `messageId` to every message chunk and update. -6. **Tool calls.** Stop sending `tool_call`; send `tool_call_update` for creation and patches. Use `tool_call_content_chunk` to stream content. Respect omit/`null`/value patch semantics. +6. **Tool calls.** Stop sending `tool_call`. Send `tool_call_update` for creation and patches. Use `tool_call_content_chunk` to stream content. Respect omit/`null`/value patch semantics. 7. **Diffs.** Replace `oldText`/`newText` with `changes` (+ `fileType`/`mimeType` where known) and provide a `git_patch` whenever feasible. -8. **Permissions.** Put prompt copy in required `title` (+ optional `description`); wrap tool-call context in `subject: { "type": "tool_call", "toolCall": ... }`. +8. **Permissions.** Put prompt copy in required `title` (+ optional `description`). Wrap tool-call context in `subject: { "type": "tool_call", "toolCall": ... }`. 9. **Plans.** Send `plan_update` with `{ "type": "items", "planId": ..., "entries": [...] }`. -10. **Modes.** Drop `session/set_mode`, `modes`, and `current_mode_update`; expose the same state as config options with `category: "mode"` and emit `config_option_update`. -11. **Replay.** Handle `replayFrom` on `session/resume`; replay history as ordinary `session/update` notifications when asked. Remove `session/load`. -12. **Client surface.** Remove all `fs/*` and `terminal/*` calls and terminal tool content; rely on client-provided MCP servers for client-side access. -13. **MCP config.** Require and emit `type` discriminators; drop SSE; advertise `session.mcp.stdio` / `session.mcp.http` as appropriate; accept omitted `mcpServers`. +10. **Modes.** Drop `session/set_mode`, `modes`, and `current_mode_update`. Expose the same state as config options with `category: "mode"` and emit `config_option_update`. +11. **Replay.** Handle `replayFrom` on `session/resume`. Replay history as ordinary `session/update` notifications when asked. Remove `session/load`. +12. **Client surface.** Remove all `fs/*` and `terminal/*` calls and terminal tool content. +13. **MCP config.** Require and emit `type` discriminators. Drop SSE. Advertise `session.mcp.stdio` / `session.mcp.http` as appropriate. 14. **Slash commands.** Add the `type: "text"` discriminator to command `input` specifications. 15. **Parsing.** Make enum handling open: preserve unknown variants where a safe fallback exists, and accept batch arrays on stdio. ## Migration checklist: Clients 1. **Initialization.** Send `protocolVersion: 2`, required `info`, and `capabilities` (drop `fs`/`terminal`). Read the Agent's capabilities from `result.capabilities` with presence checks instead of boolean checks. -2. **Rely on the baseline.** When `capabilities.session` is present, use list/resume/close/prompt/cancel without probing markers; keep checking `session.delete` and other optional extras. +2. **Rely on the baseline.** When `capabilities.session` is present, use list/resume/close/prompt/cancel without probing markers. Keep checking `session.delete` and other optional extras. 3. **Prompt lifecycle.** Don't treat the `session/prompt` response as turn completion. Drive UI from `state_update` (`running` / `idle` / `requires_action`), take the stop reason from the idle update, and render the user's message from the `user_message` acknowledgment (which carries its canonical `messageId`). 4. **Messages.** Track messages by required `messageId`. Implement upsert semantics: whole-message updates replace content (or clear it with `null`/`[]`), chunks append. -5. **Tool calls.** Create tool calls on first-seen `toolCallId` in `tool_call_update`; apply omit/`null`/value patches; append `tool_call_content_chunk` items; replace accumulated content when an update carries `content`. -6. **Diffs.** Render `patch.diff` (git patch format) when present; drive file trees and summaries from `changes`; handle patch-less diffs (binary, symlink, directory). -7. **Permissions.** Render `title`/`description`; when `subject.type` is `tool_call`, apply its `toolCall` payload to the referenced tool call like any other update; show a generic prompt for absent or unknown subjects. -8. **Plans.** Handle `plan_update` keyed by `planId`; each update replaces that plan's entries. -9. **Modes.** Drop `session/set_mode` and `current_mode_update` handling; drive mode/model UI from `configOptions` (using `category`) via `session/set_config_option` and `config_option_update`. +5. **Tool calls.** Create tool calls on first-seen `toolCallId` in `tool_call_update`. Apply omit/`null`/value patches. Append `tool_call_content_chunk` items. Replace accumulated content when an update carries `content`. +6. **Diffs.** Render `patch.diff` (git patch format) when present. Drive file trees and summaries from `changes`. Handle patch-less diffs (binary, symlink, directory). +7. **Permissions.** Render `title`/`description`. When `subject.type` is `tool_call`, apply its `toolCall` payload to the referenced tool call like any other update. Show a generic prompt for absent or unknown subjects. +8. **Plans.** Handle `plan_update` keyed by `planId`. Each update replaces that plan's entries. +9. **Modes.** Drop `session/set_mode` and `current_mode_update` handling. Drive mode/model UI from `configOptions` (using `category`) via `session/set_config_option` and `config_option_update`. 10. **Resume.** Replace `session/load` with `session/resume` + `replayFrom: { "type": "start" }`, and plain reconnects with `session/resume` without `replayFrom`. -11. **Client surface.** Remove `fs/*` and `terminal/*` implementations for v2 connections; expose that functionality through MCP servers passed in `mcpServers` if you want agents to use it. +11. **Client surface.** Remove `fs/*` and `terminal/*` implementations for v2 connections. Expose that functionality through MCP servers passed in `mcpServers` if you want agents to use it. 12. **Cancellation.** After `session/cancel`, keep accepting updates and wait for the idle `state_update` with `stopReason: "cancelled"` as confirmation. 13. **Parsing.** Preserve unknown enum variants, tolerate unknown `sessionUpdate` types, read command `input` as a tagged union, and accept batch arrays on stdio. -## Migration checklist: SDKs, validators, and proxies +## Migration checklist: SDKs 1. **Separate the versions.** Keep v1 and v2 schemas, generated models, and test fixtures fully separate, and gate unstable-v2 surfaces independently of `protocolVersion: 2`. -2. **Three-state fields.** Model omitted vs `null` vs concrete values distinctly wherever v2 defines patch semantics — a plain nullable/optional type erases a distinction the protocol depends on. -3. **Strict where it counts.** Reject malformed payloads for _known_ discriminator values instead of demoting them to the unknown-variant fallback; the fallback exists only for genuinely unknown values. +2. **Three-state fields.** Model omitted vs `null` vs concrete values distinctly wherever v2 defines patch semantics. A plain nullable/optional type erases a distinction the protocol depends on. +3. **Strict where it counts.** Reject malformed payloads for _known_ discriminator values instead of demoting them to the unknown-variant fallback. The fallback exists only for genuinely unknown values. 4. **Preserve what you don't understand.** Round-trip `_`-prefixed extension values, unknown future variants, and `_meta` when storing, replaying, proxying, or forwarding. -5. **Validation refinements.** Account for `format: "uri"`, `format: "byte"`, and the 0–1 `annotations.priority` bounds when generating validators. -6. **Fixtures.** Cover the full prompt lifecycle, resume replay, cancellation, permission flow, message replacement and clearing, tool-call content chunks, structured diffs, and JSON-RPC batches — and document anywhere your v2→v1 bridging is lossy. - -## Common migration mistakes - -- Treating the `session/prompt` response as turn completion — or treating `requires_action` as idle. -- Rendering the outgoing prompt as the canonical user message instead of the Agent's `user_message` acknowledgment. -- Appending a whole-message `content` array instead of replacing accumulated content (or replacing on chunks, which should append). -- Collapsing omitted and `null` patch fields into a single "missing" state. -- Still checking `clientCapabilities.fs` / `clientCapabilities.terminal`, or calling `session/load` or `session/set_mode`, on v2 connections. -- Assuming terminal _authentication_ was removed because terminal _execution_ was — they are separate surfaces, and only execution is gone. -- Advertising `capabilities.session` without implementing the entire baseline method set. -- Omitting the `type` discriminator on auth methods, MCP server configs, or command input because v1 had implicit defaults. -- Reading config options from `id`/`group` instead of `configId`/`groupId`. -- Minting non-underscore custom enum values, or treating an unknown non-underscore value as someone's extension when it is reserved for future ACP versions. -- Enabling unstable-v2 methods on every v2 connection instead of gating each behind its own flag. -- Treating an unknown permission outcome as approval. +5. **Fixtures.** Cover the full prompt lifecycle, resume replay, cancellation, permission flow, message replacement and clearing, tool-call content chunks, structured diffs, and JSON-RPC batches. ## Supporting v1 and v2 side by side -Version negotiation gives you one protocol version per connection, so the cleanest approach is to keep two thin protocol surfaces behind shared application logic and select one after `initialize`. Design your internal model around v2 concepts — upserts, message IDs, session states, structured diffs — and downgrade at the edge for v1 peers, because the reverse direction doesn't round-trip: - -- v1 has no way to express message-content replacement or clearing, metadata-only message updates, appended tool-call content chunks, structured diff operations (moves, copies, deletes, binary changes), or permission prompt copy separate from tool-call state. -- v2 deliberately drops v1's client file system and terminal surface and the modes API, so v1 peers relying on those need genuine fallback behavior, not just field mapping. -- The prompt lifecycles differ architecturally: emulating v1 on top of a v2 core means holding the `session/prompt` response until your v2-style idle state fires — simple in that direction, impossible in the other. - -Treat the two versions as separate wire contracts with version-specific encoders and decoders — don't build a v2 implementation on generic v1↔v2 conversion helpers. If a message can't be expressed for a v1 peer, degrade deliberately (drop the refinement, not the message) rather than letting an automatic translation decide for you, and fail clearly when a v2 feature has no v1 representation at all. - -## Next steps - -Work through the v2 docs in an order that mirrors a connection's lifecycle: [Initialization](/protocol/v2/initialization), [Authentication](/protocol/v2/authentication), [Session Setup](/protocol/v2/session-setup), [Prompt Lifecycle](/protocol/v2/prompt-lifecycle), [Content](/protocol/v2/content), [Tool Calls](/protocol/v2/tool-calls), [Agent Plan](/protocol/v2/agent-plan), [Session Config Options](/protocol/v2/session-config-options), [Extensibility](/protocol/v2/extensibility), [Transports](/protocol/v2/transports), and the [Schema](/protocol/v2/schema) reference. +Supporting both versions is the recommended path, not an edge case: v1 peers will remain in the wild well after v2 stabilizes, and dropping v1 support means losing them. Version negotiation gives you one protocol version per connection, so the cleanest approach is to keep two thin protocol surfaces behind shared application logic and select one after `initialize`. From 72b5a2059170908adfdc39415ee23d4ab8908fd5 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 8 Jul 2026 16:17:49 +0200 Subject: [PATCH 4/4] fix v2 header --- docs/docs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs.json b/docs/docs.json index 972c931e..a143c988 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -199,7 +199,7 @@ "rfds/elicitation", "rfds/streamable-http-websocket-transport", { - "group": "v2 Active", + "group": "v2", "expanded": true, "root": "rfds/v2/overview", "pages": [