Skip to content

Define the Dart a2ui_agent API and its tests, limited to protocol v0.9 - #2408

Draft
polina-c wants to merge 31 commits into
mainfrom
dart-a2ui-agent-api-and-tests
Draft

Define the Dart a2ui_agent API and its tests, limited to protocol v0.9#2408
polina-c wants to merge 31 commits into
mainfrom
dart-a2ui-agent-api-and-tests

Conversation

@polina-c

@polina-c polina-c commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Contributes to #2356, #2373.

Prerequisite for #2439.

Prompt

Implement API and tests for Dart a2ui_agent:

  1. Define API that mostly throws unimplemented exceptions when invoked, unless implementation is trivial. Limit API and implementation to a2ui protocol of version 0.9 - methods should throw an exception for messages with other or unspecified versions.
  2. Create tests that exercise this API. The tests should be skipped with the parameter skip:
    1. At least one e2e test that validates primary use case defined in blueprint for a2ui_agent
    2. Unit tests to test cover all API endpoints

Notes:

  1. Put what belongs to a2ui_core, to a2ui_core. If it breaks a2ui_core tests, update tests and/or code of a2ui_core. Update a2ui_core API and implementation minimally, to make it possible to implement needed changes in a2ui_agent.
  2. The tests should exercise the agent SDK against the protocol schema of the basic catalog, not the catalog implemented in a2ui_core.
  3. Put data for the tests into https://github.com/a2ui-project/a2ui/tree/main/conformance, so that other packages could use them.
  4. Check tests for other packages in the repository. For tests that are applicable for a2ui_agent, move the data to https://github.com/a2ui-project/a2ui/tree/main/conformance, so that all packages use the same datasets for testing. Example of a candidate for such test: https://github.com/a2ui-project/a2ui/blob/delta/renderers/web_core/src/v0_9/state/data-model.test.ts

Sources of information:

Prerequisite

Depends on #2439Extend a2ui_core for agent SDKs, limited to protocol v0.9.

Everything this PR needs from a2ui_core lives there: the Catalog<C, F> generics, A2uiRendererCapabilities, A2uiProtocolVersion, the error categories, A2uiValidator, the core/ conformance suites, and the a2ui_core blueprint. Split out in response to this review comment so that each PR reviews as one package's work.

#2439 is merged into this branch, so this diff will shrink to agent-only once #2439 lands on main. Review #2439 first.

PR Overview

Implements the API surface described by a2ui_agent.blueprint.md for the Dart agent SDK, limited to protocol v0.9, together with the tests that describe it.

Most of the API throws UnimplementedError; the mechanical parts are implemented. Tests are written against the intended behaviour and marked skip: with a reason, so dart test doubles as the implementation checklist for the follow-up PR.

Addresses the review feedback on flutter/genui#1020: renderer capabilities, validation and catalog-document handling move to a2ui_core (now #2439); FunctionApi and FunctionImplementation are separated; BundledCatalogProvider is dropped; unit test data moves to the conformance suite.

Version limiting

Every entry point that accepts a versioned payload or a capabilities object rejects anything that is not v0.9, including payloads that omit version. That gate is real, not stubbed — it routes through A2uiProtocolVersion.fromJson from #2439.

agentCapabilities follows server_capabilities.json: a map keyed by protocol version, with each registered catalog advertised under the version its own Catalog declares rather than under one version assumed for the whole registry.

dart/a2ui_agent — the API

Layout mirrors §2 Directory & Package Structure of the blueprint.

Area Types State
Facade A2uiGenerator, A2uiRequestProcessor agentCapabilities real; createProcessor, promptSnippet, parseResponse, validateExamples stubbed
Catalogs CatalogConfig, CatalogProvider, FileSystemCatalogProvider, InMemoryCatalogProvider real
Transformers CatalogTransformer, ComponentPruningTransformer, FunctionPruningTransformer real
Parsing Parser, TextPart, RawA2uiPart, A2uiPart, RawResponsePart parts and Parser.parseResponse real; format parsers stubbed
Formats InferenceFormat(Factory), DirectJsonFormat, ExpressFormat wiring real; prompt generation, compile/decompile, streaming stubbed
Negotiation resolveCatalogs stubbed

BundledCatalogProvider is deliberately absent — nothing needs to ship with the SDK.

Alongside unit coverage of each entry point there are two end-to-end walkthroughs: the blueprint's §5 primary use case (primary_use_case_test.dart), and its "Code Example" section transcribed as runnable Dart (minimal_snippet.dart) so the published example and the API cannot drift apart.

conformance/ — shared agent data

