Skip to content

fix(dart-sdk): add missing event types for protocol parity (activity + reasoning)#1663

Open
mattsp1290 wants to merge 26 commits into
ag-ui-protocol:mainfrom
mattsp1290:matt.spurlin/1018/fix-missing-event-types
Open

fix(dart-sdk): add missing event types for protocol parity (activity + reasoning)#1663
mattsp1290 wants to merge 26 commits into
ag-ui-protocol:mainfrom
mattsp1290:matt.spurlin/1018/fix-missing-event-types

Conversation

@mattsp1290
Copy link
Copy Markdown
Collaborator

Summary

Brings the community Dart SDK to full event-type parity with the canonical TypeScript and Python SDKs. Prior to this change, any AG-UI stream that emitted one of nine canonical event types would throw ArgumentError: Invalid event type, silently breaking Dart clients connected to TypeScript or Python servers.

New event types added:

  • ActivitySnapshotEvent / ActivityDeltaEvent
  • ReasoningStartEvent, ReasoningEndEvent
  • ReasoningMessageStartEvent, ReasoningMessageContentEvent, ReasoningMessageEndEvent, ReasoningMessageChunkEvent
  • ReasoningEncryptedValueEvent (with supporting ReasoningMessageRole and ReasoningEncryptedValueSubtype enums)

Deprecations (non-breaking):

  • EventType.thinkingContent / ThinkingContentEvent — never part of the canonical AG-UI protocol; deprecated in favor of ThinkingTextMessageContentEvent. Decoding remains supported; removal planned for 1.0.0.
  • EventType.thinkingTextMessageStart/Content/End — deprecated in favor of the canonical reasoningMessage* variants, mirroring the TypeScript SDK's own deprecation of THINKING_TEXT_MESSAGE_*.

Protocol compliance fixes (applied during review passes):

  • encryptedValue now plumbed through all BaseMessage subtypes (DeveloperMessage, SystemMessage, AssistantMessage, UserMessage), matching TS BaseMessageSchema and Python BaseMessage. Previously only ToolMessage and ReasoningMessage carried the field, so proxy re-emit of a MESSAGES_SNAPSHOT silently dropped encrypted fields.
  • raw_event (snake_case) now preserved on every event factory via a centralized _readRawEvent helper, fixing silent drops of Python-origin payloads.
  • ReasoningEncryptedValueEvent.fromJson no longer stores the cipher payload in BaseEvent.rawEvent, closing an unintentional cipher-data leak in error paths.
  • RunStartedEvent.fromJson rethrow now forwards e.json (the specific inner map) instead of the full outer payload, limiting encryptedValue exposure in AGUIValidationError.
  • SseParser._processField now matches the WHATWG SSE spec: data: uses _hasDataField flag so data:\ndata: x\n\n correctly yields "\nx"; repeated event: lines replace rather than append.
  • StateDeltaEvent.delta and ActivityDeltaEvent.patch typed as List<Map<String, dynamic>> (from List<dynamic>) — RFC 6902 Patch ops are always objects; non-object elements now surface as AGUIValidationError at the decoder boundary.
  • SseParser.maxDataBytes renamed to maxDataCodeUnits — the field measured UTF-16 code units, not bytes.
  • JsonDecoder.requireEitherField now distinguishes key-present-but-null from key-absent.
  • copyWith sentinel sweep completed: all nullable payload fields on all event types now support explicit-null clearing.
  • All new fromJson factories accept both camelCase (TypeScript server) and snake_case (Python server) field keys.

Version: bumps Dart SDK from 0.1.x to 0.2.0.

Test plan

  • All new event types have unit tests in test/events/event_test.dart
  • Event type round-trip coverage in test/events/event_type_test.dart
  • Fixture-based integration tests updated in test/integration/fixtures_integration_test.dart and test/integration/event_decoding_integration_test.dart
  • New JSON fixtures added to test/fixtures/events.json
  • SSE parser spec-compliance tests updated in test/sse/sse_parser_test.dart
  • Encoder/decoder round-trip tests updated
  • dart analyze clean (no analyzer errors)
  • dart test passes

🤖 Generated with Claude Code

mattsp1290 and others added 26 commits April 27, 2026 15:05
…g-ui-protocol#1018)

Brings the community Dart SDK to event-type parity with the canonical
Python and TypeScript SDKs. Previously, streams emitting any of nine
canonical event types would throw `ArgumentError: Invalid event type`
because the Dart EventType enum did not define them.

