Module 39: Forward Compatibility and _meta
A newer peer’s session updates, content blocks, and stop reasons are kept asUnknown* records and open values instead of failing a message. Dispatch with instanceof and a default branch, compare open values with equals, and carry your own data in _meta.
See Forward Compatibility for the full catalog of open value types and Unknown* variants this module exercises.
What You’ll Learn
- Why no union dispatch in this SDK is exhaustive, and the
instanceof-chain-plus-default pattern - Reading and round-tripping an
Unknown*record exactly as received - Open value types (
StopReasonand friends):equals,value(),known(),isKnown(), never== _metaas the last component of (almost) every schema record, and namespacing your keysacp-test’sInMemoryTransportPair, for exercising forward compatibility without two processes
The Code
A “newer” agent, sending variants this SDK doesn’t define
An agent built with this SDK can’t invent a protocol variant the schema doesn’t have, but it can model what a proxy or a newer peer would send by constructing theUnknown* record directly:
StopReason.of("paused_for_review") is a value this SDK’s constants don’t list; of(...) builds the open value anyway, exactly the way a genuinely newer peer’s value would deserialize.
Dispatch: known types, the Unknown* branch, then a default
switch over them: write instanceof chains that end with the Unknown* branch and a final else. An Unknown* record keeps the discriminator and every field it received (sessionUpdate(), fields() here), and writes them back unchanged; it’s how a proxy would pass a newer peer’s messages on without understanding them.
McpServer defaults to stdio and AuthMethod to the agent method for an unrecognized type, rather than having their own Unknown* record: worth knowing those two are special cases of the same idea.
Open values: compare with equals, never ==
StopReason, ToolCallStatus, PermissionOptionKind, PlanEntryStatus, PlanEntryPriority, Role, and ElicitationAction all follow this shape: a record over the wire string, with constants for the known values. ToolKind is the one exception that stays a plain enum; an unknown kind reads as OTHER rather than failing.
_meta: your own data, namespaced
_meta puts it as its last component: a free-form map for trace ids, vendor hints, or anything else peers should pass through even when they don’t understand it. Namespace your keys (acptutorial.dev/... here) so they can’t collide with another implementation’s.
Reading and writing raw JSON from a newer peer
This is the practical payoff of forward compatibility: a message this SDK has never seen the shape of still deserializes, dispatches to a safe default branch, and writes back byte-for-byte unchanged if you don’t touch it. That’s what lets a client built today keep working against an agent speaking a protocol version released after it shipped.
Source Code
View on GitHubRunning the Example
acp-test’s InMemoryTransportPair: a “newer” agent sends a session update kind, a content block type, and a stop reason this SDK doesn’t define, with _meta on the prompt and the response; then a raw JSON notification from a newer peer is read and written back unchanged. No API key, no subprocess required.