Skip to content

Fix inline catalog validation and multi-surface support#831

Open
sarthak96agarwal wants to merge 1 commit intogoogle:mainfrom
sarthak96agarwal:fix/multi-surface-validator
Open

Fix inline catalog validation and multi-surface support#831
sarthak96agarwal wants to merge 1 commit intogoogle:mainfrom
sarthak96agarwal:fix/multi-surface-validator

Conversation

@sarthak96agarwal
Copy link

Fixes #796

Summary

The contact_multiple_surfaces sample agent fails A2UI schema validation when clients send inlineCatalogs in a2uiClientCapabilities. This PR fixes the validation pipeline end-to-end: catalog merging, multi-surface root tracking, incremental update handling, and client capability plumbing.

Problems

  1. Inline catalogs replaced base catalog instead of extending it — The schema manager treated inline catalogs as complete replacements, so standard components (Text, Image, Card, etc.) were rejected when used alongside custom ones (OrgChart, WebFrame).
  2. client_ui_capabilities not passed to validation — The agent validated against the default base catalog, which has no knowledge of inline components. The executor also only extracted capabilities from request DataParts, not userAction DataParts, so UI event responses were validated against the wrong catalog.
  3. Validator assumed a single root across all surfaces — The contact_multiple_surfaces agent generates multiple surfaces (e.g., contact-card and org-chart-view) with different root IDs. The validator only tracked the first root, causing false "Missing root component" errors on subsequent surfaces.
  4. Validator rejected valid incremental updates — When the agent sends a surfaceUpdate without a beginRendering (i.e., updating an existing surface), the validator falsely reported missing roots and orphaned components because it expected a complete component tree every time.
  5. OrgChart inline schema had unresolvable $ref — The client's OrgChart registration used $ref: "#/definitions/Action" which doesn't resolve in an inline catalog context.

Changes

agent_sdks/python/src/a2ui/core/schema/manager.py

  • Merge inline catalog components onto the base catalog instead of replacing it
  • Allow both inlineCatalogs and supportedCatalogIds in client capabilities (previously raised an error)
  • Use supportedCatalogIds to select which base catalog to merge onto

agent_sdks/python/src/a2ui/core/schema/validator.py

  • Track per-surface root IDs (_find_root_id_find_root_ids) returning a map of surfaceId → root_id
  • Detect initial renders (beginRendering/createSurface) vs incremental updates in each message batch
  • Skip root existence and orphan checks for incremental updates; still enforce duplicate IDs, dangling refs, self-references, and cycles
  • Hoist ref_map computation outside the per-message loop

samples/agent/adk/contact_multiple_surfaces/agent.py

  • Remove schema from system prompt (include_schema=False) since the authoritative schema arrives per-request via client capabilities
  • Add client_ui_capabilities parameter to stream() and pass it to get_selected_catalog()

samples/agent/adk/contact_multiple_surfaces/agent_executor.py

  • Extract a2uiClientCapabilities from any DataPart that has metadata, not just request parts — fixes UI events using the wrong catalog for validation

samples/agent/adk/contact_multiple_surfaces/prompt_builder.py

  • Update inline catalog test JSON to match the actual client-side schema (OrgChart with oneOf chain, inlined action)

samples/client/lit/contact/ui/custom-components/register-components.ts

  • Inline the OrgChart action schema (was an unresolvable $ref)
  • Add oneOf path-reference variant for chain property (the agent examples use { "path": "/hierarchy" }, and LLMs follow examples over schema definitions)

agent_sdks/python/tests/core/schema/test_validator.py

  • Add multi-surface validation tests (two beginRendering + two surfaceUpdate with different roots)
  • Add incremental update tests: no-root passes, orphans pass, but self-refs/cycles/duplicates still fail
  • Update existing tests to include initial render markers where full-tree validation is expected
  • All incremental update tests run for both v0.8 and v0.9

Key Design Decisions

  1. Inline catalogs are additive, not replacements. Components from inlineCatalogs are merged on top of the base catalog. This matches how prompt construction works — the LLM generates both standard and custom components in the same response.

  2. All inline catalogs from the client are merged. If a client sends multiple inline catalogs, all their components are merged onto the base. This supports clients that split custom components across multiple catalog definitions.

  3. Both inlineCatalogs and supportedCatalogIds are allowed simultaneously. Previously this raised a ValueError. Now supportedCatalogIds selects the base catalog, and inlineCatalogs extend it. This is needed because clients may support a specific base catalog AND provide additional custom components.

  4. Incremental updates skip root and orphan checks. A surfaceUpdate/updateComponents without a corresponding beginRendering/createSurface in the same message batch is treated as a partial update. Root existence and orphan detection are meaningless for partial trees. Cycles, self-references, duplicates, and dangling references are still validated.

  5. Schema removed from system prompt when accepts_inline_catalogs=True. The correct catalog is only known after client negotiation at request time. Baking the default catalog's schema into the system prompt risks showing the LLM a schema that doesn't match the negotiated catalog (e.g., agent supports 3 catalogs, system prompt shows catalog 1, client negotiates catalog 3).

  6. OrgChart chain keeps the oneOf with path-reference variant. The agent examples (multi_surface.json, org_chart.json) use "chain": { "path": "/hierarchy" }. LLMs follow examples over schema definitions, so the client schema must accept what the agent actually generates.

