The core SDK can opt into the draft ACP protocol v2 surface with the
unstable_protocol_v2 crate feature:
agent-client-protocol = { version = "...", features = ["unstable_protocol_v2"] }This feature is separate from the broad unstable feature because protocol v2
is a versioning experiment, not just an unstable method family.
To start from working code, build and run the companion agent and client in the Runnable Protocol V2 Quickstart. The examples exercise prompt acceptance, independent session updates, and the terminal idle state over a real stdio connection.
Batch framing is a shared JSON-RPC transport feature, not a v2-only protocol feature. Both v1 and v2 accept incoming batches, preserve them through relays, and group replies into one response array. The SDK does not originate batches of requests or notifications. See Transport Architecture: JSON-RPC Batch Behavior for the complete rules.
By default, Client.builder(), Agent.builder(), and Proxy.builder()
continue to expose the stable v1 API. To use the v2 API for a connection,
construct the builder with Client.v2(), Agent.v2(), or Proxy.v2().
Fluent typed handlers, spawned tasks, close callbacks, and connect_with
receive V2ConnectionTo<_>, so the protocol version is reflected in the
high-level Rust API as well as on the wire:
use agent_client_protocol::schema::{ProtocolVersion, v2};
use agent_client_protocol::{Agent, Client};
fn implementation() -> v2::Implementation {
v2::Implementation::new("example", "0.1.0")
}
# async fn run(agent_transport: impl agent_client_protocol::ConnectTo<agent_client_protocol::Client>) -> agent_client_protocol::Result<()> {
Client
.v2()
.connect_with(agent_transport, async |cx| {
let initialize = cx
.send_request(v2::InitializeRequest::new(
ProtocolVersion::V2,
implementation(),
))
.block_task()
.await?;
assert_eq!(initialize.protocol_version, ProtocolVersion::V2);
Ok(())
})
.await?;
# Ok(())
# }
# async fn serve(client_transport: impl agent_client_protocol::ConnectTo<agent_client_protocol::Agent>) -> agent_client_protocol::Result<()> {
Agent
.v2()
.on_receive_request(
async |initialize: v2::InitializeRequest, responder, _cx| {
responder.respond(v2::InitializeResponse::new(
initialize.protocol_version,
implementation(),
))
},
agent_client_protocol::on_receive_request!(),
)
.connect_to(client_transport)
.await?;
# Ok(())
# }When v2 mode is enabled, application code should use types from
agent_client_protocol::schema::v2. The flat agent_client_protocol::schema::*
exports remain the stable v1 schema. This will likely change as v2 gets closer
to release. The preceding agent fragment demonstrates version negotiation only.
An agent that advertises session support must also implement the complete
baseline session surface shown in the runnable quickstart.
Stable callbacks receive ConnectionTo<_> and expose the protocol v1
build_session*, SessionBuilder, ActiveSession, and SessionMessage APIs.
Callbacks installed through Client.v2() receive V2ConnectionTo<_> and expose
the v2 build_session* and resume_session* helpers, plus feature-gated
fork_session* helpers when unstable_session_fork is enabled. The shared
names describe the same lifecycle operations while the connection type selects
their schema and return types at compile time. Resume and fork return
V2ResumeSessionBuilder and V2ForkSessionBuilder. Call start_session to
publish the request and obtain an OpenedV2Session containing the command
handle and complete operation-specific response.
Low-level custom with_handler and with_runner implementations continue to
receive the protocol-neutral ConnectionTo<_>, and generic send_request
remains schema-agnostic. These generic APIs do not infer a protocol version from
the Rust payload type: callers on a v2 connection must use schema::v2 types,
or deliberately send extension or untyped messages. The connection guard
enforces negotiation and initialization lifecycle, not Rust-type provenance.
Dynamic handlers registered through V2ConnectionTo::add_dynamic_handler use
the same low-level HandleDispatchFrom interface and therefore also receive
ConnectionTo<_>.
Nested connections preserve the stable ConnectionTo API while still typing
the child implementation's callbacks. On a raw ConnectionTo<_>,
spawn_connection(Client.v2(), transport) returns a raw ConnectionTo<_>, and
callbacks installed on that v2 child builder still receive
V2ConnectionTo<_>. Existing spawn_connection::<Role> calls therefore remain
source-compatible.
When a raw parent also needs a typed handle to the v2 child, the
unstable_protocol_v2 feature exposes
ConnectionTo::spawn_connection_with_context, which returns the context
selected by the child builder. V2ConnectionTo::spawn_connection likewise
follows the child builder naturally, so spawning Client.v2() through an
already-typed v2 connection returns another V2ConnectionTo<_>.
A complete client installs update and interactive-request handlers before
connecting. After session/prompt is accepted, it must keep the connection
alive and consume updates until the matching session reaches idle. The
v2_one_shot_client
example demonstrates the full sequence, while the compiled cookbook
v2_one_shot_prompt recipe shows how to embed it in an application. Permission
handlers should transfer the request and responder to application-owned work
rather than waiting for user input inside the dispatch callback.
V2 deliberately separates prompt submission from session observation:
session/promptreturns aPromptResponseas soon as the agent accepts the prompt. Itsmessage_ididentifies the inserted user message, and liveUserMessageupdates for that prompt use the same ID. Persisting the message in resumable history remains optional. The response does not indicate turn completion.V2Session::send_promptreturns that request as aSentRequest<PromptResponse>. Callers must explicitly await it, register a response callback, or detach it.V2SessionBuilder::start_sessionlikewise returns a mappedSentRequest. ItsOpenedV2Sessionresult keeps the command handle separate from the completeNewSessionResponserepresented by the linked schema, rather than reconstructing a selected subset of its fields.V2ConnectionTo::resume_sessionandresume_session_fromreturn aV2ResumeSessionBuilder. Itsstart_sessionmethod publishessession/resumeand returns anOpenedV2Sessioncontaining the completeResumeSessionResponsewithout reconstructing it.- With
unstable_session_fork,V2ConnectionTo::fork_sessionandfork_session_fromreturn aV2ForkSessionBuilder. Itsstart_sessionpublishessession/fork, preserves the completeForkSessionResponse, and uses that response's newly allocated session ID for the command handle. V2Sessionis a cloneable command handle containing only the session ID and connection. It does not own, buffer, or unregister inbound messages.- Register typed
UpdateSessionNotificationandRequestPermissionRequesthandlers onClient.v2()before connecting. Updates and interactive requests are separate protocol lanes. Permission handlers should transfer responders to application-owned work rather than waiting for user input inside the connection dispatch loop. Without matching handlers, unhandled v2 notifications are ignored and unhandled requests receive a method-not-found response. They are not retained for a later per-session receiver. session/updateevents can arrive before, during, or after a prompt request. They carry a session ID and entity IDs, but no prompt or turn ID. The SDK therefore does not attribute intervening events to a locally submitted prompt or provide a prompt-scoped text accumulator.state_updatedescribes the session-wide foreground state.idlemeans the session can accept ordinary new foreground work. It is not a wire-level boundary assigning previous events to one prompt, and background updates may continue while idle.cancel_active_worksends session-widesession/cancel. Cancellation of active foreground work completes after the requiredidleupdate with stop reasoncancelled; cancelling an idle session need not emit another completion. The client should immediately mark unfinished tool calls for the active work as cancelled and must resolve every pending permission request in that session with the cancelled outcome, without answering twice or affecting other sessions. Elicitations are not all cancelled implicitly: agents should use individual$/cancel_requestfor interactions made irrelevant by the cancelled work. Cancelling or dropping the prompt'sSentRequestis the separate JSON-RPC request-cancellation mechanism; insertion still requires a successful prompt response with its message ID.set_config_optionreturns the authoritative replacement option set, andclosereturns the complete close response. Mutable configuration is not cached on the command handle.
Install connection handlers before session/new, session/resume, and
feature-gated session/fork requests. This is especially important before
calling start_session on a V2ResumeSessionBuilder: replay updates precede
the resume response on the wire, so preinstalled typed handlers observe them in
order. If a handler forwards updates to another task, the application is
responsible for any additional projection-drained barrier it needs before
treating replay as locally applied.
See Ordered Application Dispatch for a
single-consumer queue that carries updates, response results, and closure.
For an application-owned prototype that additionally coordinates shared resume
replay, abandonment, and close, see
Session Operation Coordination. It is a
cookbook policy example, not a public SDK coordinator.
Dropping command handles has no network or inbound-routing side effect. For a
session configured with V2SessionBuilder::with_mcp_server or
V2ResumeSessionBuilder::with_mcp_server, or feature-gated
V2ForkSessionBuilder::with_mcp_server, the SDK installs the MCP routes and
initially polls their runner tasks before publishing the corresponding setup
request, so the agent can connect to those servers during setup or resume
replay. Runners may continue asynchronous initialization, so custom connectors
must be able to queue connections and messages once constructed. A successful
setup promotes the attachment to the connection lifetime. A setup failure,
including an error response after cancellation, cleans up the pending
attachment. This attachment requires both unstable_protocol_v2 and
unstable_mcp_over_acp. Fork additionally requires
unstable_session_fork.
A v2 proxy can instead attach one server globally with
Proxy.v2().with_mcp_server(...). The proxy reuses one connection-scoped
server ID and adds its declaration to v2 session/new, session/resume, and
feature-gated session/fork requests. It modifies only the mcpServers field,
preserving unrelated setup fields and extensions for downstream handlers.
V2SessionBuilder::on_proxy_session_start and
V2ResumeSessionBuilder::on_proxy_session_start, plus
V2ForkSessionBuilder::on_proxy_session_start when enabled, are the
non-blocking setup helpers for a v2 proxy:
use agent_client_protocol::schema::v2;
use agent_client_protocol::{Client, Proxy};
Proxy
.v2()
.on_receive_request_from(
Client,
async |request: v2::NewSessionRequest, responder, cx| {
cx.build_session_from(request)
.with_mcp_server(session_server)?
.on_proxy_session_start(responder, async |opened| {
let (session, setup_response) = opened.into_parts();
record_session(session.session_id(), setup_response);
Ok(())
})
},
agent_client_protocol::on_receive_request!(),
);These helpers forward request cancellation, send an ordered downstream setup
request, and forward the complete operation-specific response without
reconstruction. For session/new and session/fork, routing is installed when
the response makes the new session ID available and before later inbound
traffic is dispatched. Fork routing uses the response's new ID rather than the
source session ID. For session/resume, the ID is already known, so routing and any per-session
MCP attachment are ready before the downstream request is published. Replay
updates can therefore be forwarded upstream before the complete
ResumeSessionResponse, as required by the protocol. A failed or cancelled
downstream response drops pending routing and MCP attachment. Successful setup
keeps them for the connection lifetime. A cancellation signal itself remains
advisory: it is forwarded downstream while the helper awaits that response.
Each helper then spawns the callback outside the ordering barrier with an
OpenedV2Session containing the command-only session handle and complete setup
response. Updates and interactive requests remain independent connection
traffic and should still be handled by typed callbacks on Proxy.v2().
If an application wants stream ergonomics, it can fan typed updates out from the connection handler with an explicit buffering and subscriber policy.
Proxy authors should make the version boundary explicit. Proxy.builder() is
the stable v1 builder, while Proxy.v2() is v2-only and requires
_proxy/initialize to select protocol v2. A proxy built for one version rejects
the other version instead of parsing it through a permissive schema.
When one component must expose independently authored v1 and v2 proxies,
compose them with
Proxy.protocol_router().with_v1(v1_proxy).with_v2(v2_proxy). The conductor
has already selected and canonicalized the protocol before
_proxy/initialize, so the proxy router requires an exact v1 or v2 match,
preserves the complete initial transport frame, and does not downgrade or
convert later traffic.
Components implementing their own raw version selector can use
Proxy.builder().without_acp_version_guard() and keep protocol-neutral
ConnectionTo callbacks. This disables the SDK's automatic version guard and
is not a substitute for selecting Proxy.v2() in an ordinary v2 proxy
implementation.
Enable unstable_protocol_v2 on agent-client-protocol-conductor to carry a v2
connection through a conductor proxy chain. The conductor inspects the raw
protocolVersion before parsing initialization, rewrites ordinary initialize
to _proxy/initialize without reserializing its parameters, and restores the
ordinary method before the request reaches the final agent. For an exact v2
request, info, capabilities, metadata, and unknown extension fields
therefore retain their wire shape across conductor-controlled rewrites. A proxy
implementation can still deliberately replace the request it forwards.
An exact v2 request can retain unknown raw fields, while a request for a later compatible version is canonicalized through the selected v2 schema before component instantiation.
Proxy implementations use
agent_client_protocol::schema::v2::InitializeProxyRequest. Its response is the
v2 InitializeResponse. The flat schema::InitializeProxyRequest remains the
stable v1 type. Static conductor component providers can carry either selected
schema, but each supplied component must support that version. Use
Agent.protocol_router() or Proxy.protocol_router() when a static component
has separate implementations.
Custom InstantiateProxiesAndAgent and InstantiateProxies implementations
opt into v2 by implementing their feature-gated v2 method. The default rejects
v2 rather than interpreting it as v1. Returning the initialize request
unchanged preserves its complete raw parameters for an exact-version request,
including unknown extensions. Returning a modified typed request makes that
serialized request authoritative. The conductor pins protocolVersion to its
selected implementation even if an instantiator attempts to change it, and
validates the final agent's initialize response against that selection.
The proxy connection also routes v2 session/new requests and responses
without interpreting them as v1 payloads.
The concrete
agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill can
participate in a v2 conductor chain when its unstable_protocol_v2 feature is
enabled. It selects v1 or v2 from _proxy/initialize, uses that version's MCP
capability and wire types, and adapts native McpServer::Acp declarations in
v2 session/new, session/resume, and feature-gated session/fork requests.
Other declarations and unrelated request fields remain unchanged. See
MCP-over-ACP Compatibility Bridge for placement and feature
configuration.
This feature extends the concrete compatibility proxy only. The core SDK's global MCP attachment and proxy-session helpers support v1 and v2 independently, as described above.
The SDK handles the initialize negotiation at the JSON-RPC boundary:
- Native
Client.v2()andAgent.v2()connections reject ordinary protocol traffic until the initialization response completes.$/cancel_requestremains available while initialization is in progress. The client is the initializer and the agent is the responder. Attempts in the opposite direction are rejected. An initialization error leaves the connection uninitialized so the client can retry, while a second initialization after a successful handshake is rejected. - A v2 client advertises protocol v2 as its latest supported version.
- A v2 client requires a v2 agent. If the agent responds with v1, the
initializerequest resolves with an error and the caller must explicitly fall back to a v1 client implementation if that is acceptable. - A native v1 agent selects v1 for requests advertising v1 or a newer version,
with or without
unstable_protocol_v2enabled. Newer initialization metadata is normalized into v1 identity and shared capabilities. The SDK never enables v1 file-system or terminal-execution capabilities from unknown newer fields. V1's original fallback-to-latest rule is retained, including a v1 selection when the client requests an unsupported older version. - A standalone v2 agent accepts a request for v2 or a newer version with
compatible initialization parameters and selects v2. The request delivered
to the handler has
protocol_versionnormalized to v2, and the response selects v2. If a client initializes with v1, the request resolves with-32022(Unsupported protocol version) anddata: { "requested": 1, "supported": [2] }. The v2 initialization handler is not invoked. Use an agent router with a v1 implementation to serve v1 sessions. - If the agent responds with any other unsupported version, the request resolves with an error so the client can close the connection.
- After initialization, the local API version and negotiated wire version must match. The SDK does not convert traffic between v1 and v2.
That means v1 and v2 implementations still need separate handlers.
Agent.v2(), Client.v2(), and Proxy.v2() are v2-only. While protocol v2
stabilizes, the unstable_protocol_v2 crate feature also exposes
Agent.protocol_router(), Proxy.protocol_router(), and
Client.protocol_connector() for composing version-specific implementations.
Keep these protocol handlers as adapters around shared application logic, not as general-purpose conversions between wire types. In particular, v1 prompt responses end a turn, while v2 prompt responses acknowledge insertion and completion arrives through session state updates. Shared storage also does not automatically make sessions resumable through both versions. See the protocol's dual-stack migration guidance for adapter boundaries and a validation checklist.
Agents can add protocol implementations independently, which makes it easy for applications built with v2 support to control v2 rollout with a runtime feature flag:
use agent_client_protocol::schema::{v1, v2};
use agent_client_protocol::{Agent, ConnectTo};
# fn implementation() -> v2::Implementation {
# v2::Implementation::new("example", "0.1.0")
# }
# async fn serve(client_transport: impl agent_client_protocol::ConnectTo<Agent>) -> agent_client_protocol::Result<()> {
# let enable_protocol_v2 = true;
let v1_agent = Agent.builder().on_receive_request(
async |initialize: v1::InitializeRequest, responder, _cx| {
responder.respond(v1::InitializeResponse::new(initialize.protocol_version))
},
agent_client_protocol::on_receive_request!(),
);
let agent = Agent.protocol_router().with_v1(v1_agent);
let agent = if enable_protocol_v2 {
let v2_agent = Agent.v2().on_receive_request(
async |initialize: v2::InitializeRequest, responder, _cx| {
responder.respond(v2::InitializeResponse::new(
initialize.protocol_version,
implementation(),
))
},
agent_client_protocol::on_receive_request!(),
);
agent.with_v2(v2_agent)
} else {
agent
};
agent
.connect_to(client_transport)
.await?;
# Ok(())
# }The agent protocol router reads the initial initialize request and selects
a version-specific implementation. Endpoints exposing v2 select the highest
configured version at or below the requested version, or reject initialization
with a structured version-mismatch error if none exists. V1-only endpoints
retain v1's original fallback-to-latest rule. The router
normalizes a v2 initialize request when selecting a v1 implementation, but does
not convert messages between v1 and v2 after routing. For compatibility, the
initial frame may be a batch whose first call-shaped entry is initialize. The
router preserves the complete frame when handing it to the selected
implementation. Response-only frames before initialization are ignored.
A v2-only endpoint rejects a v1 request without requiring v2's Client identity
or manufacturing a higher-version success response. The error code is
-32022, and its data contains requested and the endpoint's supported
versions. This diagnostic is separate from malformed initialization parameters,
which remain -32602. The router preserves response IDs and batch framing,
flushes the rejection, and closes without starting the selected application.
Custom endpoints can construct the same wire error with
agent_client_protocol::util::unsupported_protocol_version(requested, supported).
The code is defined by v2 negotiation, not by a change to the v1 protocol.
The shared SDK error model can preserve it as an unknown numeric code, so a
legacy v1 client can handle the initialization error without new v1 schema
definitions.
just test runs the negotiation tests in both the all-features workspace and
the core crate without default features, so v1 negotiation does not depend on
whether the draft v2 API is compiled in.
Clients should close after a version mismatch and display the supported versions. A successful downgrade must also be checked against the Client's actual supported versions, because advertising a latest version does not imply support for every older version.
The proxy protocol router reads _proxy/initialize after the conductor has
selected the chain's wire version. It therefore requires an exact configured
v1 or v2 implementation instead of negotiating or downgrading. It validates
the selected schema, then hands the complete, unchanged initial frame to that
strict implementation.
Clients use a connector because fallback may require opening a new transport. Both client implementations and the agent transport are factories:
use agent_client_protocol::Client;
let connector = Client
.protocol_connector()
.with_v1(|| v1_client())
.with_v2(|| v2_client());
connector.connect_to(|| open_agent_transport()).await?;The connector starts the highest configured implementation. If a successful v2 initialize response negotiates v1 and a v1 implementation is configured, the connector starts the v1 implementation and compares the complete initialize parameters it would send with the normalized v2 request already seen by the agent:
- If they match exactly, the connector reuses the current agent connection and delivers the original response to the v1 implementation with its request ID. It does not send a second initialize request.
- If they differ, the connector closes that connection, calls both factories again as needed, and performs a fresh v1 initialization on a new agent connection.
- If the agent rejects the v2 initialize request, the error is surfaced. A rejected initialize is not treated as permission to retry with v1.
The reuse probe is conservative: if parsing and serializing the raw v2 request would change any parameter, reuse is disabled and fallback opens a fresh connection. That does not turn an otherwise valid v2 request into an error.
The unstable_protocol_v2 API follows the moving draft schema. Schema 1.5 added
semantic newtypes for paths, media types, IDs, and cursors, renamed
DiffPatch.diff to DiffPatch.text, and added terminal state and output update
types. Schema 1.7 removed the former schema-wide v1/v2 conversion API:
versioned implementations should remain separate, with
purpose-specific adapters at runtime boundaries where the required state and
policy are available. These are draft API changes rather than stable v1 wire
changes. See Migrating to
v2.0 for concrete source changes.
Schema 1.6 adds Cancelled tool-call and plan-entry statuses to draft v2.
Schema 1.7 stabilizes elicitation and terminal authentication, so neither
surface requires its former SDK feature flag. It also adds context compaction
updates (stable since schema 1.11). The SDK carries them through its
existing typed session/update routing in both protocol versions. V1 clients
advertise compaction support through ClientSessionCapabilities::compaction.
Schema 1.8 stabilizes optional programmatic tool-call names in both protocol
versions. No tool-name-specific feature is required. Draft v2 users only need
unstable_protocol_v2. In v1, an omitted or null name leaves the existing
value unchanged on updates. In v2, omission leaves it unchanged, null clears
it, and a string replaces it. V1 cannot express the explicit v2 null clear
operation.
Schema 1.11 stabilizes session notices and compaction. No surface-specific
feature is required. Draft v2 users still need unstable_protocol_v2.
In draft v2, failures after a prompt's user message is inserted end active work
with StopReason::Error(ErrorStopReason) on the idle state update, optionally
including a JSON-RPC error object. Failures before insertion remain error
responses to session/prompt. StopReason now serializes as an object tagged
by stopReason, flattened into IdleStateUpdate. Its Other variant takes
OtherStopReason rather than a string and preserves unknown payload fields.
V1 prompt responses are unchanged and do not accept an error stop reason.
Schema 1.11 also rejects malformed payloads that receivers act on rather than
silently dropping invalid entries or substituting defaults. This includes
session setup's MCP servers and additional directories, terminal launch
arguments and environment, and v2 replay cursors. Typed request decoding
reports invalid parameters (-32602) without invoking the handler. null
still means empty for the defaultable acted-on lists. Omission is accepted
only where the field is optional. Capability and display metadata retain their
lenient decoding.
Client-provided system prompts are available through the independent
unstable_session_system_prompt feature. Use the complete lifecycle request
with _from helpers and negotiate the agent's capability first; see
Client-provided system prompts.
Next Edit Suggestions, their document events, and their position-encoding
capabilities have been removed from both protocol versions.
Foreground states are aggregate snapshots, not per-prompt events:
- Report
runningwhen work starts, orrequires_actionif it starts blocked on user action. Agent-initiated workflows and local commands use the same lifecycle without needing a model call or a synthetic user prompt. requires_actionmeans the foreground workflow as a whole is blocked. A pending permission or elicitation does not imply this state while other foreground work can progress or when it belongs to background activity. Once reported, sendrunningwhen progress resumes oridlewhen work ends.- When no foreground work remains, report
idle; normally include a stop reason. Repeated snapshots, including an old stop reason, are idempotent, not additional completions, cancellations, or token usage. - New sessions default to idle, but updates can precede the new-session response. Do not overwrite them with idle when that response arrives.
- Every successful resume must send a current-state snapshot after any replay and before its response, even without a replay cursor. Do not replay historical states as live activity or reset state to idle on the response. Testy and the echo example only resume sessions without foreground work, so their snapshot is idle without a new stop reason.
Prompt acceptance means logical user-message insertion with an ID and a
position, not receipt or queueing. Running may precede insertion. Rejection
before insertion requires an error response and no user-message updates for
that submission; it does not alone end foreground work. A pre-insertion
foreground failure can also require an error idle update. After insertion,
the response stays successful and failure is reported on idle instead.
This includes authentication failures: after insertion, auth_required
(-32000) belongs in the error stop reason, not an error prompt response.
Authenticating again does not restart ended work, and automatically retrying
an inserted prompt would insert another message and could repeat side effects.
When closing cancels active work, final updates and idle with cancelled
precede the close response. An uninserted prompt cancelled by close gets
-32800; an inserted one still gets its successful message ID.
Agents reject unsupported replay cursor types with -32602, rather than
substituting another cursor. Retained user messages preserve the prompt
response's ID across replay. Agent and thought message IDs may change across
reconnections: rebuild that history on full replay instead of deduplicating
only by ID. A chunk replay's empty-content primer clears only its replay ID.
Top-level _meta on upsert updates patches stored metadata by key: omission
leaves it unchanged, null clears it, and an object preserves omitted keys,
removes keys whose values are null, and replaces each other value whole.
Nested objects are not merged; nested nulls are ordinary data. An empty object
does not clear metadata. Replay uses the same rules and never implicitly resets
metadata. ToolCallUpdate::apply_update and TerminalUpdate::apply_update
apply this rule; use schema::v2::apply_meta_update for application-owned
message, session, compaction, or subagent projections. Metadata on chunks,
content, snapshots, and plan_update is not an entity patch.
Protocol v2 negotiation does not select every experimental feature. Operations such as provider configuration require their advertised capabilities. Informational notices, compaction, draft plan representations, subagent updates, and idle token usage can use their documented capability-free fallbacks; accepting an unknown variant is not a support advertisement. Unknown capability fields are ignored, while malformed known discriminators must not fall through to an unknown-variant fallback.
For v2 git_patch diffs, every path must be absolute and have no a/ or b/
prefix, including rename/copy header paths. Git quoting and /dev/null for
an added/deleted file's missing side still apply. The SDK carries patch text;
it does not rewrite paths or compute diffs from snippets.