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
Additional information
Libraries
- agentic_client
- AG-UI chat for Flutter.