fix(dart-sdk): add missing event types for protocol parity (activity + reasoning)#1663
Open
mattsp1290 wants to merge 26 commits into
Open
fix(dart-sdk): add missing event types for protocol parity (activity + reasoning)#1663mattsp1290 wants to merge 26 commits into
mattsp1290 wants to merge 26 commits into
Conversation
…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>
|
@mattsp1290 is attempting to deploy a commit to the CopilotKit Team on Vercel. A member of the Team first needs to authorize it. |
Contributor
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen 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 testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1778619345
Commit: 80a13c4 |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/middleware-starter
@ag-ui/a2a
@ag-ui/ag2
@ag-ui/adk
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/langroid
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/server-starter
@ag-ui/server-starter-all-features
@ag-ui/vercel-ai-sdk
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/ActivityDeltaEventReasoningStartEvent,ReasoningEndEventReasoningMessageStartEvent,ReasoningMessageContentEvent,ReasoningMessageEndEvent,ReasoningMessageChunkEventReasoningEncryptedValueEvent(with supportingReasoningMessageRoleandReasoningEncryptedValueSubtypeenums)Deprecations (non-breaking):
EventType.thinkingContent/ThinkingContentEvent— never part of the canonical AG-UI protocol; deprecated in favor ofThinkingTextMessageContentEvent. Decoding remains supported; removal planned for 1.0.0.EventType.thinkingTextMessageStart/Content/End— deprecated in favor of the canonicalreasoningMessage*variants, mirroring the TypeScript SDK's own deprecation ofTHINKING_TEXT_MESSAGE_*.Protocol compliance fixes (applied during review passes):
encryptedValuenow plumbed through allBaseMessagesubtypes (DeveloperMessage,SystemMessage,AssistantMessage,UserMessage), matching TSBaseMessageSchemaand PythonBaseMessage. Previously onlyToolMessageandReasoningMessagecarried the field, so proxy re-emit of aMESSAGES_SNAPSHOTsilently dropped encrypted fields.raw_event(snake_case) now preserved on every event factory via a centralized_readRawEventhelper, fixing silent drops of Python-origin payloads.ReasoningEncryptedValueEvent.fromJsonno longer stores the cipher payload inBaseEvent.rawEvent, closing an unintentional cipher-data leak in error paths.RunStartedEvent.fromJsonrethrow now forwardse.json(the specific inner map) instead of the full outer payload, limitingencryptedValueexposure inAGUIValidationError.SseParser._processFieldnow matches the WHATWG SSE spec:data:uses_hasDataFieldflag sodata:\ndata: x\n\ncorrectly yields"\nx"; repeatedevent:lines replace rather than append.StateDeltaEvent.deltaandActivityDeltaEvent.patchtyped asList<Map<String, dynamic>>(fromList<dynamic>) — RFC 6902 Patch ops are always objects; non-object elements now surface asAGUIValidationErrorat the decoder boundary.SseParser.maxDataBytesrenamed tomaxDataCodeUnits— the field measured UTF-16 code units, not bytes.JsonDecoder.requireEitherFieldnow distinguishes key-present-but-null from key-absent.copyWithsentinel sweep completed: all nullable payload fields on all event types now support explicit-null clearing.fromJsonfactories accept both camelCase (TypeScript server) and snake_case (Python server) field keys.Version: bumps Dart SDK from
0.1.xto0.2.0.Test plan
test/events/event_test.darttest/events/event_type_test.darttest/integration/fixtures_integration_test.dartandtest/integration/event_decoding_integration_test.darttest/fixtures/events.jsontest/sse/sse_parser_test.dartdart analyzeclean (no analyzer errors)dart testpasses🤖 Generated with Claude Code