Skip to content

Implement agent 1.0. - #3242

Merged
polina-c merged 28 commits into
mainfrom
agent-impl
Oct 16, 2026
Merged

polina-c merged 28 commits into
mainfrom
agent-impl

Conversation

@polina-c

@polina-c polina-c commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Contributes to: #2356

Contributes to: #3100

Summary

#3233 added the protocol v1.0 API to the Dart agent SDK, with stubs that threw UnimplementedError. This PR implements them, so a2ui_agent reads and writes v1.0 in both inference formats (direct JSON and Express). The shared conformance suites under conformance/agent now run as written in v1.0, as well as lowered to v0.9, and the Dart harnesses no longer skip any case as a known gap.

It also makes streaming direct JSON cost time in proportion to the payload: parseChunk used to read and check the whole block again on every chunk, which took 1.36 s for a 6.4 KB payload in 16-byte chunks and now takes about 0.1 s. See Performance.

Changes in a2ui_core

  • MessageProcessor.forAgent checks a callRendererFunction instead of throwing (processor.dart). The surface must exist, the catalog the call names must be held and target the message's version, and that catalog must declare the function, let the agent call it, and accept its arguments. A function with requiresUserActivation is accepted only when processMessages is given isUserActivated. Each of these throws A2uiValidationError, since a renderer answers each with INVALID_FUNCTION_CALL.
  • PayloadValidator.validateFunctionCall (validator.dart) checks a call written on its own, such as the callFunction of a callRendererFunction. The call, and each call nested in its arguments, is checked against the catalog its catalogId names, and may hold only the fields FunctionCall allows. A catalogId that is not a string is rejected. forAgent and the agent's MessageReader both use it.
  • PayloadValidator.validateFunctionArgs is public (validator.dart), so the agent can check a call nested in a component against the catalog the call names rather than the component's.
  • checkDuplicateComponentIds is exported (component_graph.dart), with an earlier argument for the ids of earlier batches, so the agent can check one message's components in several batches as they stream.
  • A MessageProcessor whose validationConfig allows dangling references, and orphan components or a missing root, checks an updateComponents without walking the whole surface (component_model.dart). The surface's component model keeps a bound on each component's chain of references (_GraphBound), and walks only the components an update reaches when the bound shows there is no cycle and no chain past the depth limit. Otherwise it walks the whole surface as before. The bound holds only while nothing has changed the model since the processor applied the batch it was found for.
  • A processor skips the composition checks when no catalog its surface draws on declares allowedParents or allowedChildren (processor.dart), rather than building every component's JSON for each update.

Changes in a2ui_agent

Direct JSON

  • A shared MessageReader (message_reader.dart) checks each v1.0 component and function call against the catalog it names, else its surface's catalog. For a surface the payload does not create, a component that names no catalog is checked against the first catalog that declares it. The parser reads with catalogs linked by linkCatalogs, so a cross-catalog $ref from #3230 resolves.
  • A PayloadChecker (payload_checker.dart) applies the messages for the surfaces a payload creates through MessageProcessor.forAgent. compile (parser.dart) then rejects a message that comes before its surface's createSurface, and a created surface with no root, a reference to a missing component, a component the root does not reach, or a cycle. A surface the payload only updates may hold components from earlier responses, so those checks skip it, as in the Python SDK.
  • The streaming parser holds a v1.0 component back until its object closes, since its catalogId may come last, and checks what has arrived, so a cycle is never emitted. An updateComponents whose closed components fail the check raises on the chunk that closes them. A string cut between the two halves of an escaped surrogate pair is cut before the pair rather than healed with U+FFFD (json_reader.dart).
  • The streaming parser reads and checks only what each chunk adds:
    • _StreamedChecks (parser.dart) keeps one PayloadChecker, and the surfaces read so far, for the whole block. A message is read and checked once, when it closes. A growing updateComponents is checked only for its new components, and for its last one again when components are not held back, with duplicate ids checked across the batches. A growing updateDataModel is checked again whole when it changes.
    • readPartialMessages takes a PartialReadCache (json_reader.dart): a list or map that closed is not read again, and a list still open is read on from its last item that closed (L308-L371). It reads the block in place between two offsets rather than a copy.
    • The scan for the close tag goes on from where the last chunk's scan stopped (_BlockScanner), and a trailing markdown fence is found by reading back from the end (_trailingFenceStart) rather than with a regular expression that tried every offset of the block.

