Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/a2ui_agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# [a2ui_agent](https://pub.dev/packages/a2ui_agent) Changelog

## 0.0.1-wip
## 0.0.1-wip001


- Initial version.
182 changes: 181 additions & 1 deletion packages/a2ui_agent/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,183 @@
# A2UI Agent SDK

TODO: add readme
The agent side of [A2UI](https://a2ui.org) for Dart:
when agent and renderer are in different processes, this SDK is
what is needed on agent side.

It covers catalog management, capability negotiation, prompt engineering,
response parsing, payload validation and transport packaging, so an agent can
generate rich UI that provably conforms to what its client can render.

Built on [`package:a2ui_core`](../a2ui_core), which supplies the protocol
models this SDK negotiates, prompts with and validates against.

## Getting started

```dart
import 'package:a2ui_agent/a2ui_agent.dart';
import 'package:a2ui_core/a2ui_core.dart';

// 1. At startup: register the catalogs this agent supports.
final generator = A2uiGenerator(
catalogs: [
CatalogConfig(
MinimalCatalog(),
transformers: [
ComponentPruningTransformer(['Column', 'Text', 'Button']),
],
),
],
examples: {'A greeting card': greetingMessages},
);

// 2. Per request: negotiate against what the renderer can render.
final processor = generator.createProcessor(
A2uiRendererCapabilities.fromJson(request.a2uiClientCapabilities),
);

// 3. Prompt the model, prepending your own role and workflow instructions.
final output = await callLlm('$roleInstructions\n${processor.promptSnippet}');

// 4. Parse and validate.
for (final part in processor.parseResponse(output)) {
switch (part) {
case TextPart(:final text):
sendText(text);
case A2uiPart(:final a2ui):
sendToRenderer(a2ui); // 5. Deliver.
}
}
```

Run the full walkthrough with
`dart run example/a2ui_agent_example.dart`.

## Architecture

The SDK separates single-responsibility primitives from a high-level facade, so
that an agent can adopt the whole pipeline or reach past it for one piece.

| Layer | Types |
| -------------------- | --------------------------------------------------------------------------------- |
| Application facade | `A2uiGenerator`, `A2uiRequestProcessor`, `CatalogConfig`, catalog providers |
| Catalog transformers | `CatalogTransformer`, `ComponentPruningTransformer`, `FunctionPruningTransformer` |
| Inference formats | `InferenceFormat`, `InferenceFormatFactory`, `DirectJsonFormat`, `ExpressFormat` |
| Prompt generation | `PromptGenerator` and the per-format generators |
| Parsing | `Parser`, `RawResponsePart`, `TextPart`, `RawA2uiPart`, `A2uiPart` |
| Validation | `A2uiPayloadValidator` |
| Negotiation | `resolveCatalogs`, `A2uiRendererCapabilities` |

### Catalogs

A catalog is a `a2ui_core` `Catalog`: either written in Dart, or loaded from a
catalog document with `FileSystemCatalogProvider`, `InMemoryCatalogProvider` or
`BundledCatalogProvider`. `CatalogConfig` pairs one with the transformers that
trim it; the pristine catalog is never mutated.

`resolveCatalogs` matches the registered catalogs against the renderer's
`a2uiClientCapabilities` and returns the transformed catalogs active for the
session. A catalog the renderer cannot render never reaches the prompt, so the
model cannot name a component the client would reject. If nothing matches, the
call throws rather than letting a doomed inference run.

### Inference formats

A format pairs a prompt generator with a parser.

**Direct JSON** (the default) has the model emit A2UI wire JSON inside
`<a2ui-json>` tags:

```
<a2ui-json>
[{"version": "v0.9", "updateComponents": {"surfaceId": "s", "components": [
{"id": "root", "component": "Text", "text": "Hello"}
]}}]
</a2ui-json>
```

**Express** trades that verbosity for a positional DSL inside
`<a2ui-express>` tags, which cuts output tokens substantially — the reason the
format exists:

```
<a2ui-express>
surface("s")
root = Column([title, cta])
title = Text($/heading, "h1")
cta = Button(Text("Continue"), Event("continue"))
</a2ui-express>
```

Express is catalog-agnostic: positional arguments are mapped onto property
names through the catalog signature, and the same function renders the
signature shown to the model, so the syntax it is taught is exactly the syntax
its output is parsed against. `ExpressDecompiler` converts payloads back to
Express, which is how few-shot examples authored as A2UI JSON are shown to the
model in the compact syntax.

Select a format per agent or per request:

```dart
generator.createProcessor(
capabilities,
inferenceFormatFactory: const ExpressFormatFactory(),
);
```

### Streaming

Both formats parse incrementally. `parseChunk` returns only what became usable
since the previous call, and `parseStream` wraps that as a `Stream`:

```dart
await for (final part in processor.parseStream(modelChunks)) {
// TextPart and A2uiPart, in the order the model produced them.
}
```

Direct JSON emits a component as soon as it is renderable and re-emits it only
when its content actually changed, so a string grows on screen as it arrives.
Truncated values are healed only for property keys where a prefix is a
legitimate value — never for component references, enums or pattern-constrained
strings, where a prefix would be wrong rather than merely incomplete. Express
compiles statement by statement as each line completes.

A payload the model never finished is salvaged as far as it parses, rather than
discarded.

### Validation

`A2uiPayloadValidator` checks compiled payloads against the negotiated
catalogs: envelope version, catalog identity, component existence, required and
unknown properties, duplicate ids, JSON Pointer syntax, and — opt in —
dangling child references and reference cycles. Parsing runs it automatically,
so `parseResponse` either returns a conforming payload or throws.

Prompt examples are validated too, when the processor is created: an example
that names a component the negotiated catalogs lack would teach the model to
emit exactly what the renderer will reject.

## Relationship to the specification

This package implements the
[A2UI agent SDK blueprint](https://github.com/a2ui-project/a2ui/blob/main/blueprints/modules/a2ui_agent.blueprint.md).
Where Dart differs from the reference Python SDK:

- The blueprint's `AgentToRendererMessage` is `a2ui_core`'s `A2uiMessage`; the
name is available as a typedef.
- `package:a2ui_core` models the `v0.9` envelopes, so payloads compile to
`createSurface`, `updateComponents`, `updateDataModel` and `deleteSurface`.
`ProtocolVersion` names the other versions so that a catalog declaring one is
reported rather than silently mis-parsed.
- Validation lives here rather than in the core package, which does not yet
ship an `A2uiValidator`.
- Express targets `v0.9`, so a standalone function call — a `callFunction` RPC
in `v1.0` — is reported as unsupported instead of being dropped.
- `BundledCatalogProvider` serves the minimal catalog bundled with
`a2ui_core` for `v0.9`.
- Catalog transformers are pure functions over `Catalog`, and generic over the
component type, so pruning a typed catalog returns a catalog of the same
type.

`FileSystemCatalogProvider` and `CatalogConfig.fromPath` read from disk and so
require a native platform.
114 changes: 112 additions & 2 deletions packages/a2ui_agent/example/a2ui_agent_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,119 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: avoid_print

import 'package:a2ui_agent/a2ui_agent.dart';
import 'package:a2ui_core/a2ui_core.dart';

/// Walks through the agent workflow: register catalogs, negotiate with a
/// renderer, prompt a model, then parse and validate what it returns.
void main() {
var awesome = Awesome();
awesome.toString();
// 1. At startup, register every catalog the agent supports. Transformers
// trim a catalog down to what this agent is willing to generate.
final generator = A2uiGenerator(
catalogs: [
CatalogConfig(
MinimalCatalog(),
transformers: [
ComponentPruningTransformer(const ['Column', 'Text', 'Button']),
],
),
],
examples: {'A greeting with a dismiss button': _greetingExample()},
);

// 2. Per request, negotiate against what the renderer says it can render.
final capabilities = A2uiRendererCapabilities.fromJson({
'v0.9': {
'supportedCatalogIds': [MinimalCatalog().id],
},
});
final A2uiRequestProcessor processor = generator.createProcessor(
capabilities,
);

// 3. Prepend the prompt snippet to the agent's own system instructions.
print('--- system prompt snippet (truncated) ---');
print(processor.promptSnippet.split('\n').take(12).join('\n'));

// 4. Parse and validate the model's response.
final List<ResponsePart> parts = processor.parseResponse(_fakeLlmOutput);

// 5. Deliver the payloads to the renderer.
print('\n--- parsed response ---');
for (final part in parts) {
switch (part) {
case TextPart(text: final String text):
print('text: $text');
case A2uiPart(a2ui: final List<AgentToRendererMessage> messages):
for (final message in messages) {
print('a2ui: ${message.toJson()}');
}
}
}

// The same catalogs also drive the compact Express format, where the model
// writes positional statements instead of JSON.
final expressProcessor = A2uiRequestProcessor(
catalogs: processor.activeCatalogs,
formatFactory: const ExpressFormatFactory(),
);
print('\n--- express ---');
for (final ResponsePart part in expressProcessor.parseResponse(
_fakeExpressOutput,
)) {
if (part is A2uiPart) {
for (final AgentToRendererMessage message in part.a2ui) {
print('a2ui: ${message.toJson()}');
}
}
}
}

List<AgentToRendererMessage> _greetingExample() => [
CreateSurfaceMessage(surfaceId: 'greeting', catalogId: MinimalCatalog().id),
UpdateComponentsMessage(
surfaceId: 'greeting',
components: [
{
'id': 'root',
'component': 'Column',
'children': ['title'],
},
{'id': 'title', 'component': 'Text', 'text': 'Hello!', 'variant': 'h1'},
],
),
];

const String _fakeLlmOutput = '''
Here is the panel you asked for.
<a2ui-json>
[
{
"version": "v0.9",
"createSurface": {"surfaceId": "s1", "catalogId": "https://a2ui.org/specification/v0_9/catalogs/minimal/minimal_catalog.json"}
},
{
"version": "v0.9",
"updateComponents": {
"surfaceId": "s1",
"components": [
{"id": "root", "component": "Column", "children": ["greeting"]},
{"id": "greeting", "component": "Text", "text": "Good morning"}
]
}
}
]
</a2ui-json>
Let me know if you want a different layout.
''';

const String _fakeExpressOutput = '''
<a2ui-express>
surface("s2")
root = Column([greeting, dismiss])
greeting = Text("Good morning", "h1")
dismiss = Button(Text("Dismiss"), Event("dismiss"))
</a2ui-express>
''';
57 changes: 52 additions & 5 deletions packages/a2ui_agent/lib/a2ui_agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,58 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Support for doing something awesome.
/// The A2UI agent SDK.
///
/// More dartdocs go here.
/// This package gives an agent everything it needs between "the model is about
/// to be called" and "the renderer receives A2UI": catalog management,
/// capability negotiation, prompt engineering, response parsing and payload
/// validation.
///
/// Start with `A2uiGenerator`, which holds the catalogs an agent supports and
/// hands out an `A2uiRequestProcessor` per renderer:
///
/// ```dart
/// final generator = A2uiGenerator(
/// catalogs: [CatalogConfig(MinimalCatalog())],
/// );
/// final processor = generator.createProcessor(rendererCapabilities);
/// final output = await callLlm(processor.promptSnippet, request);
/// final parts = processor.parseResponse(output);
/// ```
library;

export 'src/a2ui_agent_base.dart';

// TODO: Export any libraries intended for clients of this package.
export 'src/catalog_transformers/base.dart';
export 'src/catalog_transformers/pruning.dart';
export 'src/inference_format.dart';
export 'src/inference_formats/direct_json/constants.dart';
export 'src/inference_formats/direct_json/format.dart';
export 'src/inference_formats/direct_json/parser.dart';
export 'src/inference_formats/direct_json/payload_fixer.dart';
export 'src/inference_formats/direct_json/prompt_generator.dart';
export 'src/inference_formats/direct_json/streaming.dart';
export 'src/inference_formats/express/ast.dart';
export 'src/inference_formats/express/compiler.dart';
export 'src/inference_formats/express/constants.dart';
export 'src/inference_formats/express/decompiler.dart';
export 'src/inference_formats/express/format.dart';
export 'src/inference_formats/express/lexer.dart';
export 'src/inference_formats/express/parser.dart';
export 'src/inference_formats/express/prompt_generator.dart';
export 'src/inference_formats/express/statement_splitter.dart';
export 'src/inference_formats/express/syntax_parser.dart';
export 'src/parser/incremental_processor.dart';
export 'src/parser/parser.dart';
export 'src/parser/response_part.dart';
export 'src/parser/sentinel_tokenizer.dart';
export 'src/primitives/errors.dart';
export 'src/primitives/protocol_version.dart';
export 'src/processor/catalog_config.dart';
export 'src/processor/catalog_providers.dart';
export 'src/processor/generator.dart';
export 'src/processor/processor.dart';
export 'src/processor/renderer_capabilities.dart';
export 'src/prompt/generator.dart';
export 'src/utils/catalog_document.dart';
export 'src/utils/catalog_resolver.dart';
export 'src/utils/schema_utils.dart';
export 'src/validation/payload_validator.dart';
17 changes: 17 additions & 0 deletions packages/a2ui_agent/lib/src/catalog_transformers/base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2025 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:a2ui_core/a2ui_core.dart';

/// A transformation rule applied to a catalog before prompt engineering and
/// payload validation.
///
/// Transformers are pure: they take a pristine catalog and return a modified
/// copy of the same component type, leaving the original untouched.
abstract class CatalogTransformer {
const CatalogTransformer();

/// Transforms [catalog] into a modified catalog of the same component type.
Catalog<T> transform<T extends ComponentApi>(Catalog<T> catalog);
}
Loading
Loading