Validation Steps

  • All 47 validator tests pass (pytest tests/core/schema/test_validator.py -v)
  • Run the contact_multiple_surfaces agent + contact client end-to-end
  • Verify both contact card and org chart surfaces render on initial search
  • Click an org chart node and verify the update validates and renders correctly
  • Verify incremental data model updates (no full re-render) work without validation errors

Pre-launch Checklist

If you need help, consider asking for advice on the discussion board.

@google-cla
Copy link

google-cla bot commented Mar 12, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

The pull request refactors the UI catalog selection and validation logic to support multi-surface rendering and incremental updates. The _select_catalog function now merges inline catalogs with a base catalog, and the component validation (_validate_component_integrity, _validate_topology) distinguishes between initial renders (where root component and orphan checks apply) and incremental updates (where these checks are skipped). Corresponding test cases have been added or updated to reflect this new behavior. Additionally, the OrgChart component schema was updated to allow chain to be either a path reference or an array, and the action schema was detailed. A review comment suggests improving the readability and maintainability of a long JSON string in prompt_builder.py by defining it as a Python dictionary and then serializing it.

Comment on lines 89 to 92
client_ui_capabilities_str = (
'{"inlineCatalogs":[{"catalogId": "inline_catalog",'
' "components":{"OrgChart":{"type":"object","properties":{"chain":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"name":{"type":"string"}},"required":["title","name"]}},"action":{"$ref":"#/definitions/Action"}},"required":["chain"]},"WebFrame":{"type":"object","properties":{"url":{"type":"string"},"html":{"type":"string"},"height":{"type":"number"},"interactionMode":{"type":"string","enum":["readOnly","interactive"]},"allowedEvents":{"type":"array","items":{"type":"string"}}}}}}]}'
' "components":{"OrgChart":{"type":"object","properties":{"chain":{"oneOf":[{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]},{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"name":{"type":"string"}},"required":["title","name"]}}]},"action":{"type":"object","properties":{"name":{"type":"string"},"context":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"object","properties":{"path":{"type":"string"},"literalString":{"type":"string"},"literalNumber":{"type":"number"},"literalBoolean":{"type":"boolean"}}}},"required":["key","value"]}}},"required":["name"]}},"required":["chain"]},"WebFrame":{"type":"object","properties":{"url":{"type":"string"},"html":{"type":"string"},"height":{"type":"number"},"interactionMode":{"type":"string","enum":["readOnly","interactive"]},"allowedEvents":{"type":"array","items":{"type":"string"}}}}}}]}'
)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This long, single-line JSON string is difficult to read and maintain. To improve readability and reduce the chance of syntax errors, consider defining this structure as a Python dictionary and then serializing it to a JSON string using json.dumps.

  client_ui_capabilities_dict = {
      "inlineCatalogs": [{
          "catalogId": "inline_catalog",
          "components": {
              "OrgChart": {
                  "type": "object",
                  "properties": {
                      "chain": {"oneOf": [
                          {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]},
                          {"type": "array", "items": {"type": "object", "properties": {"title": {"type": "string"}, "name": {"type": "string"}}, "required": ["title", "name"]}}
                      ]},
                      "action": {"type": "object", "properties": {
                          "name": {"type": "string"},
                          "context": {"type": "array", "items": {"type": "object", "properties": {
                              "key": {"type": "string"},
                              "value": {"type": "object", "properties": {
                                  "path": {"type": "string"}, "literalString": {"type": "string"},
                                  "literalNumber": {"type": "number"}, "literalBoolean": {"type": "boolean"}
                              }}
                          }, "required": ["key", "value"]}}
                      }, "required": ["name"]}
                  },
                  "required": ["chain"]
              },
              "WebFrame": {
                  "type": "object",
                  "properties": {
                      "url": {"type": "string"}, "html": {"type": "string"}, "height": {"type": "number"},
                      "interactionMode": {"type": "string", "enum": ["readOnly", "interactive"]},
                      "allowedEvents": {"type": "array", "items": {"type": "string"}}
                  }
              }
          }
      }]
  }
  client_ui_capabilities_str = json.dumps(client_ui_capabilities_dict)

- Validator: support multiple surfaces with different root IDs by
  tracking per-surface roots instead of a single global root
- Validator: distinguish initial renders (beginRendering/createSurface)
  from incremental updates; skip root and orphan checks on updates
  while still catching cycles, self-refs, and duplicates
- Manager: merge inline catalog components onto the base catalog
  instead of using inline-only; allow both inlineCatalogs and
  supportedCatalogIds in client capabilities
- Agent: remove schema from system prompt (provided per-request via
  client capabilities); pass client_ui_capabilities through to
  catalog selection and validation
- Executor: extract a2uiClientCapabilities from all DataParts (not
  just request parts) so UI events use the correct catalog
- Client: inline OrgChart action schema (was unresolvable $ref) and
  add path-reference oneOf for chain (matches agent examples)
@sarthak96agarwal sarthak96agarwal force-pushed the fix/multi-surface-validator branch from 193b962 to 7b6f441 Compare March 12, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Inline catalog validation fails in contact_multiple_surfaces sample agent

1 participant