Express

  • The compiler (compiler.dart) writes the v1.0 message shapes: createSurface carries the components and data model, and bindings and calls use @path and @call. A component, function call or check names its catalog with a catalogId argument, which a name declared by several catalogs needs, and which must be a string literal (L766-L788). A function call written as a statement of its own compiles to a callRendererFunction (L433-L483), with the id call_<n> or the one a functionCallId argument gives, and may name the variables of the surface it is written in, the default surface included (L171-L176). Two calls of a block may not share an id (L103-L124), and an inline component may not take an id another component has (L972-L986). The output goes through the same MessageReader as direct JSON.
  • The decompiler (decompiler.dart) writes catalogId only where the compiler could not tell the catalog from the name alone, and functionCallId only when the id is not call_<n>. A call that belongs to no active catalog throws A2uiValidationError rather than being written as a map the compiler rejects. It writes a single check without brackets, a multiline string with backslashes as a raw triple-quoted string, and an updateDataModel below the root as assignments at its path, as conformance/agent/express/decompiler.yaml expects.
  • The prompt (prompt_generator.dart) explains how to name a catalog when several are active and lists the names that need one. When callRendererFunction is allowed, it lists the functions the agent may call. It also carries each catalog's instructions, with their JSON examples rewritten in Express, and each function's description (L331-L355).

Request processor

  • validatePayloads (validation.dart) throws A2uiValidationError for a v1.0 component or call that names a catalog that is not active, then runs the payload through MessageProcessor.forAgent. It skips a component's metadata, as the processor does.

Changes in a2ui_flutter

  • video_player is ^2.11.1 rather than ^2.14.1. Version 2.14.1 needs Dart 3.12 and Flutter 3.44, above the package's declared Dart 3.10 and Flutter 3.38, and 2.11.1 is the oldest release that genui ^0.8.0, another package of the workspace, accepts.

Performance

Streaming a payload of one createSurface and one updateComponents with a Column and N Text children, in 16-byte chunks, on a MacBook:

Components Payload Before After compile
100 6.4 KB 1.36 s 0.10 s 0.02 s
1600 105 KB 0.29 s 0.07 s
3200 214 KB 0.64 s 0.13 s

Before, each chunk read the whole block again, replayed every message through a new MessageProcessor, and ran a regular expression over the block. Doubling the payload now about doubles the time; most of the 0.10 s for 100 components is the first run's warm-up. #3285 proposes performance tests in CI that would catch a regression like this one.

Decisions to review

  • allowedCallers is enforced: a response may call only a function marked agentOnly or rendererOrAgent. The default is rendererOnly, so the v1.0 test catalogs set rendererOrAgent on the functions the tests call. parseResponse enforces it; a parser's compile and parseChunk check only a call's arguments, as the README says.
  • A v1.0 parser always holds streamed components back. bufferIncompleteComponents now applies only to catalogs before v1.0.
  • In every version, a streamed message other than updateComponents and updateDataModel is emitted once, after its object closes, and the messages after it wait until then (parser.dart). Before, a createSurface or callRendererFunction was emitted while it grew, so a renderer could run a call twice, the first time with partial arguments.
  • A streamed message that grows is emitted again with the component maps it emitted before, as the same objects, rather than with new copies.
  • The graph check only skips the walk of the whole surface when a bound proves the walk would pass, so it never changes which updates fail or with which error. It applies to a processor that allows dangling references and orphans or a missing root, as the agent's checks of a payload still arriving do; a stricter config walks the whole surface as before. A catalog that declares composition constraints still has them checked against the whole surface on every update, which keeps their error order and rule precedence as they were.
  • Direct JSON compile checks the surfaces a payload creates whole. The basic catalog example 31_incremental-dashboard.json leaves its loading placeholders unreachable, so specification_round_trip_test.dart compiles its later updates as payloads of their own, as later responses would send them.
  • The Express prompt has a rule for function calls written as statements of their own, and it names the functions that may be called this way. The Python prompt has no rule for them.
  • The Express compiler rejects two standalone calls with the same functionCallId. The Python compiler gives both the same id.
  • A check's default message is now Required check failed, without the trailing period, as in Python, TypeScript and the suite.
  • An updateDataModel below the root decompiles to assignments that compile to an update of the root, as the suite expects, which replaces the rest of the data model. An update of a list item, such as /restaurants/3 in 00_incremental.json, still throws, since the assignment would turn the list into a map.
  • Four cases in multi_catalog_formats.yaml put a function call in Chart.caption, which custom_catalog_v1_0.json types as a plain string. A renderer would reject the call there, and so does this SDK. The cases now place the call in Text.text, a DynamicString, and test the same catalog resolution. The inline catalogs of two cases also referenced common.json, which after feat(agent,core): support cross-catalog $ref resolution across peer catalogs #3230 is an unknown document that a2ui_core rejects. They now reference common_types.json. The Python and TypeScript SDKs pass the edited suite.

