ag_ui 0.3.0
ag_ui: ^0.3.0 copied to clipboard
Dart SDK for AG-UI protocol - standardizing agent-user interactions through event-based communication
Changelog #
All notable changes to the AG-UI Dart SDK will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.3.0 #
Breaking Changes (review-fix pass) #
StateDeltaEvent.deltaandActivityDeltaEvent.patchare nowList<Map<String, dynamic>>instead ofList<dynamic>. RFC 6902 JSON Patch operations are always objects. UsingrequireListField<Map<String, dynamic>>surfaces non-object elements asAGUIValidationErrorat the decoder boundary with afield: 'delta[$i]'/field: 'patch[$i]'index, rather than leaking a downstreamTypeErrorat the firstop['op']access. Direct consumers ofevent.delta[i]who are already casting to Map are unaffected; consumers storing the list asList<dynamic>will need a type annotation update. Migration: changeList<dynamic>type annotations onevent.delta/event.patchtoList<Map<String, dynamic>>. Code that already accessesop['op']/op['path']without an explicit cast is already correct.SseParser.maxDataBytesrenamed tomaxDataCodeUnits. The field already measured UTF-16 code units, not bytes — the rename corrects the misleading name.SseParser(maxDataBytes: ...)call sites must be updated toSseParser(maxDataCodeUnits: ...).
Fixed (review-fix pass) #
ActivityMessage.fromJsonnow silently stripsencryptedValue/encrypted_valueinstead of throwingAGUIValidationError.ActivityMessageis not aBaseMessageextension in the canonical protocol, so the field does not apply. Dart was the only SDK that tore down the stream on encountering the field; TS strips silently (zod default) and Python preserves it. The change restores forward compatibility when a proxy emits the field.ReasoningEncryptedValueEvent.fromJsonno longer stores the cipher payload inBaseEvent.rawEvent. PreviouslyrawEvent: _readRawEvent(json)stored the full wire JSON (includingencryptedValue) in the inheritedrawEventfield, undoing the cipher-data scrubbing in every error path.rawEventis now alwaysnullfor this event type; proxies that need the raw wire form should retain it before callingfromJson.RunStartedEvent.fromJsonno longer attaches the offending payload toAGUIValidationError.jsonon rethrow. The full outer payload (and the innerRunAgentInput, which can carryencryptedValueviainput.messages[*]) are both omitted, so cipher data cannot leak through validation errors — matching the existing scrub inMessagesSnapshotEvent.fromJson.MessagesSnapshotEvent.fromJsonrethrow now dropsjson:entirely. Forwardinge.jsonpreviously exposed the inner Message map on the outer error; for Tool/Reasoning subtypes that map can carryencryptedValue. Dropsjson:to matchAssistantMessage.fromJson's tool-call IIFE, which already uses the cautious default.JsonDecoder.requireEitherFieldnow distinguishes "key present but null" from "key absent". Previously both cases produced the same "Missing required field 'X' (or 'Y')" message, misleading consumers into thinking the snake_case alias might work when the camelCase key was explicitly null. Now: key-present-but-null produces "Required field 'X' is present but null"; both-absent still produces the dual-key error.copyWithsentinel sweep completed.ThinkingStartEvent.title,ToolCallResultEvent.role,StateSnapshotEvent.snapshot, andRunErrorEvent.codenow use thekUnsetSentinelpattern so callers can clear these nullable fields viacopyWith(field: null). The "Known parity gaps" list is now empty for payload fields.EventEncoder.acceptsProtobufandEventDecoder.decodeBinarynow carry explicit dartdoc warnings that protobuf is not yet implemented end-to-end. A client negotiatingapplication/vnd.ag-ui.event+protowould receive a misleading "Invalid UTF-8 data" error; the docs now direct consumers to use SSE transport until protobuf support lands.groupRelatedEventsdartdoc now documents theReasoningStart/ReasoningEndasymmetry. Phase-level reasoning events are emitted as standalone singletons; only message-levelREASONING_MESSAGE_*events are grouped. Consumers that need to associate phase-level markers with message groups must track phase boundaries in their own state.processChunkresetserrorRoutedInChunkafter the for-loop. The flag was previously only set inside the loop; future throw sites after the loop body could have silently swallowed unrelated errors.SseParsererror message corrected. The OOM-guard error now says "code-unit limit" (not "byte limit") to match what the cap actually measures.SseParser._processFieldnow useswrite('\\n')instead ofwriteln()for the inter-data:separator.writeln()is equivalent on all Dart platforms but the explicit form removes any ambiguity about whether a platform line terminator is emitted.EventType.fromStringdartdoc strengthened with an explicit contract note: callers must not change the throw type fromArgumentError, becauseBaseEvent.fromJsonuses a narrowon ArgumentErrorcatch to distinguish unknown event types from factory bugs.
Fixed (review pass — protocol parity) #
encryptedValueis now plumbed through every BaseMessage subtype (DeveloperMessage,SystemMessage,AssistantMessage,UserMessage) on the baseMessageclass. Mirrors canonical TSBaseMessageSchema.encryptedValue: z.string().optional()and PythonBaseMessage.encrypted_value: Optional[str]. Previously the field was only present onToolMessageandReasoningMessage, so a Dart proxy decoding aMESSAGES_SNAPSHOTwhose assistant or user message carriedencryptedValuefrom a TS or Python server silently dropped the value at decode and could not re-emit it on the next hop. Decode accepts bothencryptedValue(TS-canonical) andencrypted_value(Python-canonical);toJsonemits camelCase; each subtype'scopyWithaccepts an explicit-null clear via the sentinel pattern. TheToolMessageandReasoningMessagefield declarations were removed in favor of inheriting from the base — the wire shape is unchanged.raw_event(snake_case) is now preserved on every event factory. All ~30BaseEventsubclasses now readrawEventvia a centralized_readRawEventhelper that usescontainsKeyprecedence: the camelCase key wins when present (even when explicitlynull), and the snake_case key is consulted only when camelCase is absent. Previously every factory readjson['rawEvent']directly, silently dropping Python-styleraw_eventpayloads.toJsoncontinues to emit camelCase only.REASONING_ENCRYPTED_VALUEno longer rejects emptyentityId/encryptedValuestrings. Canonical TS usesz.string()and Python usesstr— neither imposes a minimum length. The Dart-only empty-string rejection (in bothReasoningEncryptedValueEvent.fromJsonandEventDecoder.validate) was over-strict and would reject payloads that the canonical SDKs accept. The strict subtype discriminator stays — unknown subtypes still throw.SseParser._processFieldnow matches the WHATWG SSE spec for empty leadingdata:lines and repeatedevent:lines. Thedata:case used_dataBuffer.isNotEmptyas a "have we written data yet?" heuristic, which collapseddata:\ndata: x\n\nto"x"instead of the spec-correct"\nx". Now uses the_hasDataFieldflag (mirroring theinDataBlockpattern inEventStreamAdapter.appendDataLine). Theevent:case appended on everyevent:line; per spec it must REPLACE.EventStreamAdapter.fromRawSseStreamnow propagates downstream cancellation, pause, and resume to the upstream raw SSE subscription. Previously the upstreamrawStream.listen(...)subscription was fire-and-forget — a consumer that cancelled the adapted stream early left the upstream draining indefinitely (a real resource leak on long-lived agent streams).SseParser.parseBytesnow flushes any final unterminated event on stream close. Routed throughparseLinesso the final_dispatchEvent()flush inparseLinesfires for byte-stream sources too. A byte source that ended without a trailing blank line previously lost its last buffered message.copyWithsentinel sweep.RawEvent.source,RunAgentInput.state,RunAgentInput.forwardedProps, andRun.resultpreviously used the standard?? this.fieldpattern, so a caller could not clear them viacopyWith(field: null). They now use the existing sentinel pattern. The "Known parity gaps" list below has been updated.JsonDecoder.optionalEitherFieldnow resolves on KEY presence, not value-non-null. A payload carrying bothcamelKey: nullandsnake_key: <value>previously fell through to the snake_case value; the documented contract onrequireEitherFieldis that camelCase wins when its key is present (even when explicitlynull). The implementation now matches the dartdoc. The inlineforwardedPropsdecode incontext.dartwas migrated to the samecontainsKeyrule for consistency.ToolMessage.fromJsonandToolResult.fromJsonnow userequireEitherFieldinstead of the olderoptionalEitherField + manual null-check + custom throwpattern, matching the migration already done forRunAgentInput.fromJsonandRun.fromJson.Validators.validateMessageContentis nowString-only. The pre-0.2.0 permissiveMap/Listbranches were dead code (no caller in the SDK passed those types) and disagreed with canonicalBaseMessage.content: Optional[str]. MultimodalUserMessage.contentremains a tracked parity gap.Validators.validateUrlnow rejects URLs containing C0 control characters or DEL (\x00–\x1f,\x7f).Uri.parseis permissive with embedded\n/\r/\t, which can flow into HTTP request lines as a header-injection vector.JsonDecoder.requireFieldandoptionalFieldtransform-failure paths now preservecause: ewhen wrapping an inner exception inAGUIValidationError. The structured cause was previously flattened into the message via'$e'interpolation only.
Documented #
AGUIValidationError.jsondartdoc now carries 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. Prefer.fieldand.valueon log lines shipped to external sinks.EventDecoder.validatedartdoc now documents the dual-source error class asymmetry:validate()raisesclient/errors.dart'sValidationError;fromJson-side eager rejections raisetypes/base.dart'sAGUIValidationError. Both surface uniformly asDecodingErrorthrough the publicdecode/decodeJsonboundary; both extendAGUIError.BaseEvent.rawEventdartdoc now notes the round-trip emission consequence — anything assigned to this field WILL be re-emitted on the nextencode. SetrawEvent: nullon the in-flight event if a proxy doesn't want the upstream payload echoed downstream.- README adds a "Proxy notes: wire-spelling normalization" paragraph
documenting that the SDK accepts both camelCase and snake_case on
fromJsonbut always emits camelCase ontoJson. The Error Handling section is refreshed to use the current error-hierarchy class names (TransportError,DecodingError,ValidationError,CancellationError, all underAGUIError). AgUiClient.runAgentdartdocThrows:list refreshed to match the current error hierarchy.EventStreamAdapter.groupRelatedEventsdartdoc now carries an explicit unbounded-state warning — open groups (where*Startwas received but*Endhas not arrived) are held in memory until the matching end event or stream completion. Same caveat applies toaccumulateTextMessages.
Fixed (review pass — behavior) #
Tool.copyWith(parameters: null)now correctly clearsparameters. The previousparameters ?? this.parameterspattern silently kept the existing value whennullwas passed; the field now uses the_unsetToolsentinel pattern, consistent withToolCall.encryptedValueand every other nullable field in the SDK. This gap was omitted from the 0.2.0 "Known parity gaps" list — it has been corrected here.EventStreamAdapter.fromRawSseStreamnow subscribes to the upstream lazily (insidecontroller.onListen) rather than eagerly at call time. A caller that obtained the returned stream but never subscribed would previously leak the upstream SSE connection until the server closed it. The cancellation, pause, and resume propagation added in the prior review pass is preserved; subscription lifecycle callbacks now use null-safe?.calls since the subscription is no longerlate final.Message.fromJsonnow preserves the wire JSON payload inAGUIValidationErrorwhenMessageRole.fromStringfails. Previously the error was thrown withoutjson:set, making it impossible to identify which message in aMESSAGES_SNAPSHOThad the unrecognized role. The re-thrown error carries the originatingjsonmap so the decoder pipeline can surface it as aDecodingErrorwith full context.
Changed #
TimeoutErrorrenamed toAGUITimeoutErrorto avoid shadowing the built-indart:async.TimeoutError(raised byFuture.timeout(...)/Stream.timeout(...)). The bare name is preserved as a deprecated typedef alias for backward compat and will be removed in 1.0.0. Internal call sites inAgUiClientthrow the new name directly. The README "Errors" recipe and "Migrating from 0.1.0" section call out the rename so consumers using bothpackage:ag_ui/ag_ui.dartanddart:asynccan avoid the symbol collision.- Empty
deltais now accepted onTEXT_MESSAGE_CONTENT,TOOL_CALL_ARGS, andREASONING_MESSAGE_CONTENT, and emptycontentis accepted onTOOL_CALL_RESULT, to match the canonical TS/Python schemas (z.string()/strwith nomin(1)constraint). Previously the Dart SDK rejected empty values at both thefromJsonfactory and theEventDecoder.validatepipeline; a Python or TS server that legitimately emitted a deliberate empty chunk (e.g. a noop content refresh) would fail decode in Dart but pass in the canonical SDKs. Empty cipher payloads onREASONING_ENCRYPTED_VALUE(entityId,encryptedValue) continue to be rejected — the "no graceful default for cipher payloads" contract stays.
Fixed #
ToolCallnow carries the optionalencryptedValuefield for parity with canonical TS (ToolCallSchema.encryptedValue: z.string().optional()) and Python (ToolCall.encrypted_value: Optional[str]). Previously a message arriving withtoolCalls: [{..., encryptedValue: "..."}]silently dropped the value at decode and could not re-emit it on a proxy hop. Decode accepts bothencryptedValueandencrypted_value;toJsonemits the camelCase key when present;copyWithuses the sentinel pattern so callers can explicitly clear it viacopyWith(encryptedValue: null).RunAgentInputnow carries the optionalparentRunIdfield for parity with canonical TS (RunAgentInputSchema.parentRunId: z.string().optional()) and Python (RunAgentInput.parent_run_id). Previously aRUN_STARTEDpayload withinput.parentRunId: '...'decoded with the field silently dropped, even thoughRunStartedEvent.parentRunIditself was preserved. Decode accepts bothparentRunIdandparent_run_id;toJsonemits camelCase when present;copyWithuses the sentinel pattern.EventStreamAdapter.fromRawSseStreamnow handles CRLF (\r\n) line terminators, not just LF. Previously a CRLF-emitting SSE server produced"\r"lines that never matched the empty-line event-boundary signal, so events buffered until stream close. The line splitter now strips a trailing\rafter splitting on\n. The same fix is applied toEventDecoder.decodeSSE, which now usesLineSplitter(handling\n,\r, and\r\nper the WHATWG SSE spec).JsonDecoder.optionalListFieldandrequireListFieldnow eagerly type-check elements (raisingAGUIValidationError(field: '$field[$i]')on the first wrong-typed element) instead of returning a lazycast<T>()view that surfaced as a rawTypeErrorat access time and was flattened tofield: 'json'by the decoder catch-all.AssistantMessage.fromJsonnow usesJsonDecoder.optionalEitherFieldon thetoolCalls/tool_callskey itself, instead of a??chain on the post-.map(...).toList()value. The previous chain only fired on null, so an emptytoolCalls: []short-circuited the snake_case fallback even whentool_calls: [...]was populated.AssistantMessage.toJsonnow emitstoolCallswhenever the in-memory field is non-null (including empty lists), so the round-tripfromJson(m.toJson()) == mis symmetric.- Decoder pipeline now rethrows
EncoderError/DecodeError/EncodeErrorunchanged instead of re-wrapping them as a generic "Failed to decode event" via the catch-all. EventEncoder.encodeSSEno longer strips fields whose value isnull. The blanketjson.removeWhere((k, v) => v == null)was silently dropping fields that intentionally serialize asnull(ActivitySnapshotEvent.content,RawEvent.event,CustomEvent.value,StateSnapshotEvent.snapshot), breaking the encode→decode round-trip because the matching factories require the key to be present and reject it withAGUIValidationError. EachtoJson()already usesif (field != null) 'field': fieldfor fields that opt in to omission, so the strip pass was redundant in addition to harmful. Pinned by a new round-trip test infixtures_integration_test.dart.EventStreamAdapter.fromRawSseStreamnow handles WHATWG-spec lone-\rline terminators in addition to\nand\r\n. The previous chunk scanner only split on\n, so a producer using bare\r(rare in practice but spec-valid) buffered indefinitely. The new multi-terminator scanner defers a trailing\rat chunk boundaries to disambiguate from a chunk-spanning\r\nand consumes it on stream close. Steady-state emission for CRLF-encoded streams is unchanged.EventStreamAdapter.fromSseStreamandfromRawSseStreamnow preserve anyAGUIErrorsubtype (AgUiError,AGUIValidationError,EncoderError) raised by the decoder instead of re-wrapping the encoder-family errors as a genericDecodingError. Mirrors the unified-error-surface contract thatEventDecoder.decode/decodeJsonalready honor.TestHelpers.findToolCalls(test-only helper) now uses the typedAssistantMessage.toolCallsaccessor. Previously it round-tripped throughtoJsonand read the snake_case keytool_calls, butAssistantMessage.toJsonemits camelCasetoolCalls— the helper silently always returned an empty list. Currently unreferenced by the test suite, so this is a latent-bug fix.
Added #
JsonDecoder.optionalEitherListField<T>helper combining the dual-key resolution rule fromoptionalEitherFieldwith the index-aware element-type validation fromrequireListField/optionalListField.AssistantMessage.fromJsonnow uses it so a malformed nestedtoolCalls[i]raisesAGUIValidationError(field: 'toolCalls[$i]')instead of leaking a rawTypeErrorfrom the per-element cast.
Changed #
MessagesubclasscopyWithmethods (DeveloperMessage,SystemMessage,UserMessage,AssistantMessage,ToolMessage,ReasoningMessage) now use the_unsetMessagesentinel pattern for nullable fields, matching the event-class discipline. Callers can explicitly clear a nullable field viacopyWith(field: null)— previously?? this.fieldcould not distinguish "argument omitted" from "argument explicitly null".JsonDecoder.optionalIntField(new helper) acceptsintornumand coerces via.toInt(). Every event factory now readstimestampvia this helper, so a TS server emitting a fractional number (e.g.Date.now() / 1000) no longer fails decode withAGUIValidationError(field: 'timestamp').- Error-hierarchy unification:
AgUiErrornow extendsAGUIError, andAGUIValidationErrornow extendsAGUIErrorinstead of bareimplements Exception. Callers canon AGUIError catch (e)to cover the entire SDK error surface (including direct-factory validation, encoder-side failures, runtime/transport, and decoder errors).on AgUiErrorstill scopes to runtime/transport/decoding as before. Added an "Errors" section to the README documenting the recommended catch recipe. AGUIValidationErrorgained an optionalcauseparameter so thetransform-rethrow path inJsonDecodercan preserve structured error info instead of flattening to'Failed to transform field: $e'.SseParserdocumented its per-connection state semantics (sticky_lastEventId); a newreset()method clears all parser state for callers that explicitly want to reuse an instance across independent streams.Validators.maxTimeoutexposed asstatic const Durationso callers can introspect the limit (10 minutes). The cap value is unchanged; raising it is deferred to a future release.RunAgentInput.fromJsonandRun.fromJsonmigrated toJsonDecoder.requireEitherFieldfor consistency with every other factory in the SDK. Behavior preserved; the "Missing required field 'X' (or 'Y')" wording shifts slightly to match the helper's standard error message.- Long
@Deprecatedmessages on theTHINKING_*enum values and event classes hoisted into top-levelconststrings (event_type.dart,events.dart). Surfaces the planned-removal version in one place per context and reduces drift risk if it ever changes. No behavior change.
Documentation #
UserMessagedocumented as a known parity gap with the canonical multimodal schema (TSUnion[string, InputContent[]], PythonUnion[str, List[InputContent]]); the Dart SDK currently only supports the string variant.Message.iddocumented as nullable-by-type but required-by-convention (every concrete subtype constructor declares itrequired); a future major version may tighten the type to non-nullable for parity with canonicalBaseMessageSchema.id: z.string().EventDecoder.validate'sThinking*deprecated cases gained comments explaining why they don't validatemessageId(the deprecated wire shape has no such field; the migration targetREASONING_*does).EventDecoder.validate'sActivityDeltaEventcase gained a comment noting that an emptypatchis intentional per the canonical TS/Python schemas (z.array(...).min(0)/ list with no length floor).BaseEvent.rawEventfield gained a dartdoc note clarifying that the field is unvalidated (typeddynamicbecause the protocol does not constrain the shape).ToolCallResultEvent.role,StateSnapshotEvent.snapshot, andRunErrorEvent.codefield declarations gained a dartdoc note thatcopyWith(field: null)does NOT clear the field (these three are the remaining cases listed in "Known parity gaps"). Construct a new instance directly to drop.MessageRole.activityandMessageRole.reasoningenum values gained wire-spelling-pinning dartdoc, mirroring theReasoningEncryptedValueSubtype.toolCallstyle.EventDecoder.validate'sThinkingTextMessageContentEventcase gained a clarified rationale comment: the deprecated path keeps the pre-0.2.0 stricter "non-emptydelta" contract intentionally — sibling content events (TextMessageContentEvent,ToolCallArgsEvent,ToolCallResultEvent,ReasoningMessageContentEvent) were RELAXED to accept empty strings in 0.2.0 for canonical TS/Python parity, but loosening a deprecated contract retroactively serves no one.ReasoningEncryptedValueEvent.fromJsonempty-string rejection comment updated to reflect the post-0.2.0 sibling state — it is intentionally stricter than the relaxed sibling content events because cipher payloads have no defensible "empty" semantic.BaseEvent.fromJsonandMessage.fromJsonswitches gained an explicit trailing comment stating the analyzer-enforced exhaustiveness so future contributors don't add adefaultclause "to be safe."EventStreamAdapteradopted an internal_appendDataLine/flushDataBlockdecomposition to share the per-line andonDoneflush paths infromRawSseStream. No behavior change.- README "Migrating from 0.1.0"
TimeoutError→AGUITimeoutErrorsection gained a paragraph clarifying the symmetric case: consumers who previously meantdart:async.TimeoutErrorand were accidentally catching SDK instances will see different runtime behavior after they fix the import.
Known parity gaps #
requireNonEmptyonmessageId,threadId, andrunIdfields is stricter than the canonicalz.string()/strschemas (which allow empty strings).EventDecoder.validate()rejects empty ID strings; a TS or Python server that legitimately emits an emptymessageIdwould fail decode in Dart. The strict behavior is intentional (empty IDs have no valid semantic in the current protocol) and is tracked for review at 1.0.0 alignment.BaseEvent.toJsonalways emitsrawEventin camelCase even when the original wire payload usedraw_event(snake_case). Proxies that must forward the exact wire spelling should read the value before callingfromJsonand re-attach it to the outbound payload manually, or preserve the original byte stream instead of round-tripping through the Dart event model.ActivityDeltaEvent.patchdecodes asList<Map<String, dynamic>>and rejects non-object patch elements at the decoder boundary. Canonical TS (z.array(z.any())) and Python (List[Any]) accept any element type and defer validation to downstream RFC-6902 consumers. Producers emitting non-object patch elements (legal per canonical schemas, illegal per RFC 6902) will be rejected by the Dart decoder.AssistantMessage.toJsonemitstoolCalls: []when the in-memory list is non-null but empty. The canonical TS/Python SDKs omit the key when the list is empty. This ensures round-trip symmetry (fromJson(m.toJson()) == m) but diverges from canonical wire output for messages whosetoolCallsfield was decoded from an absent key. Consumers producing wire output for external TS/Python clients should treat an empty list and an absent key as equivalent.
Breaking Changes (activity/reasoning events — 2026-04-30) #
ToolCallResultEvent.roleis now typedToolCallResultRole?instead ofString?. Callers constructing the event directly must use the enum (e.g.ToolCallResultRole.tool) instead of a raw string. Wire decoding is unaffected: an unknown role string on the wire is absorbed viaToolCallResultRole.fromStringand falls back toToolCallResultRole.tool(forward-compatible with future canonical roles). The newroleenum exists for parity with the PythonLiteral["tool"]/ TypeScriptz.literal("tool")canonical role surface.
Added #
- Activity events for event-type parity with the Python and TypeScript SDKs
(#1018):
ActivitySnapshotEvent(ACTIVITY_SNAPSHOT)ActivityDeltaEvent(ACTIVITY_DELTA)
- Reasoning events for event-type parity:
ReasoningStartEvent(REASONING_START)ReasoningMessageStartEvent(REASONING_MESSAGE_START)ReasoningMessageContentEvent(REASONING_MESSAGE_CONTENT)ReasoningMessageEndEvent(REASONING_MESSAGE_END)ReasoningMessageChunkEvent(REASONING_MESSAGE_CHUNK)ReasoningEndEvent(REASONING_END)ReasoningEncryptedValueEvent(REASONING_ENCRYPTED_VALUE)
- Supporting enums:
ReasoningMessageRole,ReasoningEncryptedValueSubtype. ActivityMessageandReasoningMessageMessagesubtypes (withMessageRole.activity/MessageRole.reasoning) soMESSAGES_SNAPSHOTpayloads carrying those roles decode in Dart with the same schema as the canonical TypeScript and Python SDKs. TheactivityType/activity_typeandencryptedValue/encrypted_valuekeys both decode for camelCase/snake_case parity with the wider protocol.- Field-level parity for canonical events that previously dropped wire data
on decode:
TextMessageStartEvent.name,TextMessageChunkEvent.name,RunStartedEvent.parentRunId, andRunStartedEvent.inputare now decoded and re-emitted bytoJsonso a Dart proxy preserves upstream metadata. - All event
fromJsonfactories now accept both camelCase (TypeScript server) and snake_case (Python server) field keys, including the pre-existingTextMessage*andToolCall*events that were previously camelCase-only. - Decoder-boundary non-empty validation extended to
ToolCallArgsEvent,ToolCallEndEvent,ToolCallResultEvent,RunFinishedEvent,StepStartedEvent,StepFinishedEvent,StateSnapshotEvent,RawEvent, andCustomEventso wire payloads with empty required identifiers or missing required content fail atEventDecoder.decodeJsoninstead of reaching consumer code as a null/empty value.
Changed #
REASONING_MESSAGE_START.roleis now required during decoding to match the canonical TypeScript and Python schemas. A payload missingrolenow raisesAGUIValidationError(wrapped asDecodingErrorthroughEventDecoder); an unknown role string still falls back toReasoningMessageRole.reasoningfor forward-compatibility.TextMessageRole.fromStringnow throwsArgumentErroron unknown values, mirroringReasoningMessageRole.fromString. Wire decoding is unaffected:TextMessageStartEvent.fromJsonandTextMessageChunkEvent.fromJsonabsorb the throw and fall back toTextMessageRole.assistantfor forward compatibility — only direct callers ofTextMessageRole.fromStringsee the new visible failure mode.ReasoningEncryptedValueEvent.fromJsonnow wraps an unknownsubtypeasAGUIValidationError(matching the class-level dartdoc contract), instead of leaking the rawArgumentErrorfromReasoningEncryptedValueSubtype.fromString. TheEventDecoderpipeline still surfaces it asDecodingError.ActivitySnapshotEvent.copyWith(content),RawEvent.copyWith(event),CustomEvent.copyWith(value), andRunFinishedEvent.copyWith(result) now use an internal sentinel parameter so callers can intentionally clear the field tonull(matching each factory contract that already accepted explicit-null payloads). OthercopyWithmethods retain the standard?? this.fieldpattern (see Known parity gaps).EventDecoder.decodeJsonnow wrapsAGUIValidationError(thrown byfromJsonfactories) explicitly so the resultingDecodingErrorpreserves the original failing field —role,messageId,subtype, etc. — instead of flattening tofield: 'json'. Pre-fix, the wrapper relied on theAgUiError-based catch path, whichAGUIValidationError(which onlyimplements Exception) bypassed.EventDecoder.validatenow rejects an emptymessageIdonTextMessageEndEvent, restoring symmetry withTextMessageStartEventandTextMessageContentEvent(and the new reasoning-end events).
Deprecated #
EventType.thinkingContentandThinkingContentEvent— not part of the canonical AG-UI protocol. UseEventType.reasoningMessageContent/ReasoningMessageContentEventinstead. Decoding remains supported for backward compatibility; scheduled for removal in 1.0.0.EventType.thinkingTextMessageStart/EventType.thinkingTextMessageContent/EventType.thinkingTextMessageEnd(and their event classes:ThinkingTextMessageStartEvent,ThinkingTextMessageContentEvent,ThinkingTextMessageEndEvent). Mirrors the canonical TypeScript SDK's deprecation ofTHINKING_TEXT_MESSAGE_*in favor ofREASONING_*. UseReasoningMessageStartEvent/ReasoningMessageContentEvent/ReasoningMessageEndEventinstead. Decoding remains supported for backward compatibility; scheduled for removal in 1.0.0.
Known parity gaps (follow-up) #
copyWithsentinel sweep is now complete for all nullable payload fields. The sentinel pattern (kUnsetSentinel/identicalcheck) is in place forActivitySnapshotEvent.content,RawEvent.event,RawEvent.source,CustomEvent.value,RunFinishedEvent.result, the optional fields ofTextMessageStartEvent/TextMessageChunkEvent,ToolCallStartEvent.parentMessageId, the optional fields ofToolCallChunkEventandReasoningMessageChunkEvent,RunStartedEvent.parentRunId/RunStartedEvent.input,RunAgentInput.parentRunId/RunAgentInput.state/RunAgentInput.forwardedProps,Run.result, the message-class nullables (name,content,toolCalls,error,encryptedValue),ThinkingStartEvent.title,ToolCallResultEvent.role,StateSnapshotEvent.snapshot, andRunErrorEvent.code.RunFinishedEvent.resultis dropped fromtoJson()when null: an inbound explicit-null'result': nulldoes not survive a Dart→Dart re-serialization round-trip. This matches the canonical TS/Python schemas (z.any().optional()/Optional[Any] = None), so cross-SDK forwarding is unaffected. Consumers relying on byte-for-byte round-trip fidelity should readrawEventinstead of re-serializing.
0.2.0 - 2026-05-28 #
Added #
- Multimodal
UserMessagecontent.UserMessage.contentnow accepts either a plain string or an ordered list of typed parts, matching the canonical protocol (string | InputContent[]). - New content types:
UserMessageContent(TextContent,MultimodalContent),InputContent(TextInputContent,ImageInputContent,AudioInputContent,VideoInputContent,DocumentInputContent, legacyBinaryInputContent), andInputContentSource(DataSource,UrlSource). UserMessage.multimodal(...)andUserMessage.fromContent(...)constructors.Validators.validateUserMessageContent(...).
Changed #
- Breaking:
UserMessage.contentgetter is nowString?(was non-nullString) and returnsnullfor multimodal messages. ReadUserMessage.messageContentfor the typed union. - Breaking:
UserMessage.copyWithnow takesmessageContentinstead ofcontent. - Breaking: the default
UserMessage({required id, required content})constructor is no longerconst(it wraps the string intoTextContentat runtime). UseUserMessage.fromContent(id:, messageContent: const TextContent(...))for a compile-time constant.
0.1.0 - 2025-01-21 #
Added #
- Initial release of the AG-UI Dart SDK
- Core protocol implementation with full event type support
- HTTP client with Server-Sent Events (SSE) streaming
- Strongly-typed models for all AG-UI protocol entities
- Support for tool interactions and generative UI
- State management with snapshots and JSON Patch deltas (RFC 6902)
- Message history tracking across multiple runs
- Comprehensive error handling with typed exceptions
- Cancel token support for aborting long-running operations
- Environment variable configuration support
- Example CLI application demonstrating key features
- Integration tests validating protocol compliance
Features #
AgUiClient- Main client for AG-UI server interactionsSimpleRunAgentInput- Simplified input structure for common use cases- Event streaming with backpressure handling
- Tool call processing and result handling
- State synchronization across agent runs
- Message accumulation and conversation context
Known Limitations #
- WebSocket transport not yet implemented
- Binary protocol encoding/decoding not yet supported
- Advanced retry strategies planned for future release
- Event caching and offline support planned for future release