Skip to content

[dart] Extend a2ui_core for agent SDKs, limited to protocol v0.9 - #2439

Merged
polina-c merged 56 commits into
mainfrom
dart-a2ui-core
Sep 3, 2026
Merged

[dart] Extend a2ui_core for agent SDKs, limited to protocol v0.9#2439
polina-c merged 56 commits into
mainfrom
dart-a2ui-core

Conversation

@polina-c

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

Copy link
Copy Markdown
Collaborator

Contributes to #2356, #2373.

Prerequisite for #2408 (Dart a2ui_agent API), split out of it in response to review feedback asking that the agent PR carry agent work only. Everything here is a change to a2ui_core and necessary support in infrastructure.

dart/a2ui_core

Catalog

  • Catalog<C extends ComponentApi, F extends FunctionApi> — breaking, 0.1.10.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. Catalog documents round-trip through core, and a narrowed catalog renders a narrowed document with $defs/anyComponent and $defs/anyFunction narrowed to match — so the schema knowledge lives in one place and an agent's transformers stay trivial.

Capabilities, versions, errors

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 below surfaced it.

Notification on an unchanged value is fixed the same way: DataModel hands the signal a copy of a container rather than bypassing the equality check, so an observer whose own value did not change is no longer woken. Both behaviours moved into the shared suite rather than being excluded from it.

conformance/ — shared data

renderers/web_core — consuming the shared data

Each harness builds catalogs natively from the case's catalog id, because neither renderer builds a catalog from JSON Schema.

Divergences the migration surfaced

Comparing two live implementations turned up disagreements that are decisions, not formatting:

  1. List index caps. Dart lists are dense, so /items/999999999 must be rejected; JavaScript arrays are sparse, so the same write is cheap and allowed. Sparse arrays protect web_core's heap but not its serialized size, which makes this an amplification vector when sendDataModel ships the model back to the agent — filed as web_core: DataModel accepts unbounded list indices, amplifying the serialized client data model #2420 rather than changed here, since capping accepted input for a shipped renderer is a product decision, not a side effect of a test migration.
  2. Language-level differences stay excluded from the suite: prototype-pollution guards, null/undefined path arguments, undefined versus a removed key, leading-zero indices.

These want a maintainer decision.

blueprints/modules/a2ui_core.blueprint.md

The gaps that produced the review findings on #2408, written down so the next generated implementation does not repeat them:

  • Package Boundary & Non-Goalsa2ui_agent depends on a2ui_core and never the reverse; core does not own catalog narrowing, prompt generation, response parsing or capability negotiation; a core change made to unblock an agent should be minimal and land as its own reviewable unit (this PR).
  • Catalog — catalogs are immutable, and protocolVersion is a property of each catalog rather than of the process.
  • Capabilities Objects (new) — both directions are version-keyed maps with a normative JSON Schema, and the filenames differ per version (client_capabilities.json / server_capabilities.json in v0.9, renderer_capabilities.json / agent_capabilities.json in v1.0, a third spelling in v0.8).
  • Exceptions — completes the hierarchy, which listed five classes while implementations use ten, and adds the rule that parsing wire JSON raises from it rather than throwing a raw TypeError / ClassCastException.
  • Conformance Test Plan (new — core had none) — the core/ vs agent/ split is about ownership, not subject matter, and an existing suite must be looked for on every active spec branch before a new one is authored.

CI

flutter_packages_test.yml now 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

Two lines, both consequences of the above rather than agent work:

  • the a2ui_core constraint follows the major bump (^0.1.1^0.2.0);
  • the unnecessary library; directive in the stub is removed, which the new CI coverage would otherwise fail on.

Verification

All run locally:

Check Result
a2ui_core: format, dart analyze --fatal-infos, flutter test clean, 261 passed / 25 for v0.8 skipped
a2ui_agent (stub): dart analyze --fatal-infos, flutter test clean, 1 passed
web_core: yarn test, yarn lint 389 passed, 0 errors
conformance: pytest (suite self-validation) 10 passed
yarn build:all pass
prettier --check, validate_blueprints.py clean

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.
@github-project-automation github-project-automation Bot moved this to Todo in A2UI Aug 28, 2026
polina-c added a commit that referenced this pull request Aug 28, 2026
# Conflicts:
#	conformance/conformance_schema.json
#	dart/a2ui_agent/lib/a2ui_agent.dart
#	dart/a2ui_agent/pubspec.yaml

@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 updates the A2UI Core SDK by introducing the A2uiValidator for multi-stage payload validation, refactoring Catalog to support generic type parameters, and adding conformance test suites for the data model and message processor in both Dart and TypeScript. It also refines DataModel observer notifications and improves message parsing error handling. The review feedback highlights several critical areas in the Dart implementation where strict type checks (e.g., Map<String, dynamic>) could cause runtime TypeErrors when handling programmatically constructed maps (e.g., Map<dynamic, dynamic>). It is recommended to apply the suggested robust casting and pattern-matching patterns to prevent these failures.