Testing

  • flutter test: a2ui_agent 1021 passed and 70 skipped, a2ui_core 1798 passed and 5 skipped, a2ui_flutter 309 passed, the last also with video_player 2.11.1, and a2ui_cli 35 passed. The skips are v0.8 cases and cases v0.9 cannot state.
  • protocol_v1_0_test.dart covers negotiation, both formats with one catalog and with several, streaming, including a repeated id and a cycle that a later chunk closes, and A2uiRequestProcessor. processor_for_agent_test.dart covers the callRendererFunction check. The v1.0 group of specification_round_trip_test.dart round-trips every v1.0 basic catalog example through direct JSON, and all but 00_incremental.json through Express.
  • json_reader_test.dart reads every start of several payloads with and without a PartialReadCache and expects the same messages. component_graph_bound_test.dart applies random batches to two component models, one that keeps the bound and one that walks the whole graph every time, and expects the same verdict and error for every batch; it fails when the bound skips a step.
  • flutter analyze --fatal-infos and dart format are clean for the four packages, and Prettier 3.8.4 is clean for the changed Markdown and YAML.
  • The Python agent conformance tests for multi_catalog_formats.yaml pass (34 cases), and so do the TypeScript a2ui_agent tests.
  • The e2e tests pass against Gemini for v0.9 and v1.0, including direct JSON read as it streams.

Outside this PR

  • a2ui_agent uses a2ui_core API that is not released yet. Publishing it needs an a2ui_core release and a higher constraint than ^0.3.0.
  • a2ui_core declares Dart 3.5, but it depends on json_schema_builder ^0.1.7, and every release from 0.1.5 needs Dart 3.10, so the package resolves on Dart 3.10 and later only, as a2ui_agent and a2ui_flutter do. a2ui_core declares Dart 3.5 but resolves only on Dart 3.10 and later #3289 tracks the fix.

🤖 Agent-generated

@github-project-automation github-project-automation Bot moved this to Todo in A2UI Oct 15, 2026
@polina-c polina-c changed the title Agent impl Implement agent 1.0. Oct 15, 2026

@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 implements the A2UI protocol v1.0 across the Dart SDK, including full support for compiling, decompiling, and validating v1.0 messages in both direct JSON and Express formats, as well as enabling multi-catalog resolution and standalone renderer function calls. While the implementation is comprehensive and well-tested, the reviewer correctly identified several critical syntax errors in both the compiler and test files where invalid null-aware map insertion syntax (e.g., 'catalogId': ?catalogId) was used. These syntax errors will prevent compilation and must be resolved using Dart's collection if elements.

polina-c and others added 7 commits October 15, 2026 16:03
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: Polina Cherkasova <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: Polina Cherkasova <[email protected]>
@polina-c polina-c mentioned this pull request Oct 16, 2026
24 tasks done
@polina-c
polina-c requested a review from gspencergoog October 16, 2026 01:44

@gspencergoog gspencergoog 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.

