Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
031c055
feat(dart-sdk): add activity + reasoning events for protocol parity (…
mattsp1290 Apr 27, 2026
323404b
chore(dart-sdk): finish #1018 wiring — version, helpers, validate, da…
mattsp1290 Apr 30, 2026
5b37bde
chore(dart-sdk): #1018 follow-up — event-class polish, tests, README/…
mattsp1290 May 1, 2026
60b941e
chore(dart-sdk): #1018 review fixes — copyWith parity, enum alignment…
mattsp1290 May 3, 2026
9495d89
chore(dart-sdk): #1018 review-fix pass — sibling symmetry + parity tests
mattsp1290 May 3, 2026
06ae282
chore(dart-sdk): #1018 review-fix pass — MESSAGES_SNAPSHOT parity + r…
mattsp1290 May 3, 2026
43bd02a
chore(dart-sdk): #1018 review-fix pass — ToolMessage encryptedValue +…
mattsp1290 May 3, 2026
334f302
chore(dart-sdk): #1018 review-fix pass — CRLF SSE parser + error-root…
mattsp1290 May 3, 2026
e2c4e3f
chore(dart-sdk): #1018 review-fix pass — encoder round-trip + lone-CR…
mattsp1290 May 3, 2026
bd73bf1
chore(dart-sdk): #1018 review-fix pass — orphaned alignment (TimeoutE…
mattsp1290 May 3, 2026
ebd97ff
chore(dart-sdk): #1018 review-fix pass — BaseMessage encryptedValue +…
mattsp1290 May 4, 2026
3ecccf6
chore(dart-sdk): #1018 review-fix pass — Tool sentinel + lazy SSE sub…
mattsp1290 May 4, 2026
4991a87
chore(dart-sdk): #1018 fix-review pass — dual-reviewer findings
mattsp1290 May 4, 2026
5a1d1d9
chore(dart-sdk): #1018 review-fix pass — 13 important items from dual…
mattsp1290 May 4, 2026
0a71ec1
chore(dart-sdk): #1018 review-fix pass — 11 important items from dual…
mattsp1290 May 4, 2026
d09620f
chore(dart-sdk): #1018 review-fix pass — 19 important items from dual…
mattsp1290 May 5, 2026
f43e5b1
chore(dart-sdk): #1018 review-fix pass — 9 items from dual-reviewer pass
mattsp1290 May 5, 2026
14f4677
chore(dart-sdk): #1018 fix-review pass — 9 important items from dual-…
mattsp1290 May 5, 2026
69ab8da
chore(dart-sdk): #1018 fix-review pass — 15 important items from dual…
mattsp1290 May 5, 2026
447cddb
chore(dart-sdk): #1018 review-fix pass — 14 important items from dual…
mattsp1290 May 6, 2026
0cf75d5
chore(dart-sdk): #1018 review-fix pass — 15 items from dual-reviewer …
mattsp1290 May 6, 2026
bdfdd46
chore(dart-sdk): #1018 review-fix pass — 9 important items from dual-…
mattsp1290 May 7, 2026
298b753
chore(dart-sdk): #1018 review-fix pass — 8 important items from dual-…
mattsp1290 May 7, 2026
aa05af2
chore(dart-sdk): #1018 review-fix pass — 9 items from dual-reviewer pass
mattsp1290 May 7, 2026
b41a611
chore(dart-sdk): #1018 review-fix pass — 10 important items from dual…
mattsp1290 May 7, 2026
5993e17
chore(dart-sdk): #1018 review-fix pass — 15 important items from dual…
mattsp1290 May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
532 changes: 531 additions & 1 deletion sdks/community/dart/CHANGELOG.md

Large diffs are not rendered by default.

221 changes: 210 additions & 11 deletions sdks/community/dart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ Or add to your `pubspec.yaml`:

```yaml
dependencies:
ag_ui: ^0.1.0
ag_ui: ^0.2.0
```

## Features

- 🎯 **Dart-native** – Idiomatic Dart APIs with full type safety and null safety
- 🔗 **HTTP connectivity** – `AgUiClient` for direct server connections with SSE streaming
- 📡 **Event streaming** – 16 core event types for real-time agent communication
- 📡 **Event streaming** – Event-type parity with the canonical Python and TypeScript SDKs (text messages, tool calls, state, activity, reasoning, lifecycle, and more) for real-time agent communication.
- 🔄 **State management** – Automatic message/state tracking with JSON Patch support
- 🛠️ **Tool interactions** – Full support for tool calls and generative UI
- ⚡ **High performance** – Efficient event decoding with backpressure handling
Expand Down Expand Up @@ -52,7 +52,7 @@ final input = SimpleRunAgentInput(
// Stream response events
await for (final event in client.runAgent('agentic_chat', input)) {
if (event is TextMessageContentEvent) {
print('Assistant: ${event.text}');
print('Assistant: ${event.delta}');
}
}
```
Expand Down Expand Up @@ -99,9 +99,9 @@ final input = SimpleRunAgentInput(
);

await for (final event in client.runAgent('agentic_chat', input)) {
switch (event.type) {
switch (event.eventType) {
case EventType.textMessageContent:
final text = (event as TextMessageContentEvent).text;
final text = (event as TextMessageContentEvent).delta;
print(text); // Stream tokens
break;
case EventType.runFinished:
Expand All @@ -111,6 +111,40 @@ await for (final event in client.runAgent('agentic_chat', input)) {
}
```

### Activity & Reasoning Events

```dart
import 'dart:io'; // for `stderr` in the example below

await for (final event in client.runAgent('agentic_chat', input)) {
if (event is ActivitySnapshotEvent) {
// `content` is `Object?` — the Python reference server may emit a
// primitive or `null`. Guard before treating it as a structured record.
final content = event.content;
if (content is Map<String, dynamic>) {
// `event.replace == true` → discard prior content for this messageId.
// `event.replace == false` → merge/extend on top of existing content.
print(
'Activity (${event.activityType}, replace=${event.replace}): $content',
);
} else {
// Wire-protocol surprise: log and skip rather than crash.
stderr.writeln(
'ActivitySnapshotEvent.content is ${content.runtimeType}, '
'expected Map<String, dynamic>',
);
}
} else if (event is ActivityDeltaEvent) {
print('Activity patch (${event.activityType}): ${event.patch}');
} else if (event is ReasoningMessageContentEvent) {
print('Reasoning: ${event.delta}');
} else if (event is ReasoningEncryptedValueEvent) {
// Opaque cipher payload — pass through to the next agent rather than
// attempting to decode locally.
}
}
```

### Tool-Based Interactions

```dart
Expand Down Expand Up @@ -152,7 +186,7 @@ Map<String, dynamic> state = {};
List<Message> messages = [];

await for (final event in client.runSharedState(input)) {
switch (event.type) {
switch (event.eventType) {
case EventType.stateSnapshot:
state = (event as StateSnapshotEvent).snapshot;
break;
Expand All @@ -169,6 +203,8 @@ await for (final event in client.runSharedState(input)) {

### Error Handling

The Dart SDK errors form a single hierarchy under [`AGUIError`](https://pub.dev/documentation/ag_ui/latest/ag_ui/AGUIError-class.html). Catch that base if you want one handler for everything; catch the specific subclasses below for targeted recovery. Through [`EventDecoder`](https://pub.dev/documentation/ag_ui/latest/ag_ui/EventDecoder-class.html) the wire-decode side throws [`DecodingError`]; the client-side request/transport layer throws [`TransportError`] and [`ValidationError`]; cancellation surfaces as [`CancellationError`].

```dart
final cancelToken = CancelToken();

Expand All @@ -180,15 +216,45 @@ try {
break;
}
}
} on ConnectionException catch (e) {
} on TransportError catch (e) {
print('Connection error: ${e.message}');
} on DecodingError catch (e) {
print('Decode error: ${e.message}');
} on ValidationError catch (e) {
print('Validation error: ${e.message}');
} on CancelledException {
} on CancellationError {
print('Request cancelled');
} on AGUIError catch (e) {
// Catch-all for any AG-UI-originated error (covers
// AGUIValidationError thrown directly from a `Type.fromJson` call
// when the event isn't routed through the EventDecoder pipeline).
print('AG-UI error: $e');
}
```

> **Cancellation note:** `CancelToken.cancel()` stops event delivery to your stream, but does **not** abort the underlying HTTP socket. The connection releases when the server closes it or the OS idle-timeout fires. If you need true connection abort, provide a custom `IOClient` per request.

### Proxy notes: wire-spelling normalization

The Dart SDK accepts both **camelCase** (TypeScript-canonical, e.g. `threadId`,
`runId`, `parentRunId`, `encryptedValue`, `rawEvent`) and **snake_case**
(Python-canonical, e.g. `thread_id`, `run_id`, `parent_run_id`,
`encrypted_value`, `raw_event`) on every `fromJson` factory, but always
emits **camelCase** on `toJson` — there is no opt-in to snake_case wire
output.

If you use the Dart SDK as a proxy between a snake_case-emitting Python
server and a strictly snake_case-only consumer, you must convert keys
back at the boundary. The TypeScript and Python canonical SDKs both
tolerate the camelCase form on input, so this is rarely an issue in
practice — but a strict snake_case consumer is technically protocol-valid
and will see a normalized payload from a Dart middle-tier.

Within a single `BaseEvent.rawEvent` round-trip the spelling is
preserved by the helper that reads both keys (`rawEvent` /
`raw_event`); the camelCase emit on the Dart side is the only
normalization point.

## Complete Example

```dart
Expand Down Expand Up @@ -222,10 +288,10 @@ void main() async {
stdout.write('Assistant: ');
await for (final event in client.runAgent('agentic_chat', input)) {
if (event is TextMessageContentEvent) {
stdout.write(event.text);
stdout.write(event.delta);
} else if (event is ToolCallStartEvent) {
print('\nCalling tool: ${event.toolName}');
} else if (event.type == EventType.runFinished) {
print('\nCalling tool: ${event.toolCallName}');
} else if (event.eventType == EventType.runFinished) {
print('\nDone!');
break;
}
Expand All @@ -235,6 +301,110 @@ void main() async {
}
```

## Migrating from 0.1.0

0.2.0 introduces one source-breaking change for callers that construct
events directly:

- **`ToolCallResultEvent.role` is now `ToolCallResultRole?` instead of
`String?`.** Update direct constructions:

```dart
// Before (0.1.0)
ToolCallResultEvent(
messageId: '...',
toolCallId: '...',
content: '...',
role: 'tool',
);

// After (0.2.0)
ToolCallResultEvent(
messageId: '...',
toolCallId: '...',
content: '...',
role: ToolCallResultRole.tool,
);
```

Wire decoding is unaffected: an unknown `role` string on the wire is
absorbed via `ToolCallResultRole.fromString` and falls back to
`ToolCallResultRole.tool` for forward compatibility. See
[`CHANGELOG.md`](CHANGELOG.md) "Breaking Changes" for the full
rationale.

- **`TimeoutError` was renamed to `AGUITimeoutError`** to avoid
shadowing `dart:async.TimeoutError` (raised by `Future.timeout(...)` /
`Stream.timeout(...)`). The bare name is preserved as a deprecated
typedef alias and will be removed in 1.0.0:

```dart
// Before (0.1.0)
} on TimeoutError catch (e) { /* ... */ }

// After (0.2.0)
} on AGUITimeoutError catch (e) { /* ... */ }
```

If you import both `package:ag_ui/ag_ui.dart` and `dart:async`, prefer
the new name to avoid a symbol collision and to ensure raw
`dart:async.TimeoutError` instances (very common from any
`.timeout(...)` call) are not silently absorbed by an `on TimeoutError`
arm targeting the SDK type.

Note for the inverse case: if you previously meant
`dart:async.TimeoutError` and were accidentally catching SDK instances
(because `package:ag_ui/ag_ui.dart`'s `TimeoutError` won the unqualified
name resolution), the rename surfaces the prior collision. After you
migrate to `AGUITimeoutError`, the bare `TimeoutError` arm now
unambiguously refers to `dart:async.TimeoutError` — runtime behavior
changes accordingly.

The `THINKING_TEXT_MESSAGE_*` event types are also deprecated in 0.2.0
in favor of the canonical `REASONING_*` events; decoding remains
supported until 1.0.0. See `CHANGELOG.md` "Deprecated" for the migration
mapping.

## Errors

The SDK exposes a small error hierarchy that is intentionally split by origin:

- `AGUIError` — the SDK-wide root. Catching `on AGUIError` covers every
error the SDK can raise: runtime, transport, decoding, AND direct-factory
validation. Use this when you want a single catch-all.
- `AgUiError` — extends `AGUIError`. Covers runtime / transport / decoding:
`TransportError`, `AGUITimeoutError`, `CancellationError`, `DecodingError`,
and the client-side `ValidationError`. Catch this when you want to scope
to "the SDK encountered a runtime problem" but explicitly do NOT want to
catch direct-factory validation errors. (`TimeoutError` is preserved as
a deprecated alias for `AGUITimeoutError`; prefer the new name to avoid
shadowing `dart:async.TimeoutError`.)
- `AGUIValidationError` — extends `AGUIError` (NOT `AgUiError`). Thrown by
`*.fromJson` factory constructors at the wire-decoding boundary. When
events flow through `EventDecoder`, this is wrapped as `DecodingError`,
so consumers using the decoder pipeline never see this directly. Direct
factory callers (`TextMessageStartEvent.fromJson(...)`) do.
- `EncoderError` and its subtypes (`DecodeError`, `EncodeError`,
encoder-side `ValidationError`) extend `AGUIError`. The `EventDecoder`
pipeline rethrows these unchanged so callers can pattern-match by type.

Recommended catch recipe in production code that uses `EventDecoder`:

```dart
try {
for (final event in stream) { handle(event); }
} on DecodingError catch (e) {
// Wire-format problem — log e.field, e.expectedType, e.actualValue.
} on TransportError catch (e) {
// HTTP / SSE transport failure.
} on AgUiError catch (e) {
// Anything else from the runtime/transport family.
} on AGUIError catch (e) {
// Catch-all (would also catch direct-factory AGUIValidationError if you
// ever bypass the decoder).
}
```

## Examples

See the [`example/`](example/) directory for:
Expand Down Expand Up @@ -265,6 +435,35 @@ Contributions are welcome! Please:
4. Ensure all tests pass
5. Submit a pull request

## Cipher-data preservation

Some AG-UI events (`ReasoningEncryptedValueEvent`, `ReasoningMessage`, `ToolMessage`) carry
opaque cipher payloads that must be forwarded verbatim between agents. This SDK implements
defense-in-depth around those payloads:

**Success paths** — the `rawEvent` field on every `BaseEvent` is set to the verbatim
wire-format map read from the SSE stream. A proxy that needs to re-emit a
`ReasoningEncryptedValueEvent` should read `rawEvent` (or maintain its own copy of the raw
bytes) and forward it unchanged rather than calling `toJson()`, which emits only the
parsed fields.

**Error paths** — when a factory (`fromJson`) fails to decode an event, the thrown
`AGUIValidationError` intentionally omits the raw JSON map (`json:` field) for any event
that may carry cipher data. This prevents raw cipher bytes from leaking through
reflection-based log shippers or error serializers that walk the exception cause chain.

**`ReasoningEncryptedValueEvent` specifically** sets `rawEvent: null` unconditionally —
unlike every other factory, forwarding `_readRawEvent(json)` would store the full cipher
payload in-memory on `BaseEvent.rawEvent`, undoing the per-field cipher scrubbing above.
Proxy operators that need the verbatim wire form must maintain their own copy before
calling `fromJson`.

**`copyWith` and `rawEvent`** — the `copyWith` methods across all event types treat
`rawEvent` as "sticky": passing `null` keeps the existing value (i.e. `rawEvent ?? this.rawEvent`).
To clear `rawEvent`, construct the event directly with `rawEvent: null`. This prevents an
accidental `copyWith()` call from silently preserving a cipher payload that the caller
intended to drop.

## License

This SDK is part of the AG-UI Protocol project. See the [main repository](https://github.com/ag-ui-protocol/ag-ui) for license information.
Expand Down
8 changes: 5 additions & 3 deletions sdks/community/dart/lib/ag_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,17 @@ export 'src/client/config.dart';
export 'src/client/errors.dart';
export 'src/client/validators.dart';

// Client codec (hide ToolResult since it's defined in types/tool.dart)
export 'src/encoder/client_codec.dart' hide ToolResult;
// Client codec — ClientToolResult is an outbound-only model used by
// Encoder.encodeToolResult; it must remain visible so callers can construct
// values to pass to that method.
export 'src/encoder/client_codec.dart';

// Core exports will be added in subsequent tasks
// export 'src/agent.dart';
// export 'src/transport.dart';

/// SDK version
const String agUiVersion = '0.1.0';
const String agUiVersion = '0.2.0';

/// Initialize the AG-UI SDK
void initAgUI() {
Expand Down
Loading
Loading