Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8140edc
docs(chat): state that no library component reads CHAT_CONFIG
blove Sep 6, 2026
95bc591
docs(chat): rewrite chat-select around the body-level overlay portal
blove Sep 6, 2026
c9e9a4b
docs(chat): correct mock-agent error typing, classifier/store injecti…
blove Sep 6, 2026
11b7865
docs(a2ui): correct surface-component links, the four-argument resolv…
blove Sep 6, 2026
da7e852
docs(deep-agents): say how each capability is installed, drop the und…
blove Sep 6, 2026
78f0bc6
docs(chat): fix message-list template guard and messageContent() cont…
blove Sep 6, 2026
a193612
docs(render): correct nine render pages against the library source
blove Sep 6, 2026
0974f5c
docs(chat): correct popup/sidebar CSS hooks and slots, tool-call card…
blove Sep 6, 2026
e3e18fa
docs(langgraph): rewrite the testing guide around runnable specs
blove Sep 6, 2026
73d5773
docs(chat): correct A2UI catalog props and surface-component handler/…
blove Sep 6, 2026
7f28ede
docs(langgraph): rewrite the threads adapter page from source
blove Sep 6, 2026
950fc1f
docs(langgraph): correct the transport, injectAgent, and provideAgent…
blove Sep 6, 2026
87dfca8
docs(langgraph): fix the getting-started dependency contract and fences
blove Sep 6, 2026
a434d57
docs(langgraph): correct the lifecycle signal derivation and AGENT_LI…
blove Sep 6, 2026
09e667b
docs(ag-ui): document both provideAgent overloads and the AgentRef su…
blove Sep 6, 2026
6e49172
docs(ag-ui): state the real CUSTOM delivery paths under ag-ui-langgra…
blove Sep 6, 2026
fb35850
docs(ag-ui): fix peer table, interrupt sources, and provider coverage…
blove Sep 6, 2026
f349941
docs(langgraph): correct AgentError typing, checkpoint fields, and st…
blove Sep 6, 2026
a0aeaf7
docs(chat): regenerate the ChatComponent API section and correct inte…
blove Sep 6, 2026
605cc28
docs(chat): rewrite the changelog from tags and state provideChat() i…
blove Sep 6, 2026
24b88b6
docs(chat): correct the guides — inert CHAT_CONFIG, scoped lifecycle …
blove Sep 6, 2026
90ef3cd
docs(runtimes): correct prerequisites, wire shapes, and rewrite the q…
blove Sep 6, 2026
7682f33
docs(middleware): make the LangGraph.js skeleton compile, complete th…
blove Sep 6, 2026
41c63ee
docs(choosing-an-adapter): correct the component claim and emitter si…
blove Sep 6, 2026
5d0c1c5
fix(examples): repair six demos the docs were describing around
blove Sep 6, 2026
f354c6c
chore(prompts): make every example prompt describe what its code does
blove Sep 6, 2026
c3ab6db
fix(scripts): stop serve-example spawning a shell project that does n…
blove Sep 6, 2026
27b70a5
docs: describe the fixed demos on the six pages that documented their…
blove Sep 6, 2026
2ddba0f
docs: regenerate api-docs after the JSDoc fixes; drop the unsupported…
blove Sep 6, 2026
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
2 changes: 1 addition & 1 deletion apps/website/content/docs/a2ui/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1568,7 +1568,7 @@
{
"name": "createA2uiFunctionRegistry",
"kind": "function",
"description": "Creates an A2UI client-side function registry containing the standard\nbasic-catalog functions (`formatString`, `formatNumber`, `formatCurrency`,\n`formatDate`, `pluralize`, `and`, `or`, `not`), optionally extended or\noverridden with custom implementations.",
"description": "Creates an A2UI client-side function registry containing the standard\nbasic-catalog functions `formatString`, `formatNumber`, `formatCurrency`,\n`formatDate`, `pluralize`, `and`, `or`, `not`, plus the check-rule validators\n`required`, `regex`, `length`, `numeric`, and `email` — optionally extended or\noverridden with custom implementations.",
"signature": "createA2uiFunctionRegistry(overrides: Record<string, A2uiFunctionImpl>): A2uiFunctionRegistry",
"params": [
{
Expand Down
16 changes: 10 additions & 6 deletions apps/website/content/docs/a2ui/getting-started/quickstart.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
---
description: Parse an A2UI JSONL stream, apply an updateDataModel envelope with the pointer helpers, and resolve a dynamic value with resolveDynamic.
---

# Quick Start

Parse an A2UI stream, build its data model, and resolve a dynamic value — end to end, in a few minutes.

`@threadplane/a2ui` is the protocol layer. It parses the JSONL message stream, gives you typed envelopes, and resolves dynamic values against a data model. It does not render anything. Rendering is `@threadplane/chat`'s [`<a2ui-surface>`](/docs/chat/getting-started/introduction). This library is what sits underneath it.
`@threadplane/a2ui` is the protocol layer. It parses the JSONL message stream, gives you typed envelopes, and resolves dynamic values against a data model. It does not render anything. Rendering is `@threadplane/chat`'s [`<a2ui-surface>`](/docs/chat/a2ui/surface-component). This library is what sits underneath it.

## Goals

By the end of this page you'll be able to:
By the end of this page you will be able to:

- Install `@threadplane/a2ui`.
- Parse a newline-delimited A2UI stream into typed messages.
Expand All @@ -23,7 +27,7 @@ The package has no peer dependencies.

## Parse a stream

Let's start with a real stream. An agent emits A2UI as newline-delimited JSON — one envelope per line, each stamped with `"version": "v0.9"`. Here's a booking form, in emission order: the surface is created first, then its data, then the component tree (whose first component is `root`).
Start with a real stream. An agent emits A2UI as newline-delimited JSON — one envelope per line, each stamped with `"version": "v0.9"`. Here is a booking form, in emission order: the surface is created first, then its data, then the component tree (whose first component is `root`).

```text
---a2ui_JSON---
Expand All @@ -45,14 +49,14 @@ const messages = parser.push(
// messages -> 1 message: { version: 'v0.9', createSurface: { surfaceId: 's1', catalogId: '...' } }
```

The parser is line-oriented. A line is only parsed once a newline arrives, so partial JSON buffers until it's complete:
The parser is line-oriented. A line is only parsed once a newline arrives, so partial JSON buffers until it is complete:

```ts
parser.push('{"version":"v0.9","deleteSurface":'); // -> [] (incomplete, buffered)
parser.push('{"surfaceId":"s1"}}\n'); // -> 1 message
```

That buffering is deliberate. Agent output streams in fragments, and a half-finished line shouldn't throw mid-render. A missing `version` field defaults to `v0.9`, and unknown envelope keys — such as future v1.0 messages — are skipped rather than treated as errors.
That buffering is deliberate. Agent output streams in fragments, and a half-finished line must not throw mid-render. A missing `version` field defaults to `v0.9`, and unknown envelope keys — such as future v1.0 messages — are skipped rather than treated as errors.

## Build the data model

Expand Down Expand Up @@ -107,7 +111,7 @@ A bare literal (string, number, boolean) passes through unchanged. A `{ path }`

## Conclusion

That's the full loop: stream in, model built, value resolved. From here, the three guides go deeper:
That is the full loop: stream in, model built, value resolved. From here, the three guides go deeper:

- [The A2UI message protocol](/docs/a2ui/guides/message-protocol) — surfaces, components, dynamic values, and the four envelopes.
- [Working with the data model](/docs/a2ui/guides/data-model) — the pointer helpers, immutability, and scopes.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
description: Consume an A2UI stream with the parser, narrow dynamic values with the guards, build test payloads, and back a custom renderer.
---

# Validating and adapting an A2UI stream

Take a streaming agent response, turn it into typed A2UI messages, narrow the dynamic values, and feed a renderer — with the right amount of validation for your trust level.
Expand Down Expand Up @@ -51,7 +55,7 @@ isFunctionCall({ call: 'formatString' }); // true
In v0.9, literals are bare JSON values — `"x"`, `5`, `true`, `["a", "b"]` — with no wrapper objects. A `typeof` check (or simply not matching either guard) is all it takes to identify one, so there are no literal guards to import.
</Callout>

For most rendering you don't branch on guards at all — `resolveDynamic` already handles literals, paths, arrays, function calls, and passthrough in one call. Reach for the guards when you need to *narrow a type* or make a decision before resolving.
For most rendering you do not branch on guards at all — `resolveDynamic` already handles literals, paths, arrays, function calls, and passthrough in one call. Reach for the guards when you need to *narrow a type* or make a decision before resolving.

## Build payloads for tests

Expand Down Expand Up @@ -89,11 +93,11 @@ function renderText(props: { text: unknown }, model: Record<string, unknown>) {
}
```

The full mechanics — component resolution, event dispatch, action emission, surface store — are exactly what Threadplane's own Angular renderer, `@threadplane/chat`'s [`<a2ui-surface>`](/docs/chat/getting-started/introduction), already implements. If you're on Angular, use it rather than re-deriving it. A custom renderer makes sense when you're on another platform or have rendering needs the component doesn't cover.
The full mechanics — component resolution, event dispatch, action emission, surface store — are exactly what Threadplane's own Angular renderer, `@threadplane/chat`'s [`<a2ui-surface>`](/docs/chat/a2ui/surface-component), already implements. If you are on Angular, use it rather than re-deriving it. A custom renderer makes sense when you are on another platform or have rendering needs the component does not cover.

## A tradeoff: the parser swallows parse errors

For me, the parser's silent-skip behavior is the right default — it's what lets a half-streamed line not blow up a live render, and it's why feeding raw agent output Just Works. The cost is honest: the parser is not a validator. It will quietly drop a malformed line and ignore an unknown envelope, so a structurally-wrong payload simply produces fewer messages, not an error you can catch.
The parser's silent-skip behavior is the right default: it is what lets a half-streamed line avoid blowing up a live render, and it is what makes feeding raw agent output straight through work. The cost is real: the parser is not a validator. It will quietly drop a malformed line and ignore an unknown envelope, so a structurally-wrong payload simply produces fewer messages, not an error you can catch.

So the rule of thumb: if you need strictness, validate the parsed `A2uiMessage[]` *after* `push` returns — assert the envelope kinds and shapes you expect, rather than counting on the parser to reject bad input. The parser optimizes for streaming resilience; strict validation is your boundary's job.

Expand Down
20 changes: 12 additions & 8 deletions apps/website/content/docs/a2ui/guides/data-model.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
description: Read and write an A2UI surface data model with the pointer helpers, apply updateDataModel envelopes, and resolve dynamic values in scope.
---

# Working with the data model

A surface's data lives in a plain object, and you read and write it through three pointer helpers plus a resolver. This guide covers all four.
Expand Down Expand Up @@ -28,7 +32,7 @@ next.user.name; // "Bob"
original.user.name; // "Alice" — unchanged
```

It also creates intermediate objects along the way, so you don't have to pre-build nesting:
It also creates intermediate objects along the way, so you do not have to pre-build nesting:

```ts
setByPointer({}, '/a/b/c', 42); // { a: { b: { c: 42 } } }
Expand All @@ -42,7 +46,7 @@ import { deleteByPointer } from '@threadplane/a2ui';
deleteByPointer({ a: 1, b: 2 }, '/a'); // { b: 2 }
```

If the parent of the target doesn't exist, `deleteByPointer` returns the original model unchanged rather than fabricating a path to delete from.
If the parent of the target does not exist, `deleteByPointer` returns the original model unchanged rather than fabricating a path to delete from.

One v0.9-specific rule: deleting an **array index** does not splice. The index is set to `undefined` and the array's length is preserved, so sibling indices stay stable for other bindings:

Expand All @@ -52,7 +56,7 @@ deleteByPointer({ items: ['a', 'b', 'c'] }, '/items/1');
```

<Callout type="warning" title="No RFC-6901 escaping">
These helpers use JSON-Pointer-style syntax but do **not** implement RFC 6901's `~0` / `~1` unescaping. A path is split on `/` and the segments are used as literal keys. So keys that themselves contain `/` or `~` aren't addressablethere's no escape sequence to reach them.
These helpers use JSON-Pointer-style syntax but do **not** implement RFC 6901's `~0` / `~1` unescaping. A path is split on `/` and the segments are used as literal keys. So keys that themselves contain `/` or `~` are not addressable: there is no escape sequence to reach them.
</Callout>

## Applying updateDataModel envelopes
Expand Down Expand Up @@ -90,11 +94,11 @@ Nesting is just JSON: `value: { name: 'Ada', address: { city: 'London' } }` writ

## Resolving dynamic values

`resolveDynamic` collapses a component's prop to a concrete value against the model. The order is fixed:
`resolveDynamic` collapses a component's prop to a concrete value against the model. Its full signature is `resolveDynamic(value, model, scope?, registry?)` — the function registry is the optional fourth argument, after `scope`. The resolution order is fixed:

1. `null` / `undefined` pass through as-is.
2. Arrays are mapped recursively — each element resolved in turn.
3. A `{ call }` function-call value executes through the function registry passed to `resolveDynamic` (standard set: `formatString`, `formatNumber`, `formatCurrency`, `formatDate`, `pluralize`, `and`, `or`, `not`); args resolve recursively, so they may be bindings or nested calls. Without a registry, or for unknown names, the value resolves to `undefined`. Checked before path refs so a call's `args` never masquerade as a binding.
3. A `{ call }` function-call value executes through the function registry passed as the fourth argument (standard set: `formatString`, `formatNumber`, `formatCurrency`, `formatDate`, `pluralize`, `and`, `or`, `not`, plus the validators `required`, `regex`, `length`, `numeric`, and `email`); args resolve recursively, so they may be bindings or nested calls. Without a registry, or for unknown names, the value resolves to `undefined`. Checked before path refs so a call's `args` never masquerade as a binding.
4. A `{ path }` reference reads from the model.
5. Anything else — a bare string, number, boolean, or plain object — passes through unchanged. Bare values *are* the v0.9 literal form; there are no wrapper objects.

Expand All @@ -115,7 +119,7 @@ A missing path resolves to `undefined`, never an error. That keeps a half-stream

How do you resolve a relative path, like inside a repeated template row?

`resolveDynamic` takes an optional third argument, an `A2uiScope`:
`resolveDynamic` takes an `A2uiScope` as its optional third argument:

```ts
export interface A2uiScope {
Expand All @@ -133,7 +137,7 @@ Path resolution depends on the leading slash:
resolveDynamic({ path: 'name' }, model, { basePath: '', item: undefined }); // "Brian"
```

With `basePath: ''`, the relative path `name` resolves to `/name`. That's the lever children **templates** pull. When a container's `children` is `{ "path": "/items", "componentId": "tpl" }`, it repeats the template component over the array at `/items` and resolves each instance's props with a per-item scope:
With `basePath: ''`, the relative path `name` resolves to `/name`. That is the lever children **templates** pull. When a container's `children` is `{ "path": "/items", "componentId": "tpl" }`, it repeats the template component over the array at `/items` and resolves each instance's props with a per-item scope:

```ts
items.forEach((_, i) => {
Expand All @@ -144,7 +148,7 @@ items.forEach((_, i) => {
```

<Callout type="warning" title="scope.item is informational">
`A2uiScope` carries an `item` field, but the resolver only reads `basePath` to rewrite relative paths. `item` is typed for callers that want the bound element on hand, yet `resolveDynamic` itself never touches it. Don't expect setting `item` to change resolution.
`A2uiScope` carries an `item` field, but the resolver only reads `basePath` to rewrite relative paths. `item` is typed for callers that want the bound element on hand, yet `resolveDynamic` itself never touches it. Do not expect setting `item` to change resolution.
</Callout>

## Next
Expand Down
24 changes: 14 additions & 10 deletions apps/website/content/docs/a2ui/guides/message-protocol.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
---
description: Surfaces, flat components, dynamic values, the four v0.9 envelopes, and how a rendered surface sends an action message back to the agent.
---

# The A2UI message protocol

A2UI is a declarative, streamed wire format: the agent describes a UI, sends it as newline-delimited JSON, and the client renders it and ships actions back.

This page walks the shapes. Everything here is what `@threadplane/a2ui` types and parses; rendering belongs to `@threadplane/chat`'s [`<a2ui-surface>`](/docs/chat/getting-started/introduction). Threadplane implements the **A2UI v0.9.1 stable release**: every envelope carries `"version": "v0.9"`, and the standardized MIME type is `application/a2ui+json` (exported as `A2UI_MIME_TYPE`).
This page walks the shapes. Everything here is what `@threadplane/a2ui` types and parses; rendering belongs to `@threadplane/chat`'s [`<a2ui-surface>`](/docs/chat/a2ui/surface-component). Threadplane implements the **A2UI v0.9.1 stable release**: every envelope carries `"version": "v0.9"`, and the standardized MIME type is `application/a2ui+json` (exported as `A2UI_MIME_TYPE`).

## What's a surface?
## What is a surface?

A surface is one self-contained unit of UI. It owns its own component set and its own data model, and it's addressed by a `surfaceId`.
A surface is one self-contained unit of UI. It owns its own component set and its own data model, and it is addressed by a `surfaceId`.

Every envelope carries that `surfaceId`. An `updateDataModel` for `"booking"` only touches the `booking` surface's data; an `updateComponents` for `"booking"` only defines its components. One stream can drive several surfaces in parallel, kept separate by id.

Expand Down Expand Up @@ -36,9 +40,9 @@ Containers reference their children with `A2uiChildren`, which has two forms:

The container instantiates `componentId` once per element of the array at `path`. Each instance resolves its dynamic values against that element — relative paths inside the template resolve per item. The [data model guide](/docs/a2ui/guides/data-model) covers how that per-item resolution works.

## What's a dynamic value?
## What is a dynamic value?

A prop that's either a literal baked into the message, a reference into the data model, or a client-side function call.
A prop that is either a literal baked into the message, a reference into the data model, or a client-side function call.

In v0.9 literals are **bare values** — no wrapper objects:

Expand All @@ -58,7 +62,7 @@ A function call is `{"call":"formatDate","args":{...}}` — a typed invocation o

## What are the four envelopes?

The stream is a sequence of envelope objects, each with a `version` field and exactly one envelope key. The parser recognizes four keys; anything else is ignored (which keeps the client forward-compatible with future protocol versions).
The stream is a sequence of envelope objects, and each envelope should carry a `version` field and exactly one envelope key. The parser reads only the first key it recognizes, in the order `createSurface`, `updateComponents`, `updateDataModel`, `deleteSurface`, and it defaults a missing `version` to `v0.9`. A line with none of those four keys is ignored, which keeps the client forward-compatible with future protocol versions.

### `createSurface`

Expand Down Expand Up @@ -118,16 +122,16 @@ A user interacts — clicks the Button — and the client sends an `A2uiActionMe
Details worth pinning down:

- **Context is resolved.** The inbound Button's `action.event.context` is a plain object whose values are dynamic values (often `{ path }` bindings). The outbound message's `action.context` is the same keys with each value already resolved against the current data model — here `{ "path": "/origin" }` became `["LAX"]`.
- **`label` is a Threadplane extension.** It's derived from the source component's authored text — for a Button-with-Text-child, the child Text's bare literal string ("Search flights"). It's optional; the transcript renderer uses it to label the user bubble, and backends may ignore it.
- **`label` is a Threadplane extension.** It is derived from the source component's authored text — for a Button-with-Text-child, the child Text's bare literal string ("Search flights"). It is optional; the transcript renderer uses it to label the user bubble, and backends may ignore it.

The client's current data model is only attached as `metadata.a2uiClientDataModel` when the surface's `createSurface` set `sendDataModel: true`. It's omitted otherwise. When present, it's an `A2uiClientDataModel` — `{ surfaces: Record<surfaceId, Record<string, unknown>> }`, the per-surface **live** model keyed by `surfaceId` — user edits included, renderer-internal keys stripped. See [the schema reference](/docs/a2ui/reference/schema#outbound-action-messages) for the full outbound shape.
The client's current data model is only attached as `metadata.a2uiClientDataModel` when the surface's `createSurface` set `sendDataModel: true`. It is omitted otherwise. When present, it is an `A2uiClientDataModel` — `{ surfaces: Record<surfaceId, Record<string, unknown>> }`, the per-surface **live** model keyed by `surfaceId` — user edits included, renderer-internal keys stripped. See [the schema reference](/docs/a2ui/reference/schema#outbound-action-messages) for the full outbound shape.

## Relationship to Google's A2UI

Threadplane implements Google's open [A2UI protocol](https://a2ui.org) ([source](https://github.com/google/A2UI)) at the **v0.9.1 stable release**: the same envelopes, the same flat component shape, the same basic catalog, and the same `v0.9` wire version you'll see stamped on every message. The linked spec is the normative reference; `@threadplane/a2ui` is its TypeScript type system and parsing layer.
Threadplane implements Google's open [A2UI protocol](https://a2ui.org) ([source](https://github.com/google/A2UI)) at the **v0.9.1 stable release**: the same envelopes, the same flat component shape, the same basic catalog, and the same `v0.9` wire version stamped on every message. The linked spec is the normative reference; `@threadplane/a2ui` is its TypeScript type system and parsing layer.

## Next

- [Working with the data model](/docs/a2ui/guides/data-model) — pointers, immutability, and template scopes.
- [Validating and adapting an A2UI stream](/docs/a2ui/guides/adapters-and-validation) — guards and test payloads.
- Rendering these surfaces in Angular: [`<a2ui-surface>`](/docs/chat/getting-started/introduction) in `@threadplane/chat`.
- Rendering these surfaces in Angular: [`<a2ui-surface>`](/docs/chat/a2ui/surface-component) in `@threadplane/chat`.
Loading
Loading