Skip to content

refactor(samples): per-request processors in the ADK toolset samples - #3274

Open
nan-yu wants to merge 19 commits into
docs/python-agent-renderer-statefrom
refactor/samples-adk-toolset-per-request-processors
Open

nan-yu wants to merge 19 commits into
docs/python-agent-renderer-statefrom
refactor/samples-adk-toolset-per-request-processors

Conversation

@nan-yu

@nan-yu nan-yu commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #3271.

rizzcharts, mcp_app_proxy and file_upload_summarizer send A2UI through SendA2uiToClientToolset. They built their processor, prompt and catalogs once, at startup, from a default renderer. They now negotiate per request, and speak A2UI v1.0 as well.

  1. The executor creates an A2uiRequestProcessor for each request from the capabilities the client sent, unchanged, and writes what the request needs into the session state: the processor's format rules and the negotiated catalogs under system:a2ui_catalogs, plus the preferred catalog and its examples in rizzcharts (agent_executor.py). The state follows from the negotiated version and the client's capabilities alone, so the executor records a digest of them (system:a2ui_negotiation) and adds a session event only when they change, instead of on every request.
  2. The instruction is built from the session state on each request, and the toolset and the A2UI event converter read the catalogs from the same keys, so the prompt, the tool's schema and its validation follow the request's catalogs. A client that sends no capabilities, or names none of the agent's catalogs, gets text answers, and so does a v1.0 client that lacks a catalog the agent's surfaces need (see point 7).
  3. Each README explains how the agent negotiates A2UI. The rizzcharts README's cd path points at the sample's current location, and the READMEs ask for Python 3.10, as the pyproject.toml files do.
  4. The rizzcharts v0.9 examples name the catalog id that the v0.9 rizzcharts catalog declares; the old GitHub URL they named isn't the id of any active catalog. The agent loads and validates each catalog's examples once, at startup, against the catalogs their surfaces use (agent.py), and gives a request the examples of the client's most preferred catalog whose examples use only catalogs the client named.
  5. The samples pin a2ui-agent-sdk>=0.9.0, as does the orchestrator sample, and the unused jsonschema dependency is dropped (with the matching uv.lock entries). The three executors take override from typing_extensions, since typing.override needs Python 3.12, so the samples depend on typing-extensions>=4.4.0.

v1.0 support

  1. rizzcharts adds a v1.0 generator next to v0.8 and v0.9, and mcp_app_proxy and file_upload_summarizer next to v0.9. mcp_app_proxy no longer speaks v0.8: its tools write v0.9 messages for anything but v1.0, so a v0.8 client got messages it can't read, and the unused v0.8 catalog is removed. For v1.0 the executors read the renderer capabilities from a2uiRendererCapabilities, as the v1.0 A2A extension defines; v0.8 and v0.9 keep a2uiClientCapabilities. The key is A2UI_RENDERER_CAPABILITIES_KEY in a2ui.constants, next to A2UI_CLIENT_CAPABILITIES_KEY; docs(python/agent): document per-request processors, RendererState and streaming #3271 adds it.
  2. On v1.0 the samples mix catalogs on one surface, so each v1.0 custom catalog holds only its custom components, has its own v1.0 catalog id, and says so in its instructions: Canvas, Chart and GoogleMap in rizzcharts_catalog_definition.json, FileUpload in file_upload_catalog.json, and the MCP App components in mcp_app_catalog.json, which drops the v0.9 Column that no surface uses. A v1.0 surface takes the basic catalog as its default (createSurface.catalogId), and each custom component names its catalog in its own catalogId; the basic catalog also resolves file_upload's required check. A v1.0 client names both catalogs in supportedCatalogIds, and the rizzcharts executor then passes both to the prompt and the toolset's validation; a basic-only client gets the basic catalog and the standard examples, and a client that names only the rizzcharts catalog gets text answers, since a v1.0 rizzcharts surface defaults to the basic catalog. file_upload_summarizer's v1.0 generator registers the basic catalog next to its own, and a v1.0 client that names only one of the two gets text answers. mcp_app_proxy's surfaces use only MCP App components, so they keep that catalog as their default. v0.8 and v0.9 are unchanged: a surface there uses one catalog, so their custom catalogs keep the copied basic components.
  3. The four rizzcharts examples have v1.0 copies under rizzcharts_catalog/1.0 and standard_catalog/1.0, with {"@path": ...} bindings and no theme. The rizzcharts*catalog ones use the basic catalog as the surface default, and their Canvas, Chart and GoogleMap name the rizzcharts catalog; the standard_catalog ones use the basic catalog alone. As with the basic catalog's v1.0 Text, headings are Markdown, and component ids use *instead of-.
  4. The rizzcharts examples of every version set the data model to flat keys such as "chart.items[0].label" and bound /chart.items, which names no data, and the components a List template repeats bound absolute paths such as /label. They now send a nested data model, bind /chart/items and the like, and bind template items relative to the item (label, ${name}). v0.8 can't nest more than one level in a dataModelUpdate, so the v0.8 examples send one per level; the v0.8 renderer of the Angular orchestrator client resolves the new paths.
  5. mcp_app_proxy and file_upload_summarizer write the request's version to the session state (system:a2ui_version), and their tools write v1.0 messages (@path, @call, the v1.0 catalog id) for a v1.0 request. The event converter sends tool responses on unvalidated, so the file upload tools validate their messages against the negotiated catalogs and return an error to the model instead of a surface the client can't render. The v0.9 file upload catalog gains the required function of the v0.9 basic catalog, which the v0.9 uploader's button check calls and the Angular file upload client already implements.
  6. part_converter.py: A2uiPartConverter defaulted to v0.8, and the toolset and event converter pass no version, so v1.0 parts went out with the deprecated application/json+a2ui MIME type. The version now defaults to the protocol version of the catalogs, so v0.8 and v0.9 output is unchanged.
  7. The orchestrator sample is unchanged: its subagents advertise v0.8 and v0.9 but not v1.0, and supporting v1.0 there also needs the SDK's data model stripping to read a2uiRendererDataModel.