This pull request implements A2UI protocol v1.0 across dart/a2ui_agent and adds agent-side callRendererFunction validation in dart/a2ui_core. Both the Direct JSON and Express inference formats now support v1.0 message shapes, per-component and per-call catalogId resolution across multiple catalogs, standalone function calls compiled to callRendererFunction, catalog instructions and function descriptions in Express prompts, and v1.0 component buffering during streaming. The change also removes v1.0 skips across the conformance and e2e test suites, updates conformance/agent/multi_catalog_formats.yaml to use common_types.json and Text with percent, and lowers the video_player constraint in dart/a2ui_flutter/pubspec.yaml to match the package's declared Flutter 3.38 floor.

The review panel verified two high-severity validation edge cases, three medium-severity issues (an Express ordering bug when a standalone call precedes default-surface assignments, documentation drift around allowedCallers in streaming, and per-chunk PayloadChecker replay overhead), and one low-severity changelog ambiguity.

🤖 Agent-generated

Comment on lines +768 to +772
if (arg case MapNode(
entries: {'catalogId': LiteralNode(value: final String catalogId)},
) when arg.entries.length == 1) {
return ([...check.args]..removeAt(index), catalogId);
}

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.

_checkArguments only strips {catalogId: ...} when its value is a String literal. A non-string catalogId stays in written as a positional argument instead of failing as a bad catalogId. Checks with a non-string catalogId either fail with a misleading argument-count error or pass {catalogId: ...} as a positional parameter.

Suggested change
if (arg case MapNode(
entries: {'catalogId': LiteralNode(value: final String catalogId)},
) when arg.entries.length == 1) {
return ([...check.args]..removeAt(index), catalogId);
}
if (arg case MapNode(
entries: {'catalogId': final ExpressNode node},
) when arg.entries.length == 1) {
if (node case LiteralNode(value: final String catalogId)) {
return ([...check.args]..removeAt(index), catalogId);
}
throw A2uiValidationError(
"Argument 'catalogId' of ?${check.name}(...) must be a string "
'literal.',
);
}

Repro test

test('check rejects non-string catalogId directly', () {
  final catalog = CatalogDefinition.fromJson({
    'catalogId': 'basic',
    'a2uiVersion': 'v1.0',
    'components': {
      'TextField': {
        'type': 'object',
        'properties': {
          'component': {'const': 'TextField'},
          'label': {'type': 'string'},
          'value': {'type': 'string'},
          'checks': {'type': 'array'},
        },
        'required': ['component', 'label'],
      },
    },
    'functions': {
      'required': {
        'parameters': {
          'type': 'object',
          'properties': {'value': {}},
          'required': ['value'],
        },
        'returnType': 'boolean',
      },
    },
  });
  final compiler = ExpressCompiler([catalog]);
  expect(
    () => compiler.compile(
      'root = TextField("Name", \$/name, checks=?required({catalogId: 123}))',
    ),
    throwsA(
      isA<A2uiValidationError>().having(
        (e) => e.message,
        'message',
        contains('catalogId'),
      ),
    ),
  );
});

🤖 Agent-generated

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.

Fixed as suggested in _checkArguments. ?required({catalogId: 3}) now fails with "Argument 'catalogId' of ?required(...) must be a string literal.", the same message a call's catalogId keyword argument gives. Test.

🤖 Agent-generated

);
case ExpressionStatement(value: final CallNode call)
when _v1 && !_declaresComponent(call):
operations.add(_StandaloneCall(call, current));

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.

When a standalone function call appears before any surface(...) statement, _operations passes current (null) to _StandaloneCall. Subsequent assignments on default_surface create a separate _Surface instance. A standalone function call at the top of an implicit default surface block fails to resolve variables assigned later in that block.

         case ExpressionStatement(value: final CallNode call)
             when _v1 && !_declaresComponent(call):
+          current ??= _Surface(defaultSurfaceId, _surfaceHelper(null));
           operations.add(_StandaloneCall(call, current));
         case ExpressionStatement(value: CallNode(:final String name)):
@@ -188,4 +189,4 @@
         case AssignmentStatement():
