Skip to content

Conversation

@jacobsimionato
Copy link
Collaborator

@jacobsimionato jacobsimionato commented Nov 27, 2025

This PR implements the A2UI catalog negotiation process, allowing the client to declare its supported UI component catalogs and for the server to select a compatible catalog for rendering a given UI surface.

This addresses a key part of the A2UI protocol that was previously missing.

Changes

  • Catalog and UiDefinition models in packages/genui updated to include a catalogId.
  • CoreCatalogItems in packages/genui now uses the standard catalog ID.
  • GenUiManager updated to support a list of catalogs.
  • GenUiSurface now performs catalog validation. If the catalogId from a beginRendering message does not match a supported catalog, an error is logged and an empty container is rendered.
  • ContentGenerator interface updated to pass A2UiClientCapabilities. All implementations have been updated.
  • A2uiAgentConnector updated to include a2uiClientCapabilities in the A2A message metadata.
  • All examples and tests have been migrated to the new APIs.

Fixes #373 and #426

@jacobsimionato
Copy link
Collaborator Author

/gemini review

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

This pull request successfully implements the A2UI catalog negotiation process. The changes are comprehensive, updating models, managers, and connectors to support multiple catalogs and client capabilities. The introduction of catalogId and the associated validation logic in GenUiSurface are well-implemented. The migration of examples and tests to the new APIs is also thorough.

My review includes two main points:

  1. A potential regression in an example app due to a missing configuration during the API migration.
  2. A suggestion to refactor duplicated code in GenUiSurface to improve maintainability.

Overall, this is a solid implementation of a key feature for the A2UI protocol.

Comment on lines 66 to 68
final genUiManager = GenUiManager.withSingleCatalog(
catalog: travelAppCatalog,
configuration: const GenUiConfiguration(
actions: ActionsConfig(
allowCreate: true,
allowUpdate: true,
allowDelete: true,
),
),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

When migrating to GenUiManager.withSingleCatalog, the GenUiConfiguration was removed. The default configuration disables all actions (allowCreate, allowUpdate, allowDelete are false by default), which might break the functionality of this example app. This configuration should be passed to the new constructor to maintain the original behavior.

Suggested change
final genUiManager = GenUiManager.withSingleCatalog(
catalog: travelAppCatalog,
configuration: const GenUiConfiguration(
actions: ActionsConfig(
allowCreate: true,
allowUpdate: true,
allowDelete: true,
),
),
);
final genUiManager = GenUiManager.withSingleCatalog(
catalog: travelAppCatalog,
configuration: const GenUiConfiguration(
actions: ActionsConfig(
allowCreate: true,
allowUpdate: true,
allowDelete: true,
),
),
);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Don't worry about this - we don't need it

Comment on lines 65 to 78
final String catalogId =
definition.catalogId ?? CoreCatalogItems.standardCatalogId;
final Catalog? catalog = widget.host.catalogs.firstWhereOrNull(
(c) => c.catalogId == catalogId,
);

if (catalog == null) {
genUiLogger.severe(
'Catalog with id "$catalogId" not found for surface '
'"${widget.surfaceId}". Ensure the catalog is provided to '
'GenUiManager.',
);
return Container();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic to find the correct catalog based on the catalogId is duplicated here and in the _dispatchEvent method (lines 136-148). To improve maintainability and reduce code duplication, this logic could be extracted into a private helper method within the _GenUiSurfaceState class.

For example, you could add a method that returns both the catalog and its ID:

(Catalog?, String) _findCatalogForDefinition(UiDefinition definition) {
  final String catalogId =
      definition.catalogId ?? CoreCatalogItems.standardCatalogId;
  final Catalog? catalog = widget.host.catalogs.firstWhereOrNull(
    (c) => c.catalogId == catalogId,
  );
  return (catalog, catalogId);
}

Then, you can use this helper in both build and _dispatchEvent to simplify the code and logging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's fix this

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

This pull request successfully implements the A2UI catalog negotiation feature, a crucial part of the protocol. The changes are extensive, touching models, the GenUiManager, the UI surface rendering, and various backend connectors. The introduction of GenUiManager.withSingleCatalog is a thoughtful touch for backward compatibility, and the new tests for catalog validation are excellent.

I've identified a few issues:

  • A regression in an example app where a configuration was accidentally removed, which will likely break its functionality.
  • A couple of the ContentGenerator implementations (FirebaseAiContentGenerator and GoogleGenerativeAiContentGenerator) were updated to accept clientCapabilities but don't actually pass this information to the model, which defeats the purpose of this feature for those backends.
  • A minor code duplication in GenUiSurface that could be refactored for better maintainability.

Overall, this is a solid implementation of a key feature, and with these fixes, it will be ready to merge.

@jacobsimionato jacobsimionato changed the title feat: Implement A2UI catalog negotiation Implement A2UI catalog negotiation Nov 28, 2025
if (catalog == null) {
genUiLogger.severe(
'Cannot show modal for surface "${widget.surfaceId}" because '
'a catalog was not found.',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe log the catalog ID of the catalog it was looking for?

// found in the LICENSE file.

/// The catalog ID for the standard catalog.
const String standardCatalogId = 'a2ui.org:standard_catalog_0_8_0';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be 0_8_1? And while I thought we changed the naming of the field, I thought we were still going to use URIs as the ID, no? They're nicely self-documenting.

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.

Implement clientUiCapabilities event from client to agent as part of A2UI

2 participants