Tests run against the published basic catalog schema, referenced by relative path rather than copied, so suites cannot drift from the spec. One suite per blueprint module:

Suite Action Covers
agent/request_processor.yaml process_request the blueprint's §5 primary use case end to end, plus three rejections
agent/catalog_resolver.yaml resolve_catalogs capability negotiation: preference order, fallback, the inline-catalog gate, two CatalogErrors and a ValidationError
agent/catalog_provider.yaml load_catalog loading a catalog document
agent/catalog_transformer.yaml prune narrowing a catalog, including the $defs unions that reference the pruned entries

resolve_catalogs is a new action: select_catalog is the pre-v1.0 single-catalog helper and is not part of the blueprint. Basic-catalog cases are also added to agent/parser.yaml.

conformance_schema.json gains the process_request and resolve_catalogs actions; agent_sdks/python/.../test_conformance.py reads the two relocated suites so the Python SDK keeps running those cases.

blueprints/modules/a2ui_agent.blueprint.md

The gaps that produced this round of review findings, written down so the next generated implementation does not repeat them:

  • §3.A Transformer Requirements (new) — transformation is agent-owned, and a transformer must narrow $defs/anyComponent / $defs/anyFunction alongside the entries it drops.
  • §3.F agent_capabilities (new) — the blueprint said nothing about what an agent advertises back. Now specified: version-keyed per the normative schema, grouped by each catalog's own version, pristine ids not transformed ones. accepts_inline_catalogs becomes a constructor parameter; it fed resolve_catalogs with no stated source before.
  • §3.F Catalog ProvidersBundledCatalogProvider removed, with the reason and the errors a provider must raise.
  • §3.G resolve_catalogs — five numbered behaviour rules, and a note that it supersedes select_catalog.
  • §5 Code Example — registered a mixed-version catalog set without comment; now says so and shows the capabilities publish step.
  • §6 Conformance Test Plan — was three lines delegating to the conformance README. Now an API→suite map, five authoring rules, and a Legacy Surfaces table.

CI

flutter_packages_test.yml now discovers packages under dart/ as well as samples/ — in #2439, since the dart/ packages were not built, analyzed or tested by any workflow before this and #2439 lands first.

Verification

All run locally, on this branch with #2439 merged in:

Check Result
a2ui_agent: format, dart analyze --fatal-infos, flutter test clean, 110 passed / 228 skipped
a2ui_core: format, dart analyze --fatal-infos, flutter test clean, 261 passed / 25 skipped
web_core: yarn test, yarn lint 389 passed, 0 errors
conformance: pytest (suite self-validation) 14 passed
Python agent SDK conformance harness 206 passed
yarn build:all pass
prettier --check, validate_blueprints.py clean

Follow-ups

Prompt generation, response parsing and streaming, capability negotiation, the EXPRESS grammar, and A2uiValidator's deep checks. Each has skipped tests describing the target behaviour.

Implements the API surface described by blueprints/modules/a2ui_agent.blueprint.md
for the Dart agent SDK, moves the pieces that belong to a2ui_core into a2ui_core,
and moves shared test data into conformance/ so every SDK is measured against one
dataset.

Most of the agent API throws UnimplementedError; the mechanical parts are
implemented. Tests describe the intended behaviour of everything still stubbed
and are marked skip: with the reason, so `dart test` doubles as the
implementation checklist.

a2ui_core
- Catalog now takes two type parameters, Catalog<C extends ComponentApi,
  F extends FunctionApi>, so agents can hold schema-only functions. Breaking.
- Adds A2uiProtocolVersion, Catalog.fromJson/catalogSchema/copyWith,
  A2uiRendererCapabilities, A2uiValidator and the conformance error categories.
- Fixes DataModel.set silently dropping a write through a primitive.

conformance/
- New core/data_model.yaml, migrated from web_core's data-model.test.ts.
- New agent/request_processor.yaml for the blueprint's primary use case.
- Basic-catalog cases added to core/catalog.yaml, agent/parser.yaml and
  agent/inference_format.yaml, referencing the published spec catalog by path.

renderers/web_core
- data-model.test.ts becomes a harness over the shared dataset; JavaScript
  specific behaviour stays local and is documented on both sides.

CI
- Adds a dart_packages job so the Dart packages are formatted, analyzed with
  --fatal-infos, and tested.

@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 introduces a shared conformance test suite for the reactive data model and request processor, alongside updates to the Dart a2ui_agent and a2ui_core packages to support protocol version gating (v0.9), schema-only catalogs, and payload validation. It also fixes a bug in DataModel.set to throw an error when writing through a primitive parent path. The review feedback highlights opportunities to improve Dart idiomaticity by leveraging compile-time exhaustiveness checking for sealed classes, enhancing runtime safety during inline catalog parsing, and optimizing repeated map lookups in catalog schema generation.