-          if (current == null) {
-            current = _Surface(defaultSurfaceId, _surfaceHelper(null));
+          current ??= _Surface(defaultSurfaceId, _surfaceHelper(null));
+          if (!operations.contains(current)) {
             operations.add(current);
           }

Repro test

test('standalone call resolves variable assigned later on default surface', () {
  final catalog = CatalogDefinition.fromJson({
    'catalogId': 'basic',
    'a2uiVersion': 'v1.0',
    'components': {
      'Text': {
        'type': 'object',
        'properties': {
          'component': {'const': 'Text'},
          'text': {'type': 'string'},
        },
        'required': ['component', 'text'],
      },
    },
    'functions': {
      'openUrl': {
        'parameters': {
          'type': 'object',
          'properties': {
            'url': {'type': 'string'},
          },
          'required': ['url'],
        },
        'returnType': 'void',
      },
    },
  });
  final compiler = ExpressCompiler([catalog]);
  final messages = compiler.compile('''
openUrl(target)
target = "https://example.com"
root = Text("Hi")
''');
  expect(messages, hasLength(2));
});

🤖 Agent-generated

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.

Fixed as suggested in _operations. A standalone call before any surface(...) line now shares the default surface with the assignments after it, as it already did after an explicit surface(...) line. The default surface is still created only on its first assignment, so a block of calls alone emits no empty createSurface. Test.

🤖 Agent-generated

Comment thread dart/a2ui_agent/README.md Resolved
break;
// The block is not whole yet, so a message for a surface it creates later
// waits for compile, and so do the checks of a whole surface.
final checker = PayloadChecker(reader.catalogs, reader.version);

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.

_emitReady constructs a new PayloadChecker and MessageProcessor.forAgent on every chunk and replays all messages read so far. Disabling checker.add cut 16-byte chunk streaming of a 6.4 KB payload from 5.1 s to 1.8 s. Token-sized streaming chunks multiply validator initialization and message replay cost, taking seconds of CPU where parseResponse takes 20 ms.

Reuse validator instances across chunks or validate only messages that changed since the previous chunk.

Repro test

final String response = block([
  {'version': 'v1.0', 'createSurface': {'surfaceId': 's', 'catalogId': basic.id}},
  {'version': 'v1.0', 'updateComponents': {'surfaceId': 's', 'components': [
    {'id': 'root', 'component': 'Column', 'children': [for (var i = 0; i < 100; i++) 'c$i']},
    for (var i = 0; i < 100; i++) {'id': 'c$i', 'component': 'Text', 'text': 'Item number $i'},
  ]}},
]);
final Parser parser =
    const DirectJsonFormatFactory().createFormat([basic]).createParser();
final sw = Stopwatch()..start();
for (var i = 0; i < response.length; i += 16) {
  parser.parseChunk(response.substring(
      i, i + 16 > response.length ? response.length : i + 16));
}
print('stream=${sw.elapsedMilliseconds} ms');

🤖 Agent-generated

@polina-c polina-c Oct 16, 2026 •

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.

TL;DR: Before, every chunk read the whole block again and replayed all its messages through a new MessageProcessor, so the cost of streaming grew with the square of the payload. Now each chunk reads and checks only what it adds, and a2ui_core no longer walks the whole surface graph on every update.

  • Your repro (6.4 KB in 16-byte chunks):
    • before: ~ 1.36 s
    • now: ~ 0.1 s
  • Larger payloads now: 105 KB in 0.29 s and 214 KB in 0.64 s. Doubling the payload about doubles the time.

Details below.


I measured your repro (6.4 KB, 16-byte chunks): 1.36 s in total. Replaying the messages through checker.add took 860 ms and reading them again 393 ms, while constructing PayloadChecker took 6 ms, so reusing validator instances alone would have saved little.

parseChunk now reads and checks only what each chunk adds:

  • _StreamedChecks keeps one PayloadChecker, and the surfaces read so far, for the whole block. A message is read and checked once, when it closes. A growing updateComponents is checked only for its new components, with duplicate ids checked across the batches.
  • readPartialMessages takes a PartialReadCache, so a list or map that closed is not read again, and a list still open is read on from its last item that closed. The block is read in place rather than copied, and the scan for the close tag and the check for a trailing markdown fence no longer read the whole block.
  • In a2ui_core, MessageProcessor walked the whole surface graph on every updateComponents. SurfaceComponentsModel.checkComponentsUpdate now keeps a bound on each component's chain of references and walks only what an update reaches when the bound shows the surface has no cycle and no chain past the depth limit. Otherwise it walks the whole surface as before, so the same updates fail with the same errors.

Your repro now takes about 0.1 s, and the time grows with the payload rather than with its square: 1600 components (105 KB) take 0.29 s and 3200 components (214 KB) 0.64 s. Tests cover a repeated id and a cycle that a later chunk closes, which must still fail on that chunk. json_reader_test.dart reads every start of several payloads with and without the cache, and component_graph_bound_test.dart checks the bound's verdict against the full walk on random batches.

I filed #3285 for performance tests in CI that would catch a regression like this one.

🤖 Agent-generated

Comment thread dart/a2ui_agent/CHANGELOG.md Resolved
@gspencergoog

Copy link
Copy Markdown
Collaborator

Regarding the "Decisions to review" section in the PR description, the protocol choices look solid.

Two follow-up questions on Decision 1 (allowedCallers) and the boundary between a2ui_core and a2ui_agent:

  1. A2uiRequestProcessor.parseResponse checks allowedCallers and requiresUserActivation through MessageProcessor.forAgent, whereas ExpressParser.compile and DirectJsonParser (compile, parse, and parseChunk) use MessageReader._checkV1 and ExpressCompiler.callRendererFunction, which skip both checks. ExpressFormat.createParser().compile(...) therefore compiles a standalone call to a rendererOnly function, and DirectJsonParser.parseChunk emits it before parseResponse runs. Should MessageReader._checkV1 (or ExpressCompiler.callRendererFunction) enforce the same caller and user-activation checks as MessageProcessor.forAgent?
  2. In blueprints/modules/a2ui_core.blueprint.md (Section 3C), PayloadValidator is scoped to a single catalog, while MessageProcessor owns multi-catalog routing. Adding validatorFor to PayloadValidator.validateFunctionCall and running a separate pass in MessageReader duplicates those routing rules and caused the divergences on nested-call catalog fallback and allowedCallers. Could MessageReader delegate its per-message checks to MessageProcessor.forAgent, or share the routing helper, so those rules live in one place?
    🤖 Agent-generated

@polina-c

Copy link
Copy Markdown
Collaborator Author

Regarding the "Decisions to review" section ... (comment)

@gspencergoog Both are fixed in 19278e4.

  1. Yes. MessageReader now passes each callRendererFunction to a new core method, MessageProcessor.checkRendererFunctionCall. It runs the same check as forAgent in processMessages, allowedCallers included, except that it does not look up the surface, since a call may name a surface an earlier response created. Express compile and direct JSON compile now reject a call to a rendererOnly function. parseChunk holds the call back and throws when the block closes, so the call is never emitted. Tests: Express, and direct JSON with your repro.

    A parser has no user activation, so it rejects a function with requiresUserActivation, as parseResponse does. catalog_definition.json requires such a function to be rendererOnly anyway.

    This broke three shared conformance cases, which compiled standalone calls to openUrl and percent. Both functions are rendererOnly in the shared fixtures. The cases now call notify from a new fixture, agent_functions_catalog_v1_0.json, which the agent may call. The cases are test_compile_express_standalone_function_call, test_express_compile_standalone_function_catalog_override and test_decompile_express_renderer_function_call. Python passes all three, and TypeScript already lists them in KNOWN_GAPS.md. I did not add the rejection to the shared suite, because the Python and TypeScript compilers don't check callers.

  2. Yes, for function calls. The routing now lives in MessageProcessor alone:

    1. PayloadValidator checks against one catalog again. validateFunctionCall and its validatorFor callback are gone. The new validateFunctionCallFields checks the fields of one call against one catalog.
    2. One walk, _validateNestedFunctionCalls, checks the calls nested in a component's properties and in a standalone call's arguments. Only the resolver differs. A component uses SurfaceModel.resolveCatalog. A standalone call uses the catalog a nested call names, or else the call's own catalog, which stands in for a surface default (_checkFunctionCall).
    3. MessageReader no longer checks calls itself.

    MessageReader still resolves the catalog of a component on a surface the payload does not create: the component's own catalog, else the first catalog that declares it. That rule can't move to forAgent, because the processor holds no such surface, and the surface's default catalog was sent in an earlier response. For the surfaces the payload creates, PayloadChecker runs the components through forAgent.

Other cleanups in the same commit, none of which changes behavior:

  1. MessageReader dropped v0.9 checks that AgentToRendererMessage.parseAll or PayloadValidator.validateComponent already make before it runs: the envelope and body field check, its own walk over nested function calls, the missing catalogId of a createSurface, and an empty components list. _checkV0_9 is what is left.
  2. Direct JSON compile checks a whole payload with one call, PayloadChecker.check, which builds the checker, applies the messages, checks each created surface and releases the surfaces. The created argument, createdBy and finish are no longer public (caller).
  3. readPartialMessages builds its reader in one local function, used for the text as written and with straightened quotes (json_reader.dart).
  4. The Express prompt generator linked the catalogs, and then ExpressDecompiler linked them again. It now uses the decompiler's linked catalogs (prompt_generator.dart).
  5. The a2ui_core CHANGELOG said this PR made PayloadValidator.validateFunctionArgs public, which main already had. That entry now describes checkRendererFunctionCall and validateFunctionCallFields.

🤖 Agent-generated

@polina-c
polina-c enabled auto-merge (squash) October 16, 2026 23:45
@polina-c
polina-c merged commit 0238bca into main Oct 16, 2026
44 checks passed
@polina-c
polina-c deleted the agent-impl branch October 16, 2026 23:49
@github-project-automation github-project-automation Bot moved this from Todo to Done in A2UI Oct 16, 2026
gspencergoog added a commit to gspencergoog/A2UI that referenced this pull request Oct 16, 2026
Prepares the first publish of a2ui_core since 0.2.2 and of a2ui_agent
since 0.0.1-wip004. Neither 0.3.0 nor 0.0.1-wip005 reached pub.dev when
their pubspecs were bumped (a2ui-project#3192, a2ui-project#2890), so this folds each package's
`## Unreleased` section into that version's section instead of opening a
third one.

While folding, the entries were checked against lib/ and corrected:

- a2ui_core: `PayloadValidator.protocolVersion`, `commonTypesSchema`,
  `checkVersion`, `forVersion` and `MessageProcessor.commonTypesSchema`
  were described as removed but still exist. `DataContext.isV10`,
  `A2uiCompileError`, `Catalog.protocolVersion` as a `String?` and the
  flattened `ComponentApi.schema` were never in a published version, so
  they are described in their final form rather than as renames or
  removals. `DataContext.dispose` does not exist. The new `intl` and
  `logging` dependencies are listed. Notes about tests and tooling are
  dropped and overlong lines are wrapped.
- a2ui_agent: payload checks go through `MessageProcessor.forAgent`, not
  `Catalog.withFunctions`. Entries that revised unpublished wip005
  entries are merged into them, including the v1.0 entries from a2ui-project#3242.

No code changes. The versions in pubspec.yaml were already set.
ditman added a commit that referenced this pull request Oct 20, 2026
…n catalog_transformer.yaml

- Allow ComponentPruningTransformer and FunctionPruningTransformer to accept null for allowedComponents and allowedFunctions, treating null the same as [] (#3242).
- Add conformance test runner for conformance/agent/catalog_transformer.yaml.
ditman added a commit that referenced this pull request Oct 20, 2026
…n catalog_transformer.yaml (#3330)

* fix(ts_agent): support null allowlists in pruning transformers and run catalog_transformer.yaml

- Allow ComponentPruningTransformer and FunctionPruningTransformer to accept null for allowedComponents and allowedFunctions, treating null the same as [] (#3242).
- Add conformance test runner for conformance/agent/catalog_transformer.yaml.

* test(ts_agent): default testCase.args and use optional chaining in catalog-transformer.test.ts
@ditman ditman mentioned this pull request Oct 24, 2026
4 of 8 tasks
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