Testing

  1. uv run pytest (the whole suite), including a new part converter test and two new sample test modules: test_rizzcharts_examples.py validates each rizzcharts example against exactly the catalogs its surfaces use and checks that every binding resolves in the data model it sends, and test_community_adk_toolset_samples.py drives each executor's _prepare_session without a model: the text fallbacks of points 6 and 7, the rizzcharts example choice, the session events of point 1, and the file upload tools' payloads and errors.
  2. A script that creates a processor for each version in each sample, with clients naming every catalog, the catalogs in reverse order, and each catalog alone, builds the prompt, and validates the examples and tool payloads against the negotiated catalogs.
  3. A real v1.0 request to rizzcharts with gemini-3.6-flash (at 651616f, before the data model change of point 9), from a client naming both catalogs: it returned createSurface on the basic catalog, updateComponents with Canvas and Chart naming the rizzcharts catalog, and updateDataModel, as application/a2ui+json.
  4. ruff check, pyink and Prettier on the changed files; uv run prompt_builder.py 0.8|0.9|1.0 for rizzcharts.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors A2UI negotiation across multiple agents (file_upload_summarizer, mcp_app_proxy, and rizzcharts) to use a single runner that dynamically adapts its instructions based on session state, while upgrading the a2ui-agent-sdk dependency and removing jsonschema. The review feedback correctly identifies a copy-paste bug in the rizzcharts agent's name initialization, a type mismatch in the examples path dictionary annotation, and an unnecessary performance bottleneck caused by validating static examples on every request.

Comment thread samples/community/agent/adk/rizzcharts/python/agent.py
Comment thread samples/community/agent/adk/rizzcharts/python/agent.py Outdated
Comment thread samples/community/agent/adk/rizzcharts/python/agent_executor.py Outdated
@nan-yu
nan-yu force-pushed the refactor/samples-adk-toolset-per-request-processors branch from cd889cb to f1c1184 Compare October 16, 2026 19:34
@nan-yu
nan-yu force-pushed the docs/python-agent-renderer-state branch from 0df8f19 to cf5db13 Compare October 16, 2026 19:34
@nan-yu
nan-yu force-pushed the refactor/samples-adk-toolset-per-request-processors branch from f1c1184 to c3dbf78 Compare October 16, 2026 19:51
@nan-yu
nan-yu force-pushed the docs/python-agent-renderer-state branch 2 times, most recently from 6e471f6 to 74fb3d8 Compare October 16, 2026 19:53
@nan-yu
nan-yu force-pushed the refactor/samples-adk-toolset-per-request-processors branch 5 times, most recently from ca5ae15 to ee487d1 Compare October 16, 2026 22:57
@nan-yu
nan-yu force-pushed the docs/python-agent-renderer-state branch from 229788b to 7b4baed Compare October 16, 2026 22:57
rizzcharts, mcp_app_proxy and file_upload_summarizer send A2UI through
`SendA2uiToClientToolset`. They built their processor, prompt and
catalogs once, at startup, from a default renderer.

