diff --git a/apps/website/content/docs/a2ui/api/api-docs.json b/apps/website/content/docs/a2ui/api/api-docs.json index 15fb63358..cdee2f7cb 100644 --- a/apps/website/content/docs/a2ui/api/api-docs.json +++ b/apps/website/content/docs/a2ui/api/api-docs.json @@ -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): A2uiFunctionRegistry", "params": [ { diff --git a/apps/website/content/docs/a2ui/getting-started/quickstart.mdx b/apps/website/content/docs/a2ui/getting-started/quickstart.mdx index 77c7b65de..a7ee0859c 100644 --- a/apps/website/content/docs/a2ui/getting-started/quickstart.mdx +++ b/apps/website/content/docs/a2ui/getting-started/quickstart.mdx @@ -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 [``](/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 [``](/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. @@ -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--- @@ -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 @@ -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. diff --git a/apps/website/content/docs/a2ui/guides/adapters-and-validation.mdx b/apps/website/content/docs/a2ui/guides/adapters-and-validation.mdx index 73608a8de..608571828 100644 --- a/apps/website/content/docs/a2ui/guides/adapters-and-validation.mdx +++ b/apps/website/content/docs/a2ui/guides/adapters-and-validation.mdx @@ -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. @@ -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. -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 @@ -89,11 +93,11 @@ function renderText(props: { text: unknown }, model: Record) { } ``` -The full mechanics — component resolution, event dispatch, action emission, surface store — are exactly what Threadplane's own Angular renderer, `@threadplane/chat`'s [``](/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 [``](/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. diff --git a/apps/website/content/docs/a2ui/guides/data-model.mdx b/apps/website/content/docs/a2ui/guides/data-model.mdx index 142237d58..1f27b32e5 100644 --- a/apps/website/content/docs/a2ui/guides/data-model.mdx +++ b/apps/website/content/docs/a2ui/guides/data-model.mdx @@ -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. @@ -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 } } } @@ -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: @@ -52,7 +56,7 @@ deleteByPointer({ items: ['a', 'b', 'c'] }, '/items/1'); ``` -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 addressable — there'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. ## Applying updateDataModel envelopes @@ -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. @@ -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 { @@ -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) => { @@ -144,7 +148,7 @@ items.forEach((_, i) => { ``` -`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. ## Next diff --git a/apps/website/content/docs/a2ui/guides/message-protocol.mdx b/apps/website/content/docs/a2ui/guides/message-protocol.mdx index 42b1e5062..d01dd1f14 100644 --- a/apps/website/content/docs/a2ui/guides/message-protocol.mdx +++ b/apps/website/content/docs/a2ui/guides/message-protocol.mdx @@ -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 [``](/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 [``](/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. @@ -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: @@ -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` @@ -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> }`, 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> }`, 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: [``](/docs/chat/getting-started/introduction) in `@threadplane/chat`. +- Rendering these surfaces in Angular: [``](/docs/chat/a2ui/surface-component) in `@threadplane/chat`. diff --git a/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx b/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx index 1870304bf..aafdb4b8f 100644 --- a/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx +++ b/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx @@ -1,6 +1,10 @@ +--- +description: The runtime API of @threadplane/a2ui - the JSONL parser, resolveDynamic, the pointer helpers, the two guards, and the client-side function registry. +--- + # Parser, Resolver, and Guards -`@threadplane/a2ui` exports small helpers that keep stream parsing and dynamic value resolution consistent across packages. +`@threadplane/a2ui` exports the runtime half of the package: a stream parser, a dynamic-value resolver, three pointer helpers, two type guards, and the client-side function registry the resolver dispatches through. The [schema reference](/docs/a2ui/reference/schema) covers the types these helpers operate on. ## createA2uiMessageParser() @@ -24,7 +28,7 @@ Important behavior from source: - a missing `version` field defaults to `'v0.9'`; a present `version` is preserved; - multiple messages can be returned from one chunk. -The recognized envelope keys are `createSurface`, `updateComponents`, `updateDataModel`, and `deleteSurface`. The parser checks only for a known envelope key and a non-null object value. It does not validate each nested field. +The recognized envelope keys are `createSurface`, `updateComponents`, `updateDataModel`, and `deleteSurface`, checked in that order — the first one present on a line wins. The parser checks only for a known envelope key and a non-null object value. It does not validate each nested field. ## resolveDynamic() @@ -40,7 +44,7 @@ resolveDynamic({ path: '/customer/name' }, model); // "Ada" resolveDynamic(2, model); // 2 ``` -`resolveDynamic(value, model, scope?)` handles: +`resolveDynamic(value, model, scope?, registry?)` handles: | Input shape | Result | |-------------|--------| @@ -87,8 +91,9 @@ Current behavior is intentionally small: - empty pointer and `/` point at the root; - missing paths read as `undefined`; -- `setByPointer()` returns a cloned object path rather than mutating the original root; +- `setByPointer()` returns a cloned object path rather than mutating the original root, and creates missing intermediate objects; - `deleteByPointer()` returns the original model when the parent path does not exist; +- `deleteByPointer()` with an empty pointer or `/` returns `{}`; - `deleteByPointer()` on an **array index** sets it to `undefined` and preserves the array's length (the v0.9 array-delete rule). 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. @@ -106,13 +111,75 @@ isFunctionCall(value) // narrows to { call: string; args?: Record` seeded with the standard functions. Entries in `overrides` are added to the map, replacing a standard function of the same name. + +```ts +import type { A2uiFunctionImpl } from '@threadplane/a2ui'; + +const shout: A2uiFunctionImpl = (args, ctx) => + String(ctx.resolveArg(args['value']) ?? '').toUpperCase(); + +const registry = createA2uiFunctionRegistry({ shout }); +``` + +Every implementation receives the raw `args` object and an `A2uiFunctionContext`: + +```ts +interface A2uiFunctionContext { + resolveArg(value: unknown): unknown; + locale?: string; +} +``` + +`resolveArg()` resolves one argument against the same model and scope the outer `resolveDynamic()` call was given, so an argument may itself be a `{ path }` binding or a nested `{ call }`. Arguments are **not** pre-resolved: an implementation that ignores `resolveArg` sees the wire value. `locale` is a BCP 47 tag for `Intl`-based formatting; `resolveDynamic()` does not set it, so the standard formatters fall back to the host default locale. + +The standard set covers formatting, logic, and validation: + +| Function | Args | Returns | +|---|---|---| +| `formatString` | `value` (template) | the template with each `${…}` expression interpolated | +| `formatNumber` | `value`, `decimals?`, `grouping?` | an `Intl.NumberFormat` string | +| `formatCurrency` | `value`, `currency`, `decimals?`, `grouping?` | an `Intl.NumberFormat` currency string | +| `formatDate` | `value`, `format` | the date rendered through a Unicode TR35 pattern subset (`yyyy`, `MMMM`, `dd`, `HH`, `mm`, `a`, …) | +| `pluralize` | `value`, plus a message per plural category (`zero?`, `one`, `other`, …) | the message for the value's `Intl.PluralRules` category | +| `and` / `or` | `values` (array) | whether every / any resolved element is exactly `true` | +| `not` | `value` | `true` unless the resolved value is exactly `true` | +| `required` | `value` | `false` for `null`, `undefined`, an empty string, or an empty array | +| `regex` | `value`, `pattern` | whether the string matches the pattern | +| `length` | `value`, `min?`, `max?` | whether the string length falls in range | +| `numeric` | `value`, `min?`, `max?` | whether the number falls in range | +| `email` | `value` | whether the string has a `local@domain.tld` shape | + +The last five are the validators a check rule's `condition` typically calls — see `checks` in the [schema reference](/docs/a2ui/reference/schema). They return booleans; this package computes them but does nothing with the result. + +Inside `formatString`, a `${…}` expression may be a JSON-pointer path (absolute or relative), a nested named-argument call such as `${formatCurrency(value: /total, currency: 'USD')}`, a quoted string, a number, or a boolean. Write `\${` for a literal `${`. Nested calls dispatch through the same registry. + +An unknown function name resolves to `undefined` and logs a one-time console warning per name. + ## Validation vs handler wiring -This package does not run validation rules, map actions to Angular handlers, or call user functions. It gives you typed values and parsing helpers. +This package does not gate action dispatch on check rules and does not map actions to Angular handlers. `@threadplane/chat` does both: its surface component evaluates every check rule against the live data model before an event action dispatches and emits a `VALIDATION_FAILED` error message when one fails. This package does execute client-side function calls, including your own, through the registry you pass to `resolveDynamic()`. A practical boundary is: -- use `@threadplane/a2ui` to parse and inspect the protocol stream; +- use `@threadplane/a2ui` to parse and inspect the protocol stream, and to resolve values against a model; - use app or server validation to decide whether a message is trusted; - use `@threadplane/chat` and `@threadplane/render` to display surfaces and wire interactions. diff --git a/apps/website/content/docs/a2ui/reference/schema.mdx b/apps/website/content/docs/a2ui/reference/schema.mdx index aa666d5c2..52072b5bb 100644 --- a/apps/website/content/docs/a2ui/reference/schema.mdx +++ b/apps/website/content/docs/a2ui/reference/schema.mdx @@ -1,6 +1,10 @@ +--- +description: The TypeScript model of the A2UI v0.9 protocol: constants, dynamic values, actions, the basic-catalog components, envelopes, and outbound messages. +--- + # A2UI Schema -The `@threadplane/a2ui` schema is a TypeScript model of the A2UI v0.9 protocol shapes the framework uses. It's a contract for agent output and custom integrations, but it's not a runtime validator. +The `@threadplane/a2ui` schema is a TypeScript model of the A2UI v0.9 protocol shapes the framework uses. It is a contract for agent output and custom integrations, but it is not a runtime validator. ## Protocol constants @@ -108,9 +112,9 @@ The basic-catalog component shapes are: | `ChoicePicker` | `options` (array of `{ label, value }`), `value`, `variant`, `displayStyle`, `filterable`, `label` | | `Slider` | `value`, `max`, `min`, `label` | -Note that `Button` has no text prop — its label is a `child` Text component referenced by id. `ChoicePicker` replaces the pre-v0.9 `MultipleChoice` component. +Note that `Button` has no text prop — its label is a `child` Text component referenced by id. -Several fields are constrained to a fixed enum. Emit one of the listed values — an unknown value isn't validated at the protocol layer, but a renderer may ignore it or fall back to a default: +Several fields are constrained to a fixed enum. Emit one of the listed values — an unknown value is not validated at the protocol layer, but a renderer may ignore it or fall back to a default: | Field | On | Allowed values | |-------|----|----------------| @@ -126,7 +130,7 @@ Several fields are constrained to a fixed enum. Emit one of the listed values | `direction` | `List` | `'vertical'` \| `'horizontal'` | | `axis` | `Divider` | `'horizontal'` \| `'vertical'` | -`Tabs` is the one container that doesn't use `A2uiChildren`. Its `tabs` field is an array of `{ title: DynamicString; child: string }` pairs. +`Tabs` is the one container that does not use `A2uiChildren`. Its `tabs` field is an array of `{ title: DynamicString; child: string }` pairs. The union of the basic-catalog shapes is exported as `A2uiCatalogComponent`. The broader `A2uiComponent` also admits non-basic-catalog components (`A2uiComponentBase & Record`) — renderers treat unknown `component` strings as unrenderable and fall back gracefully. diff --git a/apps/website/content/docs/ag-ui/api/fake-agent.mdx b/apps/website/content/docs/ag-ui/api/fake-agent.mdx index 1fe1776f7..b192eeeeb 100644 --- a/apps/website/content/docs/ag-ui/api/fake-agent.mdx +++ b/apps/website/content/docs/ag-ui/api/fake-agent.mdx @@ -1,10 +1,14 @@ +--- +description: FakeAgent and provideFakeAgent() stream a canned AG-UI response in process, with an optional script of raw events for exact test streams. +--- + # FakeAgent `FakeAgent` is an in-process AG-UI test double that emits a canned streaming response without a real backend. Use it for offline development, CI, and component tests. ## provideFakeAgent() -`provideFakeAgent()` is the DI-friendly entry point. It is a drop-in replacement for `provideAgent({ url })` when no backend is available. +`provideFakeAgent()` is the DI-friendly entry point. It is a drop-in replacement for the ref-less `provideAgent({ url })` when no backend is available: it registers the fake under the same shared token, so `injectAgent()` resolves it. ```ts import { bootstrapApplication } from '@angular/platform-browser'; @@ -52,6 +56,23 @@ const agent = toAgent(new FakeAgent({ `FakeAgent` extends `AbstractAgent` from `@ag-ui/client`. Its `run()` method returns an `Observable` that emits the full event sequence — `RUN_STARTED`, optional reasoning events, `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` tokens, `TEXT_MESSAGE_END`, `RUN_FINISHED` — then completes. +### script + +The constructor accepts a fourth option that `FakeAgentConfig` does not carry, so it is reachable only by constructing `FakeAgent` yourself: + +```ts +script?: readonly { + when: 'initial' | { toolMessageFor: string }; + events: readonly BaseEvent[]; +}[]; +``` + +Each branch supplies a raw AG-UI event sequence for tests that need an exact stream — tool calls, `STATE_SNAPSHOT`, `CUSTOM` events, anything the protocol defines. `when: 'initial'` matches the first turn; `{ toolMessageFor: id }` matches the turn whose input carries a tool result for that tool call id. The first matching branch wins, and `FakeAgent` wraps its `events` in `RUN_STARTED` and `RUN_FINISHED` for you. When no branch matches, the canned token reply is emitted instead. + +| Option | Type | Description | +|--------|------|-------------| +| `script` | `readonly { when: 'initial' \| { toolMessageFor: string }; events: readonly BaseEvent[] }[]` | Deterministic event branches. Constructor only — not part of `FakeAgentConfig`, so `provideFakeAgent()` cannot set it. | + ## TestBed example ```ts @@ -99,21 +120,18 @@ See also: [Fake Agent guide](/docs/ag-ui/guides/fake-agent) for practical offlin Practical patterns for offline demos and rapid prototyping with FakeAgent. Full testing patterns for components that use `injectAgent()`. The production provider `provideFakeAgent()` replaces in tests. diff --git a/apps/website/content/docs/ag-ui/api/inject-agent.mdx b/apps/website/content/docs/ag-ui/api/inject-agent.mdx index 791cf8bb4..32e6b3224 100644 --- a/apps/website/content/docs/ag-ui/api/inject-agent.mdx +++ b/apps/website/content/docs/ag-ui/api/inject-agent.mdx @@ -6,7 +6,7 @@ description: injectAgent() returns the AG-UI agent configured by provideAgent() `injectAgent()` retrieves the AG-UI agent from Angular's dependency injection container. Call it in an Angular injection context — typically as a component field initializer. The returned object exposes Angular Signals for reactive UI state and async methods for user actions. -Configuration is supplied globally via [provideAgent()](/docs/ag-ui/api/provide-agent) — `injectAgent()` itself takes no arguments. +Configuration is supplied by [provideAgent()](/docs/ag-ui/api/provide-agent). The no-argument form resolves the shared agent; pass the same `AgentRef` supplied to `provideAgent(ref, …)` to carry the state type through DI. ```ts import { injectAgent } from '@threadplane/ag-ui'; @@ -58,13 +58,14 @@ These fields are stable across runtime adapters and are what chat components con | `messages()` | `Message[]` | Chat messages with `role`, `content`, optional `toolCallIds`, citations, and reasoning. | | `status()` | `'idle' \| 'running' \| 'error'` | UI lifecycle status. | | `isLoading()` | `boolean` | Convenience signal for active streaming. | -| `error()` | `unknown` | Latest runtime error, when present. | +| `error()` | `AgentError \| undefined` | Latest runtime error, when present. | | `toolCalls()` | `ToolCall[]` | Tool calls projected into the chat contract. | | `state()` | `Record` | Latest agent state projected as a plain object. | | `interrupt()` | `AgentInterrupt \| undefined` | Current interrupt, when the backend pauses for human input. | | `events$` | `Observable` | Runtime-neutral observable of transient events (`state_update` / `custom`). Subscribe for side-effects; not a signal. | | `submit(input, opts?)` | `Promise` | Submit a user message or resume payload. | | `stop()` | `Promise` | Abort the active run. | +| `retry()` | `Promise` | Re-run the last submitted input after a failure. No-op while a run is in flight or when there is nothing to retry. | | `regenerate(index)` | `Promise` | Remove the assistant message at `index` and rerun from the preceding user message. | ## AG-UI-specific surface @@ -73,9 +74,9 @@ The AG-UI adapter extends the neutral `Agent` contract with AG-UI-specific proto | Field | Type | Description | |-------|------|-------------| -| `customEvents()` | `CustomStreamEvent[]` | Custom events emitted by the backend during a run. Accumulates per run; resets on each new `submit()`. | +| `customEvents()` | `CustomStreamEvent[]` | Custom events emitted by the backend during a run. Accumulates per run; resets when `RUN_STARTED` arrives. | | `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution used by ``. | -| `subagents()` | `Map` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the legacy `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. | +| `subagents()` | `Map` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. | `injectAgent()` returns the `AgUiAgent` type — the neutral `Agent` contract plus these AG-UI-specific fields — so they are reachable directly, no cast required: @@ -89,7 +90,7 @@ chat.subagents(); // Map The chat a2ui bridge reads `customEvents` to light up live generative-UI streaming when your backend emits `a2ui-partial` events. The consuming side is documented in chat's [A2UI overview](/docs/chat/a2ui/overview). See the [Custom Events guide](/docs/ag-ui/guides/custom-events) for backend wiring details. -Don't confuse `customEvents()` with the neutral `events$` listed above. Each `CUSTOM` event is fanned out to **both**: `events$` is the runtime-neutral `Observable` you subscribe to for transient side-effects (telemetry, toasts), while `customEvents()` is the AG-UI-specific signal that accumulates `CustomStreamEvent[]` as a per-run snapshot for reactive rendering. See the [Event Mapping reference](/docs/ag-ui/reference/event-mapping#custom-events) for the full fan-out. +Do not confuse `customEvents()` with the neutral `events$` listed above. Each `CUSTOM` event is fanned out to **both**: `events$` is the runtime-neutral `Observable` you subscribe to for transient side-effects (telemetry, toasts), while `customEvents()` is the AG-UI-specific signal that accumulates `CustomStreamEvent[]` as a per-run snapshot for reactive rendering. See the [Event Mapping reference](/docs/ag-ui/reference/event-mapping#custom-events-and-interrupts) for the full fan-out. ## Submit and resume @@ -126,21 +127,18 @@ The method throws when the selected index is not an assistant message, when no p Configure the endpoint URL, headers, and lifecycle sink for the agent provider. Wire `customEvents` to live generative-UI streaming from any AG-UI backend. Test components that call `injectAgent()` with the in-process FakeAgent. diff --git a/apps/website/content/docs/ag-ui/api/provide-agent.mdx b/apps/website/content/docs/ag-ui/api/provide-agent.mdx index b4da349f3..6c0e6d5bb 100644 --- a/apps/website/content/docs/ag-ui/api/provide-agent.mdx +++ b/apps/website/content/docs/ag-ui/api/provide-agent.mdx @@ -1,8 +1,23 @@ +--- +description: Register an AG-UI agent in Angular DI with provideAgent() — the ref-less form, the typed AgentRef form, factory config, and the last-ref-wins rule. +--- + # provideAgent() -`provideAgent()` registers the singleton AG-UI agent configuration for every [injectAgent()](/docs/ag-ui/api/inject-agent) call in an Angular application. Call it once in `bootstrapApplication` or an `ApplicationConfig` to wire up the endpoint URL, optional identifiers, custom headers, and telemetry. +`provideAgent()` builds an AG-UI agent from connection options and registers it in Angular's dependency injection container, where [injectAgent()](/docs/ag-ui/api/inject-agent) reads it back. Call it in `bootstrapApplication`, in an `ApplicationConfig`, or in a route's or component's `providers` array to wire up the endpoint URL, optional identifiers, custom headers, and telemetry. + +The function has two overloads: + +```ts +provideAgent(configOrFactory: AgentConfig | (() => AgentConfig)): Provider[]; +provideAgent(ref: AgentRef, configOrFactory: AgentConfig | (() => AgentConfig)): Provider[]; +``` + +The ref-less form registers the agent under a single shared token that the no-argument `injectAgent()` resolves. The `AgentRef` form registers it under that ref's own token, which carries the state type to `injectAgent(ref)` and lets more than one agent coexist at one injector level. + +## Ref-less form -`injectAgent()` itself takes no arguments — all configuration flows through `provideAgent()`. +Most applications talk to one backend and need one agent. Pass the config on its own: ```ts import { bootstrapApplication } from '@angular/platform-browser'; @@ -18,21 +33,31 @@ bootstrapApplication(AppComponent, { }); ``` +Every `injectAgent()` call under that injector then resolves the same agent instance: + +```ts +const agent = injectAgent(); // AgUiAgent> +``` + ## Configuration options | Option | Type | Description | |--------|------|-------------| | `url` | `string` | HTTP endpoint for the AG-UI backend agent. Required. | -| `agentId` | `string` | Optional agent identifier forwarded to the backend. | -| `threadId` | `string` | Optional thread identifier for session continuity. | -| `headers` | `Record` | Optional custom HTTP headers included on every request. | -| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Optional app-owned sink. Supply one to receive runtime lifecycle events. | +| `agentId` | `string` | Agent identifier, when the endpoint serves more than one agent. | +| `threadId` | `string` | Thread to connect to on start. Omit to begin a fresh conversation. | +| `headers` | `Record` | Extra HTTP headers sent with every request. | +| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Omit for automatic development-only collection, pass `false` to disable it, or pass an app-owned sink to receive the runtime lifecycle events yourself. | -## Static vs factory config +## Static versus factory config -Pass a plain `AgentConfig` object when the URL is known up front. Pass a `() => AgentConfig` factory when the config depends on runtime DI state — the factory runs inside an Angular injection context, so it may call `inject()` to read services or environment tokens. +Pass a plain `AgentConfig` object when the URL is known up front. Pass a `() => AgentConfig` factory when the config depends on runtime DI state — the factory runs inside an Angular injection context, so it may call `inject()` to read services, route params, or environment tokens. ```ts +import { inject } from '@angular/core'; +import { provideAgent } from '@threadplane/ag-ui'; +import { APP_ENV } from './app-env'; + // Factory form — reads an environment token at runtime provideAgent(() => { const env = inject(APP_ENV); @@ -43,37 +68,74 @@ provideAgent(() => { }); ``` -## Singleton model +Both overloads accept either shape, so the factory form works with an `AgentRef` too. + +## Typed state with AgentRef -A single `provideAgent({...})` call configures the entire application. Every `injectAgent()` call resolves to the same configured agent. +AG-UI shared state arrives on `agent.state()`. Declaring an `AgentRef` once flows that state shape from the provider to every injection site, so the generic does not have to be restated at each call: ```ts -provideAgent({ url: 'https://api.example.com/agent' }); +import { createAgentRef } from '@threadplane/chat'; +import { provideAgent, injectAgent } from '@threadplane/ag-ui'; -// Elsewhere, inside an injection context: -const chat = injectAgent(); +interface TripState { + day: number; + places: string[]; +} + +export const TRIP = createAgentRef('trip'); + +// app.config.ts +providers: [provideAgent(TRIP, { url: 'http://localhost:8000/trip' })]; + +// component +const agent = injectAgent(TRIP); // AgUiAgent +const day = agent.state().day; // number, not unknown ``` +`createAgentRef` is exported from `@threadplane/chat`, which `@threadplane/ag-ui` already depends on. + +## Several agents at one injector level + +Each `provideAgent(ref, …)` call builds its own agent under its own ref token, so two or more refs may sit side by side in a single `providers` array and return distinct agents: + +```ts +export const TRIP = createAgentRef('trip'); +export const SUPPORT = createAgentRef('support'); + +providers: [ + provideAgent(TRIP, { url: 'http://localhost:8000/trip' }), + provideAgent(SUPPORT, { url: 'http://localhost:8000/support' }), +]; + +// component +const trip = injectAgent(TRIP); // the trip agent +const support = injectAgent(SUPPORT); // the support agent +``` + + +The ref form also aliases the shared token so that the no-argument `injectAgent()` keeps working. That token can only point at one agent, so when several refs are provided at the same injector level the **last** `provideAgent(ref, …)` call wins. In the example above, a bare `injectAgent()` returns the support agent. Always inject by ref when an injector provides more than one agent. + + +With a single ref the alias is exact: one instance, one config evaluation, reachable both as `injectAgent(TRIP)` and as `injectAgent()`. + ## What's Next The primitive you call inside components after registering a provider. End-to-end setup connecting a real AG-UI backend in minutes. Swap the live backend for an in-process test double during development. diff --git a/apps/website/content/docs/ag-ui/api/to-agent.mdx b/apps/website/content/docs/ag-ui/api/to-agent.mdx index ae7d40ae8..162d7c63a 100644 --- a/apps/website/content/docs/ag-ui/api/to-agent.mdx +++ b/apps/website/content/docs/ag-ui/api/to-agent.mdx @@ -1,3 +1,7 @@ +--- +description: toAgent() wraps a raw AG-UI AbstractAgent into the runtime-neutral Agent contract, adding customEvents, clientTools, and subagents. +--- + # toAgent() `toAgent()` is the lower-level adapter function that wraps a raw AG-UI `AbstractAgent` into the runtime-neutral `Agent` contract used by `@threadplane/chat` components. @@ -21,6 +25,7 @@ const agent = toAgent(source, { telemetry: myTelemetrySink }); | Option | Type | Description | |--------|------|-------------| | `telemetry` | `AgentRuntimeTelemetrySink \| false` | Optional app-owned sink. Supply one to receive runtime lifecycle events. | +| `a2uiClientCapabilities` | `{ supportedCatalogIds: string[]; inlineCatalogs?: unknown[] }` | A2UI catalog negotiation to advertise to the agent. Seeded once into the AG-UI shared state under the `a2ui_client_capabilities` key, so every `RunAgentInput.state` carries it. Use `a2uiClientCapabilities()` from `@threadplane/chat` for the renderer's standard value. | ## AgUiAgent @@ -30,9 +35,9 @@ const agent = toAgent(source, { telemetry: myTelemetrySink }); |-------|------|-------------| | `customEvents()` | `Signal` | Custom events accumulated during a run. Resets at the start of each new run. | | `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution. The chat composition uses this when you pass ``. | -| `subagents()` | `Signal>` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the legacy `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. | +| `subagents()` | `Signal>` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. | -The standard `Agent` signals (`messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`, `interrupt`) and actions (`submit`, `stop`, `regenerate`) are all present. +The standard `Agent` signals (`messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`, `interrupt`) and actions (`submit`, `retry`, `stop`, `regenerate`) are all present. ## CustomStreamEvent @@ -74,21 +79,18 @@ The returned `AgUiAgent` does not manage its own lifetime. When using DI via `pr The DI-friendly wrapper around `toAgent()` for most Angular apps. How the AG-UI adapter fits into the broader `@threadplane/chat` design. Which AG-UI protocol events map to which signals and actions. diff --git a/apps/website/content/docs/ag-ui/concepts/architecture.mdx b/apps/website/content/docs/ag-ui/concepts/architecture.mdx index dbc0ac765..5d3deea46 100644 --- a/apps/website/content/docs/ag-ui/concepts/architecture.mdx +++ b/apps/website/content/docs/ag-ui/concepts/architecture.mdx @@ -1,3 +1,7 @@ +--- +description: How the AG-UI adapter reduces protocol events into Angular signals, which providers register an agent, and what the adapter deliberately leaves out. +--- + # Architecture `@threadplane/ag-ui` is an adapter. It does not replace `@threadplane/chat`, and it does not define a new chat runtime. @@ -63,7 +67,7 @@ const agent = injectAgent(); ## Runtime data flow -`toAgent()` subscribes to `source.subscribe({ onEvent, onRunFailed })`. +`toAgent()` subscribes to `source.subscribe({ onRunInitialized, onEvent, onRunFailed })`. Every AG-UI event is passed through the reducer. The reducer updates Angular signals: @@ -71,7 +75,7 @@ Every AG-UI event is passed through the reducer. The reducer updates Angular sig - `status`, `isLoading`, and `error` for run lifecycle. - `toolCalls` for tool call starts, arguments, results, and completion. - `state` for AG-UI state snapshots and JSON Patch deltas. -- `interrupt` cleared on `RUN_STARTED` and set by the `CUSTOM` `on_interrupt` event. +- `interrupt` cleared on `RUN_STARTED`, and set by the `CUSTOM` `on_interrupt` event or by a `RUN_FINISHED` carrying `outcome: { type: 'interrupt' }`. - `events$` for runtime-neutral custom-event side effects. - `customEvents` for accumulated non-`on_interrupt` `CUSTOM` events used by live a2ui and app-specific reactive UI. - `subagents` for `SUBAGENT_*` events and for `ACTIVITY_*` events with `activityType: 'subagent'`. @@ -145,6 +149,29 @@ This is the cleanest way to derive `threadId` (or auth headers) from runtime sta - **Recreate the provider.** Inject `provideAgent({ ..., threadId: newId })` from a fresh injector when the active thread changes. Any prior message history must come from your own host service — pre-populate `setMessages()` on the source before the adapter boots, or render a "loading…" surface while you fetch it. - **Use the LangGraph adapter instead.** `@threadplane/langgraph` accepts `threadId: Signal` and hydrates messages from the latest checkpoint on every change. See its [Persistence guide](/docs/langgraph/guides/persistence). Use AG-UI when your runtime publishes events without checkpoint storage; use LangGraph when the server owns durable thread state. +### Typed state and several agents at one level + +`provideAgent()` has a second overload that takes an `AgentRef` first. The ref carries the state shape from the provider to `injectAgent(ref)`, and it registers the agent under the ref's own token, so more than one agent can be provided at a single injector level: + +```ts +import { createAgentRef } from '@threadplane/chat'; +import { provideAgent, injectAgent } from '@threadplane/ag-ui'; + +export const TRIP = createAgentRef('trip'); +export const SUPPORT = createAgentRef('support'); + +providers: [ + provideAgent(TRIP, { url: '/api/trip' }), + provideAgent(SUPPORT, { url: '/api/support' }), +]; + +// component +const trip = injectAgent(TRIP); // AgUiAgent +const support = injectAgent(SUPPORT); // AgUiAgent +``` + +The ref form also aliases the shared token that the no-argument `injectAgent()` reads. That token can only point at one agent, so when several refs are provided at the same level the **last** `provideAgent(ref, …)` call wins — inject by ref whenever an injector provides more than one agent. See [provideAgent()](/docs/ag-ui/api/provide-agent) for the full rule. + Use `provideFakeAgent()` when you need the UI to run without a backend: ```ts @@ -168,6 +195,8 @@ In Angular apps, prefer the provider API so the agent instance is scoped by DI. `stop()` calls `source.abortRun()`. The actual cancellation behavior depends on the AG-UI source. `HttpAgent` implements abort behavior; a custom source may treat it as a no-op unless you implement cancellation. +`retry()` re-runs the last submitted input without re-appending it to the message history. It clears `error` and sets loading, and it silently does nothing while a run is already in flight or when no input has been submitted yet. + `regenerate(index)` is supported by the shared `Agent` contract. It requires the target message to be an assistant message, finds the preceding user message, trims later messages, syncs the trimmed list back to the AG-UI source with `setMessages()`, and runs again. It throws if another run is loading. ## Current scope @@ -181,14 +210,14 @@ The AG-UI adapter currently covers: - Shared state from `STATE_SNAPSHOT` and `STATE_DELTA`. - Message replacement from `MESSAGES_SNAPSHOT`. - Custom events from non-`on_interrupt` `CUSTOM` events, surfaced through both `events$` and `customEvents`. -- Interrupts from `CUSTOM` events named `on_interrupt`. +- Interrupts from `CUSTOM` events named `on_interrupt`, or from a `RUN_FINISHED` carrying `outcome: { type: 'interrupt' }`. - Browser client tools via `AgUiAgent.clientTools`. - Subagent progress from `SUBAGENT_STARTED`/`SUBAGENT_FINISHED`/`SUBAGENT_ERROR` events with `subagentRunId`-attributed content, and from `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA` events whose `activityType` is `subagent`. - Citations stored under `state.citations`. These features are intentionally out of scope for the AG-UI adapter today: -- **History and time-travel.** AG-UI is an event-stream protocol — it doesn't define a server-side "fetch state of thread X" endpoint, so the adapter can't hydrate prior messages on a `threadId` change the way a checkpoint-aware runtime can. The [Provider choices](#provider-choices) section above describes the two patterns AG-UI consumers use to work around this. +- **History and time-travel.** AG-UI is an event-stream protocol — it does not define a server-side "fetch state of thread X" endpoint, so the adapter cannot hydrate prior messages on a `threadId` change the way a checkpoint-aware runtime can. The [Provider choices](#provider-choices) section above describes the two patterns AG-UI consumers use to work around this. If server-side history or time-travel is central to your product, use the LangGraph adapter for that surface or build a custom adapter against the `@threadplane/chat` `Agent` contract. The [Writing an Adapter guide](/docs/chat/guides/writing-an-adapter#hydrating-from-a-server-stored-thread) walks through the thread-loading design choice in detail. diff --git a/apps/website/content/docs/ag-ui/getting-started/installation.mdx b/apps/website/content/docs/ag-ui/getting-started/installation.mdx index 2eec1cd03..b12ada26f 100644 --- a/apps/website/content/docs/ag-ui/getting-started/installation.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/installation.mdx @@ -1,3 +1,7 @@ +--- +description: Install @threadplane/ag-ui and its peers, register provideAgent() in an Angular app config, and read the agent in a component. +--- + # Installation Supported Angular majors: 20, 21, and 22. @@ -24,7 +28,8 @@ npm install @threadplane/chat @threadplane/ag-ui @ag-ui/client @ag-ui/core marke | Package | Version | | ------------------- | ----------------------------------- | -| `@threadplane/chat` | `*` | +| `@threadplane/chat` | `0.0.66` | +| `@threadplane/telemetry` | `^0.0.66` | | `@angular/core` | `^20.0.0 \|\| ^21.0.0 \|\| ^22.0.0` | | `@ag-ui/client` | `*` | | `@ag-ui/core` | `*` | @@ -60,7 +65,7 @@ export const appConfig: ApplicationConfig = { ## Use in a component ```ts -import { Component, inject } from '@angular/core'; +import { Component } from '@angular/core'; import { ChatComponent } from '@threadplane/chat'; import { injectAgent } from '@threadplane/ag-ui'; @@ -93,7 +98,7 @@ export const appConfig: ApplicationConfig = { }; ``` -`FakeAgent` extends `AbstractAgent` and emits a canned `RUN_STARTED -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT x N -> TEXT_MESSAGE_END -> RUN_FINISHED` sequence. It's a drop-in replacement for `provideAgent({ url })` while you're prototyping. +`FakeAgent` extends `AbstractAgent` and emits a canned `RUN_STARTED -> TEXT_MESSAGE_START -> TEXT_MESSAGE_CONTENT x N -> TEXT_MESSAGE_END -> RUN_FINISHED` sequence. It is a drop-in replacement for the ref-less `provideAgent({ url })` while you are prototyping. `provideFakeAgent` accepts: @@ -117,9 +122,12 @@ import { Observable } from 'rxjs'; import { toAgent } from '@threadplane/ag-ui'; class MyCustomAgent extends AbstractAgent { - protected run(input: RunAgentInput): Observable { + run(input: RunAgentInput): Observable { // Your custom transport (WebSocket, in-process worker, etc.) // emits BaseEvent events. + return new Observable((observer) => { + observer.complete(); + }); } } diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index 5e18aef07..be4014dee 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -1,6 +1,10 @@ +--- +description: What @threadplane/ag-ui does, which parts of the AG-UI protocol it covers, and how it fits between an AG-UI backend and the chat UI primitives. +--- + # Introduction -> **Picking an adapter?** This guide covers `@threadplane/ag-ui` — the AG-UI protocol adapter. If you're talking to LangGraph Platform directly via the LangGraph SDK, use [`@threadplane/langgraph`](/langgraph) instead. See [Choosing an adapter](/docs/choosing-an-adapter) for a side-by-side comparison. +> **Picking an adapter?** This guide covers `@threadplane/ag-ui` — the AG-UI protocol adapter. If you are talking to LangGraph Platform directly via the LangGraph SDK, use [`@threadplane/langgraph`](/langgraph) instead. See [Choosing an adapter](/docs/choosing-an-adapter) for a side-by-side comparison. `@threadplane/ag-ui` is the runtime adapter that wraps an [AG-UI](https://github.com/ag-ui-protocol/ag-ui) `AbstractAgent` into the runtime-neutral `Agent` contract from `@threadplane/chat`. The chat UI primitives consume the Agent contract, and the AG-UI adapter translates between the contract and the AG-UI event protocol. @@ -26,19 +30,19 @@ The AG-UI demo runs this exact chat surface against an AG-UI backend — streami ## What you get -- **`toAgent(source: AbstractAgent): Agent`** - wraps any `AbstractAgent` subclass (custom transports, mocks) into the runtime-neutral `Agent` contract. +- **`toAgent(source: AbstractAgent): AgUiAgent`** - wraps any `AbstractAgent` subclass (custom transports, mocks) into the runtime-neutral `Agent` contract. - **`provideAgent({ url })`** - DI convenience that instantiates `HttpAgent` under the hood for the common SSE/HTTP case. - **`FakeAgent`** - in-process `AbstractAgent` subclass that emits canned streaming events for offline demos and tests. ## What's covered -Here's what the first release handles: +This is what the adapter handles today: - `messages` (streaming token deltas via `TEXT_MESSAGE_*` events) - `status` / `isLoading` / `error` (lifecycle via `RUN_STARTED/FINISHED/ERROR`) - `toolCalls` (streaming tool calls via `TOOL_CALL_*` events) - `state` (snapshots and JSON-Patch deltas) - `events$` (custom events; discriminates `state_update`) -- Interrupts (from `CUSTOM` events named `on_interrupt`) +- Interrupts (from `CUSTOM` events named `on_interrupt`, or a `RUN_FINISHED` carrying an interrupt outcome) - Subagent progress (from `SUBAGENT_*` events, or `ACTIVITY_*` with `activityType: 'subagent'`) Out of scope for now (use `@threadplane/langgraph` if you need LangGraph Platform-specific APIs): diff --git a/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx b/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx index 3ae9f0eef..5b9065a5f 100644 --- a/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx @@ -1,6 +1,10 @@ +--- +description: Bind the chat composition to an AG-UI backend in three steps: install the packages, register provideAgent(), and read the agent with injectAgent(). +--- + # Quick Start -Let's bind `` from `@threadplane/chat` to an AG-UI backend in 5 minutes. +Bind `` from `@threadplane/chat` to an AG-UI backend in 5 minutes. Angular 20–22 project using a Node.js version supported by that Angular major. If you need setup help, see the [Installation](/docs/ag-ui/getting-started/installation) guide. @@ -48,7 +52,7 @@ No backend yet? Swap to `provideFakeAgent({})` - it serves canned streaming resp ```ts -import { Component, inject } from '@angular/core'; +import { Component } from '@angular/core'; import { ChatComponent } from '@threadplane/chat'; import { injectAgent } from '@threadplane/ag-ui'; @@ -63,7 +67,7 @@ export class StreamingComponent { } ``` -That's it. The `` composition handles streaming messages, tool calls, errors, and submit - all bound to the AG-UI backend through the `Agent` contract. +That is all. The `` composition handles streaming messages, tool calls, errors, and submit - all bound to the AG-UI backend through the `Agent` contract. @@ -79,7 +83,7 @@ That's it. The `` composition handles streaming messages, tool calls, erro ## Switching backends without changing UI -For me, the runtime-neutral `Agent` contract is the whole payoff: your component never learns which protocol it's talking to. The cost is a thin translation layer per adapter, but you pay it once and your UI code stops caring. +The runtime-neutral `Agent` contract is the payoff: your component never learns which protocol it is talking to. The cost is a thin translation layer per adapter, paid once, after which the UI code stops caring. So swapping backends is a one-line change in `app.config.ts`, and the component code stays the same: @@ -90,4 +94,4 @@ So swapping backends is a one-line change in `app.config.ts`, and the component + providers: [provideAgent({ url: '...' })], // AG-UI ``` -These are two different `provideAgent` functions from two different packages — the import source changes along with the config key (`apiUrl` for LangGraph, `url` for AG-UI). It's not one symbol that accepts both. +These are two different `provideAgent` functions from two different packages — the import source changes along with the config key (`apiUrl` for LangGraph, `url` for AG-UI). It is not one symbol that accepts both. diff --git a/apps/website/content/docs/ag-ui/guides/citations.mdx b/apps/website/content/docs/ag-ui/guides/citations.mdx index 108788e5e..5abace854 100644 --- a/apps/website/content/docs/ag-ui/guides/citations.mdx +++ b/apps/website/content/docs/ag-ui/guides/citations.mdx @@ -1,3 +1,7 @@ +--- +description: Attach sources to AG-UI assistant messages through state.citations, which field names the bridge normalizes, and when the merge runs. +--- + # Citations `@threadplane/ag-ui` can copy citations from AG-UI state onto chat messages. @@ -75,6 +79,9 @@ The bridge normalizes a few common field names: | `title` | `title` or `name` | | `url` | `url`, `href`, or `source` | | `snippet` | `snippet`, `content`, or `excerpt` | +| `sourceType` | `sourceType` string | +| `iconUrl` | `iconUrl` string | +| `publishedAt` | `publishedAt` as a string, a number, or a `Date` | | `extra` | `extra` object | String entries are also accepted: @@ -102,7 +109,7 @@ Citations are merged after: They are not merged after plain text events. If your backend streams the final answer first and citations later, send a state event after citation data is available. -If your backend sends citations before the matching message exists, send another state event after the message is created or use `MESSAGES_SNAPSHOT` with messages that already include citations. +If your backend sends citations before the matching message exists, send another state event after the message is created, or send a `MESSAGES_SNAPSHOT`: the adapter re-bridges the current `state.citations` over the replaced list. ## Manual bridge @@ -123,6 +130,6 @@ Most apps should not need this directly. The built-in AG-UI reducer already call Citation matching is by message id, not by order. Stable message ids matter. -The bridge returns messages unchanged when `state.citations` is missing, not an object, or the entry for a message is empty. +The bridge returns messages unchanged when `state.citations` is missing, is not an object, or the entry for a message is absent, not an array, or empty. The bridge normalizes citation shape; it does not fetch metadata, validate URLs, or deduplicate sources across messages. diff --git a/apps/website/content/docs/ag-ui/guides/custom-events.mdx b/apps/website/content/docs/ag-ui/guides/custom-events.mdx index 062ae00b4..9e4987223 100644 --- a/apps/website/content/docs/ag-ui/guides/custom-events.mdx +++ b/apps/website/content/docs/ag-ui/guides/custom-events.mdx @@ -1,3 +1,7 @@ +--- +description: How an AG-UI backend produces CUSTOM events, which delivery paths reach the adapter under ag-ui-langgraph, and how Angular reads the customEvents signal. +--- + # Custom Events AG-UI `CUSTOM` events let a backend node push arbitrary data to the Angular client while a run is in progress. The adapter accumulates these events into a `customEvents` signal on the `AgUiAgent` returned by `injectAgent()` — reachable directly, no cast required (shown in [Reading Custom Events](#reading-custom-events-in-angular) below). @@ -17,35 +21,49 @@ The special `CUSTOM` event with `name: "on_interrupt"` is handled separately — ## Where Custom Events Come From -A LangGraph node emits a custom event by writing to the stream writer with `stream_mode='custom'`: +Exactly one wire event feeds `customEvents`: an AG-UI `CUSTOM` frame whose `name` is anything other than `on_interrupt`. Every backend path below is judged by whether it produces that frame. + +```json +{ + "type": "CUSTOM", + "name": "analysis_progress", + "value": { "step": "scoring", "pct": 42 } +} +``` + +The adapter JSON-parses `value` when it arrives as a string, so consumers always receive the structured object. The event is appended to `customEvents` as `{ name: "analysis_progress", data: { step: "scoring", pct: 42 } }`. + +### The working path under ag-ui-langgraph + +The `ag-ui-langgraph` bridge consumes the graph through `astream_events`, and it forwards every `on_custom_event` it sees one-for-one as an AG-UI `CUSTOM` frame carrying the same name and payload. LangChain's `adispatch_custom_event` is what puts an `on_custom_event` on that stream, so it is the call a node (or a callback handler running inside one) makes to reach `customEvents`: ```python +from langchain_core.callbacks import adispatch_custom_event from langchain_core.runnables import RunnableConfig -from langgraph.config import get_stream_writer - -def analysis_node(state: State, config: RunnableConfig) -> State: - writer = get_stream_writer() +async def analysis_node(state: State, config: RunnableConfig) -> State: # Emit a partial result as the node runs - writer({"name": "analysis_progress", "data": {"step": "scoring", "pct": 42}}) + await adispatch_custom_event( + "analysis_progress", {"step": "scoring", "pct": 42} + ) # ... do more work ... - writer({"name": "analysis_progress", "data": {"step": "scoring", "pct": 100}}) + await adispatch_custom_event( + "analysis_progress", {"step": "scoring", "pct": 100} + ) return state ``` -The `ag-ui-langgraph` package surfaces this as an AG-UI `CUSTOM` event on the wire: +The event name becomes `CustomStreamEvent.name` and the payload becomes `CustomStreamEvent.data`. This is the mechanism the [subagents example](/docs/ag-ui/guides/subagents) uses to stream child-agent tokens from a callback handler. -```json -{ - "type": "CUSTOM", - "name": "analysis_progress", - "value": { "step": "scoring", "pct": 42 } -} -``` + +Writing to `get_stream_writer()` with `stream_mode='custom'` does **not** produce a `CUSTOM` frame under `ag-ui-langgraph`. The bridge reads `astream_events`, where a stream-writer write surfaces at most as a raw event, so nothing is appended to `customEvents`. Use `adispatch_custom_event` instead. Other AG-UI runtimes that emit `CUSTOM` frames directly are unaffected by this constraint — the adapter only cares that a `CUSTOM` frame arrives. + -The adapter JSON-parses `value` when it arrives as a string, so consumers always receive the structured object. The event is appended to `customEvents` as `{ name: "analysis_progress", data: { step: "scoring", pct: 42 } }`. +### Graph state is a different signal + +The other way for an `ag-ui-langgraph` node to push data mid-run is to return it as a top-level graph state field. The bridge auto-emits state as `STATE_SNAPSHOT` and `STATE_DELTA` frames, and the adapter reduces those into `agent.state()` — not into `customEvents`. Reach for state when the client needs the current value of something, and for a `CUSTOM` event when the client needs the individual occurrences. The [Generative UI guide](/docs/ag-ui/guides/json-render) takes the state route for exactly this reason. ## Reading Custom Events in Angular @@ -97,7 +115,6 @@ When you only need to derive a value, `computed` is more concise: import { Component, ChangeDetectionStrategy, computed } from '@angular/core'; import { ChatComponent } from '@threadplane/chat'; import { injectAgent } from '@threadplane/ag-ui'; -import type { CustomStreamEvent } from '@threadplane/ag-ui'; @Component({ standalone: true, @@ -113,7 +130,7 @@ export class AnalysisComponent { protected readonly progressEvents = computed(() => this.agent.customEvents().filter( - (e): e is CustomStreamEvent => e.name === 'analysis_progress', + (e) => e.name === 'analysis_progress', ), ); } diff --git a/apps/website/content/docs/ag-ui/guides/fake-agent.mdx b/apps/website/content/docs/ag-ui/guides/fake-agent.mdx index b33cb663b..c247734c2 100644 --- a/apps/website/content/docs/ag-ui/guides/fake-agent.mdx +++ b/apps/website/content/docs/ag-ui/guides/fake-agent.mdx @@ -1,6 +1,10 @@ +--- +description: Run the chat UI with no backend using provideFakeAgent(), what the canned stream contains, and when to construct FakeAgent with a script instead. +--- + # Fake Agent -`FakeAgent` is an in-process AG-UI `AbstractAgent` for frontend work when the backend isn't ready yet. +`FakeAgent` is an in-process AG-UI `AbstractAgent` for frontend work when the backend is not ready yet. It emits a canned stream: @@ -11,7 +15,7 @@ It emits a canned stream: 5. `TEXT_MESSAGE_END` 6. `RUN_FINISHED` -It's for demos, story-like development, and tests. It's not a production transport. +It is for demos, story-like development, and tests. It is not a production transport. ## Use the provider @@ -93,11 +97,12 @@ That gives you the same `Agent` contract as `provideFakeAgent()`. | `tokens` | `string[]` | A short canned greeting | Emitted as text deltas in order. | | `reasoningTokens` | `string[]` | `[]` | Emitted before text deltas. | | `delayMs` | `number` | `60` | Delay between events after the initial start delay. | +| `script` | `readonly { when: 'initial' \| { toolMessageFor: string }; events: readonly BaseEvent[] }[]` | `[]` | Constructor only — not part of `FakeAgentConfig`, so `provideFakeAgent()` cannot set it. Supplies a raw AG-UI event sequence per branch, wrapped in `RUN_STARTED` / `RUN_FINISHED`. | ## What it does not do -`FakeAgent` does not call a model, execute tools, persist history, or simulate interrupts. +`provideFakeAgent()` does not call a model, execute tools, persist history, or simulate interrupts. Constructed directly, `FakeAgent` accepts a `script` of raw AG-UI events for tests that need an exact stream, so tool calls, state, and interrupts are reachable that way. -It's deliberately small. Use it to keep UI work moving, not to validate backend behavior. +It is deliberately small. Use it to keep UI work moving, not to validate backend behavior. For backend integration, test against your real AG-UI endpoint and the event map in [Event Mapping](/docs/ag-ui/reference/event-mapping). diff --git a/apps/website/content/docs/ag-ui/guides/testing.mdx b/apps/website/content/docs/ag-ui/guides/testing.mdx index 943f4cbf6..ecd0dc716 100644 --- a/apps/website/content/docs/ag-ui/guides/testing.mdx +++ b/apps/website/content/docs/ag-ui/guides/testing.mdx @@ -1,15 +1,19 @@ +--- +description: Test AG-UI components with provideFakeAgent(), the neutral mockAgent(), or a scripted AbstractAgent, and know which double covers which surface. +--- + # Testing `@threadplane/ag-ui` gives you two test doubles, smallest scope first: - **`provideFakeAgent()`** — a one-call fake backend that runs the real adapter pipeline and streams canned tokens. No server, no LLM. -- **`mockAgent()`** (from `@threadplane/chat`) — a writable-signal mock for component/unit tests. The AG-UI agent _is_ the neutral `Agent` contract, so there is no AG-UI-specific mock — use the neutral one directly. +- **`mockAgent()`** (from `@threadplane/chat`) — a writable-signal mock for component/unit tests. It covers the neutral `Agent` surface; a component that reads the AG-UI extensions needs one of the doubles above instead. See [Choosing an adapter → Testing](/docs/choosing-an-adapter#testing) for when to use each test double. ## Fake backend: `provideFakeAgent()` -`provideFakeAgent({ tokens, reasoningTokens, delayMs })` wires a fake backend into Angular DI in one call. It exercises the **real adapter pipeline** — `injectAgent()`, status transitions, message accumulation — but the wire events are canned, so there's no server and no LLM. Use it for adapter-integration tests, in-browser demos, and offline development. +`provideFakeAgent({ tokens, reasoningTokens, delayMs })` wires a fake backend into Angular DI in one call. It exercises the **real adapter pipeline** — `injectAgent()`, status transitions, message accumulation — but the wire events are canned, so there is no server and no LLM. Use it for adapter-integration tests, in-browser demos, and offline development. It drops in exactly where `provideAgent()` would go — the call shape matches the LangGraph adapter: @@ -87,15 +91,17 @@ describe('chat via provideFakeAgent', () => { ## Contract mock: `mockAgent()` -For component and unit tests where you don't need a streaming pipeline at all, use the neutral `mockAgent(initial)` from `@threadplane/chat`. It returns an `Agent` whose state surface is exposed as **writable signals** — set state directly and assert your component reacts. Nothing is real. +For component and unit tests where you do not need a streaming pipeline at all, use the neutral `mockAgent(initial)` from `@threadplane/chat`. It returns an `Agent` whose state surface is exposed as **writable signals** — set state directly and assert your component reacts. Nothing is real. -The AG-UI adapter exposes exactly the neutral `Agent` contract, so there is no AG-UI-specific mock — `mockAgent()` is the right tool. +`mockAgent()` covers the neutral `Agent` surface. A component that reads the AG-UI extensions (`customEvents`, `clientTools`, `subagents`) needs `provideFakeAgent()` or a scripted `AbstractAgent` instead, because `mockAgent()` implements only `Agent`. ```typescript -import { mockAgent } from '@threadplane/chat'; +import { mockAgent, staticDelivery } from '@threadplane/chat'; const m = mockAgent({ status: 'running' }); -m.messages.set([{ role: 'assistant', content: 'Hello!' }]); +m.messages.set([ + { id: '1', role: 'assistant', content: 'Hello!', delivery: staticDelivery('1') }, +]); expect(m.messages()[0].content).toBe('Hello!'); expect(m.status()).toBe('running'); @@ -103,7 +109,7 @@ expect(m.status()).toBe('running'); ## Testing tool calls, state, and custom events -`FakeAgent` only scripts `RUN_*`, `REASONING_MESSAGE_*`, and `TEXT_MESSAGE_*` events — it never emits `TOOL_CALL_*`, `STATE_SNAPSHOT`/`STATE_DELTA`, or `CUSTOM`. To exercise the reducer's headline non-text features — tool-call rendering, shared state, citations, custom events — script your own `AbstractAgent` and feed it through `toAgent()`. The adapter reduces your scripted events into `toolCalls()`, `state()`, and `customEvents()` exactly as it would real wire events. +`provideFakeAgent()` only produces `RUN_*`, `REASONING_MESSAGE_*`, and `TEXT_MESSAGE_*` events — it never emits `TOOL_CALL_*`, `STATE_SNAPSHOT`/`STATE_DELTA`, or `CUSTOM`. To exercise the reducer's headline non-text features — tool-call rendering, shared state, citations, custom events — either pass a `script` of raw events to a directly-constructed `FakeAgent`, or script your own `AbstractAgent` and feed it through `toAgent()`. The adapter reduces your scripted events into `toolCalls()`, `state()`, and `customEvents()` exactly as it would real wire events. ```typescript import { describe, it, expect } from 'vitest'; @@ -147,4 +153,4 @@ describe('scripted AG-UI events', () => { }); ``` -`customEvents()` is the AG-UI-specific signal — `toAgent()` returns an `AgUiAgent`, so it's reachable directly here without a cast. A `CUSTOM` event named `on_interrupt` would instead populate `agent.interrupt()`; see the [Interrupts guide](/docs/ag-ui/guides/interrupts). +`customEvents()` is the AG-UI-specific signal — `toAgent()` returns an `AgUiAgent`, so it is reachable directly here without a cast. A `CUSTOM` event named `on_interrupt` would instead populate `agent.interrupt()`; see the [Interrupts guide](/docs/ag-ui/guides/interrupts). diff --git a/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx index a46fb7099..10518a7da 100644 --- a/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx +++ b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx @@ -1,3 +1,7 @@ +--- +description: Diagnose AG-UI integration failures — nothing renders, loading never stops, errors stay hidden, state or citations do not arrive, retry does nothing. +--- + # Troubleshooting Most AG-UI integration issues are event-shape problems. Start by checking what your backend actually emits against the [Event Mapping](/docs/ag-ui/reference/event-mapping). @@ -55,6 +59,8 @@ It sets `isLoading` back to `false` on: - `RUN_FINISHED` - `RUN_ERROR` - `onRunFailed` +- a pause — a `CUSTOM` `on_interrupt` event, or a `RUN_FINISHED` carrying `outcome: { type: 'interrupt' }` +- `stop()`, which aborts the active run If loading never stops, your backend likely did not emit a terminal event or the AG-UI source did not report a failure. @@ -133,11 +139,15 @@ Cancellation depends on the AG-UI source implementation. `HttpAgent` supports ab Pass the index of the assistant message you want to replace, not the user message. +## Retry does nothing + +`retry()` re-runs the last submitted input. It is a silent no-op in two cases: a run is already in flight, or nothing has been submitted on this agent yet. Neither case throws and neither sets `error`, so a retry button that appears to do nothing usually means one of the two. Check `isLoading()` before calling it, and note that a fresh agent has nothing to retry until the first `submit()`. + ## History or time-travel does not work History and time-travel are not implemented by the AG-UI adapter today. -Current scope is messages, status/loading/error, tool calls, state/custom events, reasoning messages, message snapshots, interrupts from `CUSTOM` `on_interrupt` events, subagent progress from `SUBAGENT_*` and `ACTIVITY_*` events, and citations from state. +Current scope is messages, status/loading/error, tool calls, state/custom events, reasoning messages, message snapshots, interrupts from `CUSTOM` `on_interrupt` events and from `RUN_FINISHED` interrupt outcomes, subagent progress from `SUBAGENT_*` and `ACTIVITY_*` events, and citations from state. Use `@threadplane/langgraph` if you need LangGraph Platform thread history APIs, or write a custom adapter against the `@threadplane/chat` `Agent` contract if you need AG-UI plus product-specific behavior. diff --git a/apps/website/content/docs/chat/a2ui/catalog.mdx b/apps/website/content/docs/chat/a2ui/catalog.mdx index 808eebb43..67159e6b9 100644 --- a/apps/website/content/docs/chat/a2ui/catalog.mdx +++ b/apps/website/content/docs/chat/a2ui/catalog.mdx @@ -1,3 +1,7 @@ +--- +description: Every component in the built-in A2UI catalog - its Angular class, selector, props and defaults, plus how bound inputs write values back. +--- + # Component Catalog The built-in A2UI catalog provides 18 Angular components implementing the A2UI v0.9 basic catalog — display, layout, interactive controls, media, and advanced inputs. Pass `a2uiBasicCatalog()` to the `ChatComponent` `views` input to enable A2UI rendering, or instantiate it directly for custom setups. @@ -103,6 +107,7 @@ Arranges children horizontally with a flex row layout. | `childKeys` | `string[]` | Ordered list of child component IDs (from the wire `children` array) | | `justify` | `'start' \| 'center' \| 'end' \| 'spaceAround' \| 'spaceBetween' \| 'spaceEvenly' \| 'stretch'` | Main-axis arrangement. Defaults to `start` | | `align` | `'start' \| 'center' \| 'end' \| 'stretch'` | Cross-axis alignment. Defaults to `stretch` | +| `gap` | `number \| 'small' \| 'medium' \| 'large' \| undefined` | Space between children. A number is a spacing unit rendered as `n * 4` pixels; `small`/`medium`/`large` render as 8/12/16 pixels. Unset falls back to the stylesheet default | | `spec` | `Spec` | Injected automatically by the render engine | ### Column @@ -118,6 +123,7 @@ Arranges children vertically with a flex column layout. | `childKeys` | `string[]` | Ordered list of child component IDs | | `justify` | `'start' \| 'center' \| 'end' \| 'spaceAround' \| 'spaceBetween' \| 'spaceEvenly' \| 'stretch'` | Main-axis arrangement. Defaults to `start` | | `align` | `'start' \| 'center' \| 'end' \| 'stretch'` | Cross-axis alignment. Defaults to `stretch` | +| `gap` | `number \| 'small' \| 'medium' \| 'large' \| undefined` | Space between children. A number is a spacing unit rendered as `n * 4` pixels; `small`/`medium`/`large` render as 8/12/16 pixels. Unset falls back to the stylesheet default | | `spec` | `Spec` | Injected automatically by the render engine | ### Card @@ -197,6 +203,10 @@ Renders a button that dispatches an action when clicked. On the wire, a Button h Context values can be path references (resolved at click time) or bare literals. The resulting `A2uiActionMessage` is emitted on ``'s `(action)` output. + +The five components below (`TextField`, `CheckBox`, `ChoicePicker`, `DateTimeInput`, `Slider`) do not declare an `emit` prop. A user edit is written back by calling `emitBinding()` against the component's `_bindings` map, which the render engine populates from the path references in the wire payload. + + ### TextField A text input with optional label, supporting single-line, multi-line, numeric, and obscured variants. @@ -213,7 +223,6 @@ A text input with optional label, supporting single-line, multi-line, numeric, a | `placeholder` | `string` | Placeholder text | | `validationRegexp` | `string` | Client-side validation pattern | | `_bindings` | `Record` | Auto-populated from path references | -| `emit` | injected | Event emitter provided by the render engine | ```json { @@ -239,11 +248,10 @@ A labeled checkbox with two-way binding for its checked state. | `label` | `string` | Checkbox label | | `value` | `boolean` | Current checked state (resolved from a path reference) | | `_bindings` | `Record` | Auto-populated from path references | -| `emit` | injected | Event emitter provided by the render engine | ### ChoicePicker -Selects one or more options from a list. Replaces the pre-v0.9 `MultipleChoice` component. +Selects one or more options from a list. | A2UI type | Angular component | Selector | |-----------|-------------------|----------| @@ -258,7 +266,6 @@ Selects one or more options from a list. Replaces the pre-v0.9 `MultipleChoice` | `displayStyle` | `'checkbox' \| 'chips'` | Visual style. Defaults to `checkbox` | | `filterable` | `boolean` | Shows a client-side option filter input when `true` | | `_bindings` | `Record` | Auto-populated from path references | -| `emit` | injected | Event emitter provided by the render engine | ```json { @@ -291,7 +298,6 @@ A date, time, or datetime input with two-way binding. | `min` | `string` | ISO 8601 lower bound (native `min`) | | `max` | `string` | ISO 8601 upper bound (native `max`) | | `_bindings` | `Record` | Auto-populated from path references | -| `emit` | injected | Event emitter provided by the render engine | The HTML input type (`date`, `time`, or `datetime-local`) is derived internally from `enableDate` and `enableTime`. @@ -319,9 +325,8 @@ A range slider input with two-way binding. | `label` | `string` | Slider label | | `value` | `number` | Current value (bind via a path reference) | | `min` | `number` | Minimum value. Defaults to `0` | -| `max` | `number` | Maximum value | +| `max` | `number` | Maximum value. Defaults to `100` | | `_bindings` | `Record` | Auto-populated from path references | -| `emit` | injected | Event emitter provided by the render engine | ```json { @@ -397,7 +402,7 @@ Renders an HTML5 `