Repository navigation
feat(dart): stub the remaining a2ui_agent API for protocol v0.9 - #2890
Conversation
Declares the rest of the agent SDK blueprint API in dart/a2ui_agent, limited to protocol v0.9. Catalog transformers, resolveCatalogs, prompt example plumbing and Express wrap/hasFormatContent are implemented. Catalog providers, the direct JSON format, Express decompilation, prompt examples in the Express prompt and inline catalogs throw UnimplementedError.
There was a problem hiding this comment.
Code Review
This pull request expands the A2UI agent SDK for protocol v0.9 by declaring the remaining blueprint APIs (such as CatalogProvider and the direct JSON format) as stubs, introducing catalog transformers (ComponentPruningTransformer and FunctionPruningTransformer), and adding support for prompt examples. It also updates the Parser interface with new members like hasFormatContent and wrap, which are implemented for the Express parser. Feedback on the changes suggests that ExpressParser.hasFormatContent should explicitly return false if a direct JSON payload tag is present to avoid unexpected parse errors.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Polina Cherkasova <[email protected]>
jacobsimionato
left a comment
There was a problem hiding this comment.
Looks great overall. Approval so you can submit in your timezone if you feel happy enough!
| /// Writes [a2uiPayload] in this format's notation, without sentinel tags. | ||
| /// | ||
| /// The inverse of [compile]. | ||
| String decompile(List<AgentToRendererMessage> a2uiPayload); |
There was a problem hiding this comment.
Yay, symmetry and type safety!
| @override | ||
| InferenceFormat createFormat( | ||
| List<SchemaCatalog> catalogs, { | ||
| List<List<AgentToRendererMessage>> examples = const [], |
There was a problem hiding this comment.
Do we want to expose allowedMessages here too? And possibly plumb through customProgressiveKeys to the parser?
I think we should either provide full plumbing as above, or else consider dropping directly JSON streaming and just implementing it in the express compiler. Currently, JSON supports streaming and Express does not, but it makes more sense for it to be the other way around, seeing as Express is the format people will choose when they care about latency.
There was a problem hiding this comment.
Agreed on the plumbing. As written, neither option could be reached through A2uiGenerator: it only calls createFormat(catalogs, examples: ...), so allowedMessages was only settable if you built a DirectJsonFormat yourself, and progressive keys couldn't be set at all.
createFormat is the shared interface and the SDK calls it once per request, so the options go on the factory instead: DirectJsonFormatFactory({allowedMessages, progressiveKeys}). The factory passes them to each DirectJsonFormat, which passes progressiveKeys to each parser it creates. The factory is still const, so the A2uiGenerator default doesn't change. The blueprint had the same gap, so I updated §4.A in this PR to match.
On streaming, I'd rather not change it in this PR. The blueprint says Express doesn't stream on purpose: an Express block resolves references across its whole body, so a partial block names components that aren't defined yet (§4.B). Making Express stream means redesigning the format (for example, streaming statement by statement once each statement's references resolve), not just the parser. I agree that latency is the reason people pick Express, so I'll open an issue to discuss Express streaming and whether direct JSON streaming is still worth having.
|
|
||
| /// The string properties whose value a streamed payload may show before | ||
| /// the value is complete, such as the text of a `Text` component. | ||
| Set<String> get progressiveKeys => |
There was a problem hiding this comment.
I think we should just have progressiveKeys passed as an argument, rather than having a default and then an override (customProgressiveKeys).
Any default value is probably tied to a specific catalog, e.g. the basic catalog, so we want to avoid it.
There was a problem hiding this comment.
Agreed. Any default set would be a guess about one catalog (text in basic), and inference formats have to stay catalog-agnostic. The schema has no marker for which properties hold prose, so we can't derive a set from it either.
The parser now takes progressiveKeys directly, defaulting to the empty set. customProgressiveKeys and the getter that threw UnimplementedError are gone. The conformance suite already treats an empty set as "healing off" (response_streaming.yaml), so the default is defined behaviour and not tied to any catalog. I kept an empty default rather than making it required, so the default DirectJsonFormatFactory() still works. Streaming then just holds a message back until its strings are complete. I'm happy to make it required if you'd rather callers always choose. I made the same change to the blueprint (§4.A).
| /// one request. | ||
| abstract class PromptGenerator { | ||
| const PromptGenerator(); | ||
| const PromptGenerator(this.catalogs, {this.examples = const []}); |
There was a problem hiding this comment.
Does the abstract class need to expose Catalogs, Examples, Constructor etc? Could it just expose generate() and then each inference format implementation can have whatever constructor it wants? I don't expect application code to need to access catalogs, examples etc from the PromptGenerator. If anything, I'd expect them exposed on InferenceFormat.
There was a problem hiding this comment.
Agreed. App code only calls generate(), and each format builds its generator from different inputs (direct JSON also takes allowedMessages). PromptGenerator is back to const PromptGenerator() plus String generate(), as it is on main. ExpressPromptGenerator and DirectJsonPromptGenerator now own catalogs and examples. This also drops the breaking change for custom PromptGenerator subclasses from the CHANGELOG.
The blueprint had catalogs and examples on the base class (§3.B), so I updated it too. The rule that examples render in order and carry no label is now prose that applies to every format's generator. Dart is the first SDK to implement this part of the blueprint (the Python SDK still has the older template-method PromptGenerator), so nothing else needs to change.
I didn't add them to InferenceFormat, since nothing needs them there yet. The processor already holds its catalogs and examples.
|
|
||
| @override | ||
| InferenceFormat createFormat( | ||
| List<SchemaCatalog> catalogs, { |
There was a problem hiding this comment.
Nit: Can we call SchemaCatalog CatalogApi instead? This would mirror ComponentApi and FunctionApi.
I know it's not in this PR specifically, but I think it would be more consistent.
There was a problem hiding this comment.
Agreed, CatalogApi is the better name. Catalog<ComponentApi, FunctionApi> is a catalog made only of APIs, so it belongs with ComponentApi and FunctionApi. SchemaCatalog names it after how it's stored instead. It's also clearer next to FunctionImplementation: a renderer's catalog carries implementations, an agent's catalog carries APIs.
I'm keeping it out of this PR because it's bigger than a2ui_agent:
SchemaCatalogis defined and exported bya2ui_core, which is published at 0.2.x, and about 30 files acrossa2ui_coreanda2ui_agentuse it. Renaming it needsCatalogApiadded, all uses moved over, andSchemaCatalogkept as a@Deprecatedalias for a release.- The core blueprint doesn't name this type at all; it only writes
Catalog<TComponent, TFunction>. The TypeScript design doc usesSchemaCatalog, and the other SDKs write the generic type out. If only Dart renamed it, the languages would drift further apart. The name should go in the blueprint first so every SDK uses the same one.
Filed #2900. It defines CatalogApi in a2ui_core.blueprint.md and uses it in a2ui_agent.blueprint.md, then updates Dart (with the deprecated alias), TypeScript, Python, Swift and Kotlin, plus the docs.
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.
Contributes to #2356. Follows #2747 and #2757.
Prompt
Create stubbed API to enable the 0.9 protocol, following the agent SDK blueprint. Previous PR: #2747.
Summary
#2747 added the part of the
a2ui_agentAPI that one Express turn needs, and #2757 implemented it. This PR declares the rest of the agent SDK blueprint API in Dart, still limited to protocol v0.9. As in #2747, a member that is not trivial to implement throwsUnimplementedError, so follow-up PRs can fill in bodies without changing signatures.The layout follows section 2 of the blueprint:
catalog_transformers/,processor/catalog_providers.dart,utils/catalog_resolver.dart,inference_formats/direct_json/andinference_formats/express/decompiler.dartare new.Implemented
Each of these is a few lines over code that already existed:
CatalogTransformer,ComponentPruningTransformerandFunctionPruningTransformer. Pruning isCatalog.copyWithwith a filter.CatalogConfig.transformers, andCatalogConfig.transformedCatalog, which applies them in order.resolveCatalogs, which now holds the negotiation that used to be insideA2uiGenerator.createProcessor. It returns the transformed catalogs.examplesonA2uiGenerator,A2uiRequestProcessor,PromptGeneratorandInferenceFormatFactory.createFormat. The processor checks each example with the existingvalidatePayloadswhen it is created, so an example that uses a pruned component fails at creation withA2uiValidationError.inferenceFormatFactoryargument onA2uiGenerator.createProcessor, which overrides the generator's format for one processor.Parser.supportsStreaming, false by default, andParser.parseChunk, which throwsUnsupportedErrorunless a parser overrides it.ExpressParser.hasFormatContentandExpressParser.wrap, using the tag scanningunwrapalready had.Stubbed
These throw
UnimplementedError:FileSystemCatalogProvider.loadandInMemoryCatalogProvider.load, and soCatalogConfig.fromPath.DirectJsonPromptGenerator.generateand everyDirectJsonParsermethod, includingparseChunkand the defaultprogressiveKeys.DirectJsonFormatFactory,DirectJsonFormat(withallowedMessages) andDirectJsonParser.supportsStreamingare real.ExpressDecompiler.decompile, whichExpressParser.decompilecalls.ExpressPromptGenerator.generatewhen examples are given.resolveCatalogs(acceptsInlineCatalogs: true)when the renderer sends inline catalogs.A2uiProtocolVersionhas onlyv0_9, so the providers cannot be given another version.resolveCatalogskeeps the check that the renderer declared v0.9 capabilities. The stub docs say that providers andDirectJsonParser.compilereject other and missing versions.Decisions
A2uiGenerator.inferenceFormatFactoryandA2uiRequestProcessor.formatFactorynow default toDirectJsonFormatFactory, as the blueprint says, instead of being required as in feat(dart): minimal a2ui_agent API for the Express format, protocol v0.9 #2747. Until direct JSON is implemented, an agent that omits the format gets a processor whosepromptSnippetandparseResponsethrowUnimplementedError. The example and the e2e test passExpressFormatFactoryand are unaffected. Keeping the format required until direct JSON works is a small revert if we prefer it.parseResponsechecks a response. An example that only updates a surface created in an earlier turn is therefore rejected.PromptGeneratorholdscatalogsandexamplesin the base class, as in the blueprint, so subclasses pass them tosuper.allowedMessagesandcustomProgressiveKeyscan only be set through their constructors. Express still exports onlyExpressFormatFactory, since everything it takes goes through the factory.protocolVersion: A2uiProtocolVersion.v0_9.RawResponsePartkeeps the Implement express, v0.9, in dart agent. #2757 shape, withisFinalonRawA2uiPartrather than a wrapper around the part.These change the API for custom formats:
Parsersubclasses must implementhasFormatContent,wrapanddecompile, andInferenceFormatFactory.createFormattakesexamples.Tests
test/generator_test.dartcovers transformed catalogs, the format override, the direct JSON default, examples reaching the format, an example rejected because its component was pruned, andresolveCatalogsignoring inline catalogs by default.test/express_test.dartcovershasFormatContent, including a close tag inside a string, and that Express does not stream.test/conformance/catalog_transformer_conformance_test.dartrunsconformance/agent/catalog_transformer.yaml. The suite loading code it shares with the Express harness moved totest/conformance/suites.dart.wrapcases and applies transformers.Skipped conformance cases went from 23 to 18. Still skipped, with the reason in the test output: 12 decompile cases, 2 prompt example cases, 1 message allowlist case, 1
callRendererFunctioncase, and two new ones:test_function_pruning_on_catalog_without_functionsloads a catalog document with nocatalogId, which only a catalog provider could supply.test_express_snippet_omits_a_pruned_componentfails because the Express syntax rules end with the exampleroot = Card(...), soCard(is in the snippet even whenCardis pruned. The transformer skip hid this before. The fix is to change that example in the rules, which is left out of this PR because it changes the prompt.Verification
flutter analyze --fatal-infosat the repository root reports no issues, anddart formatmakes no changes to the files in this PR.dart testindart/a2ui_agent: 120 tests pass and 18 are skipped. Onmainit is 90 and 23.a2ui_agentis bumped to0.0.1-wip005, with a changelog entry.