1. The executor creates an `A2uiRequestProcessor` for each request from
   the `a2uiClientCapabilities` the client sent, unchanged, and writes
   what the request needs into the session state: the processor's format
   rules and the negotiated catalogs under `system:a2ui_catalogs`, plus
   the preferred catalog and its examples in rizzcharts.
2. The instruction is built from the session state on each request, and
   the toolset and the A2UI event converter read the catalogs from the
   same keys, so the prompt, the tool's schema and its validation follow
   the request's catalogs. A client that sends no capabilities, or names
   none of the agent's catalogs, gets text answers.
3. Each README explains how the agent negotiates A2UI. The rizzcharts
   README's `cd` path points at the sample's current location.
4. The samples, and the orchestrator that runs them, pin
   `a2ui-agent-sdk>=0.9.0`, and the unused `jsonschema` dependency is
   dropped.

Signed-off-by: Nan Yu <nanyu@google.com>
The executor now loads the examples with validate=True against the
request's catalogs, and the v0.9 examples named the old GitHub URL, which
no catalog declares. They now name the id that the v0.9 rizzcharts
catalog declares.

Signed-off-by: Nan Yu <nanyu@google.com>
…es paths

1. The rizzcharts agent and runner used the name "mcp_app_proxy_agent",
   copied from another sample. They now use "rizzcharts_agent".
2. The examples paths are keyed by ProtocolVersion in their annotation,
   as they are in the code.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the docs/python-agent-renderer-state branch from 7b4baed to eebbda5 Compare October 16, 2026 23:07
@nan-yu
nan-yu force-pushed the refactor/samples-adk-toolset-per-request-processors branch from ee487d1 to 4c414a9 Compare October 16, 2026 23:07
nan-yu added 10 commits October 19, 2026 18:07
…' into refactor/samples-adk-toolset-per-request-processors

Signed-off-by: Nan Yu <nanyu@google.com>
…logs

A2uiPartConverter defaulted its version to v0.8, so A2uiEventConverter and
SendA2uiToClientToolset.get_part_converter, which pass no version, tagged
v1.0 parts with the deprecated MIME type. The converter now defaults to the
protocol version of its catalogs.

Signed-off-by: Nan Yu <nanyu@google.com>
Add a v1.0 generator to the rizzcharts, mcp_app_proxy and
file_upload_summarizer samples, with v1.0 catalogs mirroring the 0.9 ones
and v1.0 versions of the rizzcharts examples. The executors read the
renderer capabilities from a2uiRendererCapabilities for v1.0, and the
mcp_app_proxy and file_upload_summarizer tools write their messages in the
negotiated version.

Signed-off-by: Nan Yu <nanyu@google.com>
…the ADK toolset samples

v1.0 mixes catalogs on one surface, so the v1.0 custom catalogs hold only
their custom components, and v1.0 surfaces take the basic catalog as their
default while each custom component names its own catalog in catalogId.
rizzcharts passes every negotiated v1.0 catalog to the prompt and the
toolset, and file_upload_summarizer registers the basic catalog in its v1.0
generator. mcp_app_proxy's surfaces use only its own components, so its
v1.0 catalog stays their default and drops the unused Column. v0.8 and v0.9
are unchanged.

Signed-off-by: Nan Yu <nanyu@google.com>
…G bullet

The Unreleased section said both that A2uiPartConverter defaults its
version to the protocol version of its catalogs and that it defaults to
ProtocolVersion.V0_8. Keep the existing bullet, which now states the new
default, the v0.8 fallback without catalogs and the MIME type it sets.

Signed-off-by: Nan Yu <nanyu@google.com>
v1.0 renames the client to the renderer, and the message metadata key of
its capabilities to a2uiRendererCapabilities. a2ui.constants only had the
v0.8/v0.9 key, so each sample hardcoded the v1.0 one.

Signed-off-by: Nan Yu <nanyu@google.com>
…s relatively

The rizzcharts examples of every version set the data model to flat keys
such as "chart.items[0].label" and bound "/chart.items", a JSON Pointer
to a key that doesn't exist, so the chart, the map and the lists rendered
empty, and the examples taught the model the same pattern. The components
that a List template repeats also bound absolute paths ("/label",
"${/name}"), which resolve from the root rather than the list item.

