Skip to content

Commit 7960088

Browse files
bloveclaude
andcommitted
docs(a2ui): the introduction teaches through the running example
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 2146894 commit 7960088

5 files changed

Lines changed: 147 additions & 256 deletions

File tree

apps/website/content/docs/a2ui/getting-started/introduction.mdx

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,120 @@
1+
---
2+
description: The protocol layer for A2UI messages, taught through the flight-booking demo whose agent authors every surface and streams it as v0.9 JSONL.
3+
---
4+
15
# Introduction
26

37
`@threadplane/a2ui` is the protocol layer for A2UI messages. It gives the rest of the framework a shared TypeScript vocabulary for agent-built surfaces, streamed JSONL messages, dynamic values, and outbound action payloads.
48

59
It does not render Angular components. It does not register handler functions. It does not decide how an agent should respond to a button click. Those jobs sit in `@threadplane/chat` and `@threadplane/render`.
610

11+
The running example is a flight booking flow. The agent authors each screen as an A2UI surface, and this page walks the files that produce it.
12+
13+
## What the demo does
14+
15+
The Run tab shows the prebuilt `<chat>` composition with the A2UI catalog registered on it. Choose the welcome suggestion "Book LAX → JFK" and the agent replies with a booking form rather than with prose: origin and destination pickers already set to those two airports, a departure date, a passenger count, and a fare class.
16+
17+
Fill the form and press "Search flights". The button does not post a chat message; it sends a structured A2UI action back to the agent, which searches the flight fixtures and answers with a second surface listing the matching flights. Selecting one produces a third surface, the booking confirmation, whose "Modify search" button returns to the form with the earlier values already filled in.
18+
19+
The second suggestion, "Book SFO → SEA", runs the same pattern on a different route, which is worth trying because nothing about the form is hardcoded on the client.
20+
21+
## How it is built
22+
23+
Four files carry the feature: a LangGraph graph that authors the surfaces, a FastAPI server that exposes it over AG-UI, an application config that registers the agent, and a component that hands the A2UI catalog to `<chat>`. Open the Code tab to read them in full.
24+
25+
### The component shape the model must satisfy
26+
27+
A2UI v0.9 components are flat. Each entry carries an `id`, a `component` name from the catalog, and its props at the same level of the same object. The example models that as a Pydantic class and uses it as the structured-output schema for the LLM, so the model authors the component list under a validator instead of free-typing JSON.
28+
29+
<ExampleCode file="graph.py" region="component-schema" title="graph.py — the component schema" />
30+
31+
The validator is the safety gate: an unknown `component` name renders nothing visible, so the schema rejects it and the model is re-prompted with the error.
32+
33+
### The three parts of a surface
34+
35+
Every surface in this demo is the same triple: an id, an initial data model, and a flat list of components. Three subclasses give each node its own schema name and description without changing the shape.
36+
37+
<ExampleCode file="graph.py" region="surface-spec" title="graph.py — the surface spec" />
38+
39+
The `data_model` is what path bindings such as `{"path": "/origin"}` resolve against.
40+
41+
### Wrapping a surface in v0.9 envelopes
42+
43+
The model authors the components; the code writes the wire format. `_wrap_envelopes` emits the sentinel prefix and then one JSON envelope per line, each stamped `"version": "v0.9"`.
44+
45+
<ExampleCode file="graph.py" region="envelope-wrapping" title="graph.py — the envelope wrapping" />
46+
47+
Order matters, and the comment above the function states it: `createSurface` first, then `updateComponents`, then `updateDataModel`.
48+
49+
<Callout type="info" title="The sentinel is what switches the client into A2UI mode">
50+
`A2UI_PREFIX` is `---a2ui_JSON---`. The content classifier in `@threadplane/chat` looks for exactly that string at the start of assistant content and routes the rest of the message into the A2UI pipeline instead of the markdown renderer.
51+
</Callout>
52+
53+
### The node that authors the form
54+
55+
`build_form` runs on the first turn and again on a "Modify search" turn. It recovers any prior submission from the message history, seeds the origin and destination from a phrase such as "I want to fly LAX to JFK", substitutes those values into the system prompt as the form defaults, and asks the model for a `BookingFormSpec`.
56+
57+
<ExampleCode file="graph.py" region="build-form-node" title="graph.py — the build_form node" />
58+
59+
The node returns an ordinary `AIMessage` whose content is the wrapped JSONL, which is why no custom event type is needed to carry a surface.
60+
61+
<Callout type="warning" title="An LLM-authored surface needs a fallback">
62+
`_emit_with_retry` re-prompts the model with the validation error up to three attempts in total, and `build_form` falls back to a hand-written sentinel form when they all fail. A surface is the whole response here, so a validation failure with no fallback is a blank turn.
63+
</Callout>
64+
65+
### Routing an action message back into the graph
66+
67+
When the user presses a button, the surface sends an A2UI action message back to the agent as the next user message, and its content is JSON rather than prose. The entry node reads that last message and dispatches on the action name.
68+
69+
<ExampleCode file="graph.py" region="route" title="graph.py — the route node" />
70+
71+
`_is_submit_event` and `_is_flight_select_event` each parse the content and compare `action.name` against `bookingSubmit` and `flightSelect`, so anything that is not one of those two is treated as a fresh request for the form.
72+
73+
### The graph and its checkpointer
74+
75+
The wiring is a fan-out from `route` into the three surface-authoring nodes, each of which ends through background title generation.
76+
77+
<ExampleCode file="graph.py" region="graph-wiring" title="graph.py — graph wiring" />
78+
79+
<Callout type="warning" title="This graph compiles its own checkpointer">
80+
`ag-ui-langgraph` reads thread state through `graph.aget_state`, so a graph served this way must compile a checkpointer, and the example uses `MemorySaver` for development. That is the opposite of a graph served by `langgraph dev` or LangGraph Platform, where the platform supplies persistence and compiling your own saver is an error.
81+
</Callout>
82+
83+
### Serving the graph over AG-UI
84+
85+
The server is the standard `ag-ui-langgraph` mount: wrap the compiled graph in a `LangGraphAgent` and attach it to a FastAPI application at a path.
86+
87+
<ExampleCode file="server.py" title="server.py" />
88+
89+
Nothing in this file is specific to A2UI; the surfaces travel as assistant message content over the same event stream as any other reply.
90+
91+
### Registering the AG-UI agent
92+
93+
`provideAgent()` from `@threadplane/ag-ui` needs the URL of that endpoint. The example passes a factory because it resolves the URL at runtime from the host that serves the demo; an application of your own passes `url` directly.
94+
95+
<ExampleCode file="app.config.ts" title="app.config.ts" />
96+
97+
`provideChat({})` registers the chat composition defaults alongside it.
98+
99+
### Giving the chat composition a catalog
100+
101+
The client side is one input. `a2uiBasicCatalog()` from `@threadplane/chat` returns a view registry covering all eighteen components of the A2UI basic catalog, and passing it as `[views]` is what lets `<chat>` mount a surface.
102+
103+
<ExampleCode file="a2ui.component.ts" title="a2ui.component.ts" />
104+
105+
No handler wiring appears here: `<chat>` builds the action message from the surface and submits it to the agent for you.
106+
107+
<Callout type="info" title="Where the action context values come from">
108+
`<a2ui-surface>` keeps a live store of the current data-model values, so the `{"path": "/origin"}` bindings inside the `action.event.context` on the submit button resolve to what the user typed, not to the values the agent seeded. `buildA2uiActionMessage()` stamps the surface id, the source component id, and a timestamp onto the result.
109+
</Callout>
110+
7111
## How it fits
8112

