Skip to content

Protocol Versioning and Forward Compat

The JSON-stream protocol is designed so that an older host running against a newer engine receives no surprises. New event variants and new fields on existing events are introduced through a capability-advertisement system rather than unconditional emission. An older host that does not know about a new variant simply drops the line; the core turn stream (stream_start, text_delta, tool_request, tool_result, stream_end) is unaffected.


The current protocol version is 0.2.0, carried in the ready event’s version field. Minor-version bumps are additive: new optional events or fields. Major-version bumps are breaking.

A host can read ready.version to detect which protocol generation it is talking to. For most hosts the practical compatibility guarantee is the W0 contract described below, not the version string.


The baseline event set is what every engine emits, regardless of configuration. These events have been present since v0.1.21 and their shapes are frozen:

Event "type"Key fields
readyversion, session_id?, capabilities
stream_startmsg_id
text_deltatext, msg_id
thinkingtext, msg_id
tool_requestmsg_id, call_id, tool (name, category, args, description)
tool_runningmsg_id, call_id, tool_name
tool_resultmsg_id, call_id, tool_name, status, output, output_type, metadata?
tool_cancelledmsg_id, call_id, reason
stream_endmsg_id, finish_reason, usage?
errormsg_id?, error (code, message, retryable)
infomsg_id, message
config_changedcapabilities
mcp_readyname, tools
pong(none)

These are always emitted when triggered; no capability flag gates them.

The following diagnostic events are also always emitted, regardless of W0 flags. They carry no capability flag because a host that does not recognize them drops them silently per the decoder contract, and suppressing them would mean a buggy host misses an incident:

  • provider_circuit_event: circuit-breaker state transitions (closed/open/half_open), always-on per audit F4
  • budget_exceeded: fires once when the first budget cap trips
  • tool_panicked: emitted alongside the synthetic ToolResult when a tool’s execute handler panicked
  • plugin_registration_failed: emitted when a plugin partially fails to register

ready.capabilities is a Capabilities struct (defined in crates/wcore-protocol/src/events.rs:388). The v0.1.21 baseline fields are always present. All W0 forward-additive flags default to false and are omitted from the serialized JSON when off, using #[serde(skip_serializing_if = "is_false")]:

fn is_false(b: &bool) -> bool { !*b }

When all W0 flags are off, the serialized capabilities object is byte-identical to the v0.1.21 shape. This is enforced by a golden test (events.rs:835):

capabilities_default_off_serializes_without_new_flag_keys

That test asserts that every W0 key (streaming_tools, sub_agent_traces, cost_attribution, hitl_suspend, non_destructive_compact, structured_traces, rpc_tool_script, browser_suite, computer_use, plugins, gepa_enabled) is absent from the JSON object when default-off.

A flag being true in ready.capabilities means the engine is advertising that it will emit the corresponding event variants during this session. It is not an opt-in that the host must acknowledge. The host’s obligation is described in the decoder contract below.

Capability flagWaveEvent type(s) gated
streaming_toolsW7tool_chunk
sub_agent_tracesW7sub_agent_event
cost_attributionW6session_cost
hitl_suspendW7approval_required, suspend, approval_resume (echo)
non_destructive_compactW5compact_offload
structured_tracesW1trace_event
rpc_tool_scriptW4expands tool_result.metadata shape
browser_suiteW8browser_event, browser_policy_denied
computer_useW8cua_event, cua_policy_denied
pluginsW2.5/W8plugin_event
gepa_enabledW10Bevolution_event
online_evolutionW7-Nevolution_event (live session, not just offline runs)

gepa_enabled and structured_traces are separate flags by design (F6 audit fix, W10B rev-2): a host that wants W1 turn traces should not be forced to also accept thousands of evolution_event entries per evolve run.

Additional forward-additive fields on Capabilities

Section titled “Additional forward-additive fields on Capabilities”

Two string fields on Capabilities use skip_serializing_if = "String::is_empty" instead of is_false, following the same omit-when-default pattern:

  • user_model_backend: the active user-model backend tag ("local" or "honcho"); empty string when memory is disabled (F-093)
  • (baseline modes and current_mode are always present as Vec<String> and String)

The contract is documented in docs/json-stream-protocol.md in the engine repository and enforced by crates/wcore-protocol/tests/host_decoder_contract.rs. Every host implementation must satisfy four rules:

Do not attempt to deserialize each line directly into a closed event enum. Parse to a generic JSON value first, then dispatch on "type".

Each line produces exactly one of three outcomes:

  1. Known type, valid shape: render the event normally.
  2. Unknown "type" value: drop the line silently; do not log, alert, or error.
  3. Malformed JSON: log with rate-limiting; do not crash the session.

Rule 3: tolerate unknown fields on known variants

Section titled “Rule 3: tolerate unknown fields on known variants”

New fields may be added to existing event variants in minor releases. A host must not fail when it encounters a field it does not recognize on a known variant. Use a permissive deserialization strategy (serde(deny_unknown_fields) is wrong for host-side parsers; serde(default) on additive fields is correct).

Rule 4: capabilities are advisory, not gates

Section titled “Rule 4: capabilities are advisory, not gates”

The capabilities object in ready is advisory: it tells the host which event families will be emitted so the host can allocate handlers. A host must not refuse to render a known event type merely because the corresponding capability flag was false or absent. The flag may have been added in a later engine version that the host has not yet seen. If the host knows how to render an event, it should render it.


When a host connects to an engine whose protocol version is newer:

  • New events on known variants: handled by Rule 3 (tolerate unknown fields).
  • Entirely new event types: handled by Rule 2 (drop unknown types silently).
  • New W0 flag present in ready.capabilities: the host receives an advisory it has not seen before; it should ignore it rather than treating it as an error.

When a host connects to an engine whose protocol version is older:

  • A W0 flag the host expects may be absent. The host should treat absence as false (the engine will not emit the corresponding events) and degrade gracefully.
  • Capabilities derives Deserialize with #[serde(default)] on all W0 fields, so deserializing a v0.1.21 capabilities object into the current Capabilities struct produces false for every missing W0 flag cleanly.

The docs/json-stream-protocol.md spec document contains an example capabilities.modes array listing "yolo". The engine serializes "force" (the canonical variant name). The "yolo" alias exists only on the deserialization path for SessionMode (for foreign-agent compat). A host that reads modes[] to build a UI will see "force", not "yolo". The spec-doc example is stale on this point.