The v0.9 and v1.0 examples now send a nested data model, bind
"/chart/items" and the like, and bind template items relative to the
item ("label", "${name}"). v0.8 can't nest more than one level in a
dataModelUpdate, so the v0.8 examples send one dataModelUpdate per level.
The v0.8 renderer of the Angular orchestrator client resolves the new
paths too, and now also shows the item labels of the basic chart.

test_rizzcharts_examples.py validates each example against exactly the
catalogs its surfaces use, and checks that every binding, inside
templates too, resolves in the data model the example sends. The prompt
no longer says that every example holds three messages.

Signed-off-by: Nan Yu <nanyu@google.com>
… the catalogs

A v1.0 client that named only the rizzcharts catalog got a processor, and
the per-request load_examples(validate=True) then failed outside any try:
the rizzcharts examples default the surface to the basic catalog, so the
A2A task failed instead of falling back to text. The examples were also
read from disk and validated on every request.

The agent now loads and validates each catalog's examples once at startup,
against the catalogs their surfaces use, and gives a request the examples
of the client's most preferred catalog whose examples use only catalogs
the client named. A request that no examples fit, or whose format rules
can't be built, is answered in text.

The executor also stops appending a session event on every request: the
state follows from the negotiated version and the client's capabilities,
so a request that negotiates as the previous one did reuses the state.
It reads the v1.0 capabilities key from a2ui.constants, and takes
override from typing_extensions, since typing.override needs Python 3.12
and the sample supports 3.10 (the README said 3.9).

Signed-off-by: Nan Yu <nanyu@google.com>
…render

The v1.0 uploader surface defaults to the basic catalog and its FileUpload
names the file upload catalog, but a v1.0 client that named only one of
the two still got A2UI, and the tool sent the surface unvalidated (the
event converter bypasses validation for tool responses). The v0.9 surface
also failed validation: its Button checks 'required', a function that the
v0.9 file upload catalog didn't define.

A request whose client lacks a catalog that the uploader surface uses is
now answered in text. The tools validate their messages against the
negotiated catalogs and return an error to the model instead of an
invalid surface. The v0.9 file upload catalog gains the 'required'
function of the v0.9 basic catalog, which the Angular file upload client
already implements under that catalog.

The executor also stops appending a session event on every request,
drops the examples it always wrote empty, reads the v1.0 capabilities key
from a2ui.constants and takes override from typing_extensions, since
typing.override needs Python 3.12 and the sample supports 3.10.

Signed-off-by: Nan Yu <nanyu@google.com>
The agent advertised and negotiated v0.8, but its tools write v0.9
messages for anything but v1.0, so a v0.8 client got messages it can't
read. The agent now holds generators for v0.9 and v1.0 only, so a v0.8
client gets text answers, and the unused v0.8 catalog is removed.

The executor also stops appending a session event on every request,
drops the examples it always wrote empty (with its TODO), reads the v1.0
capabilities key from a2ui.constants and takes override from
typing_extensions, since typing.override needs Python 3.12 and the
sample supports 3.10 (the README said 3.9).

Signed-off-by: Nan Yu <nanyu@google.com>
Drive each executor's _prepare_session with an A2UI request and the
client's capabilities, without a model, and check the session state:
rizzcharts and file_upload_summarizer answer in text when the v1.0 client
lacks a catalog their surfaces use, rizzcharts picks the examples of the
preferred catalog and adds a session event only when the negotiation
changes, the file upload tools send valid surfaces and return an error
instead of an invalid one, and the MCP app proxy answers v0.8 in text.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 19, 2026
The v1.0 A2A extension sends the renderer capabilities under
a2uiRendererCapabilities; v0.8 and v0.9 use a2uiClientCapabilities. The
sample PRs stacked on this one (#3272, #3274) each added the constant, so
it lands here once.

Signed-off-by: Nan Yu <nanyu@google.com>
…' into refactor/samples-adk-toolset-per-request-processors

Signed-off-by: Nan Yu <nanyu@google.com>

# Conflicts:
#	python/a2ui_agent/CHANGELOG.md
#	python/a2ui_agent/src/a2ui/constants.py
…lper

__main__.py still imported get_a2ui_examples, which the per-request
executor no longer defines, so `uv run .` failed with ImportError. It
used none of the three helpers it imported.

Signed-off-by: Nan Yu <nanyu@google.com>
… tests

Signed-off-by: Nan Yu <nanyu@google.com>
…onftest.py

Signed-off-by: Nan Yu <nanyu@google.com>
…-adk-toolset-per-request-processors

Signed-off-by: Nan Yu <nanyu@google.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant