Skip to content

Commit 22b2fd1

Browse files
authored
docs: expand customer-facing package guidance (#361)
1 parent 5a917fc commit 22b2fd1

63 files changed

Lines changed: 3341 additions & 729 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Introduction
2+
3+
`@ngaf/a2ui` is the protocol layer for A2UI messages. It gives the rest of the framework a shared TypeScript vocabulary for agent-built surfaces, streamed JSONL messages, dynamic values, and outbound action payloads.
4+
5+
It does not render Angular components. It does not register handler functions. It does not decide how an agent should respond to a button click. Those jobs sit in `@ngaf/chat` and `@ngaf/render`.
6+
7+
## What the package owns
8+
9+
The public entry point exports four groups of tools:
10+
11+
| Area | Exports |
12+
|------|---------|
13+
| Wire types | `A2uiMessage`, `A2uiComponent`, component prop interfaces, data-model update types, action message types |
14+
| Stream parsing | `createA2uiMessageParser()` |
15+
| Data access | `getByPointer()`, `setByPointer()`, `deleteByPointer()` |
16+
| Dynamic values | `resolveDynamic()`, `A2uiScope`, literal/path guards |
17+
18+
Use this package when you are building an adapter, validating an agent stream, testing A2UI payloads, or integrating a custom renderer with the same protocol surface that `@ngaf/chat` uses.
19+
20+
## Message flow
21+
22+
The parser expects newline-delimited JSON. Each line is checked for one known envelope key:
23+
24+
```text
25+
surfaceUpdate
26+
dataModelUpdate
27+
beginRendering
28+
deleteSurface
29+
```
30+
31+
When a line parses and has one of those envelope keys, it is returned as an `A2uiMessage`. Unknown envelopes are ignored. Malformed lines are skipped. Incomplete JSON waits in the internal buffer until a newline arrives.
32+
33+
```ts
34+
import { createA2uiMessageParser } from '@ngaf/a2ui';
35+
36+
const parser = createA2uiMessageParser();
37+
38+
const messages = parser.push(
39+
'{"beginRendering":{"surfaceId":"checkout","root":"root"}}\n',
40+
);
41+
```
42+
43+
That posture is intentional. Agent streams are partial by nature. The low-level parser favors safe continuation over throwing during render.
44+
45+
## Relationship to chat and render
46+
47+
`@ngaf/chat` detects A2UI content in assistant output, feeds the JSONL stream into `createA2uiMessageParser()`, applies messages to its surface store, and renders those surfaces through the A2UI render components.
48+
49+
`@ngaf/render` owns Angular component resolution, event dispatch, state updates, and handler execution. The A2UI package only describes protocol shapes and helper behavior.
50+
51+
That separation matters when debugging:
52+
53+
- If JSONL chunks are not becoming messages, inspect `@ngaf/a2ui`.
54+
- If messages are not becoming surfaces, inspect the chat A2UI surface store.
55+
- If components render incorrectly or handlers do not run, inspect the render/chat integration.
56+
57+
## Safe fallback posture
58+
59+
The parser and resolver are deliberately conservative:
60+
61+
- malformed JSONL lines are skipped;
62+
- unknown envelope keys are ignored;
63+
- missing data-model paths resolve to `undefined`;
64+
- unrecognized dynamic-value shapes pass through unchanged.
65+
66+
This makes the protocol layer suitable for streaming, but it is not a full schema validator. If you accept untrusted agent output, validate the payload at your boundary before wiring it to privileged handlers.
67+
68+
## Install
69+
70+
```bash
71+
npm install @ngaf/a2ui
72+
```
73+
74+
The package has no peer dependencies.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Parser, Resolver, and Guards
2+
3+
`@ngaf/a2ui` exports small helpers that keep stream parsing and dynamic value resolution consistent across packages.
4+
5+
## createA2uiMessageParser()
6+
7+
```ts
8+
import { createA2uiMessageParser } from '@ngaf/a2ui';
9+
10+
const parser = createA2uiMessageParser();
11+
const messages = parser.push(chunk);
12+
```
13+
14+
`push(chunk)` appends the chunk to an internal buffer and returns every complete message found before the last newline.
15+
16+
Important behavior from source:
17+
18+
- the parser is JSONL-based;
19+
- a complete message requires a trailing newline;
20+
- CRLF works because each line is trimmed;
21+
- empty lines are ignored;
22+
- malformed lines are skipped silently;
23+
- unknown top-level envelopes are ignored;
24+
- multiple messages can be returned from one chunk.
25+
26+
The parser checks only for the known envelope key and a non-null object value. It does not validate each nested field.
27+
28+
## resolveDynamic()
29+
30+
```ts
31+
import { resolveDynamic } from '@ngaf/a2ui';
32+
33+
const model = {
34+
customer: { name: 'Ada' },
35+
count: 2,
36+
};
37+
38+
resolveDynamic({ path: '/customer/name' }, model); // "Ada"
39+
resolveDynamic({ literalNumber: 2 }, model); // 2
40+
```
41+
42+
`resolveDynamic(value, model, scope?)` handles:
43+
44+
| Input shape | Result |
45+
|-------------|--------|
46+
| `{ literalString }` | the wrapped string |
47+
| `{ literalNumber }` | the wrapped number |
48+
| `{ literalBoolean }` | the wrapped boolean |
49+
| `{ literalArray }` | the wrapped array |
50+
| `{ path }` | the value at that model path |
51+
| arrays | recursively resolved array values |
52+
| `null` or `undefined` | returned as-is |
53+
| unrecognized shapes | returned as-is |
54+
55+
Absolute paths start with `/`.
56+
57+
Relative paths resolve against `scope.basePath` when a scope is supplied. Without a scope, a relative path is treated as root-relative by prefixing `/`.
58+
59+
```ts
60+
resolveDynamic(
61+
{ path: 'name' },
62+
{ items: [{ name: 'Ada' }] },
63+
{ basePath: '/items/0', item: { name: 'Ada' } },
64+
); // "Ada"
65+
```
66+
67+
`A2uiScope.item` is part of the public type, but the current resolver only uses `basePath`.
68+
69+
## Pointer helpers
70+
71+
```ts
72+
import { getByPointer, setByPointer, deleteByPointer } from '@ngaf/a2ui';
73+
```
74+
75+
The pointer helpers use slash-separated paths:
76+
77+
```ts
78+
const model = { customer: { name: 'Ada' } };
79+
80+
getByPointer(model, '/customer/name'); // "Ada"
81+
setByPointer(model, '/customer/name', 'Grace');
82+
deleteByPointer(model, '/customer/name');
83+
```
84+
85+
Current behavior is intentionally small:
86+
87+
- empty pointer and `/` point at the root;
88+
- missing paths read as `undefined`;
89+
- `setByPointer()` returns a cloned object path rather than mutating the original root;
90+
- `deleteByPointer()` returns the original model when the parent path does not exist.
91+
92+
These helpers do not implement full RFC 6901 escaping semantics. Avoid keys that require `~0` or `~1` escaping unless you normalize them before they enter A2UI state.
93+
94+
## Guards
95+
96+
The public guards are:
97+
98+
```ts
99+
isLiteralString(value)
100+
isLiteralNumber(value)
101+
isLiteralBoolean(value)
102+
isPathRef(value)
103+
```
104+
105+
They are shape checks for dynamic wrapper objects. The literal guards check for the presence of the wrapper key. `isPathRef()` also verifies that `path` is a string.
106+
107+
Use them when you need to branch on protocol values without importing internal renderer code.
108+
109+
## Validation vs handler wiring
110+
111+
This package does not run validation rules, map actions to Angular handlers, or call user functions. It gives you typed values and parsing helpers.
112+
113+
A practical boundary is:
114+
115+
- use `@ngaf/a2ui` to parse and inspect the protocol stream;
116+
- use app or server validation to decide whether a message is trusted;
117+
- use `@ngaf/chat` and `@ngaf/render` to display surfaces and wire interactions.
118+
119+
That split keeps protocol parsing deterministic and keeps privileged behavior in the host application.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# A2UI Schema
2+
3+
The `@ngaf/a2ui` schema is a TypeScript model of the protocol shapes used by the framework. It is useful as a contract for agent output and custom integrations, but it is not a runtime validator.
4+
5+
## Dynamic values
6+
7+
Dynamic values are wrapped objects. A value can be literal or resolved from the surface data model by path.
8+
9+
```ts
10+
type DynamicString =
11+
| { literalString: string }
12+
| { path: string };
13+
14+
type DynamicNumber =
15+
| { literalNumber: number }
16+
| { path: string };
17+
18+
type DynamicBoolean =
19+
| { literalBoolean: boolean }
20+
| { path: string };
21+
22+
type DynamicStringList =
23+
| { literalArray: string[] }
24+
| { path: string };
25+
```
26+
27+
Absolute paths start with `/` and are resolved from the model root. Relative paths are resolved from an optional `A2uiScope`.
28+
29+
## Children
30+
31+
Layout components use either explicit child IDs or a template declaration.
32+
33+
```ts
34+
type A2uiChildren =
35+
| { explicitList: string[] }
36+
| { template: { componentId: string; dataBinding: string } };
37+
```
38+
39+
The protocol layer only types this shape. Template expansion is renderer behavior.
40+
41+
## Actions
42+
43+
An action has a name and optional context entries. Context values use the same dynamic wrappers as component props.
44+
45+
```ts
46+
interface A2uiAction {
47+
name: string;
48+
context?: A2uiActionContextEntry[];
49+
}
50+
```
51+
52+
`@ngaf/a2ui` does not execute actions. It only describes the payload that chat/render code can turn into an outbound `A2uiActionMessage`.
53+
54+
## Components
55+
56+
Every component has an `id`, optional `weight`, and a single-key `component` union.
57+
58+
```ts
59+
interface A2uiComponent {
60+
id: string;
61+
weight?: number;
62+
component: A2uiComponentDef;
63+
}
64+
```
65+
66+
The exported component definitions are:
67+
68+
| Definition | Main fields |
69+
|------------|-------------|
70+
| `Text` | `text`, `usageHint` |
71+
| `Image` | `url`, `alt`, `width`, `height` |
72+
| `Icon` | `icon`, `size` |
73+
| `Video` | `url`, `autoPlay`, `controls` |
74+
| `AudioPlayer` | `url`, `autoPlay`, `controls` |
75+
| `Row` | `children`, `gap`, `alignment`, `distribution` |
76+
| `Column` | `children`, `gap`, `alignment` |
77+
| `List` | `children`, `direction` |
78+
| `Card` | `child` |
79+
| `Tabs` | `tabItems` |
80+
| `Divider` | `direction` |
81+
| `Modal` | `entryPointChild`, `contentChild`, `title` |
82+
| `Button` | `child`, `primary`, `action` |
83+
| `CheckBox` | `label`, `checked`, `action` |
84+
| `TextField` | `label`, `text`, `textFieldType`, `validationRegexp` |
85+
| `DateTimeInput` | `label`, `value`, `enableDate`, `enableTime` |
86+
| `MultipleChoice` | `selections`, `options`, `maxAllowedSelections`, `label` |
87+
| `Slider` | `value`, `minValue`, `maxValue`, `step`, `label` |
88+
89+
The schema exposes `validationRegexp` on `TextField`, but validation execution is not implemented in this package. Treat schema fields as protocol data until a renderer wires behavior.
90+
91+
## Message envelopes
92+
93+
The parser recognizes four top-level envelopes:
94+
95+
```ts
96+
type A2uiMessage =
97+
| { surfaceUpdate: A2uiSurfaceUpdate }
98+
| { dataModelUpdate: A2uiDataModelUpdate }
99+
| { beginRendering: A2uiBeginRendering }
100+
| { deleteSurface: A2uiDeleteSurface };
101+
```
102+
103+
### surfaceUpdate
104+
105+
Adds or replaces components for a surface.
106+
107+
```json
108+
{
109+
"surfaceUpdate": {
110+
"surfaceId": "checkout",
111+
"components": [
112+
{ "id": "root", "component": { "Card": { "child": "title" } } },
113+
{ "id": "title", "component": { "Text": { "text": { "literalString": "Checkout" } } } }
114+
]
115+
}
116+
}
117+
```
118+
119+
### dataModelUpdate
120+
121+
Carries nested data-model entries. `path` is optional.
122+
123+
```json
124+
{
125+
"dataModelUpdate": {
126+
"surfaceId": "checkout",
127+
"path": "/customer",
128+
"contents": [
129+
{ "key": "name", "valueString": "Ada" },
130+
{ "key": "active", "valueBoolean": true }
131+
]
132+
}
133+
}
134+
```
135+
136+
Each `A2uiDataModelEntry` has a `key` plus one of `valueString`, `valueNumber`, `valueBoolean`, or `valueMap`.
137+
138+
### beginRendering
139+
140+
Identifies the root component to render for a surface.
141+
142+
```json
143+
{
144+
"beginRendering": {
145+
"surfaceId": "checkout",
146+
"root": "root",
147+
"styles": {
148+
"font": "Inter",
149+
"primaryColor": "#2563eb"
150+
}
151+
}
152+
}
153+
```
154+
155+
The source comments describe `styles.font` and `styles.primaryColor` as the canonical style fields.
156+
157+
### deleteSurface
158+
159+
Removes a surface by ID.
160+
161+
```json
162+
{ "deleteSurface": { "surfaceId": "checkout" } }
163+
```
164+
165+
## Internal surface model
166+
167+
`A2uiSurface` is an internal model used after messages are applied:
168+
169+
```ts
170+
interface A2uiSurface {
171+
surfaceId: string;
172+
catalogId: string;
173+
theme?: A2uiTheme;
174+
sendDataModel?: boolean;
175+
components: Map<string, A2uiComponent>;
176+
dataModel: Record<string, unknown>;
177+
styles?: { font?: string; primaryColor?: string };
178+
}
179+
```
180+
181+
This shape is not constrained to the wire format. Do not assume an agent sends it directly.
182+
183+
## Outbound action messages
184+
185+
When a rendered surface sends an action back to the agent, the typed outbound shape is:
186+
187+
```ts
188+
interface A2uiActionMessage {
189+
version: 'v0.9';
190+
action: {
191+
name: string;
192+
surfaceId: string;
193+
sourceComponentId: string;
194+
timestamp: string;
195+
context: Record<string, unknown>;
196+
};
197+
metadata?: {
198+
a2uiClientDataModel: A2uiClientDataModel;
199+
};
200+
}
201+
```
202+
203+
The outbound action version is currently typed as `v0.9` in source.

0 commit comments

Comments
 (0)