Added events:
- ActivitySnapshotEvent / ActivityDeltaEvent (issue ag-ui-protocol#1018)
- ReasoningStartEvent, ReasoningMessageStartEvent,
  ReasoningMessageContentEvent, ReasoningMessageEndEvent,
  ReasoningMessageChunkEvent, ReasoningEndEvent,
  ReasoningEncryptedValueEvent

Supporting enums: ReasoningMessageRole, ReasoningEncryptedValueSubtype.

All new fromJson factories accept both camelCase (TypeScript server)
and snake_case (Python server) field keys, matching the existing
RunStartedEvent pattern.

Deprecated EventType.thinkingContent / ThinkingContentEvent — these are
not part of the canonical AG-UI protocol. Decoding remains supported
for backward compatibility; users should migrate to
ThinkingTextMessageContentEvent. Removal is planned for a future major.

Also fixed a pre-existing analyzer error in test_helpers.dart by typing
the onError parameter as Object so it can be passed to
Completer.completeError.

Bumps SDK to 0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, validate, dartdoc

Companion follow-up to 031c055. Contains the supporting changes the
new activity + reasoning event classes depend on but that landed
unstaged in the working tree:

- Bump `agUiVersion` constant to 0.2.0 and update the lock-in test.
- Add `JsonDecoder.requireEitherField` / `optionalEitherField` helpers
  that the new event factories rely on for camelCase/snake_case parity.
- Extend `EventDecoder.validate` to an exhaustive switch over every
  sealed `BaseEvent` subtype (no `default:`) so the analyzer flags any
  future event added without a validate decision; tighten the
  decoder's `on ValidationError` boundary so the two error classes
  consistently surface as `DecodingError`.
- Document the two-class error setup (`AGUIValidationError` vs
  `ValidationError`) on both classes and document
  `EventStreamAdapter.fromSseStream`'s `skipInvalidEvents` semantics,
  including the silent-drop note for `REASONING_ENCRYPTED_VALUE`
  events with unknown subtypes.
- Tighten the `THINKING_CONTENT` `@Deprecated` message with a
  scheduled removal version (1.0.0).
- Extend the round-trip integration fixture to cover the new
  activity + reasoning events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tests, README/CHANGELOG, review fixes

Combines the remaining ag-ui-protocol#1018 working-tree edits with review-driven
follow-ups. The bulk of the diff is the pre-existing finishing work
on the activity/reasoning event hierarchy (event classes, dartdoc,
tests, fixtures, CHANGELOG entry, README rework); the targeted
review fixes are layered on the same files.

Pre-existing ag-ui-protocol#1018 finishing work:
- events.dart: full implementations of ActivitySnapshot/Delta and the
  seven Reasoning* events with dartdoc, dual-key fromJson, toJson,
  copyWith, and the documented "throw at the enum, absorb at the
  factory" forward-compat pattern on REASONING_MESSAGE_START.role.
- event_test.dart: round-trip, snake_case, dispatch, missing-field,
  empty-delta, and forward-compat coverage for every new event class.
- event_decoding_integration_test.dart: Python/TS dispatch tests,
  empty-id boundary contract, and present-but-null payload checks
  for STATE_SNAPSHOT / RAW / CUSTOM.
- README.md: replaced the stale "16 core event types" bullet with
  a parity-aware feature description and an Activity & Reasoning
  Events usage section.
- CHANGELOG.md: [0.2.0] entry covering Added / Changed / Deprecated
  and a "Known parity gaps" section tracking the
  RunStartedEvent.parentRunId / TextMessageStart.name / copyWith
  sentinel work scheduled for a follow-up.

Review-driven fixes (Opus, /review on this branch):
- README: corrected pre-existing field-name bugs in code samples
  (`event.text` → `event.delta`, `event.toolName` → `event.toolCallName`,
  `ConnectionException` → `TransportError`, `CancelledException` →
  `CancellationError`).
- events.dart: ActivitySnapshotEvent.fromJson now requires the
  `content` key (mirrors StateSnapshotEvent / RawEvent — the dartdoc
  permissive-on-value note conflated type-Any with required-ness);
  BaseEvent.fromJson wraps EventType.fromString's ArgumentError as
  AGUIValidationError so direct callers see the same error surface
  as every other validation failure; tightened the
  ReasoningMessageStartEvent on-ArgumentError catch dartdoc to
  spell out the value-vs-shape distinction so a future maintainer
  doesn't widen the catch; documented the always-emit-`replace`
  choice and the absence of ==/hashCode on BaseEvent; renamed
  RawEvent.copyWith param `event` → `newEvent` to remove field
  shadowing; added single-variant rationale dartdoc on
  ReasoningMessageRole and a semantics dartdoc on
  ActivitySnapshotEvent.replace.
- event_test.dart: locked in `rejects missing content key` and
  `accepts explicit-null content` for ActivitySnapshotEvent;
  cross-referenced the `invalid event type` factory test with the
  decoder-boundary integration counterpart; updated the test to
  expect AGUIValidationError after the BaseEvent.fromJson wrap.
- event_decoding_integration_test.dart: extended `emptyIdPayloads`
  to cover all activity + reasoning empty-id cases (12 new entries);
  added two E2E tests for ReasoningEncryptedValueSubtype's
  no-fallback design (unknown subtype → DecodingError; same payload
  skipped under skipInvalidEvents: true).
- CHANGELOG.md: corrected release date to merge date.

All 469 tests pass (was 451 before the review fixes); no new analyzer
warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…enum alignment, encrypted-subtype contract

Address dual-reviewer findings on the 0.2.0 event-parity branch:

- Restore RawEvent.copyWith(event:) parameter (silent-breaking rename to
  newEvent: would have invalidated 0.1.x callers; no migration entry was
  in CHANGELOG, so revert is the simpler fix).
- Add a private _unsetCopyWith sentinel so ActivitySnapshotEvent.copyWith
  can intentionally clear content to null, matching the factory's
  explicit-null contract that was already locked in by tests.
- Align TextMessageRole.fromString to throw on unknown values, mirroring
  ReasoningMessageRole. Wire decoding is unchanged: TextMessageStartEvent
  and TextMessageChunkEvent now absorb the throw and fall back to
  assistant for forward compatibility.
- Wrap an unknown REASONING_ENCRYPTED_VALUE.subtype as AGUIValidationError
  in ReasoningEncryptedValueEvent.fromJson so direct factory callers see
  the documented dartdoc contract instead of a raw ArgumentError.

README: fix two switch examples that mixed event.type (String) with
EventType case labels.

Tests: add four direct-factory regressions (TextMessageRole throws on
bogus, both TextMessage* factories absorb, ActivitySnapshot copyWith
clears content, ReasoningEncryptedValue rejects unknown subtype) and
strengthen the round-trip integration test with field-value assertions
on new activity/reasoning events.

CHANGELOG: drop the resolved TextMessageRole parity-gap entry and
record the four behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ry + parity tests

Addresses dual-reviewer (Opus + ChatGPT) feedback on the activity/reasoning
parity branch.

- `ToolCallArgsEvent.fromJson` now rejects empty `delta` at the factory
  boundary, restoring symmetry with the four `*Content` siblings (the
  decoder-side guard already existed; direct factory callers were the
  asymmetric path).
- Three new regression tests pin contracts that were previously implicit:
  every-event `toJson` preserves the discriminator across `...super.toJson()`
  spread; `ToolCallChunkEvent` tolerates an entirely empty payload (mirroring
  the deliberate `case ToolCallChunkEvent(): break;` in `validate()`);
  `TextMessageStart.name`, `TextMessageChunk.name`, `RunStartedEvent.parentRunId`,
  and `RunStartedEvent.input` round-trip cleanly through camelCase, snake_case,
  and omitted-field shapes (regression guards for the ag-ui-protocol#1018-era field-drop bug).
- Sentinel `copyWith(...: null)` tests added for the wider nullable surface
  (`ToolCallStart.parentMessageId`, `ReasoningMessageChunk.{messageId,delta}`,
  `TextMessageStart.name`, `RunStarted.{parentRunId,input}`) so the
  omitted-vs-explicit-null distinction is locked in.
- Cross-reference `// See _Unset (top of file) for the sentinel rationale.`
  on all 10 sentinel-using `copyWith` methods — readers landing on a single
  call site can find the global doc in one hop.
- Extract a private `_wrapValidation` helper in `EventDecoder.decodeJson`
  to deduplicate the two `on …catch` blocks for `ValidationError` and
  `AGUIValidationError`. Forensic comments on each branch retained.
- Dartdoc additions: `EventType.fromString` (throw-vs-wrap contract),
  `ReasoningEncryptedValueSubtype.toolCall` (the wire dash in `'tool-call'`
  is intentional, mirroring TS/Python literals), `JsonDecoder` class doc
  on why single-word keys don't need `*EitherField`, and an inline note on
  `ActivitySnapshotEvent.toJson`'s always-emitted `replace` field.
- README: `import 'dart:io';` so the activity/reasoning example is
  copy-paste-complete, and an extra line demonstrating `replace`
  (overwrite vs merge) semantics.
- `test_helpers.dart`: replace the stale "Will need to check the actual
  implementation" comment with the actual rationale.
- CHANGELOG: correct the "remaining `?? this.field` cases" list —
  `ToolCallStartEvent`, `ToolCallChunkEvent`, and `ReasoningMessageChunkEvent`
  were already migrated to the sentinel pattern; the genuine remaining
  cases are `ToolCallResultEvent.role`, `StateSnapshotEvent.snapshot`, and
  `RunErrorEvent.code`.

All 490 tests pass (up from 479 baseline); analyzer clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…HOT parity + reviewer fixups

Addresses dual-reviewer findings on the ag-ui-protocol#1018 protocol-parity branch:

- MESSAGES_SNAPSHOT now decodes `activity` and `reasoning` messages
  (ChatGPT REQUEST_CHANGES driver). Adds `MessageRole.activity` /
  `MessageRole.reasoning`, new `ActivityMessage` and `ReasoningMessage`
  classes (with camelCase/snake_case parity for `activityType` /
  `encryptedValue`), `Message.fromJson` dispatch, a fixture, and
  factory + integration round-trip tests covering the canonical TS/Python
  message-union shape.
- `EventDecoder.validate` now rejects empty `delta` on
  `ThinkingTextMessageContentEvent`, restoring symmetry with sibling
  `*ContentEvent` validators.
- `ReasoningEncryptedValueEvent.fromJson` now rejects empty
  `entityId` / `encryptedValue` at the factory boundary so direct
  callers cannot produce an event with a mis-attributed cipher payload.
- `EventStreamAdapter.fromRawSseStream` `onDone` final-flush now wraps
  non-`AgUiError` causes as `DecodingError`, matching the per-line
  error-routing contract; dartdoc documents the abnormal-mid-line-
  termination drop edge case.
- CHANGELOG: explicit `### Breaking Changes` callout for the
  `ToolCallResultEvent.role` `String? → ToolCallResultRole?` type
  change; README `## Migrating from 0.1.0` subsection covering the
  same.
- `THINKING_TEXT_MESSAGE_*` enum values and event classes deprecated
  in favor of `REASONING_*`, mirroring the canonical TypeScript SDK.
  Decoding remains supported until 1.0.0.
- Dartdoc clarifications: `RunFinishedEvent.result` (explicit-null vs
  absent are wire-equivalent), `RunStartedEvent.input` (wrong-typed
  rejection at decode), `requireEitherField` / `optionalEitherField`
  (`??` only fires on `null`, falsy non-null camelKey values are
  preserved). Adds `actualValue: runtimeType.toString()` to the
  decoder shape-mismatch error and a TODO breadcrumb on
  `ReasoningEncryptedValueSubtype` for a future `unknown` member.
- New tests: ActivityMessage / ReasoningMessage round-trip + parity,
  `MESSAGES_SNAPSHOT` mixed activity/reasoning end-to-end,
  `ActivitySnapshotEvent.toJson` always-emit-`replace` invariant,
  factory-level empty-string rejection on `ReasoningEncryptedValueEvent`,
  and `EventDecoder.validate` empty-delta on the thinking-text content
  event. All 505 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cryptedValue + required id

Closes the two cross-SDK parity gaps Opus flagged in the latest review:

- Add optional `encryptedValue` field to `ToolMessage` (camelCase + snake_case
  decode parity, omit-when-null on encode, threaded through `copyWith`).
  Mirrors the canonical TS `ToolMessageSchema` and Python `ToolMessage` and
  closes the cipher-payload-drop gap that became conspicuous next to
  `ReasoningMessage`, which already round-trips `encryptedValue`.

- Tighten `ToolMessage.id` to `required` on the constructor and to
  `JsonDecoder.requireField<String>` in `fromJson`, matching the canonical
  schemas (both declare `id: str` as required) and aligning with every
  sibling subtype (`Developer`, `System`, `User`, `Activity`, `Reasoning`).

All 505 Dart tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r + error-root unification + copyWith message parity

Addresses both Opus and ChatGPT review findings on the ag-ui-protocol#1018 branch:

- Critical: EventStreamAdapter.fromRawSseStream now strips trailing \r
  after splitting on \n so CRLF (\r\n) terminators dispatch on the
  empty-line signal instead of buffering until stream close. Same fix
  applied to EventDecoder.decodeSSE via LineSplitter (handles \n, \r,
  \r\n per the WHATWG SSE spec). Three regression tests cover CRLF-only,
  mixed LF/CRLF, and decodeSSE CRLF — they assert pre-close emission
  so a future regression of the steady-state path fails loudly.

- Error roots unified: AgUiError now extends AGUIError, and
  AGUIValidationError extends AGUIError instead of bare implements
  Exception. Callers can on AGUIError catch (e) to cover the entire
  SDK error surface (factory validation + encoder-side + runtime/
  transport + decoder). EncoderError/DecodeError/EncodeError are now
  rethrown unchanged from decode()/decodeJson(). README gained an
  "Errors" section with the recommended catch recipe.

- AssistantMessage.fromJson uses optionalEitherField on the toolCalls
  / tool_calls KEY (was a ?? chain on the post-.map().toList() value
  that short-circuited on empty []). Round-trip fix in toJson emits
  toolCalls when non-null even if empty so fromJson(m.toJson()) == m
  is symmetric.

- Message subclass copyWith methods (Developer/System/User/Assistant/
  Tool/Reasoning) gained the _unsetMessage sentinel for nullable
  fields, matching the event-class discipline. copyWith(field: null)
  now clears; copyWith() preserves.

- New JsonDecoder.optionalIntField helper accepts int OR num and
  coerces via .toInt(). All 34 timestamp call sites in events.dart
  migrated, so a TS server emitting a fractional timestamp no longer
  fails decode with AGUIValidationError(field: 'timestamp').

- optionalListField/requireListField now eager-validate elements with
  field: '$field[$i]' instead of returning a lazy cast<T>() view.
  Wrong-typed elements now surface as AGUIValidationError with the
  originating index instead of leaking TypeError to the catch-all
  and getting flattened to field: 'json'.

- AGUIValidationError gained an optional cause parameter so the
  transform-rethrow path in JsonDecoder preserves structured info.

- SseParser documented its per-connection state semantics (sticky
  _lastEventId per spec) and gained a reset() method for callers
  reusing a parser instance across independent streams.

- UserMessage documented as a known parity gap with the canonical
  multimodal schema (string-only vs union[string, InputContent[]]).
  Message.id documented as nullable-by-type / required-by-convention.

- Doc/comment fixups: ActivityDeltaEvent.validate notes empty patch
  is intentional per canonical TS/Python; deprecated Thinking*
  validate cases note why no messageId check (deprecated wire shape
  has none); BaseEvent.rawEvent gained a dynamic/unvalidated note;
  MessageRole.fromString dartdoc expanded for sibling-enum parity.

- Tests: 11 new tests (CRLF parsing × 3, copyWith null-clearing × 5,
  AssistantMessage dual-key precedence × 1, float timestamp × 1,
  decodeSSE CRLF × 1). Test names "rejects" → "throws" for sibling-
  enum consistency. Suite: 516 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trip + lone-CR SSE + AGUIError preservation

Addresses the dual-reviewer (Opus + ChatGPT) review at
reviews/matt.spurlin-1018-fix-missing-event-types-2026-05-03/. All 6
Important findings + 12 Suggestions resolved; 526/526 tests pass;
dart analyze clean.

Important fixes:
- EventEncoder.encodeSSE no longer strips fields whose value is null.
  The blanket removeWhere was breaking the encode→decode round-trip
  for ActivitySnapshotEvent.content / RawEvent.event / CustomEvent.value
  / StateSnapshotEvent.snapshot — their factories require key presence
  and reject missing-key with AGUIValidationError. Pinned by a new
  round-trip test in fixtures_integration_test.dart.
- EventStreamAdapter.fromRawSseStream now handles WHATWG-spec lone-\r
  line terminators in addition to \n and \r\n. Multi-terminator scanner
  with a deferred-\r heuristic that disambiguates chunk-spanning \r\n
  while not stalling steady-state lone-CR streams: when the previous
  terminator in the same scan was also a lone \r, the trailing \r is
  consumed immediately (CRLF producers can never trigger this branch).
  Pinned by 2 new regression tests (lone-CR steady state + chunk-
  spanning CRLF disambiguation).
- Stream adapters preserve any AGUIError subtype (AgUiError,
  AGUIValidationError, EncoderError) instead of re-wrapping the
  encoder-family errors as a generic DecodingError. Honors the
  unified-error-surface contract that EventDecoder already follows.
- TestHelpers.findToolCalls now uses the typed AssistantMessage.toolCalls
  accessor (was reading snake_case key while toJson emits camelCase —
  silent zero-result; helper currently unreferenced).
- decoder.dart ThinkingTextMessageContentEvent validate-case rationale
  comment rewritten: sibling content events were RELAXED in 0.2.0 for
  TS/Python parity; the deprecated path keeps the stricter pre-0.2.0
  contract on purpose.
- Field-level dartdoc warnings on ToolCallResultEvent.role,
  StateSnapshotEvent.snapshot, RunErrorEvent.code documenting that
  copyWith(field: null) does NOT clear (CHANGELOG-acknowledged
  "Known parity gaps").

Suggestions:
- New JsonDecoder.optionalEitherListField<T> helper combining dual-key
  resolution with index-aware element-type validation; wired into
  AssistantMessage.fromJson (so a malformed nested toolCalls[i] now
  raises AGUIValidationError(field: 'toolCalls[$i]') instead of leaking
  a TypeError).
- RunAgentInput.fromJson and Run.fromJson migrated to
  JsonDecoder.requireEitherField for consistency with the rest of the
  SDK; forwardedProps inline-?? choice documented (truly-dynamic field).
- EventStreamAdapter internal _appendDataLine + flushDataBlock
  decomposition to share per-line and onDone flush paths.
- @deprecated messages hoisted into top-level const strings in
  events.dart (4 strings) and event_type.dart (4 strings) — reduces
  drift risk if the planned 1.0.0 removal version changes.
- Validators.maxTimeout exposed as static const Duration so callers can
  introspect the limit (10 minutes; cap value unchanged).
- Wire-spelling-pinning dartdoc on MessageRole.activity / .reasoning
  mirroring the ReasoningEncryptedValueSubtype.toolCall style.
- Explicit "// No default — exhaustive switch" trailing comments on
  BaseEvent.fromJson and Message.fromJson switches.
- Stale comments updated on ReasoningEncryptedValueEvent.fromJson
  (cipher contract is intentionally stricter than relaxed siblings) and
  AssistantMessage.fromJson toolCalls precedence (camelCase wins on
  KEY presence, even when the list is empty).
- README "Migrating from 0.1.0" TimeoutError section gained a paragraph
  on the inverse case (consumers who meant dart:async.TimeoutError but
  were silently catching SDK instances).

CHANGELOG [Unreleased] updated with all of the above grouped by
Fixed / Added / Changed / Documentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ment (TimeoutError callers + ToolCall.encryptedValue + delta-test relaxation)

Picks up changes that were CHANGELOG-documented in prior review-fix
commits but left unstaged in the working tree. No new behavior; brings
the committed code in line with the already-published CHANGELOG entries.

Production:
- Internal AgUiClient call sites now throw AGUITimeoutError directly
  (the rename was applied to the type in 334f302 but the caller
  updates didn't get staged). client/errors.dart adds the
  AGUITimeoutError class definition + deprecated TimeoutError typedef
  bridge for backward compat. Aligns with CHANGELOG → "TimeoutError
  renamed to AGUITimeoutError to avoid shadowing dart:async.TimeoutError".
- ToolCall now carries the optional encryptedValue field (with sentinel
  copyWith for nullable-clear semantics). Aligns with CHANGELOG →
  "ToolCall now carries the optional encryptedValue field for parity
  with canonical TS/Python".

Tests:
- test/client/{client,errors,http_endpoints}_test.dart updated to
  catch AGUITimeoutError and pin the deprecated TimeoutError typedef
  bridge resolves to the new class.
- test/encoder/decoder_test.dart and test/events/event_test.dart
  empty-delta tests relaxed (TextMessageContentEvent, ToolCallArgsEvent,
  ReasoningMessageContentEvent now accept empty delta, matching
  canonical TS/Python z.string() / pydantic delta:str schemas). Aligns
  with CHANGELOG → "Empty delta is now accepted on TEXT_MESSAGE_CONTENT,
  TOOL_CALL_ARGS, and REASONING_MESSAGE_CONTENT, and empty content is
  accepted on TOOL_CALL_RESULT".
- test/events/event_test.dart adds RunStartedEvent.input.parentRunId
  round-trip test (camelCase + snake_case), pinning the embedded
  RunAgentInput.parentRunId field that mirrors canonical schemas.
- test/types/message_test.dart adds ToolCall.encryptedValue parity
  group: round-trip via AssistantMessage.toolCalls, snake_case alias,
  toJson omission when null, and copyWith null-clearing.

dart analyze clean; 526/526 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cryptedValue + raw_event sweep + cipher-empty parity + sentinel sweep + SSE/cancel + doc batch

Addresses the dual-reviewer (Opus + ChatGPT) review at
reviews/matt.spurlin-1018-fix-missing-event-types-2026-05-03/. Both
reviewers returned REQUEST_CHANGES; this commit resolves all 3
Critical, all 7 Important, and 10 of 14 Suggestions (4 deferred per
plan as pure cleanup/style with reviewer "optional" notes). 540/540
tests pass; dart analyze clean (no new warnings/errors).

Critical:
- encryptedValue plumbed through the base Message sealed class so
  every BaseMessage subtype (Developer/System/Assistant/User/Tool)
  carries it. Closes the cross-SDK parity gap with canonical TS
  BaseMessageSchema.encryptedValue: z.string().optional() and Python
  BaseMessage.encrypted_value: Optional[str]. Pre-fix, a Dart proxy
  decoding a MESSAGES_SNAPSHOT whose assistant or user message
  carried encryptedValue from a TS or Python server silently dropped
  the value at decode and could not re-emit it on the next hop —
  same bug class this branch already fixed for ToolCall.encryptedValue
  in bd73bf1, just not extended to messages. ToolMessage and
  ReasoningMessage drop their own field declarations and inherit from
  the base; ActivityMessage inherits a no-op nullable (canonical
  ActivityMessage doesn't extend BaseMessage; the field is never
  read or emitted for that subtype). Decode accepts encryptedValue
  and encrypted_value via optionalEitherField; toJson emits camelCase;
  copyWith uses the existing _unsetMessage sentinel for explicit-null
  clear. Tests cover round-trip, dual-key, and copyWith for Assistant
  and User; events.json fixtures gain an assistant.encryptedValue
  entry so fixtures_integration_test.dart exercises proxy round-trip.
- raw_event (snake_case) is now preserved on every event factory.
  Centralized _readRawEvent(json) helper using containsKey precedence
  (camelCase wins when key present, even when explicitly null;
  snake_case is consulted only when camelCase is absent). All 34
  json['rawEvent'] sites in events.dart swept. New regression covers
  snake-case-wins-when-camel-absent, camel-wins-when-both-present,
  and explicit-null-camel-wins (containsKey precedence).
- ReasoningEncryptedValueEvent.fromJson and EventDecoder.validate no
  longer reject empty entityId / encryptedValue. Canonical TS uses
  z.string() (no .min(1)) and Python uses str (no min_length); the
  Dart-only rejection was over-strict and would reject payloads the
  canonical SDKs accept. Strict subtype discriminator stays. The
  existing integration test entries that pinned the rejection are
  removed; new positive-accept tests at the factory level.

Important:
- copyWith sentinel sweep on RawEvent.source (events.dart),
  RunAgentInput.state / forwardedProps and Run.result (context.dart).
  Pre-fix, these used standard ?? this.field — copyWith(field: null)
  could not clear them. Now use _unsetCopyWith / _unsetContext with
  identical(...) check + cast. New explicit-null clear regressions.
  CHANGELOG "Known parity gaps" updated to reflect the smaller
  remaining set (ToolCallResultEvent.role, StateSnapshotEvent.snapshot,
  RunErrorEvent.code).
- SseParser._processField data-case fix: switched from the
  _dataBuffer.isNotEmpty heuristic (which collapsed
  data:\\ndata: x\\n\\n to "x" instead of spec-correct "\\nx") to the
  _hasDataField flag pattern that matches EventStreamAdapter's
  inDataBlock flag. WHATWG-compliant.
- SseParser._processField event-case fix: switched from append to
  clear+write. Per WHATWG: "If the field name is 'event', set the
  event type buffer to field value." Repeated event: lines within
  one dispatch block now REPLACE rather than concatenate.
  Regression tests cover both spec fixes.
- EventStreamAdapter.fromRawSseStream now propagates downstream
  cancellation, pause, and resume to the upstream raw SSE
  subscription. Pre-fix, rawStream.listen(...) was fire-and-forget;
  a consumer that cancelled the adapted stream early left the
  upstream draining indefinitely (a real leak on long-lived agent
  streams). New regression asserts rawController.hasListener flips
  false on downstream cancel.
- AGUIValidationError.json dartdoc gained an explicit sensitive-data
  warning: the field captures the entire wire payload including
  cipher fields. toString() does not emit it (safe by default), but
  reflection-based serializers used by some logging frameworks will
  leak. Recommend .field and .value for log lines shipped to
  external sinks.
- EventDecoder.validate dartdoc documents the dual-source error
  class asymmetry: validate() raises client/errors.dart's
  ValidationError; fromJson-side eager rejections raise
  types/base.dart's AGUIValidationError. Both surface uniformly as
  DecodingError through the public decode/decodeJson boundary; both
  extend AGUIError so a single on AGUIError catch (e) covers both.
- README adds a "Proxy notes: wire-spelling normalization" paragraph
  documenting that the SDK accepts camelCase and snake_case on
  fromJson but always emits camelCase on toJson. The Error Handling
  section is refreshed to use the current error-hierarchy class
  names (TransportError / DecodingError / ValidationError /
  CancellationError, all under AGUIError).
- AgUiClient.runAgent dartdoc Throws: list refreshed to match the
  current error hierarchy.

Suggestions:
- ToolMessage.fromJson and ToolResult.fromJson migrated from the
  older optionalEitherField + manual null-check + custom throw
  pattern to JsonDecoder.requireEitherField (matching the migration
  already done for RunAgentInput.fromJson and Run.fromJson).
- JsonDecoder.optionalEitherField switched from ?? optionalField
  chain to containsKey-based precedence so the dartdoc and
  implementation agree (the dartdoc on requireEitherField promised
  KEY-presence resolution; the implementation was VALUE-non-null).
  forwardedProps inline decode in context.dart migrated to the same
  containsKey rule.
- Validators.validateMessageContent tightened to String-only with a
  documented rationale comment. The pre-0.2.0 permissive Map/List
  branches were dead code (no caller in the SDK passed those types)
  and disagreed with canonical BaseMessage.content: Optional[str].
  Multimodal UserMessage.content stays a tracked parity gap.
- Validators.validateUrl rejects URLs containing C0 control
  characters or DEL (\\x00–\\x1f, \\x7f) before delegating to
  Uri.parse. Closes a header-injection vector via embedded \\n in
  the URL path.
- JsonDecoder.requireField and optionalField transform-failure
  paths now preserve cause: e when wrapping an inner exception as
  AGUIValidationError. The new cause field on AGUIValidationError
  was added in 334f302 but two transform paths weren't passing it.
- SseParser.parseBytes routed through parseLines so the final
  _dispatchEvent flush also fires for byte-stream sources. A byte
  source ending without a trailing blank line previously lost its
  last buffered event.
- BaseEvent.rawEvent dartdoc gained a "Consumer note: round-trip
  emission" paragraph — anything assigned to this field WILL be
  re-emitted on the next encode. Set rawEvent: null on the in-flight
  event if a proxy doesn't want the upstream payload echoed
  downstream.
- EventStreamAdapter.groupRelatedEvents dartdoc gained an explicit
  unbounded-state warning for the open-groups map.

Deferred (per plan; reviewer-acknowledged optional):
- _Unset sentinel duplicated across 4 files — pure cleanup, no
  behavior change.
- decodeBinary / encodeBinary protobuf TODO.
- _eagerCast perf (reviewer says "no change required for now").
- RunStartedEvent.fromJson inputJson local-vs-inline style.

CHANGELOG updated with all of the above grouped by Fixed (review pass
— protocol parity) / Documented / Changed; "Known parity gaps" list
trimmed to reflect the sentinel sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ lazy SSE subscription + message json context

- Tool.copyWith(parameters) now uses _unsetTool sentinel so callers can
  explicitly clear parameters via copyWith(parameters: null); previously
  null was indistinguishable from "omitted" and silently preserved the
  old value (unlisted parity gap alongside the three in CHANGELOG)
- fromRawSseStream defers rawStream.listen() to controller.onListen so
  an unconsumed returned stream does not leak the upstream subscription;
  mirrors the cancel-path fix already on this branch; subscription is
  now StreamSubscription<String>? with null-safe lifecycle callbacks
- Message.fromJson wraps MessageRole.fromString in try/catch and
  re-throws AGUIValidationError with json: populated, preserving wire
  payload context when a bad role arrives deep in a MESSAGES_SNAPSHOT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…findings

Important (3):
- base.dart: requireEitherField uses containsKey precedence (not ??-chain),
  matching optionalEitherField; a present-but-null camelCase key no longer
  falls through to the snake_case alias
- stream_adapter.dart: groupRelatedEvents + accumulateTextMessages now use
  lazy subscription (deferred to controller.onListen) with full lifecycle
  wiring (onCancel/onPause/onResume), matching the pattern in fromRawSseStream
- client.dart: _sendWithCancellation late-error handling via unawaited() +
  explicit swallow with comment; documents known HTTP connection limitation

Suggestions (7 of 8; S-8 reverted — snake_case wire names are correct):
- base.dart: AGUIValidationError.toString() truncates value to 100 chars
- decoder.dart: Error.throwWithStackTrace preserves original stack on rethrow
- events.dart: ThinkingContentEvent deprecation points at ReasoningMessageContentEvent
  directly (not the also-deprecated ThinkingTextMessageContentEvent)
- events.dart: BaseEvent.rawEvent dartdoc clarifies copyWith(rawEvent:null) is no-op
- event_type.dart: thinkingContent enum deprecation likewise updated
- tool.dart: ToolResult.copyWith(error:) uses _unsetTool sentinel to allow clearing
- message.dart: ActivityMessage dartdoc explains encryptedValue intentionally omitted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

Implements all Important items from the Opus 1 + Opus 2 review of the
fix-missing-event-types branch (no Critical items found).

Fixes applied:
- I1/Both: MessagesSnapshotEvent + AssistantMessage — per-element indexed
  try/catch so AGUIValidationError.field includes messages[$i] / toolCalls[$i]
- I2: requireField/optionalField — add `on AGUIError { rethrow; }` before
  bare catch in transform callbacks to prevent re-wrapping validation errors
- I3: Tool.copyWith — add clarifying comment on _Unset sentinel for dynamic
  `parameters` field (ergonomic symmetry, not functional necessity)
- I4: sse_parser _lastEventId — cap at 1 KB to bound memory across reconnects
- I5: fromSseStream keep-alive drop — document intentional discrepancy with
  decodeSSE/fromRawSseStream onError routing
- I6: validateEventType regex — widen to [A-Z0-9_] for future versioned types
- II1: Tool.metadata — add Map<String, dynamic>? field mirroring TS parity
- II2: _scanLines lastWasLoneCr — hoist into closure, pass/return via tuple
  so lone-CR state persists across processChunk calls; add regression test
- II3: validateUrl regex — extend to cover U+0085 NEL, U+2028 LS, U+2029 PS
- II4: _validateRunAgentInput — expand partial UserMessage check to exhaustive
  sealed switch over all Message subtypes
- II5: EventType.fromString + 4 other wire-discriminator enums — replace
  O(n) firstWhere scan with static final Map<String, T> _byValue O(1) lookup
- II6: ReasoningEncryptedValueEvent — suppress json: on AGUIValidationError
  for cipher payload to avoid leaking encrypted data through error logs
- II7: _sendWithCancellation — add developer.log() for late HTTP
  errors/responses after cancellation instead of silent swallow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

- forwarded_props: fix camelCase→snake_case in SimpleRunAgentInput.toJson (flagged by both reviewers)
- _sendWithCancellation: drain late HTTP response stream to release socket eagerly
- _runAgentInternal: use putIfAbsent for Accept header so caller-supplied value wins
- _validateRunAgentInput: validate caller-supplied runId at boundary
- _runAgentInternal: extract on TimeoutException before generic catch to prevent misbranding
- client_codec: rename ToolResult→ClientToolResult to eliminate class-name collision with types/tool.dart
- validators: mark validateEventSequence @deprecated (never wired up in lib/)
- sse_parser: correct _lastEventId size comment from "1 KB" to "≤1024 UTF-16 code units"
- message.dart: fix encryptedValue dartdoc for ReasoningMessage (no shadowing field)
- README: add cancellation socket-abort limitation note to error handling section
- stream_adapter: document on-close behavior for groupRelatedEvents (emits) and accumulateTextMessages (drops)

All 541 tests pass; dart analyze clean (errors).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

Implements all Critical+Important items from the opus/ + opus2/ dual-reviewer
review of the reasoning/activity event additions:

Both reviewers (highest confidence):
- Broaden `on AGUIValidationError` to `catch (e)` in MessagesSnapshotEvent.fromJson
  and AssistantMessage.fromJson so any nested fromJson exception preserves the
  index reframe (not just AGUIValidationError subclasses)
- Consolidate four duplicated _Unset sentinel classes (events, message, tool,
  context) into a single shared kUnsetSentinel constant in base.dart (~80 lines
  removed)

Opus1 findings:
- Extend groupRelatedEvents switch to handle ReasoningMessage{Start,Content,End}
  events — previously fell to default branch, silently breaking grouping contract
- Add "user-visible text only" scope documentation to accumulateTextMessages dartdoc
  explicitly excluding REASONING_MESSAGE_* events
- Document decodeSSE "all empty lines" behavior (No data found path)
- Add operational risk documentation to ReasoningEncryptedValueEvent validate case
  for empty entityId/encryptedValue
- Wrap response.stream.drain<void>() in try/catch for StateError on already-consumed
  late-response-after-cancellation path
- Document ActivitySnapshotEvent.replace always-emit divergence from canonical SDKs
  in Known parity gaps inline note
- Fix _Unset sentinel dartdoc to say "name field of TextMessageStartEvent" (only
  name is guarded, not role)
- Document optionalEitherField error-field-name behavior on snake_case path
- Add message.id non-null validation for all message types in _validateRunAgentInput

Opus2 findings:
- Move _validateRunAgentInput call BEFORE _requestTokens map insertion so a bad
  caller-supplied runId never enters the map before validation rejects it
- ActivityMessage.fromJson now explicitly rejects inbound encryptedValue /
  encrypted_value with AGUIValidationError (not a BaseMessage extension)
- Fix _scanLines edge case: lastWasLoneCrAtStart=true + chunk starts with \n
  would double-dispatch one logical message boundary; leading \n is now skipped
  as the CRLF complement of the prior-chunk consumed lone-CR
- Document decodeSSE (complete-frame) vs fromRawSseStream (streaming) semantic
  divergence on keep-alive routing and partial-frame handling
- Update validateRunId/validateThreadId error messages to say "(100 UTF-16 code
  units)" not "characters", add dartdoc clarification
- Add re-entrancy contract documentation to all three StreamController(sync:true)
  usages in stream helpers
- Route keep-alive sentinels in fromSseStream through onError when
  skipInvalidEvents=true for observability parity with decodeSSE/fromRawSseStream
- Clarify RunFinishedEvent.result kUnsetSentinel is purely in-memory (no wire
  effect since toJson collapses null→absent regardless)
- validateUrl now rejects URLs with non-empty userInfo component to prevent
  credential-bearing endpoints from leaking into logs/redirects

Tests added (546 total, +5):
- ActivityMessage.fromJson rejects camelCase/snake_case encryptedValue
- validateUrl rejects credential-bearing URLs (user:pass@host, token@host)
- fromRawSseStream CRLF-split-across-chunks regression (data:foo\r\r + \ndata:bar\n\n)
- groupRelatedEvents groups Reasoning{Start,Content,End}Event by messageId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ual-reviewer pass

Critical (both reviewers):
- SimpleRunAgentInput.toJson() now emits camelCase (threadId, runId,
  forwardedProps) — was emitting snake_case, contradicting the README
  camelCase-only contract and the canonical RunAgentInput.toJson().
  Updated client_codec_test and http_endpoints_test to assert camelCase.

Important:
- Un-hide ClientToolResult from ag_ui.dart — Encoder.encodeToolResult()
  took a hidden type, making it uncallable from outside the package.
- groupRelatedEvents: namespace activeGroups map keys by event family
  ('text:', 'reasoning:', 'tool:') to prevent collision when a producer
  reuses the same messageId across Text and Reasoning streams.
- accumulateTextMessages: route TextMessageChunkEvent delta into the
  active buffer when a Start/End cycle is open for that messageId,
  rather than bypassing the buffer and emitting out-of-logical order.
- Tool.copyWith: add kUnsetSentinel for metadata field (was using
  ?? this.metadata, so copyWith(metadata: null) couldn't clear it).
- RunStartedEvent.fromJson: wrap nested RunAgentInput.fromJson in
  try/catch and re-throw with field prefixed 'input.$field' to
  distinguish inner errors from the outer event's own threadId/runId.
- fromSseStream: silently discard keep-alive sentinels instead of
  routing through onError — keep-alives are not errors.
- _validateRunAgentInput: add Set<String> dedup check for message.id
  to enforce uniqueness within a single RunAgentInput.messages list.
- validators.validateUrl: hoist RegExp to static final _kUrlControlChars
  to avoid recompiling the pattern on every call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ems from dual-reviewer pass

- Remove `json: json` from error rethrows in MessagesSnapshotEvent,
  AssistantMessage, and Message.fromJson to avoid leaking cipher-payload
  wire maps to AGUIValidationError.json (reflection-based serializers)
- Add `final` to all 7 Message subclass declarations to block external
  subclassing that would break discriminator-based serialization dispatch
- Document MessageRole throw-vs-fallback intentional choice at switch site
- Route *Chunk events into active Start/End group in groupRelatedEvents
  rather than emitting as standalone single-element groups
- Flush partial content on abnormal stream close in accumulateTextMessages
  instead of silently discarding (mirrors groupRelatedEvents behavior)
- Wrap non-AGUIError exceptions in DecodingError in processChunk catch so
  consumers can distinguish SDK bugs from decode failures
- Add NUL (\x00) to SSE id: field rejection per WHATWG spec
- Add indexed error-wrapping loops in RunAgentInput.fromJson for messages,
  tools, and context lists (preserves index info on nested decode failures)
- Add TODO(1.0.0) blocks tracking deprecated Thinking* removal in
  decoder.dart and events.dart
- 4 new regression tests (chunk-routing, flush-on-close, NUL id)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

Applied all 15 Important items from the Opus 1 + Opus 2 dual review:

- I-A [Both]: Replace invisible Unicode literals in _kUrlControlChars with
  explicit �, 
, 
 escapes (validators.dart)
- I-B: _wrapValidation returns Never; absorbs stack via Error.throwWithStackTrace
  so callers are analyzer-verified as unconditionally throwing (decoder.dart)
- I-C: Hoist Random.secure() to static lazy field in AgUiClient (client.dart)
- I-E: SimpleRunAgentInput.toJson uses if-non-null discipline; null fields are
  omitted instead of defaulting to {} / [] (client.dart, client_codec_test.dart)
- I-F: Forward json: e.json through outer AGUIValidationError rewrap in
  MessagesSnapshotEvent.fromJson and three RunAgentInput IIFE blocks
  (events.dart, context.dart)
- I-G: Message.fromJson try/catch attaches json: json to the rewrapped error
  for better debuggability (message.dart)
- I-H: Add _inDispatch assert guard + expanded re-entrancy dartdoc for
  sync: true StreamController in fromRawSseStream (stream_adapter.dart)
- I-I: Track errorRoutedInChunk flag to prevent double-fire of addError when
  flushDataBlock already routed an error in the same chunk (stream_adapter.dart)
- I-J: Add one-line "standalone unless open group exists" comments to all three
  *Chunk arms in groupRelatedEvents; add regression tests for ToolCallChunk and
  ReasoningMessageChunk into-open-group cases (stream_adapter.dart + test)
- I-K: Add configurable maxDataBytes cap (default 8 MiB) to SseParser._dataBuffer
  to prevent OOM from malicious producers (sse_parser.dart)
- I-L: Document CancelToken one-shot contract and listener-accumulation behavior
  (client.dart)
- I-M: Add trace comment explaining why lastWasLoneCrAtStart is NOT involved in
  the "chunk ends exactly at \r" path (stream_adapter.dart)
- I-N: Document Message.id nullable-vs-required-outbound contract in validator
  (client.dart)
- I-O: Document _eagerCast field-naming asymmetry vs per-factory list decoders
  (base.dart)

Note: I-D was already satisfied -- _validateRunAgentInput is already outside
the try/finally block in the current code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

Code fixes:
- optionalIntField: guard NaN/Infinity → AGUIValidationError; floor() for TS Math.floor parity
- DecodingError.toString + ValidationError.toString: add cause chain (matches TransportError)
- encodeSSE: wrap jsonEncode in try/catch → EncodeError for non-JSON-encodable rawEvent
- validateMessageContent: tighten parameter type from dynamic to String?, remove dead is! String branch
- errorRoutedInChunk: defensive reset at top of onDone handler
- ActivityMessage.toJson: explicit override skipping super.content to avoid map-spread fragility
- Surrogate-safe _safeTruncate helper at all 3 substring(0,N) truncation sites

Doc/comment fixes:
- accumulateTextMessages dartdoc: stale "silently discarded" → actual flush-on-close behavior
- ReasoningEncryptedValueEvent.fromJson: uniform json: omission across all 3 cipher fields
- MessagesSnapshotEvent list-decode IIFE: document intentional json: forwarding asymmetry
- _eventBuffer in SseParser: explain why only _dataBuffer needs maxDataBytes cap
- events.dart: warn against file-level deprecated_member_use_from_same_package suppression
- CHANGELOG: document RunFinishedEvent.result round-trip drift under Known parity gaps
- validateUrl dartdoc: note percent-encoded control-char defense on credentials block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dual-reviewer pass

Critical (1):
- C1: ReasoningEncryptedValueEvent.fromJson now sets rawEvent: null instead
  of _readRawEvent(json), preventing cipher payload from leaking into the
  inherited rawEvent field despite every error path omitting json:

Breaking changes (2):
- I2: StateDeltaEvent.delta and ActivityDeltaEvent.patch changed from
  List<dynamic> to List<Map<String, dynamic>> via requireListField — RFC 6902
  ops are always objects; non-object elements now surface as AGUIValidationError
  at the decoder boundary instead of a downstream TypeError
- I8: SseParser.maxDataBytes renamed to maxDataCodeUnits — the field already
  measured UTF-16 code units, not bytes; error message corrected to match

Fixes (12 items):
- I1: ActivityMessage.fromJson silently strips encryptedValue/encrypted_value
  instead of throwing — TS strips (zod default), Python preserves; Dart was
  the only SDK that tore down the stream on encountering the field
- I3: ThinkingStartEvent.title copyWith now uses kUnsetSentinel pattern
- I4: groupRelatedEvents dartdoc documents ReasoningStart/End asymmetry
- I5: RunStartedEvent.fromJson rethrow uses e.json not full outer json,
  limiting cipher-data exposure in AGUIValidationError
- I6: requireEitherField now distinguishes "key present but null" from
  "key absent" with separate error messages
- I7: processChunk resets errorRoutedInChunk after the for-loop
- I9: EventEncoder.acceptsProtobuf and EventDecoder.decodeBinary dartdocs
  warn that protobuf is not yet implemented end-to-end
- I10: MessagesSnapshotEvent.fromJson rethrow drops json: (was e.json which
  can carry encryptedValue for Tool/Reasoning subtypes)
- I11: kUnsetSentinel applied to ToolCallResultEvent.role,
  StateSnapshotEvent.snapshot, RunErrorEvent.code — sentinel sweep complete
- I12: EventType.fromString contract comment strengthened: do NOT change
  throw type from ArgumentError (BaseEvent.fromJson narrow-catches it)
- S1: _dataBuffer.writeln() → write('\n') for unambiguous intent
- S2: RawEvent class-level dartdoc distinguishes eventType / event / rawEvent
- S3: ActivityMessage class note updated to reflect silent-strip behavior

All 553 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ems from dual-reviewer pass

I1: Remove erroneous errorRoutedInChunk reset inside processChunk for-loop
    (nullified deduplication invariant before outer catch could read it).
I2: Promote _inDispatch assert → StateError in fromRawSseStream; add
    re-entrancy guards to groupRelatedEvents and accumulateTextMessages.
I3: Remove drain() on late-arriving SSE response after cancellation
    (SSE streams never complete; drain holds socket open indefinitely).
I4: Drop _transformSseStream finally block — _runAgentInternal.finally
    already owns runId/SseClient lifecycle, avoiding double _closeStream.
I5: Defense-in-depth: check percent-decoded uri.path/query/fragment for
    control chars in validateUrl (header-injection via %0a-encoded paths).
I6: Relax empty-delta rejection for deprecated ThinkingTextMessageContent
    and ThinkingContent events — align with canonical z.string() contract.
I7: Omit cause: from AGUIValidationError rewraps in MessagesSnapshotEvent,
    RunStartedEvent, and AssistantMessage — cause chain can expose e.json
    (cipher payloads) to reflection-based log shippers.
I8: Document that exhaustive_cases lint in analysis_options.yaml enforces
    validate() switch exhaustiveness on the sealed BaseEvent hierarchy.
I9: Clarify validateMessageContent dartdoc: null rejection is defense-in-
    depth; protocol-correct callers already guard null before calling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ems from dual-reviewer pass

- events.dart: fix ToolCallResultEvent.role dartdoc (removed stale "Known
  parity gap" note; copyWith(role: null) correctly uses kUnsetSentinel)
- events.dart: add rawEvent cipher-surface note to MessagesSnapshotEvent.fromJson
- stream_adapter.dart: rename _inDispatch → inDispatch in all three function-body
  locals (leading underscore is meaningless on function locals in Dart)
- stream_adapter.dart: move re-entrancy StateError check outside outer try/catch
  in flushDataBlock so programmer errors surface as StateError, not DecodingError
- stream_adapter.dart: tighten fromRawSseStream re-entrancy guard comment to
  clarify scope (dispatch site only, not buffer-mutation path)
- stream_adapter.dart: document duplicate-Start "last-Start-wins" policy in
  groupRelatedEvents dartdoc
- base.dart: fix optionalEitherListField to pass wire key (resolved) into
  _eagerCast so error messages match the wire spelling on Python servers
- base.dart: add ±2^53 range guard to optionalIntField for Dart-on-JS safety
- test: add regression test for ToolCallResultEvent.copyWith(role: null)
- test: add regression test for RunFinishedEvent absent vs. null result key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ual-reviewer pass

Behavioral/security fixes (Opus 2 I1–I5):
- client.dart: reject duplicate caller-supplied runId before inserting into
  _requestTokens/_activeStreams — prevents SseClient leak and cross-run
  cancel/close interference when two concurrent calls share a runId
- client.dart: filter `data: :` keep-alive sentinels in _transformSseStream
  so they don't surface as spurious DecodingError (mirrors the filter already
  present in EventStreamAdapter.fromSseStream)
- context.dart: drop json: and cause: in all three RunAgentInput.fromJson
  rewrap blocks (messages, tools, context) — prevents cipher data / tool
  arguments from leaking into reflection-based log shippers via the error
  cause chain; mirrors MessagesSnapshotEvent.fromJson discipline
- stream_adapter.dart: fix accumulateTextMessages dropping TextMessageChunkEvent
  when messageId is null but delta is non-null — now emits standalone fragment,
  matching the groupRelatedEvents null-messageId policy
- validators.dart: add uri.host to the percent-decoded control-char scan in
  validateUrl (one-line change; host was the only decoded URI component
  not already checked)

Performance/doc fixes (Opus 1 II1, II4, II8, II9):
- stream_adapter.dart: rewrite _scanLines from O(n²) to O(n) using a forward
  index pointer instead of repeated indexOf + substring on the remaining string;
  all CRLF-deferral and lone-CR edge-case logic preserved unchanged
- stream_adapter.dart: document LinkedHashMap insertion-order contract on
  activeGroups/activeMessages — onDone flush relies on *Start arrival order;
  update dartdoc accordingly
- stream_adapter.dart: fix inaccurate comment in accumulateTextMessages chunk
  case (Start/Content events have not been emitted when the chunk arrives —
  the actual risk is appearing before the End-triggered buffer flush, not
  before Start/Content)
- sse_client.dart: document parseStream as stateless — creates a fresh
  SseParser per call, does not touch reconnection state, safe to call
  independently of connect()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

Both reviewers (Opus 1 + Opus 2) APPROVED the branch; this commit addresses
all 10 Important items before merge.

- I-1 [Both]   client.dart: on AgUiError → on AGUIError in _transformSseStream
                so AGUIValidationError is no longer wrapped as DecodingError
- I-2 [Opus1]  stream_adapter.dart: reset errorRoutedInChunk per-frame (not
                per-chunk) so a later frame's flush error in the same chunk is
                not silently swallowed
- I-3 [Opus1]  events.dart: RunStartedEvent.fromJson drops json: e.json from
                rethrow; e.json (inner RunAgentInput) can carry encryptedValue
- I-4 [Opus1]  message.dart: Message.fromJson drops json: and cause: from
                AGUIValidationError rethrow — both can carry cipher data
- I-5 [Opus1]  decoder.dart: prefix all four _wrapValidation() call sites with
                return so the Never return type is syntactically enforced
- I-6 [Opus2]  events.dart: TextMessageChunkEvent unknown-role fallback
                changed from TextMessageRole.assistant → null (nullable field;
                null is the correct sentinel for "present but unrecognized")
- I-7 [Opus2]  CHANGELOG.md: document requireNonEmpty vs z.string() parity gap
                in new "Known parity gaps" section under [Unreleased]
- I-8 [Opus2]  stream_adapter.dart: adaptJsonToEvents composes inner field path
                (jsonData[i].role) instead of losing it (jsonData[i])
- I-9 [Opus2]  validators.dart: validateUrl now rejects empty-host URLs like
                http:// via uri.host.isEmpty guard
- I-10 [Opus2] events.dart: RunFinishedEvent.fromJson gains a comment documenting
                that absent key == explicit null per z.any().optional()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tems from dual-reviewer pass

## sse_client.dart (I1, I2)
- Track `Timer? _reconnectTimer`; cancel in `close()` to prevent leak after close
- Add `bool _hasEverConnected`; surface first-connection failures directly to the consumer
  instead of entering the reconnect loop — a server that refused the initial connection
  is unlikely to accept a retry

## client.dart (I3, I8)
- Add `final String? parentRunId` to `SimpleRunAgentInput` with toJson + validation
  (closes outbound parity gap with TS/Python)
- Actively cancel the late-arriving SSE socket via `stream.listen((_){}).cancel()`
  instead of relying on OS reclamation

## validators.dart (II7)
- Remove misleading forward-compat justification from `validateEventType` dartdoc;
  format conformance does not imply the SDK can dispatch the type

## events.dart (II4, II6, II8)
- Split `!containsKey || == null` into two distinct error paths in
  `ReasoningEncryptedValueEvent.fromJson` for all three required fields
  (subtype, entityId, encryptedValue) so missing-key vs explicit-null produce
  different error messages
- Add file-level comment documenting rawEvent as intentionally sticky in copyWith
  (uses `??` not kUnsetSentinel; rationale: cipher-data scrubbing discipline)
- Preserve `cause: e` in AGUIValidationError re-wraps when `e.json == null`
  (inner factory already scrubbed its raw JSON) so non-cipher payloads retain
  the cause chain for ergonomic debugging

## message.dart (II2, II3, II8)
- Override `toJson()` on DeveloperMessage, SystemMessage, UserMessage, ToolMessage,
  and ReasoningMessage to emit `content` unconditionally; parent conditional is safe
  by construction but fragile
- Preserve `cause: e` in Message.fromJson role-parse re-wrap
- Add regression test: ActivityMessage.name and .encryptedValue are always null;
  toJson never emits them; fromJson strips proxy-injected values silently

## context.dart (II8)
- Preserve `cause: e` in RunAgentInput.fromJson messages/tools/context loops
  when the inner error already scrubbed its json: field

## stream_adapter.dart (II1, II9, I5)
- Add `maxDataCodeUnits` (default 8 MiB, matching SseParser) to EventStreamAdapter;
  bound `buffer` and `dataBuffer` in `fromRawSseStream` — a misbehaving server that
  streams `data:` without a blank-line terminator can no longer OOM the process
- Route `inDispatch` StateError via `controller.addError` in `groupRelatedEvents`
  and `accumulateTextMessages` (was leaking as unhandled async error)
- Add `maxOpenGroups` (default 0 = no cap) to `groupRelatedEvents` and
  `accumulateTextMessages`; evicts oldest open group on overflow for DoS resistance

## stream_adapter_test.dart (I4/II5)
- Add lone-CR + zero-length chunk test (lastWasLoneCr persists through empty chunk)
- Add three back-to-back lone-CR events in separate chunks
- Add mixed lone-CR + CRLF terminator transition test

## README.md (I6/I7)
- Add "Cipher-data preservation" section documenting success-path rawEvent,
  error-path json: scrubbing, ReasoningEncryptedValueEvent rawEvent=null rationale,
  and copyWith sticky rawEvent semantics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

@mattsp1290 is attempting to deploy a commit to the CopilotKit Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1778619345 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1778619345' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1778619345' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1778619345' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1778619345' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1778619345' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1778619345' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1778619345

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 80a13c4

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 12, 2026

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@1663

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@1663

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@1663

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@1663

@ag-ui/middleware-starter

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/middleware-starter@1663

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@1663

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@1663

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@1663

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@1663

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@1663

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@1663

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@1663

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@1663

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@1663

@ag-ui/langroid

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langroid@1663

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@1663

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@1663

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@1663

@ag-ui/server-starter

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/server-starter@1663

@ag-ui/server-starter-all-features

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/server-starter-all-features@1663

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@1663

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@1663

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@1663

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@1663

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@1663

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@1663

commit: 5993e17

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.

[Dart SDK] Missing ACTIVITY_SNAPSHOT and ACTIVITY_DELTA event types

1 participant