Comment thread dart/a2ui_core/lib/src/core/data_model.dart
Comment thread dart/a2ui_core/lib/src/core/messages.dart
Comment thread dart/a2ui_core/lib/src/core/messages.dart
Comment thread dart/a2ui_core/lib/src/core/catalog.dart Outdated
Comment thread dart/a2ui_core/lib/src/validation/validator.dart
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.
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.
@polina-c
polina-c requested review from andrewkolos and nan-yu August 28, 2026 21:24
@polina-c
polina-c requested a review from nan-yu September 3, 2026 17:40
Comment thread conformance/core/catalog.yaml Outdated
url: {type: string}
required: [component, url]
args:
allowed_components: [Text]

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.

Why do we need allowed_components in the Catalog.catalog_schema method? It is supposed to be a concept in the agent when trying to remove some components to reduce input tokens for better inference performance.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You're right, and it's removed. Pruning is an agent concern and it already has its own action — test_with_pruning_components_v09 covers exactly what the case did, including anyComponent narrowing down to the components that survive. Borrowing allowed_components into catalog_schema conflated two things and added no coverage.

catalog_schema now just parses a document and rebuilds it: same components and functions, metadata preserved, every local reference resolving, and rebuilding twice giving the same document. args is gone from CatalogSchemaTest too.

No library change either way — allowed_components was never a parameter of Catalog.catalogSchema; the case pruned through copyWith before rebuilding.

Comment thread dart/a2ui_core/lib/src/core/catalog.dart
}) : groupModel = SurfaceGroupModel<T>() {
}) : validator =
validator ??
A2uiValidator<T, FunctionImplementation>(catalogs: catalogs),

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 problematic. It takes all catalogs to initialize the validator. However, we should use the particular catalog from the messages to run validation.

A message processor defines a list of catalogs to support all surfaces and components. However, a component is only from one particular catalog.

For example,

  • component_1 in surface_1 uses catalog1.
  • component_2 in surface_2 uses catalog2.

We should use catalog1 to validate component_1, and use catalog2 to validate component_2.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The constructor takes all catalogs, but that map is not what components are validated against. Every call site passes the surface's own catalog: validateTheme(message.theme, catalog) on create, and validateComponent(compJson, surface.catalog) and validateComponentBatch(..., surface.catalog) on update. Each SurfaceModel stores the catalog named at createSurface, so your example already behaves as you describe.

I checked it with two catalogs and two surfaces:

s1 (cat1) + Alpha: ACCEPTED     s2 (cat2) + Beta:  ACCEPTED
s1 (cat1) + Beta:  REJECTED     s2 (cat2) + Alpha: REJECTED

Added that as a test, since nothing was pinning it.

But, you were right about the standalone validator, though. A2uiValidator has no surface state, and v0.9 puts catalogId on createSurface only, so for a payload that just updates a surface it could not tell which catalog applied. It picked the sole catalog when there was one and otherwise skipped those components and returned success. A bogus component was rejected with one catalog and silently accepted with two.

Fixed two ways: validate now takes an optional surfaceCatalogs map naming the catalog per surface id, and when it still cannot tell it throws A2uiCatalogError instead of skipping. Reporting a payload valid that nothing checked was the worse failure.

This is also a good argument for your other suggestion about agents driving MessageProcessor — the processor knows the mapping already, so the question does not arise there.

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.

Instead of passing a map of available catalogs to every validation method, what about scoping A2uiValidator to a single catalog?

Under this model, the MessageProcessor would instantiate a fresh validator instance scoped to the target catalog every time it processes incoming messages.

My reasoning comes from our message-handling lifecycle. While the validator is structurally designed to handle messages containing multiple surfaces, Line 293 in message-processor.ts reveals that the processor actively blocks the mixing of multiple update types within a single batch.

Since MessageProcessor already filters and splits messages down to specific, surface-isolated payloads before invoking the validation step, A2uiValidator only ever needs to validate against one active catalog context. Transitioning it to a single-catalog scope would significantly simplify our signature contracts and dependency passing.

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 requires updating MessageProcessor first to block mixed update types. As you mentioned, you'd like to get this PR in to unblock other work. I'm okay with addressing them as followups.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, will address.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

A2uiValidator is scoped to many catalogs in the blueprint.

Updated blueprint to scope it to single catalog and implemented: #2538

@polina-c
polina-c requested a review from nan-yu September 3, 2026 19:28
@polina-c
polina-c merged commit 80a45c1 into main Sep 3, 2026
41 checks passed
@polina-c
polina-c deleted the dart-a2ui-core branch September 3, 2026 19:45
@github-project-automation github-project-automation Bot moved this from Todo to Done in A2UI Sep 3, 2026
diegolopezrm added a commit to diegolopezrm/a2ui that referenced this pull request Sep 3, 2026
`dart format --set-exit-if-changed` over dart/ fails on
validator_test.dart and message_processor_conformance_test.dart, which
arrived with a2ui-project#2439. fix_format.sh formats samples/client/flutter and
renderers/flutter, never dart/, so nothing has been holding these
packages to a format until the job this PR adds.

Pure `dart format` output, with the same result under
--language-version=3.10, so it is not a language-version difference.
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.

3 participants