Comment thread dart/a2ui_agent/lib/src/parser/parser.dart
Comment thread dart/a2ui_core/lib/src/core/renderer_capabilities.dart
Comment thread dart/a2ui_core/lib/src/core/catalog.dart Outdated
Formatting: the Dart formatter in the SDK CI runs (3.13.1, via Flutter stable
3.47.1) disagreed with 3.12.2 on eight test files. Reformatted with 3.13.1.

Conformance: six cases added to the shared suites asserted behaviour only the
Dart SDK has, which broke the Python and Kotlin harnesses that already consume
those suites. A shared suite is a contract every implementation satisfies, so
these move back to Dart's own tests, where they were already covered:

- parse_full rejecting an unsupported or missing protocol version, and an
  unknown message type. The Python and Kotlin parsers do not validate the
  version while parsing.
- prune with allowed_functions. Function pruning is implemented by the Dart
  FunctionPruningTransformer only.
- select_catalog raising when renderer and agent share no catalog. Kotlin
  raises with a different message; Python returns no selection.

conformance/README.md now separates these "one SDK has it, others do not yet"
gaps from genuine language-level exclusions, and lists the three above.

The cases that do hold everywhere stay shared: basic catalog loading and
selection, component pruning with anyComponent narrowing, and parsing a v0.9
basic catalog payload.
Notifications bypassed the signal's equality check (`force: true`), so every
observer on a path related to a write was woken, including observers whose own
value was unchanged. Watching /a/b and then writing /a fired the observer even
though /a/b was absent before and after.

The check was bypassed because a Map or List mutated in place keeps its
identity and would compare equal. Hand the signal a copy of a container
instead: containers still compare unequal and still notify, while unchanged
primitive and absent values no longer do. This is what the web_core renderer
already does.

Verified: an unchanged descendant no longer fires; a root replacement still
wakes the root observer but not an unrelated one; an ancestor of an in-place
mutation still fires; rewriting a path with the value it already holds does not.

The two implementations now agree, so the behaviour moves into the shared
suite: core/data_model.yaml regains the notification assertion on root
replacement and gains cases for an unchanged descendant and a same-value
rewrite. The corresponding exclusion note and the web_core local test are
removed.
@polina-c
polina-c marked this pull request as ready for review August 27, 2026 20:34

@nan-yu nan-yu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Leaving my initial set of comments for now. I'll continue to review the remaining later.

Comment thread conformance/agent/inference_format.yaml Outdated

- name: test_select_basic_catalog_by_id_v0_9
description: A renderer that declares the basic catalog id negotiates to it.
action: select_catalog

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this to test the resolve_catalogs helper,
https://docs.google.com/document/d/17BuULV88IlsUStOFthGMgfeW1XMtLUyQDm3oflgXi-k/edit?pli=1&resourcekey=0-fltDWdFS4VEdDHSTzq--QA&tab=t.0#bookmark=id.im80bpefqd27?

select_catalog is the old legacy API, and is no longer available in the new blueprint.

Comment thread conformance/agent/inference_format.yaml Outdated

- name: test_load_basic_catalog_v0_9
description: Loads the published v0.9 basic catalog document from disk.
action: load_catalog

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is actually testing the loading function in the CatalogProvider. We could either make it a unit test, or put it in its own conformance suite, but definitely not belong to the inference_format.yaml.

Comment thread conformance/core/catalog.yaml Outdated
# catalog still advertises components the agent may not emit. The ref shapes
# here match `specification/v0_9_1/catalogs/basic/catalog.json`.

- name: test_prune_components_narrows_any_component_union

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The pruning function doesn't belong to a2ui-core, but in the a2ui-agent. Should we put these tests in a separate catalog_transformer.yaml test suite?

Comment thread conformance/core/message_processor.yaml Outdated
# messages differ between implementations, so `message` is matched as a regular
# expression against a substring the implementations share.

- name: test_processor_creates_surface

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

FYI, we added more tests on the v1_0 branch, https://github.com/a2ui-project/a2ui/blob/v1_0/conformance/core/message_processor.yaml.

If this PR is only about a2ui_agent, can we remove a2ui_core related changes?

