agentic_client 0.0.13
agentic_client: ^0.0.13 copied to clipboard
Dart chat client for rendering A2UI widgets over AGUI protocol
agentic_client #
A drop-in Flutter chat widget for AG-UI-compatible agent backends that renders the agent's generative-UI surfaces (A2UI v0.9) inline using a genui catalog of your own Flutter widgets.

Features #
- One widget, full chat surface:
AguiChatowns the transport, theSurfaceController, and the conversation log; just give it abaseUrland aCatalog. - Streamed assistant text rendered as chat bubbles with a thinking indicator while a turn is in flight.
- Generative UI inline via two configurable ingest paths:
- Tool-result envelope (default): A2UI ops in
TOOL_CALL_RESULT.contentunder ana2ui_operationsarray (the CopilotKit convention). - Ghost tool-call args (
uiRenderToolNames): the ops live inTOOL_CALL_ARGSfor a synthetic UI-render tool; the client absorbs the call and synthesizes a tool result back into history. The same path picks up server-side UI patches emitted via LangGraph'smanually_emit_tool_callcustom event, so backends can refresh a surface without re-invoking the LLM.
- Tool-result envelope (default): A2UI ops in
- Shared agent state (
onStateChanged): the client mirrors the agent'sstateviaSTATE_SNAPSHOT/STATE_DELTA(RFC 6902 JSON Patch). The backend is the source of truth: the mirror is read-only on the client and is round-tripped back asSimpleRunAgentInput.stateso stateless agents can resume across runs. WireonStateChangedto observe changes or render debug UIs over them. - UI interaction round-trip: catalog buttons (and any other
UserActionEvent-firing widgets) become an out-of-band action signal. The transport packs the action intoforwarded_props.pending_actionso your agent can handle it via a pre-LLM node or middleware without treating the click as a chat message. - Inline agent-event steps (
showAgentEvents: true): show small italic step rows like "Calling render_ui_widget…" / "Rendering 4 components" / "Updating data" as the agent works, instead of a single loader spinner. - Human-in-the-loop approvals (automatic): when the backend graph pauses
with LangGraph's
interrupt(...), the widget renders an inline approval card and resumes the paused run with the user's Approve / Reject decision — no extra configuration required.
Getting started #
Prerequisites:
- Flutter
>=3.35.7(Dart SDK^3.12.2). - An AG-UI-compatible agent endpoint reachable over HTTP. The widget POSTs each
turn to
"$baseUrl/". - A2UI ops from the agent must travel inside
TOOL_CALL_RESULT.contentunder thea2ui_operationsenvelope (the CopilotKit convention).
Add the package to your app's pubspec.yaml:
dependencies:
agentic_client: ^0.0.1
genui: ^0.9.2
Usage #
Build a Catalog of the widgets your agent is allowed to render,
then drop AguiChat into any Scaffold:
import 'package:agentic_client/agentic_client.dart';
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
class ChatScreen extends StatelessWidget {
const ChatScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Chat')),
body: AguiChat(
baseUrl: 'http://localhost:8123',
catalog: Catalog(
[
BasicCatalogItems.text,
BasicCatalogItems.column,
BasicCatalogItems.row,
// …plus your own CatalogItems
],
catalogId: 'my-app.catalog',
),
catalogDescription: '... optional A2UI prompt context ...',
hintText: 'Ask me anything…',
// Optional ghost tool-call path on top of the always-on
// tool-result envelope.
uiRenderToolNames: const {'render_ui_widget', 'emit_ui_update'},
// Observe shared state. Fires on STATE_SNAPSHOT and STATE_DELTA.
// The backend is the source of truth; the map is a fresh copy on
// every call.
onStateChanged: (state) {
// e.g. setState(() => _agentState = state);
},
// Inline step rows ("Calling …", "Rendering 4 components",
// "Updating data") instead of a single loader spinner.
showAgentEvents: true,
),
);
}
}
catalogDescription is shipped to the agent on every request via the AG-UI
context field. Use it to teach the model the wire format and the props each
of your components accepts. For fixed-schema backends (where the agent's tool
already knows the catalog), leave it null.
Shared state and UI actions #
When you wire onStateChanged, the client mirrors the agent's shared state
via STATE_SNAPSHOT / STATE_DELTA (RFC 6902 JSON Patch) and echoes the
mirror back as SimpleRunAgentInput.state on every turn. The mirror is
read-only on the client; the backend owns all writes.
When a user taps an A2UI Button (or any widget that fires a
UserActionEvent), the transport packs the action into
forwarded_props.pending_action instead of treating it as a chat message.
A backend pre-LLM hook can read state["pending_action"], mutate state,
and either short-circuit the graph (silent) or inject a synthetic
HumanMessage (chatty).
For backends that need to refresh a surface without invoking the LLM (for
example, after a silent state mutation), dispatch a LangGraph custom event
named manually_emit_tool_call with args set to a JSON-encoded
{"a2ui_operations": [...]} envelope and the tool name registered in
uiRenderToolNames. The client unwraps the envelope and applies the ops
to the surface, so components with {"path": "/..."} bindings update in
place.
Human-in-the-loop approvals #
For consequential or irreversible actions (sending money, deleting data,
placing an order), an agent can pause mid-run and ask the user to confirm
before proceeding. This uses LangGraph's interrupt(...) primitive and works
out of the box — there is no flag to enable.
The flow:
- The backend tool calls
interrupt(payload), which suspends the graph on its checkpointer (keyed by the conversation's thread id). ag_ui_langgraphsurfaces the pause as aCUSTOMAG-UI event namedon_interruptwhosevalueis thepayload.AguiChatrenders an inline approval card from that payload and waits.- When the user taps Approve / Reject, the client resumes the paused graph by
sending the decision back as
forwarded_props.command.resume— continuing the same run rather than starting a new turn.
The card reads its text and button labels from the payload, falling back to sensible defaults:
{
"question": "Approve this action?\n\nTransfer $500 to Alice", // body text (falls back to `action`)
"action": "Transfer $500 to Alice", // used if `question` is absent
"approveLabel": "Approve", // optional, defaults to "Approve"
"rejectLabel": "Reject" // optional, defaults to "Reject"
}
The decision is sent as {"approved": true} / {"approved": false}, so the
backend tool resumes by reading decision["approved"]. Requires a backend
checkpointer (e.g. LangGraph's MemorySaver) so the run can be resumed; an
in-process checkpointer is fine for a single server, but use a persistent one
to survive restarts or span multiple workers.
A complete runnable app, including custom ProductCard, WeatherTile and
Stat catalog items, lives in /example. Run it against any
AG-UI agent with:
cd example
flutter run --dart-define=AGENT_URL=http://localhost:8123