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.
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.
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.