An unofficial, transport-agnostic Dart client for the AI SDK "UI Message
Stream" protocol (the SSE format produced by createUIMessageStream /
consumed by @ai-sdk/react's useChat).
It gives you the pieces the JS SDK gives the web, minus any UI framework or backend assumptions:
UiMessageStreamParser— decodes the SSE byte stream into typedUIStreamEvents.MessageReducer— folds those events intoUIMessages (text, reasoning, tool calls).ChatController— orchestrates a conversation: appends the user message, streams the reply, tracks status, captures the serverconversationId, and surfaces tool calls awaiting human approval.
It is not affiliated with or endorsed by Vercel.
Implement a ChatTransport for your backend (URL, auth, body serialization):
class MyTransport implements ChatTransport {
@override
Stream<List<int>> send(ChatRequest request) async* {
final req = http.Request('POST', Uri.parse('https://api.example.com/chat'))
..headers['authorization'] = 'Bearer $token'
..headers['content-type'] = 'application/json'
..headers['accept'] = 'text/event-stream'
..body = jsonEncode({
'conversationId': request.conversationId,
'trigger': request.trigger,
'messages': request.messages.map((m) => m.toJson()).toList(),
// ...map request.approvalDecisions to your backend's approval fields
});
final res = await http.Client().send(req);
if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
yield* res.stream;
}
}Drive a conversation:
final chat = ChatController(
transport: MyTransport(),
idGenerator: () => const Uuid().v4(), // backend requires a UUID conversationId
);
chat.snapshots.listen((snapshot) {
// render snapshot.messages, snapshot.status, snapshot.error
});
await chat.sendText('Hello!');
// Human-in-the-loop tool approval:
for (final approval in chat.pendingApprovals) {
await chat.respondToApprovals([
ApprovalDecision(
toolCallId: approval.toolCallId,
toolName: approval.toolName,
args: approval.input ?? const {},
approved: true,
),
]);
}- Assumes one single-line JSON object per SSE
data:frame. - The approval-requested state is inferred when a turn finishes with a tool's
input available but no output (see
ToolState). Validate against a captured stream for your backend. - Rendering is the consumer's job; this package only models and reduces the stream.
Apache-2.0. See LICENSE.