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.

agentic_client example gif

Features

  • One widget, full chat surface: AguiChat owns the transport, the SurfaceController, and the conversation log; just give it a baseUrl and a Catalog.
  • 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:
    1. Tool-result envelope (default): A2UI ops in TOOL_CALL_RESULT.content under an a2ui_operations array (the CopilotKit convention).
    2. Ghost tool-call args (uiRenderToolNames): the ops live in TOOL_CALL_ARGS for 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's manually_emit_tool_call custom event, so backends can refresh a surface without re-invoking the LLM.
  • Shared agent state (onStateChanged): the client mirrors the agent's state via STATE_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 as SimpleRunAgentInput.state so stateless agents can resume across runs. Wire onStateChanged to 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 into forwarded_props.pending_action so 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.content under the a2ui_operations envelope (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:

  1. The backend tool calls interrupt(payload), which suspends the graph on its checkpointer (keyed by the conversation's thread id).
  2. ag_ui_langgraph surfaces the pause as a CUSTOM AG-UI event named on_interrupt whose value is the payload.
  3. AguiChat renders an inline approval card from that payload and waits.
  4. 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

  • Built on top of ag_ui for transport and genui for A2UI surface rendering.
  • Issues and contributions are welcome on the repository.

Libraries

agentic_client
AG-UI chat for Flutter.