Migrate React shell sample to A2UI v0.9#1262
Merged
andrewkolos merged 6 commits intogoogle:mainfrom Apr 24, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request updates the React shell and restaurant finder agent to support the A2UI v0.9 specification. It introduces a Vite middleware for A2A communication, refactors the client to support streaming responses (SSE), and updates UI components and mock data to the new schema. Feedback suggests checking for client disconnection in the streaming loop to free server resources and improving error handling in the client by verifying response status codes.
9d612c5 to
8f23e2f
Compare
- .vite/: Vite's dev-server cache - worktrees/: directory used for local git worktrees - *.tsbuildinfo: TypeScript incremental build metadata None of these should be checked in.
Introduces a Vite dev-server plugin at /a2a that proxies client requests to the A2A agent via the @a2a-js/sdk client. This extracts A2A SDK usage (agent card resolution, JSON-RPC, streaming transport) out of the shell's browser code, mirroring the middleware pattern used by the Lit samples. Request handling: - JSON bodies are forwarded as A2A `data` parts with the `application/json+a2ui` mime type (used for v0.9 client action envelopes carrying structured context like party size, time). - Plain text bodies are forwarded as `text` parts (used for user prompts like "find me a restaurant"). - A 1MB body size limit is enforced to prevent the dev server from accumulating unbounded memory if the shell misbehaves. - All outbound requests to the agent carry the `X-A2A-Extensions: https://a2ui.org/a2a-extension/a2ui/v0.9` header, signalling A2UI v0.9 support to the agent. Without this, A2UI-aware agents fall back to plain-text responses. Response handling: - Defaults to A2A streaming: opens a text/event-stream to the shell and forwards each status-update or message event's parts as a \`data:\` line. This lets the shell render partial UI as the agent generates it. - Setting \`ENABLE_STREAMING=false\` switches to non-streaming mode; the shell handles both. - Errors are returned as 500 JSON before the stream opens, or as a terminal \`data:\` line containing an error-kind part array once the stream is open. This commit only introduces the middleware infrastructure — the middleware is registered in the Vite dev server but not yet reached by any code path in the shell. The shell migration in the next commit wires it up. @types/node is added as a devDependency for the middleware's \`http\` and \`crypto\` imports; \`middleware/\` is added to the TypeScript project so the middleware is type-checked alongside the rest of the shell.
Migrates the React shell sample from the A2UI v0.8 protocol to v0.9.
The shell now uses @a2ui/web_core/v0_9's MessageProcessor and
@a2ui/react/v0_9's A2uiSurface in place of the v0.8
A2UIProvider / A2UIRenderer / useA2UIActions stack.
## Shell code (src/App.tsx, src/client.ts)
- Replaces A2UIProvider with a direct MessageProcessor instance and
A2UIRenderer with A2uiSurface, the v0.9 rendering entry point.
- Tracks surfaces reactively: surfaces state is seeded from
processor.model.surfacesMap and refreshed via onSurfaceCreated and
onSurfaceDeleted subscriptions, so the shell renders whatever
surfaces the agent creates (supporting multi-surface responses).
- User actions emitted by UI components (e.g. "Book Now" clicks) are
wrapped in the v0.9 client message envelope
\`{version: 'v0.9', action: {...}}\` before being sent to the agent.
MessageProcessor's action listener emits a raw A2uiClientAction, so
the shell has to wrap it for the wire format.
- client.ts posts to the /a2a middleware introduced in the previous
commit instead of instantiating A2AClient directly. It parses SSE
chunks and deduplicates \`createSurface\` messages across chunks:
A2A status-update events carry cumulative message parts, so
\`createSurface\` is redelivered on every chunk, and the v0.9
MessageProcessor throws on duplicate surface IDs.
- Stream errors come back as an error-kind part array matching the
Part shape the shell already parses, so error handling doesn't need
a separate path.
## Mock data (src/mock/restaurantMessages.ts)
Rewrites the three hardcoded flows (restaurant list, booking form,
confirmation) for the v0.9 message format: createSurface /
updateComponents / updateDataModel instead of beginRendering /
surfaceUpdate / dataModelUpdate.
Each surface's root component uses \`id: 'root'\`; A2uiSurface in
@a2ui/react/v0_9 hard-requires that ID as the rendering entry point.
## Dependencies
Adds @a2ui/web_core as a file dep for MessageProcessor access.
8f23e2f to
36bd8f8
Compare
jacobsimionato
approved these changes
Apr 24, 2026
| ) | ||
| for i, part in enumerate(context.message.parts): | ||
| if isinstance(part.root, DataPart): | ||
| if "userAction" in part.root.data: |
# Conflicts: # .gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #1186 (credit to @jacobsimionato for the original migration work).This PR adds some fixes for bugs surfaced while smoke testing it against the restaurant finder agent. It also addresses some feedback provided on the original PR. There are still more follow-up issues for me to file. However, none of them block getting the react shell working with the restaurant finder agent, and I'd prefer to keep the PR reviewable.
Summary
The shell now uses
MessageProcessorandA2uiSurfacefrom@a2ui/web_core/v0_9and@a2ui/react/v0_9in place of the v0.8A2UIProvider/A2UIRenderer/useA2UIActionsstack.The shell works end-to-end against the restaurant finder agent (search -> restaurant list -> "Book Now" -> booking form -> submit -> confirmation).
Being a migration, this is a really big change. I made an effort to structure changes into commits so they can be reviewed in sequence if desired.
Changes since #1186
If you are unfamiliar with #1186, you might be better off with a brief glance at that and then focusing on the commits/diffs here rather than reading the following line items.
Fixes for issues I found while testing #1186
@a2ui/markdown-itviaMarkdownContext. Without a markdown renderer, the basic catalog'sTextdisplayed unformatted markdown.'root'.A2uiSurfacein@a2ui/react/v0_9hard-requires the root component's ID to be'root'. The mock builders used other IDs, so all three mock surfaces were stuck on "Loading root…".MessageProcessor's action listener emits a rawA2uiClientAction, butA2uiClientMessage(the wire shape) is the envelope{version: 'v0.9', action: {...}}. The shell now wraps correctly. This issue was masked byany.createSurfaceacross SSE chunks. A2Astatus-updateevents carry cumulative parts, socreateSurfaceis redelivered on every chunk.MessageProcessorthrows on duplicate surface IDs, so the client-side deduplication keeps the stream flowing. This is a bit of a hack, as the client should ideally not be receiving redundant create messages to begin with, but a more proper solution is left out of scope on this PR. Prior discussion on Update React Shell to A2UI v0.9 and Add A2A Middleware #1186.SurfaceModelinstances into React state, replacing the real models with plain-object stubs and breaking rendering mid-stream.A2uiSurfacealready subscribes viauseSyncExternalStore; no manual re-render is needed.agent_executor.py— this is a new finding. The restaurant finder's executor only checked for the v0.8 envelope{userAction: {...}}, so v0.9 actions fell through tocontext.get_user_input()→ empty string, corrupting the conversation state. Independent repro in this issue: (TODO: link issue once filed).Smaller things inspired by bot comments on #1186
data:line with an[{kind: 'error', text: ...}]Part array, matching what the client parser expects.AGENT_CARD_URLenv var the suggestion proposed was added but removed here for sample simplicity. Happy to reconsider.\n\nor\r\n\r\n, so Windows-style line endings don't break it. Full SSE spec compliance is still not implemented, but isn't a practical concern since we control both ends of the stream.Issues left for follow-up (@andrewkolos to file)
createSurfaceon every step instead of reusing or explicitly deleting. Jacob (rightfully IMO) called this out on #1186 as "not really in the spirit of A2UI" and suggested a future redesign. To be re-considered in a follow up issue if this PR lands.Rowdefaultsalign-items: stretchandImagehas noalign-self: flex-start. This is a renderer-level CSS issue (not shell-specific). fix(react,v0.9): honor the schema'sweightproperty in basic catalog #1215 did help with image display issues but fix didn't totally resolve things. Will file a followup if this lands.Manually testing things
Live agent
cd samples/agent/adk/restaurant_finder && uv run .).cd samples/client/react/shell && npm run dev).Mock mode
Open http://localhost:5003/?mock=true and step through the same flow. No agent required.
Pre-launch Checklist
If you need help, consider asking for advice on the discussion board.