Step 5 · Send UI with A2UI (12 min)¶
Goal: Make the chat show a red SEV1 badge on the diagnosis, and make the Diagnose button of each incident work.
Builds on: A2UI basics: 3. Components, 4. Data model and binding, 5. Actions.
The idea: agents send UI, not HTML¶
The SRE agent sends Markdown for the model and A2UI for people.
A2UI (v0.9) describes a UI as JSON. The JSON names components from a catalog. The agent never sends HTML or code. The browser decides how each component looks:
{"version": "v0.9", "createSurface": {"surfaceId": "sre-1a2b", "catalogId": "…/catalogs/sre/v1/catalog.json"}}
{"version": "v0.9", "updateComponents": {"surfaceId": "sre-1a2b", "components": [
{"id": "root", "component": "Card", "child": "body"},
{"id": "body", "component": "Column", "children": ["severity", "title"]},
{"id": "severity", "component": "SeverityBadge", "level": "SEV1", "contribution": 99.3},
{"id": "title", "component": "Text", "text": "🔬 Incident diagnosis", "variant": "h2"}
]}}
| Topic | Details |
|---|---|
| Where the SRE agent makes the surfaces | sre_agent/src/sre_agent/a2ui_surfaces.py makes one surface for each result: the incident list, the diagnosis and the post-mortem. |
| How a surface moves | Each message is an A2A data part with the type application/json+a2ui. The SRE agent sends these parts only to callers that declare A2UI client capabilities. The Orchestrator declares them for its chat: you wrote this in step 4 (a2uiClientCapabilities and the A2UI extension). |
| How the browser shows a surface | The browser uses @a2ui/lit (agent/web/src/sre-a2ui.js, prebuilt into agent/src/agent/static/sre-a2ui.js). |
| The catalog | The SRE catalog is the A2UI basic catalog (Card, Column, List, Tabs, Text, Button…) plus two custom components: SeverityBadge and Download. Each custom component has a schema on the two sides: a Pydantic model in Python and a Zod schema in the browser. |
A surface can also send events back. A Button has an action. When the user clicks it, the
browser sends the event name and its context to the agent. The Orchestrator changes
diagnose_incident with {"traceId": ...} into the next chat turn, "Diagnose trace ….".
In the incident list, all rows use one template. The button's context is a data binding,
{"path": "traceId"}: the browser puts in the trace ID of the row that the user clicked.
The severity comes from the share of the request time that the bottleneck span owns.
classify_severity changes the share into a level: SEV1 at 90 % or more, SEV2 at 50 % or more,
else SEV3.
Your task¶
-
Find the TODOs:
git grep -n "TODO(step-5)" -
In
_badge(bottleneck_share), return[]when there is no bottleneck. Else, return oneSeverityBadgecomponent with itslevelandcontribution. - In
_button(...), add the trace to the event:"context": {"traceId": trace_id}.
The diagnosis card and the post-mortem card put the return value of _badge first.
Run it¶
-
Do these commands:
uv run workshop/check.py 5 uv run simulate_incident.py --engine-only # fresh incident telemetry uv run workshop/chat.py # the web chat -
Open http://localhost:8080/chat. (Step 4 must be solved: the chat calls the SRE agent.)
- Ask "What are the latest failures?". You see an incident list surface.
- Click Diagnose on a row. You see a diagnosis card.
- Make sure that the diagnosis card shows the red
SEV1 · bottleneck owns 99.3% of the requestbadge, the report in tabs, and a Download button. - Ask "hello". The Orchestrator calls no tool and shows no surface.
About the incident list:
- The rows come from the data model of the surface through a List template.
- Each row has a Diagnose button and a Post-mortem button.
- A button sends an A2UI action. The Orchestrator changes the action into the next chat turn.
- This chat turn also goes through the policy from step 4.
The Orchestrator sends a request to the SRE agent only when it is necessary.
Discuss¶
- Why use JSON components, and not HTML? The agent cannot inject scripts or styles. The client shows only the components that its catalog allows. The client applies the same theme as the rest of the app.
- The renderer in the browser validates each component against its schema.
sre_agent/test/test_a2ui_surfaces.pydoes the same check witha2ui-core. Thus, an incorrect surface fails in CI, not in the demo.
Stretch¶
- Run
uv run workshop/check.py all. All steps must be green ✅. - Change the data, not the UI. Open http://localhost:8080/playground, and select example 4
(A2UI basics, 4):
- Add a
Textcomponent bound to/progress, and add it to the column. - Add
updateDataModelmessages that set/progressto"1/3","2/3"and"3/3". Only the data changes: the components stay the same. - In
incident_list_surface(a2ui_surfaces.py), add a line under the title that is bound to/updated, and put the time of the scan into the data model.
- Add a
- Change the thresholds. Look at the change in the badge color.
- Add a third custom component:
- Add a Pydantic model in
a2ui_surfaces.py. - Add a Lit element in
agent/web/src/sre-a2ui.js. - Build the UI again with
cd agent/web && npm ci && npm run build.
- Add a Pydantic model in
- If you have Docker, run
docker compose up --build. This command runs the same UI. The Orchestrator, the SRE agent and the Inventory agent run as separate services. They talk A2A over HTTP. To make an incident, runcurl "http://localhost:8081/api/gateway?trigger_error=true".
Stuck? Run uv run workshop/step.py hint 5, or ask the workshop coach in Antigravity. To apply the
solution: uv run workshop/step.py solve 5.
Hint and solution¶
Try the step first. Open the hint when you are stuck, and the solution only after the hint.
Hint: where to change the code
File: sre_agent/src/sre_agent/a2ui_surfaces.py, in _button(). The TODO says:
# TODO(step-5): The click must tell the agent which trace: add a "context" with
# {"traceId": trace_id} to the event. In a list row, trace_id is a data binding
# ({"path": "traceId"}): the browser puts in the trace ID of that row.
File: sre_agent/src/sre_agent/a2ui_surfaces.py, in _badge(). The TODO says:
# TODO(step-5): Without a bottleneck there is no badge: return []. Otherwise return one
# component: {"id": "severity", "component": "SeverityBadge", "level": <classify_severity>,
# "contribution": <the share>}. The cards list it first, and the browser draws it.
Solution
--- a/sre_agent/src/sre_agent/a2ui_surfaces.py
+++ b/sre_agent/src/sre_agent/a2ui_surfaces.py
@@ -141,10 +141,7 @@
"id": component_id,
"component": "Button",
"child": f"{component_id}-label",
- # TODO(step-5): The click must tell the agent which trace: add a "context" with
- # {"traceId": trace_id} to the event. In a list row, trace_id is a data binding
- # ({"path": "traceId"}): the browser puts in the trace ID of that row.
- "action": {"event": {"name": action}},
+ "action": {"event": {"name": action, "context": {"traceId": trace_id}}},
},
_text(f"{component_id}-label", label),
]
@@ -156,10 +153,10 @@
A component is a JSON object: its ``id``, the catalog ``component`` it instantiates,
and that component's properties - here `SeverityBadge.level` and `.contribution`.
"""
- # TODO(step-5): Without a bottleneck there is no badge: return []. Otherwise return one
- # component: {"id": "severity", "component": "SeverityBadge", "level": <classify_severity>,
- # "contribution": <the share>}. The cards list it first, and the browser draws it.
- return []
+ if bottleneck_share is None:
+ return []
+ level = classify_severity(bottleneck_share)
+ return [{"id": "severity", "component": "SeverityBadge", "level": level, "contribution": bottleneck_share}]
def _card(children: list[str], components: list[dict[str, Any]]) -> list[dict[str, Any]]:
To apply it to your code: uv run workshop/step.py solve 5.