- The
session/promptresponse no longer ends the turn. It acknowledges acceptance. Foreground progress and completion arrive asstate_updatenotifications, and the stop reason moved there too. - Updates are upserts. Messages, tool calls, and plans are patched by ID with uniform semantics: omitted field = unchanged,
null= cleared, value = replaced, chunks append. - The Client file system, terminal execution, and session modes APIs are gone. Agent-owned terminal output is a separate display-only v2 surface; use client-provided MCP servers when the Agent needs Client-side tools.
- Capabilities were reorganized. One
capabilities+ requiredinfofield on both sides, session-scoped groups nested undersession, object support markers instead of booleans, and a required baseline of session methods. - 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 the same as v1.
Version negotiation
The mechanism is unchanged: the Client sends the latest protocol version it supports ininitialize, 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
Method changes
session/update variant changes
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.
authMethods array. In v2, omitting authMethods has the same availability meaning: the Agent does not advertise the authentication surface, and Clients MUST NOT call auth/login or auth/logout.
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.
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 wasagentCapabilities.promptCapabilitiesis nowcapabilities.session.prompt, andagentCapabilities.mcpCapabilitiesis nowcapabilities.session.mcp. capabilities.sessionitself 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.loadSessionis gone along withsession/load(see Session setup).- The individual
list,resume, andclosemarkers are gone: advertisingcapabilities.sessionat all now requires supporting the baseline methodssession/new,session/list,session/resume,session/close,session/prompt,session/cancel, andsession/update. Optional extras such assession.delete,session.additionalDirectories,session.prompt, andsession.mcpkeep their own markers. - The v1 Client capabilities
fsandterminalare removed entirely (see Client file system and terminal execution). Stable v2 currently defines no standard Client capability fields. Agent-owned terminal display is baseline behavior, not a Client execution capability.
Authentication
The auth methods are grouped under anauth/ prefix:
authenticate→auth/login. The params are unchanged (methodIdselecting one of the advertised auth methods). The descriptor’s identifier field is renamed fromidtomethodIdto match, and itstypediscriminator is now required.logout→auth/logout. In v1, logout support was opt-in via a capability marker. In v2, returning one or more valid entries inauthMethodsadvertises the authentication surface, and the Agent MUST implement bothauth/loginandauth/logout. IfauthMethodsis omitted or empty, Clients MUST NOT call either method. There is no logout support marker;capabilities.authremains orthogonal and advertises only authentication-related extensions.
Auth methods
Each entry inauthMethods now uses methodId instead of id, and carries a required type discriminator so future auth flows can be introduced without breaking existing implementations:
auth/login and auth/logout.
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 pendingsession/prompt request moved into session/update notifications:
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:
v2: the response is an acknowledgment
In v2, the Agent MUST respond tosession/prompt as soon as it has accepted the prompt, with an empty result:
session/update notifications:
-
User message acknowledgment. After accepting the prompt, the Agent MUST report where the user message was inserted into session history, either as a
user_messageupdate with a fullcontentarray or as streameduser_message_chunkupdates. This update is the source of truth for the agent-ownedmessageId: -
Running state. When foreground work starts or resumes, the Agent MUST send a
state_updatewith"state": "running". - Output. Messages, thoughts, plans, and tool calls stream as before (with the changes described in the following sections).
-
Completion. When the Agent is ready to process a new prompt, it MUST report
idle. When the transition ends foreground work, it MUST include the stop reason:
Session states
state_update is new in v2 and reports foreground work using one of three stable states (extensible like all v2 enums):
running: Foreground work is in progress.idle: The Agent is ready to process a new prompt. Carries astopReasonwhen the transition ends foreground work.requires_action: Foreground work is blocked on user action. Agents SHOULD send this while waiting on a permission response, and anotherrunningupdate when work resumes.
session/update notifications while the Agent reports idle. Those notifications do not change the state.
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 and describe why foreground work stopped.
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 foreground progress is now expressed entirely in notifications, the same message flow works for history replay onsession/resume, multiple clients observing one session, and future agent-initiated or queued work.
Messages and message IDs
Message IDs are required
Every message chunk and message update in v2 MUST carry amessageId. 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 addsuser_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
contentleaves existing content unchanged (useful for updating_metaor other fields without resending content). content: nullorcontent: []clears the message content.- A concrete array replaces all content currently stored for that message, including content accumulated from earlier chunks.
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 hadtool_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:
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 addstool_call_content_chunk, which appends a single ToolCallContent item to a tool call instead of resending the whole array:
tool_call_update that includes content replaces the accumulated content. Later chunks append to it.
Agent-owned terminal display
v2 retains aterminal ToolCallContent shape, but changes its ownership and meaning. It is now only a reference to a display terminal owned by the Agent:
terminal_update upsert keyed by terminalId:
terminalId is required. command, cwd, output, exitStatus, and _meta are patch fields: omission leaves the stored value unchanged, null clears it, and a concrete value replaces it. On a first-seen terminal, omitted fields start unknown or empty. Every supplied cwd must be absolute.
output is an authoritative replacement snapshot for replay, correction, or resynchronization. Its required data field is RFC 4648 base64-encoded bytes and is the complete byte sequence the Agent wants the Client to retain for the terminal at that point. A concrete snapshot replaces all previously stored bytes; Clients must not merge or splice bytes from the previous snapshot into it. Optional nullable output._meta is snapshot-scoped; omission and null both mean no snapshot metadata was provided.
A concrete exitStatus marks the terminal as exited. Its optional nullable exitCode and signal fields use omission or null when unavailable. Optional nullable exitStatus._meta is scoped to that exit information and likewise treats omission and null as equivalent. Conventional POSIX signal names include SIGTERM, SIGKILL, and SIGINT; other platforms may use platform-specific names. For example: "exitStatus": { "signal": "SIGTERM" }.
For efficient live output, the Agent sends append updates:
data is independently base64-encoded. Decode each chunk separately, then append the resulting bytes in received order for that terminal; do not concatenate encoded strings before decoding. Chunk boundaries may split UTF-8 code points and ANSI escape sequences, so text and terminal parsers retain state between chunks. A later terminal_update.output snapshot replaces all bytes accumulated so far, and subsequent chunks append to that replacement.
A Client may use an isolated terminal emulator, but it only needs to provide a safe sanitized transcript fallback. The surface is display-only: it has no input, resize, interrupt, kill, wait, release, or execution semantics.
Diff content
The v1 diff shape (a singlepath 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:
changes(required) is the authoritative list of file-level operations.add,delete, andmodifycarry apath.moveandcopycarryoldPathandpath. Each change may carry an optionalfileType(text,binary,directory,symlink) andmimeType. Clients can build file trees and summaries fromchangeswithout parsing any patch text.patch(optional) carries renderable text for some or all changes and MUST be consistent withchanges. Stable v2 definesgit_patchas one or morediff --gitsections in Git’s--patch(-p) text format. Paths MUST be absolute. Surrounding commit metadata and email envelopes MUST NOT be included. Agents SHOULD providepatchwhenever feasible. Clients MUST handle diffs wherepatchis omitted ornull(binary changes, symlink retargets, and similar cases have no useful text rendering).
oldText/newText. When migrating a v1 client, render patch.text according to patch.format, 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:
titleis required: human-readable text for the permission prompt itself. It does not update any tool call’s displayed title.descriptionis optional supporting copy.subjectis an optional tagged union describing what needs permission. Stable v2 definestype: "tool_call", whosetoolCallpayload is the sameToolCallUpdateupsert shape used in session updates, andtype: "command", which describes an Agent-executed command. Requests may omitsubjectentirely for approvals without structured context, and unknown subject types should be preserved when proxying and either rendered generically or declined by policy.
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.
A command permission subject is self-contained rather than a patch:
command and cwd are required, and cwd must be absolute. toolCallId, terminalId, and _meta are optional and nullable; omission and null are equivalent and mean the value was not provided. The IDs only associate the permission with displayed state and may be absent when permission is requested before execution. Approval authorizes the Agent to execute the command; it never asks the Client to execute it.
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.
Plans
The v1plan 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:
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:
- Omit
replayFrom(or sendnull) for v1resumebehavior: reattach without replay. "replayFrom": { "type": "start" }for v1loadbehavior: the Agent replays the entire conversation assession/updatenotifications (using the same message, tool-call, and plan updates as live traffic) before responding.
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.
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
As covered under Initialization, advertisingcapabilities.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: themodes 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 option’s identifier field is renamed from
idtoconfigId, matching the parameter name used bysession/set_config_option. - The stable
categoryvalues aremode,model,model_config, andthought_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 updatedconfigOptionsarray, since one change can affect other options. Agent-initiated changes arrive asconfig_option_updatesession updates, which replace the v1current_mode_update.
Client file system and terminal execution removed
v2 removes the entire v1 Client-provided execution surface:clientCapabilities.fsand thefs/read_text_file/fs/write_text_filemethodsclientCapabilities.terminaland theterminal/create,terminal/output,terminal/release,terminal/wait_for_exit,terminal/killmethods
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 execution paths. Report file changes through diff content; when byte-faithful command display matters, report output through the new Agent-owned terminal stream.
The v2 terminal content reference does not restore this Client execution surface. It only anchors display state supplied by the Agent.
MCP server configuration
MCP transport configuration is aligned with the current MCP transport model:- Every server config MUST carry a
typediscriminator. In v1, stdio configs had notypefield. 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:stdiois now an explicit capability (session.mcp.stdio), so agents that cannot launch subprocesses can opt out, andsession.mcp.httpcovers remote servers. - Empty arrays are no longer required:
argsandenvon stdio configs andheaderson HTTP configs are optional in v2 (v1 requiredargsandenveven when empty).
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.
If you maintain a validator or generated models, v2 also tightens schema-level details that v1 left loose: base64 payloads (image/audio and terminal data, resource blob) are marked with contentEncoding: "base64", URI fields are marked format: "uri", and annotations.priority is bounded to the inclusive range 0–1.
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:
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:
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).
Extensibility and forward compatibility
v2 makes the whole schema forward-compatible by convention:- 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. _metaeverywhere. As in v1,_metaobjects carry implementation metadata. In v2’s upsert-style updates, top-level_metafollows the same patch semantics as other fields: omitted means unchanged,nullclears. Metadata nested in a snapshot, exit-status object, content item, command permission subject, or chunk is scoped to that object; omission andnullare equivalent.- Custom methods still use the
_prefix, unchanged from v1.
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 for its current state.
Migration checklist: Agents
- Initialization. Read
params.capabilities/params.info. Returncapabilitiesand a requiredinfo. Restructure your capability object: nest prompt and MCP capabilities undersession, convert boolean markers to{}objects, droploadSessionand thelist/resume/closemarkers. - Implement the baseline. If you advertise
session, implementsession/new,session/list,session/resume,session/close,session/prompt,session/cancel, andsession/update. - Auth. Rename
authenticate→auth/login. If you return one or more validauthMethods, implement bothauth/loginandauth/logout; if you omit the field or return an empty array, Clients MUST NOT call either method. Do not add a logout support marker:capabilities.authis only for authentication-related extensions. AddtypeandmethodIdto your auth method descriptors. - Prompt lifecycle. Respond to
session/promptimmediately with{}on acceptance. Emit auser_messageacknowledgment with an agent-generatedmessageId, arunningstate update when foreground work starts, and an idlestate_updatecarrying the stop reason when it ends, includingcancelledaftersession/cancel. Background updates may continue while idle. - Message IDs. Generate and attach a
messageIdto every message chunk and update. - Tool calls. Stop sending
tool_call. Sendtool_call_updatefor creation and patches. Usetool_call_content_chunkto stream content. Respect omit/null/value patch semantics. - Terminal output. Add
terminalcontent references for Agent-owned display terminals. Sendterminal_updatefor command, absolute cwd, snapshots, and exit state; send independently base64-encodedterminal_output_chunkvalues for live bytes. Do not expect Client control methods or capability negotiation. - Diffs. Replace
oldText/newTextwithchanges(+fileType/mimeTypewhere known) and providepatchingit_patchformat whenever feasible. - Permissions. Put prompt copy in required
title(+ optionaldescription). Use thetool_callsubject for tool-call state and thecommandsubject for a command, required absolute cwd, and optional tool-call/terminal associations. - Plans. Send
plan_updatewith{ "type": "items", "planId": ..., "entries": [...] }. - Modes. Drop
session/set_mode,modes, andcurrent_mode_update. Expose the same state as config options withcategory: "mode"and emitconfig_option_update. - Replay. Handle
replayFromonsession/resume. Replay history as ordinarysession/updatenotifications when asked, including terminal replacement snapshots rather than requiring every historical output chunk. Removesession/load. - Client surface. Remove all
fs/*andterminal/*calls. The new terminal content is Agent-owned display state, not a Client resource. - MCP config. Require and emit
typediscriminators. Drop SSE. Advertisesession.mcp.stdio/session.mcp.httpas appropriate. - Slash commands. Add the
type: "text"discriminator to commandinputspecifications. - Parsing. Make enum handling open: preserve unknown variants where a safe fallback exists, and accept batch arrays on stdio.
Migration checklist: Clients
- Initialization. Send
protocolVersion: 2, requiredinfo, andcapabilities(dropfs/terminal). Read the Agent’s capabilities fromresult.capabilitieswith presence checks instead of boolean checks. - Rely on the baseline. When
capabilities.sessionis present, use list/resume/close/prompt/cancel without probing markers. Keep checkingsession.deleteand other optional extras. - Prompt lifecycle. Don’t treat the
session/promptresponse as turn completion. Drive UI fromstate_update(running/idle/requires_action), take the stop reason from the idle update, and render the user’s message from theuser_messageacknowledgment (which carries its canonicalmessageId). Keep accepting background updates while idle. - Messages. Track messages by required
messageId. Implement upsert semantics: whole-message updates replace content (or clear it withnull/[]), chunks append. - Tool calls. Create tool calls on first-seen
toolCallIdintool_call_update. Apply omit/null/value patches. Appendtool_call_content_chunkitems. Replace accumulated content when an update carriescontent. - Terminal output. Maintain Agent-owned terminal state by
terminalId. Apply update patch and snapshot-replacement semantics; independently decode and append chunk bytes. Render them in an isolated terminal emulator or sanitized transcript, with no input or process controls. - Diffs. Render
patch.textwhen present. Drive file trees and summaries fromchanges. Handle patch-less diffs (binary, symlink, directory). - Permissions. Render
title/description. Applytool_callsubjects like ordinary tool-call updates; rendercommandsubjects from their command, required absolute cwd, and optional associations. Show a generic prompt for absent or unknown subjects. - Plans. Handle
plan_updatekeyed byplanId. Each update replaces that plan’s entries. - Modes. Drop
session/set_modeandcurrent_mode_updatehandling. Drive mode/model UI fromconfigOptions(usingcategory) viasession/set_config_optionandconfig_option_update. - Resume. Replace
session/loadwithsession/resume+replayFrom: { "type": "start" }, and plain reconnects withsession/resumewithoutreplayFrom. - Client surface. Remove
fs/*andterminal/*implementations for v2 connections. Expose Client-side tools through MCP servers passed inmcpServers; do not treat display terminals as Client resources. - Cancellation. After
session/cancel, keep accepting updates and wait for the idlestate_updatewithstopReason: "cancelled"as confirmation. - Parsing. Preserve unknown enum variants, tolerate unknown
sessionUpdatetypes, read commandinputas a tagged union, and accept batch arrays on stdio.
Migration checklist: SDKs
- Separate the versions. Keep v1 and v2 schemas, generated models, and test fixtures fully separate, and gate unstable-v2 surfaces independently of
protocolVersion: 2. - Three-state fields. Model omitted vs
nullvs concrete values distinctly wherever v2 defines patch semantics. A plain nullable/optional type erases a distinction the protocol depends on. - 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.
- Preserve what you don’t understand. Round-trip
_-prefixed extension values, unknown future variants, and_metawhen storing, replaying, proxying, or forwarding. - Fixtures. Cover the full prompt lifecycle, resume replay, cancellation, permission flow, message replacement and clearing, tool-call content chunks, terminal snapshot replacement, split and invalid UTF-8 bytes, exit state, command subjects, structured diffs, and JSON-RPC batches.
Supporting v1 and v2 side by side
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 afterinitialize.