/// The capabilities this agent advertises, mirroring
/// `specification/v0_9_1/json/server_capabilities.json`.
Map<String, Object?> get agentCapabilities => {
'a2uiVersions': [A2uiProtocolVersion.v0_9.jsonValue],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Those catalogs may belong to multiple protocol versions.

Comment #4 on PR 2408 asked to remove a2ui_core changes; the suite was
dropped because v1_0 already carries 51 cases under the same filename
with an incompatible case shape. Restored instead as the two cases v1_0
does not cover -- component and data model isolation between surfaces --
written in the v1_0 vocabulary so the files concatenate on merge.

Also fixes two suite headers that cited the wrong blueprint section:
catalog providers are 3.F and transformers 3.A, not 3.B and 3.C.
Prerequisite for the Dart a2ui_agent API (#2408). Everything here is a
change to a2ui_core, or to a consumer of it, split out so the agent PR
reviews as agent work only.

Catalog and capabilities
- `Catalog<C extends ComponentApi, F extends FunctionApi>` (breaking,
  0.1.1 -> 0.2.0). Agents parameterise with `CatalogFunction` (signature
  only), renderers with `FunctionImplementation`. `SchemaCatalog` aliases
  the agent shape.
- `Catalog.fromJson` / `catalogSchema` / `copyWith`, plus schema-only
  `CatalogComponent` and `CatalogFunction`, so catalog documents round
  trip through core and a narrowed catalog renders a narrowed document
  with `$defs/anyComponent` and `$defs/anyFunction` narrowed to match.
- `A2uiRendererCapabilities`, mirroring `client_capabilities.json` and
  web_core's `A2uiClientCapabilities`.
- `A2uiProtocolVersion`, and the `A2uiParseError` / `A2uiCompileError` /
  `A2uiCatalogError` / `A2uiIntegrityError` / `A2uiRecursionError`
  categories.
- `A2uiValidator`, with the v0.9 version gate implemented and the
  structural and catalog-schema checks declared but stubbed.

One bug fixed
- `DataModel.set` silently dropped a write whose parent path resolved to
  a primitive (`/user/name/first` where `/user/name` is a string). It now
  throws `A2uiDataError`, matching web_core. The shared dataset surfaced
  this. Notification on an unchanged value is fixed the same way: the
  signal is handed a copy of a container rather than bypassing the
  equality check.

Shared conformance data
- `core/data_model.yaml` (new, `data_model` action): 37 cases migrated
  from `renderers/web_core/src/v0_9/state/data-model.test.ts`.
- `core/message_processor.yaml` (new, `process_messages` action): the two
  surface-isolation cases the v1_0 branch's suite of the same name does
  not cover, written in that branch's case vocabulary so the two files
  concatenate rather than conflict when it lands.
- `conformance_schema.json` gains the two actions and the `DataError`
  category; the file's existing formatting is preserved.

web_core consumes the shared data
- `tests/conformance/harness.ts` locates `conformance/` by walking up.
- `data-model.conformance.test.ts` and
  `message-processor.conformance.test.ts` run the two suites. Both are
  additive: the hand-written `data-model.test.ts` and
  `message-processor.test.ts` are untouched.

Also
- `blueprints/modules/a2ui_core.blueprint.md`: package boundary and
  non-goals, catalog immutability and per-catalog protocol version, the
  version-keyed capabilities objects and their normative schemas, the
  complete exception hierarchy plus the rule that parsing wire JSON
  raises from it, and a conformance section (there was none).
- `flutter_packages_test.yml` discovers packages under `dart/` as well as
  `samples/`. The `dart/` packages were not built, analyzed or tested by
  any workflow before this.
- `dart/a2ui_agent` gets the two changes that exposure requires: its
  `a2ui_core` constraint follows the major bump, and the unnecessary
  `library;` directive its stub carried is removed.
# Conflicts:
#	conformance/conformance_schema.json
#	dart/a2ui_agent/lib/a2ui_agent.dart
#	dart/a2ui_agent/pubspec.yaml
@polina-c
polina-c marked this pull request as draft August 28, 2026 18:30
A renderer can need a smaller catalog for a given use case and derives
one the same way. What is agent-owned is the named transformer rules and
the config pipeline that applies them, not narrowing itself -- which is
the actual reason their conformance data belongs under agent/.
'a capabilities payload above all' said nothing precise; it is just an
example, so name it as one. Also applies prettier, which the previous
commit skipped.
Follows 562329b on dart-a2ui-core. A renderer can need a smaller
catalog for a given use case and derives one the same way. What the
agent SDK owns is the named transformer rules and the CatalogConfig
pipeline, which is the actual reason their cases live under agent/.
The per-item prose restated what the a2ui_agent blueprint already
documents. A list of what is not in core, with one pointer to where it
is specified, carries the same boundary without the duplication.
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.

2 participants