Skip to content

spec: tolerate unknown enum values on the wire - #370

Draft
connor4312 wants to merge 1 commit into
mainfrom
agents/fix-agent-host-protocol-issue-366
Draft

spec: tolerate unknown enum values on the wire#370
connor4312 wants to merge 1 commit into
mainfrom
agents/fix-agent-host-protocol-issue-366

Conversation

@connor4312

Copy link
Copy Markdown
Member

What

Makes unrecognized string enum values decode, degrade, and round-trip instead of failing the whole message.

  • Generators (scripts/generate-{rust,kotlin,swift}.ts) now emit open string enums:
    • Rust: named variants plus Unknown(String), with hand-rolled Serialize/Deserialize and as_str().
    • Swift: named cases plus case unknown(String), RawRepresentable over String.
    • Kotlin: @JvmInline value class over String with the known values as companion constants (TurnState.COMPLETE still resolves).
    • TypeScript and Go already tolerated arbitrary strings — unchanged.
  • Reducers: an unknown PendingMessageKind is now a no-op in every client. Previously the non-steering branch was an implicit "else = queued", which — once unknown values became decodable — would have written a future kind into queuedMessages. Rust also needed .clone() in a few spots since these enums are no longer Copy.
  • docs/specification/versioning.md: spells out the ignore-unknown rule for enum values, discriminated-union variants, unknown object fields (including command params/results), and unknown error codes — not just action type.
  • Conformance: 3 round-trip fixtures (unknown optional enum, unknown required + nested enum, unknown JSON-RPC error code) and 2 reducer fixtures (unknown pending-message kind set/removed), all shared across the five clients.

Why

Closes #366. versioning.md promises additive changes are safe within a MAJOR, but adding a value to any existing discriminant enum was a hard decode failure on peers built against an earlier version. The forward-compat intent was already there for polymorphic unions (Unknown(serde_json::Value)) and bitsets (unknown bits preserved) — string enums were the gap. Worth fixing before 1.0, since after that every enum extension effectively becomes a MAJOR change.

Notes for reviewers

  • No PROTOCOL_VERSION bump. The wire format is unchanged; this only changes how clients decode values they don't recognize.
  • Source-compatible for known values, but the generated types changed shape for consumers who exhaustively match/switch on these enums or relied on Copy in Rust — they'll now need an unknown arm. That's the point of the fix, but it's the thing to sanity-check.
  • Unknown values are preserved rather than collapsed to a sentinel, so relays and proxies keep round-trip fidelity.
  • Item 4 from the issue (whether command params/results are guaranteed to tolerate unknown top-level fields) is now stated explicitly in versioning.md and covered by the existing 017-unknown-wire-keys-ignored fixture. Flagging it since it was raised as an open question rather than a given.

Validation

npm run generate (no drift) and npm test at the root; cargo test --workspace, ./gradlew build (JDK 17 container), swift test, go test ./..., and the TypeScript client's npm test && npm run build. Reducer branch coverage stays at 100%.

Also reviewed by a council of three models; their one finding — the PendingMessageKind fall-through above — is fixed and covered by the new reducer fixtures.

Adding a value to an existing string enum was documented as an additive,
compatible change, but the Rust, Kotlin, and Swift clients failed the whole
message when they decoded a value they didn't know. That made every future
enum extension a breaking change in practice.

Generate open string enums in all three clients: known values keep their
existing named cases/constants, and unrecognized values are preserved
verbatim so they round-trip unchanged. TypeScript and Go already tolerated
arbitrary strings.

Unknown values are now reachable in reducer code, so treat an unrecognized
PendingMessageKind as a no-op instead of falling through to the queued-message
path, and state the forward-compatibility rule in versioning.md for enum
values, union variants, unknown object fields, and unknown error codes.

Closes #366

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1.0.0 blocker: unknown enum values fail to decode, contradicting the additive-change guarantee

1 participant