9113
<A2uiMessageFlow />
10114

11115
## What the package owns
12116

13-
The public entry point exports four groups of tools:
117+
The demo shows the protocol from the agent side. The package is the same protocol expressed as TypeScript, and its public entry point exports four groups of tools:
14118

15119
| Area | Exports |
16120
|------|---------|
@@ -20,7 +124,7 @@ The public entry point exports four groups of tools:
20124
| Data access | `getByPointer()`, `setByPointer()`, `deleteByPointer()` |
21125
| Dynamic values | `resolveDynamic()`, `A2uiScope`, `isPathRef` / `isFunctionCall` guards |
22126

23-
Use this package when you are building an adapter, validating an agent stream, testing A2UI payloads, or integrating a custom renderer with the same protocol surface that `@threadplane/chat` uses.
127+
Use this package when you are building an adapter, validating an agent stream, testing A2UI payloads, or integrating a custom renderer with the same protocol surface that `@threadplane/chat` uses. Rendering the surfaces inside `<chat>`, as the demo does, needs none of it directly.
24128

25129
## Message flow
26130

@@ -78,3 +182,20 @@ npm install @threadplane/a2ui
78182
```
79183

80184
The package has no peer dependencies.
185+
186+
## What's Next
187+
188+
<CardGroup cols={2}>
189+
<Card title="The A2UI message protocol" href="/docs/a2ui/guides/message-protocol">
190+
Surfaces, flat components, dynamic values, the four envelopes, and how actions travel back.
191+
</Card>
192+
<Card title="Working with the data model" href="/docs/a2ui/guides/data-model">
193+
Pointer helpers, applying updateDataModel envelopes, and resolving dynamic values in scope.
194+
</Card>
195+
<Card title="Validating and adapting an A2UI stream" href="/docs/a2ui/guides/adapters-and-validation">
196+
Consume a streaming response, narrow values, build test payloads, and write a custom renderer.
197+
</Card>
198+
<Card title="A2UI Schema" href="/docs/a2ui/reference/schema">
199+
Every protocol type, component prop interface, and envelope shape in one reference.
200+
</Card>
201+
</CardGroup>

cockpit/ag-ui/a2ui/python/docs/guide.md

Lines changed: 0 additions & 127 deletions
This file was deleted.

cockpit/ag-ui/a2ui/python/src/graph.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def _lookup_flight_impl(flight_number: str) -> dict | None:
9494

9595
# ── Pydantic schemas ────────────────────────────────────────────────────────
9696

97+
# region component-schema
9798
class A2uiComponent(BaseModel):
9899
"""Single A2UI v0.9 updateComponents entry.
99100
@@ -136,8 +137,10 @@ def _known_component(cls, v: str) -> str:
136137
f"component '{v}' not in catalog. Allowed: {sorted(ALLOWED_COMPONENTS)}"
137138
)
138139
return v
140+
# endregion
139141

140142

143+
# region surface-spec
141144
class _SurfaceSpec(BaseModel):
142145
"""Common shape — both booking and results surfaces produce the same
143146
triple (surface_id, data_model, components)."""
@@ -157,10 +160,12 @@ class FlightResultsSpec(_SurfaceSpec):
157160
class ConfirmationSpec(_SurfaceSpec):
158161
"""Booking confirmation surface — selected flight + prior party context."""
159162
pass
163+
# endregion
160164

161165

162166
# ── Envelope wrapping ───────────────────────────────────────────────────────
163167

168+
# region envelope-wrapping
164169
# A2UI v0.9 wire format (a2ui.org server_to_client.json): every envelope
165170
# carries "version": "v0.9". Order matters: createSurface first (surfaceId +
166171
# catalogId), then updateComponents (flat components; exactly one has id
@@ -189,6 +194,7 @@ def _wrap_envelopes(spec: _SurfaceSpec) -> str:
189194
"value": spec.data_model,
190195
}}))
191196
return A2UI_PREFIX + "\n" + "\n".join(lines) + "\n"
197+
# endregion
192198

193199

194200
# ── LLM + retry ─────────────────────────────────────────────────────────────
@@ -386,6 +392,7 @@ def _build_sentinel_booking_form(defaults: dict[str, Any]) -> BookingFormSpec:
386392
)
387393

388394

395+
# region build-form-node
389396
async def build_form(state: MessagesState) -> dict:
390397
"""First-turn AND Modify-search node: LLM authors the booking form.
391398
@@ -417,6 +424,7 @@ async def build_form(state: MessagesState) -> dict:
417424
_logger.error("Falling back to sentinel booking form: %s", err)
418425
spec = _build_sentinel_booking_form(defaults)
419426
return {"messages": [AIMessage(content=_wrap_envelopes(spec))]}
427+
# endregion
420428

421429

422430
# ── search_flights node ─────────────────────────────────────────────────────
@@ -766,6 +774,7 @@ def _format_party(prior: dict[str, Any]) -> str:
766774
return " • ".join(parts) if parts else "(party details unavailable)"
767775

768776

777+
# region route
769778
def route(state: MessagesState) -> Command[Literal["build_form", "search_flights", "confirm_booking"]]:
770779
"""Inspect the last message — submit event → search_flights, flight-select
771780
event → confirm_booking, else build_form."""
@@ -775,6 +784,7 @@ def route(state: MessagesState) -> Command[Literal["build_form", "search_flights
775784
if _is_flight_select_event(last_content):
776785
return Command(goto="confirm_booking")
777786
return Command(goto="build_form")
787+
# endregion
778788

779789

780790
# ── generate_title node (inline; matches Pattern D from spec
@@ -833,6 +843,7 @@ async def generate_title(state: MessagesState, config) -> dict:
833843
return {}
834844

835845

846+
# region graph-wiring
836847
_builder = StateGraph(MessagesState)
837848
_builder.add_node("route", route)
838849
_builder.add_node("build_form", build_form)
@@ -846,3 +857,4 @@ async def generate_title(state: MessagesState, config) -> dict:
846857
_builder.add_edge("generate_title", END)
847858

848859
graph = _builder.compile(checkpointer=MemorySaver())
860+
# endregion

0 commit comments

Comments
 (0)