diff --git a/apps/website/content/docs/a2ui/getting-started/introduction.mdx b/apps/website/content/docs/a2ui/getting-started/introduction.mdx new file mode 100644 index 000000000..99fc405ff --- /dev/null +++ b/apps/website/content/docs/a2ui/getting-started/introduction.mdx @@ -0,0 +1,74 @@ +# Introduction + +`@ngaf/a2ui` is the protocol layer for A2UI messages. It gives the rest of the framework a shared TypeScript vocabulary for agent-built surfaces, streamed JSONL messages, dynamic values, and outbound action payloads. + +It does not render Angular components. It does not register handler functions. It does not decide how an agent should respond to a button click. Those jobs sit in `@ngaf/chat` and `@ngaf/render`. + +## What the package owns + +The public entry point exports four groups of tools: + +| Area | Exports | +|------|---------| +| Wire types | `A2uiMessage`, `A2uiComponent`, component prop interfaces, data-model update types, action message types | +| Stream parsing | `createA2uiMessageParser()` | +| Data access | `getByPointer()`, `setByPointer()`, `deleteByPointer()` | +| Dynamic values | `resolveDynamic()`, `A2uiScope`, literal/path guards | + +Use this package when you are building an adapter, validating an agent stream, testing A2UI payloads, or integrating a custom renderer with the same protocol surface that `@ngaf/chat` uses. + +## Message flow + +The parser expects newline-delimited JSON. Each line is checked for one known envelope key: + +```text +surfaceUpdate +dataModelUpdate +beginRendering +deleteSurface +``` + +When a line parses and has one of those envelope keys, it is returned as an `A2uiMessage`. Unknown envelopes are ignored. Malformed lines are skipped. Incomplete JSON waits in the internal buffer until a newline arrives. + +```ts +import { createA2uiMessageParser } from '@ngaf/a2ui'; + +const parser = createA2uiMessageParser(); + +const messages = parser.push( + '{"beginRendering":{"surfaceId":"checkout","root":"root"}}\n', +); +``` + +That posture is intentional. Agent streams are partial by nature. The low-level parser favors safe continuation over throwing during render. + +## Relationship to chat and render + +`@ngaf/chat` detects A2UI content in assistant output, feeds the JSONL stream into `createA2uiMessageParser()`, applies messages to its surface store, and renders those surfaces through the A2UI render components. + +`@ngaf/render` owns Angular component resolution, event dispatch, state updates, and handler execution. The A2UI package only describes protocol shapes and helper behavior. + +That separation matters when debugging: + +- If JSONL chunks are not becoming messages, inspect `@ngaf/a2ui`. +- If messages are not becoming surfaces, inspect the chat A2UI surface store. +- If components render incorrectly or handlers do not run, inspect the render/chat integration. + +## Safe fallback posture + +The parser and resolver are deliberately conservative: + +- malformed JSONL lines are skipped; +- unknown envelope keys are ignored; +- missing data-model paths resolve to `undefined`; +- unrecognized dynamic-value shapes pass through unchanged. + +This makes the protocol layer suitable for streaming, but it is not a full schema validator. If you accept untrusted agent output, validate the payload at your boundary before wiring it to privileged handlers. + +## Install + +```bash +npm install @ngaf/a2ui +``` + +The package has no peer dependencies. diff --git a/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx b/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx new file mode 100644 index 000000000..21359975f --- /dev/null +++ b/apps/website/content/docs/a2ui/reference/parser-resolver-guards.mdx @@ -0,0 +1,119 @@ +# Parser, Resolver, and Guards + +`@ngaf/a2ui` exports small helpers that keep stream parsing and dynamic value resolution consistent across packages. + +## createA2uiMessageParser() + +```ts +import { createA2uiMessageParser } from '@ngaf/a2ui'; + +const parser = createA2uiMessageParser(); +const messages = parser.push(chunk); +``` + +`push(chunk)` appends the chunk to an internal buffer and returns every complete message found before the last newline. + +Important behavior from source: + +- the parser is JSONL-based; +- a complete message requires a trailing newline; +- CRLF works because each line is trimmed; +- empty lines are ignored; +- malformed lines are skipped silently; +- unknown top-level envelopes are ignored; +- multiple messages can be returned from one chunk. + +The parser checks only for the known envelope key and a non-null object value. It does not validate each nested field. + +## resolveDynamic() + +```ts +import { resolveDynamic } from '@ngaf/a2ui'; + +const model = { + customer: { name: 'Ada' }, + count: 2, +}; + +resolveDynamic({ path: '/customer/name' }, model); // "Ada" +resolveDynamic({ literalNumber: 2 }, model); // 2 +``` + +`resolveDynamic(value, model, scope?)` handles: + +| Input shape | Result | +|-------------|--------| +| `{ literalString }` | the wrapped string | +| `{ literalNumber }` | the wrapped number | +| `{ literalBoolean }` | the wrapped boolean | +| `{ literalArray }` | the wrapped array | +| `{ path }` | the value at that model path | +| arrays | recursively resolved array values | +| `null` or `undefined` | returned as-is | +| unrecognized shapes | returned as-is | + +Absolute paths start with `/`. + +Relative paths resolve against `scope.basePath` when a scope is supplied. Without a scope, a relative path is treated as root-relative by prefixing `/`. + +```ts +resolveDynamic( + { path: 'name' }, + { items: [{ name: 'Ada' }] }, + { basePath: '/items/0', item: { name: 'Ada' } }, +); // "Ada" +``` + +`A2uiScope.item` is part of the public type, but the current resolver only uses `basePath`. + +## Pointer helpers + +```ts +import { getByPointer, setByPointer, deleteByPointer } from '@ngaf/a2ui'; +``` + +The pointer helpers use slash-separated paths: + +```ts +const model = { customer: { name: 'Ada' } }; + +getByPointer(model, '/customer/name'); // "Ada" +setByPointer(model, '/customer/name', 'Grace'); +deleteByPointer(model, '/customer/name'); +``` + +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; +- `deleteByPointer()` returns the original model when the parent path does not exist. + +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. + +## Guards + +The public guards are: + +```ts +isLiteralString(value) +isLiteralNumber(value) +isLiteralBoolean(value) +isPathRef(value) +``` + +They are shape checks for dynamic wrapper objects. The literal guards check for the presence of the wrapper key. `isPathRef()` also verifies that `path` is a string. + +Use them when you need to branch on protocol values without importing internal renderer code. + +## 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. + +A practical boundary is: + +- use `@ngaf/a2ui` to parse and inspect the protocol stream; +- use app or server validation to decide whether a message is trusted; +- use `@ngaf/chat` and `@ngaf/render` to display surfaces and wire interactions. + +That split keeps protocol parsing deterministic and keeps privileged behavior in the host application. diff --git a/apps/website/content/docs/a2ui/reference/schema.mdx b/apps/website/content/docs/a2ui/reference/schema.mdx new file mode 100644 index 000000000..ff0aa90d6 --- /dev/null +++ b/apps/website/content/docs/a2ui/reference/schema.mdx @@ -0,0 +1,203 @@ +# A2UI Schema + +The `@ngaf/a2ui` schema is a TypeScript model of the protocol shapes used by the framework. It is useful as a contract for agent output and custom integrations, but it is not a runtime validator. + +## Dynamic values + +Dynamic values are wrapped objects. A value can be literal or resolved from the surface data model by path. + +```ts +type DynamicString = + | { literalString: string } + | { path: string }; + +type DynamicNumber = + | { literalNumber: number } + | { path: string }; + +type DynamicBoolean = + | { literalBoolean: boolean } + | { path: string }; + +type DynamicStringList = + | { literalArray: string[] } + | { path: string }; +``` + +Absolute paths start with `/` and are resolved from the model root. Relative paths are resolved from an optional `A2uiScope`. + +## Children + +Layout components use either explicit child IDs or a template declaration. + +```ts +type A2uiChildren = + | { explicitList: string[] } + | { template: { componentId: string; dataBinding: string } }; +``` + +The protocol layer only types this shape. Template expansion is renderer behavior. + +## Actions + +An action has a name and optional context entries. Context values use the same dynamic wrappers as component props. + +```ts +interface A2uiAction { + name: string; + context?: A2uiActionContextEntry[]; +} +``` + +`@ngaf/a2ui` does not execute actions. It only describes the payload that chat/render code can turn into an outbound `A2uiActionMessage`. + +## Components + +Every component has an `id`, optional `weight`, and a single-key `component` union. + +```ts +interface A2uiComponent { + id: string; + weight?: number; + component: A2uiComponentDef; +} +``` + +The exported component definitions are: + +| Definition | Main fields | +|------------|-------------| +| `Text` | `text`, `usageHint` | +| `Image` | `url`, `alt`, `width`, `height` | +| `Icon` | `icon`, `size` | +| `Video` | `url`, `autoPlay`, `controls` | +| `AudioPlayer` | `url`, `autoPlay`, `controls` | +| `Row` | `children`, `gap`, `alignment`, `distribution` | +| `Column` | `children`, `gap`, `alignment` | +| `List` | `children`, `direction` | +| `Card` | `child` | +| `Tabs` | `tabItems` | +| `Divider` | `direction` | +| `Modal` | `entryPointChild`, `contentChild`, `title` | +| `Button` | `child`, `primary`, `action` | +| `CheckBox` | `label`, `checked`, `action` | +| `TextField` | `label`, `text`, `textFieldType`, `validationRegexp` | +| `DateTimeInput` | `label`, `value`, `enableDate`, `enableTime` | +| `MultipleChoice` | `selections`, `options`, `maxAllowedSelections`, `label` | +| `Slider` | `value`, `minValue`, `maxValue`, `step`, `label` | + +The schema exposes `validationRegexp` on `TextField`, but validation execution is not implemented in this package. Treat schema fields as protocol data until a renderer wires behavior. + +## Message envelopes + +The parser recognizes four top-level envelopes: + +```ts +type A2uiMessage = + | { surfaceUpdate: A2uiSurfaceUpdate } + | { dataModelUpdate: A2uiDataModelUpdate } + | { beginRendering: A2uiBeginRendering } + | { deleteSurface: A2uiDeleteSurface }; +``` + +### surfaceUpdate + +Adds or replaces components for a surface. + +```json +{ + "surfaceUpdate": { + "surfaceId": "checkout", + "components": [ + { "id": "root", "component": { "Card": { "child": "title" } } }, + { "id": "title", "component": { "Text": { "text": { "literalString": "Checkout" } } } } + ] + } +} +``` + +### dataModelUpdate + +Carries nested data-model entries. `path` is optional. + +```json +{ + "dataModelUpdate": { + "surfaceId": "checkout", + "path": "/customer", + "contents": [ + { "key": "name", "valueString": "Ada" }, + { "key": "active", "valueBoolean": true } + ] + } +} +``` + +Each `A2uiDataModelEntry` has a `key` plus one of `valueString`, `valueNumber`, `valueBoolean`, or `valueMap`. + +### beginRendering + +Identifies the root component to render for a surface. + +```json +{ + "beginRendering": { + "surfaceId": "checkout", + "root": "root", + "styles": { + "font": "Inter", + "primaryColor": "#2563eb" + } + } +} +``` + +The source comments describe `styles.font` and `styles.primaryColor` as the canonical style fields. + +### deleteSurface + +Removes a surface by ID. + +```json +{ "deleteSurface": { "surfaceId": "checkout" } } +``` + +## Internal surface model + +`A2uiSurface` is an internal model used after messages are applied: + +```ts +interface A2uiSurface { + surfaceId: string; + catalogId: string; + theme?: A2uiTheme; + sendDataModel?: boolean; + components: Map; + dataModel: Record; + styles?: { font?: string; primaryColor?: string }; +} +``` + +This shape is not constrained to the wire format. Do not assume an agent sends it directly. + +## Outbound action messages + +When a rendered surface sends an action back to the agent, the typed outbound shape is: + +```ts +interface A2uiActionMessage { + version: 'v0.9'; + action: { + name: string; + surfaceId: string; + sourceComponentId: string; + timestamp: string; + context: Record; + }; + metadata?: { + a2uiClientDataModel: A2uiClientDataModel; + }; +} +``` + +The outbound action version is currently typed as `v0.9` in source. diff --git a/apps/website/content/docs/ag-ui/concepts/architecture.mdx b/apps/website/content/docs/ag-ui/concepts/architecture.mdx new file mode 100644 index 000000000..ecf3bf140 --- /dev/null +++ b/apps/website/content/docs/ag-ui/concepts/architecture.mdx @@ -0,0 +1,162 @@ +# Architecture + +`@ngaf/ag-ui` is an adapter. It does not replace `@ngaf/chat`, and it does not define a new chat runtime. + +The package takes an AG-UI `AbstractAgent`, listens to its protocol events, and exposes the runtime-neutral `Agent` contract that the chat components already understand. + +```text +Angular component + | + v +@ngaf/chat Agent contract + | messages, status, isLoading, error, toolCalls, state, events$ + | + v +@ngaf/ag-ui toAgent() + | reduces AG-UI events into Angular signals + | + v +@ag-ui/client AbstractAgent + | + v +AG-UI backend or in-process fake agent +``` + +## The boundary + +The important boundary is the `Agent` contract from `@ngaf/chat`. + +Your components should depend on `Agent`, not on AG-UI transport details. That keeps the UI portable across AG-UI, LangGraph, and custom adapters. + +`toAgent(source)` is the low-level boundary. It accepts any AG-UI `AbstractAgent` implementation: + +```ts +import { toAgent } from '@ngaf/ag-ui'; +import { HttpAgent } from '@ag-ui/client'; + +const source = new HttpAgent({ url: '/api/agent' }); +const agent = toAgent(source); +``` + +Most Angular apps should use DI instead: + +```ts +import { ApplicationConfig } from '@angular/core'; +import { provideAgUiAgent } from '@ngaf/ag-ui'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAgUiAgent({ url: '/api/agent' }), + ], +}; +``` + +`provideAgUiAgent()` creates an AG-UI `HttpAgent` and registers the wrapped `Agent` under `AG_UI_AGENT`. + +```ts +import { Component } from '@angular/core'; +import { ChatComponent } from '@ngaf/chat'; +import { injectAgUiAgent } from '@ngaf/ag-ui'; + +@Component({ + standalone: true, + imports: [ChatComponent], + template: ``, +}) +export class ChatPage { + protected readonly agent = injectAgUiAgent(); +} +``` + +You can also inject the token directly: + +```ts +import { inject } from '@angular/core'; +import { AG_UI_AGENT } from '@ngaf/ag-ui'; + +const agent = inject(AG_UI_AGENT); +``` + +## Runtime data flow + +`toAgent()` subscribes to `source.subscribe({ onEvent, onRunFailed })`. + +Every AG-UI event is passed through the reducer. The reducer updates Angular signals: + +- `messages` for user, assistant, and reasoning content. +- `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. +- `events$` for custom events. + +When the user submits input, the adapter builds a user message, appends it locally, adds it to the AG-UI source with `source.addMessage()`, then calls `source.runAgent()`. + +This is optimistic on purpose. The user message appears immediately while the backend starts the run. + +## Provider choices + +Use `provideAgUiAgent()` when you have a real AG-UI HTTP endpoint. + +```ts +provideAgUiAgent({ + url: '/api/agent', + agentId: 'support-agent', + threadId: 'thread-123', + headers: { Authorization: `Bearer ${token}` }, +}); +``` + +The config maps directly to the AG-UI `HttpAgent` options currently exposed by this package: `url`, `agentId`, `threadId`, and `headers`. + +Use `provideFakeAgUiAgent()` when you need the UI to run without a backend: + +```ts +import { provideFakeAgUiAgent } from '@ngaf/ag-ui'; + +providers: [ + provideFakeAgUiAgent({ + tokens: ['Offline', ' demo', ' response.'], + delayMs: 40, + }), +]; +``` + +Use `toAgent()` directly when you own a custom `AbstractAgent` subclass or need to test a specific event stream. + +## Lifecycle gotchas + +The wrapped `Agent` does not own the AG-UI source lifecycle. `toAgent()` subscribes to the source and expects the source instance to live for the same lifetime as the adapter. + +In Angular apps, prefer the provider API so the agent instance is scoped by DI. If you construct agents manually, create one adapter per source instance and keep that pairing stable. + +`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. + +`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 + +The AG-UI adapter currently covers: + +- Streaming assistant messages from `TEXT_MESSAGE_*`. +- Reasoning messages from `REASONING_MESSAGE_*`. +- Run status and errors from `RUN_*`. +- Tool calls from `TOOL_CALL_*`. +- Shared state from `STATE_SNAPSHOT` and `STATE_DELTA`. +- Message replacement from `MESSAGES_SNAPSHOT`. +- Custom events from `CUSTOM`. +- Citations stored under `state.citations`. + +These features are intentionally out of scope for the AG-UI adapter today: + +- Interrupt workflows. +- Subagents. +- History and time-travel. + +If those are central to your product, use the LangGraph adapter for that surface or build a custom adapter against the `@ngaf/chat` `Agent` contract. + +## Next steps + +- [Event Mapping](/docs/ag-ui/reference/event-mapping) shows exactly how AG-UI events map to `Agent` fields. +- [Fake Agent](/docs/ag-ui/guides/fake-agent) covers offline demos and tests. +- [Citations](/docs/ag-ui/guides/citations) explains the `state.citations` bridge. +- [Troubleshooting](/docs/ag-ui/guides/troubleshooting) covers common integration failures. 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 9eec4990b..64796b026 100644 --- a/apps/website/content/docs/ag-ui/getting-started/installation.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/installation.mdx @@ -9,11 +9,23 @@ ## Install packages ```bash -npm install @ngaf/chat @ngaf/ag-ui +npm install @ngaf/chat @ngaf/ag-ui @ag-ui/client ``` `@ngaf/chat` provides the chat UI primitives. `@ngaf/ag-ui` provides the adapter that wires an AG-UI backend into the `Agent` contract those primitives consume. +## Peer Dependencies + +`@ngaf/ag-ui` declares the following peer dependencies: + +| Package | Version | +|---|---| +| `@ngaf/chat` | `*` | +| `@ngaf/licensing` | `*` | +| `@angular/core` | `^20.0.0 \|\| ^21.0.0` | +| `@ag-ui/client` | `*` | +| `rxjs` | `~7.8.0` | + ## Configure the provider In your app config: @@ -75,7 +87,7 @@ export const appConfig: ApplicationConfig = { }; ``` -`FakeAgent` extends `AbstractAgent` and emits a canned `RUN_STARTED → TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT × N → TEXT_MESSAGE_END → RUN_FINISHED` sequence. Drop-in replacement for `provideAgUiAgent({ 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. Drop-in replacement for `provideAgUiAgent({ url })` while you're prototyping. ## Custom transport 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 be11f1825..42b5b1024 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -8,29 +8,24 @@ AG-UI is the open agent-to-UI protocol from the CopilotKit ecosystem. It standar ## How it fits -``` -┌─────────────────────────────────────────────────────────────┐ -│ @ngaf/chat (UI primitives — runtime-neutral) │ -│ , , , , … │ -└─────────────────────────┬───────────────────────────────────┘ - │ Agent contract (signals + events$) - │ - ┌───────────┴───────────┐ - ▼ ▼ - @ngaf/langgraph @ngaf/ag-ui - (LangGraphAgent) (toAgent: AbstractAgent→Agent) - │ │ - ▼ ▼ - LangGraph Platform Any AG-UI backend - (CrewAI, Mastra, MS AF, - CopilotKit runtime, …) +```text +@ngaf/chat + , , , + | + | Agent contract (signals + events$) + | + +--> @ngaf/langgraph + | LangGraphAgent -> LangGraph Platform + | + +--> @ngaf/ag-ui + toAgent(AbstractAgent) -> AG-UI backend ``` ## What you get -- **`toAgent(source: AbstractAgent): Agent`** — wraps any `AbstractAgent` subclass (custom transports, mocks) into the runtime-neutral `Agent` contract. -- **`provideAgUiAgent({ 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. +- **`toAgent(source: AbstractAgent): Agent`** - wraps any `AbstractAgent` subclass (custom transports, mocks) into the runtime-neutral `Agent` contract. +- **`provideAgUiAgent({ 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 @@ -48,5 +43,10 @@ Out of scope for now (use `@ngaf/langgraph` if you need these): ## Next steps -- [Quick Start](/docs/ag-ui/getting-started/quickstart) — bind `` to an AG-UI backend in 5 minutes. -- [Installation](/docs/ag-ui/getting-started/installation) — npm install + provider setup. +- [Quick Start](/docs/ag-ui/getting-started/quickstart) - bind `` to an AG-UI backend in 5 minutes. +- [Installation](/docs/ag-ui/getting-started/installation) - npm install + provider setup. +- [Architecture](/docs/ag-ui/concepts/architecture) - understand the adapter boundary and provider choices. +- [Event Mapping](/docs/ag-ui/reference/event-mapping) - see how AG-UI events update `Agent` fields. +- [Fake Agent](/docs/ag-ui/guides/fake-agent) - build UI without a backend. +- [Citations](/docs/ag-ui/guides/citations) - attach sources through `state.citations`. +- [Troubleshooting](/docs/ag-ui/guides/troubleshooting) - debug provider, stream, state, and citation issues. 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 ad2035668..afa780e69 100644 --- a/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx @@ -10,7 +10,7 @@ Angular 20+ project with Node.js 22+. If you need setup help, see the [Installat ```bash -npm install @ngaf/chat @ngaf/ag-ui +npm install @ngaf/chat @ngaf/ag-ui @ag-ui/client ``` @@ -30,7 +30,7 @@ export const appConfig: ApplicationConfig = { }; ``` -For offline development without a backend, swap to `provideFakeAgUiAgent({})` — it serves canned streaming responses for UI work. +For offline development without a backend, swap to `provideFakeAgUiAgent({})` - it serves canned streaming responses for UI work. @@ -51,14 +51,18 @@ 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's it. The `` composition handles streaming messages, tool calls, errors, and submit - all bound to the AG-UI backend through the `Agent` contract. ## What to read next -- The [`@ngaf/chat` Components](/docs/chat/components/chat) reference covers the primitives the `` composition uses internally — handy if you want to compose your own layout. +- [Architecture](/docs/ag-ui/concepts/architecture) explains how `toAgent()`, `provideAgUiAgent()`, `AG_UI_AGENT`, and `injectAgUiAgent()` fit together. +- [Event Mapping](/docs/ag-ui/reference/event-mapping) is the backend compatibility checklist for emitted AG-UI events. +- [Fake Agent](/docs/ag-ui/guides/fake-agent) covers offline demos and frontend tests. +- [Troubleshooting](/docs/ag-ui/guides/troubleshooting) is the fastest place to start when a stream does not render. +- The [`@ngaf/chat` Components](/docs/chat/components/chat) reference covers the primitives the `` composition uses internally - handy if you want to compose your own layout. - Looking for LangGraph instead of AG-UI? See [`@ngaf/langgraph`](/docs/agent/getting-started/quickstart). ## Switching backends without changing UI diff --git a/apps/website/content/docs/ag-ui/guides/citations.mdx b/apps/website/content/docs/ag-ui/guides/citations.mdx new file mode 100644 index 000000000..94988c0e8 --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/citations.mdx @@ -0,0 +1,128 @@ +# Citations + +`@ngaf/ag-ui` can copy citations from AG-UI state onto chat messages. + +The bridge is intentionally simple: put citations under `state.citations`, keyed by message id. When a `STATE_SNAPSHOT` or `STATE_DELTA` arrives, the adapter merges matching citations onto `messages`. + +## State shape + +Use this shape from your AG-UI backend: + +```ts +{ + citations: { + "assistant-message-id": [ + { + id: "doc-1", + title: "Refund policy", + url: "https://example.com/refunds", + snippet: "Refunds are available within 30 days." + } + ] + } +} +``` + +The key must match the assistant message id emitted by `TEXT_MESSAGE_START` or `REASONING_MESSAGE_START`. + +```ts +{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' } +{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Refunds are available.' } +{ + type: 'STATE_SNAPSHOT', + snapshot: { + citations: { + m1: [ + { + id: 'refund-policy', + title: 'Refund policy', + url: 'https://example.com/refunds', + snippet: 'Refunds are available within 30 days.', + }, + ], + }, + }, +} +``` + +The message becomes: + +```ts +{ + id: 'm1', + role: 'assistant', + content: 'Refunds are available.', + citations: [ + { + id: 'refund-policy', + index: 1, + title: 'Refund policy', + url: 'https://example.com/refunds', + snippet: 'Refunds are available within 30 days.', + }, + ], +} +``` + +## Accepted citation fields + +The bridge normalizes a few common field names: + +| Citation field | Accepted input | +| --- | --- | +| `id` | `id`, `refId`, or generated `c1`, `c2`, ... | +| `index` | `index` or array position starting at 1 | +| `title` | `title` or `name` | +| `url` | `url`, `href`, or `source` | +| `snippet` | `snippet`, `content`, or `excerpt` | +| `extra` | `extra` object | + +String entries are also accepted: + +```ts +{ + citations: { + m1: ['https://example.com/refunds'] + } +} +``` + +That becomes: + +```ts +{ id: 'c1', index: 1, url: 'https://example.com/refunds' } +``` + +## When citations update + +Citations are merged after: + +- `STATE_SNAPSHOT` +- `STATE_DELTA` + +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. + +## Manual bridge + +`bridgeCitationsState()` is exported for advanced adapters or custom reducers. + +```ts +import { bridgeCitationsState } from '@ngaf/ag-ui'; + +const nextMessages = bridgeCitationsState( + { state: threadState }, + currentMessages, +); +``` + +Most apps should not need this directly. The built-in AG-UI reducer already calls it for state snapshots and deltas. + +## Gotchas + +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 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/fake-agent.mdx b/apps/website/content/docs/ag-ui/guides/fake-agent.mdx new file mode 100644 index 000000000..3f6dfb4ef --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/fake-agent.mdx @@ -0,0 +1,103 @@ +# Fake Agent + +`FakeAgent` is an in-process AG-UI `AbstractAgent` for frontend work when a backend is not ready. + +It emits a canned stream: + +1. `RUN_STARTED` +2. optional `REASONING_MESSAGE_*` +3. `TEXT_MESSAGE_START` +4. one `TEXT_MESSAGE_CONTENT` event per token +5. `TEXT_MESSAGE_END` +6. `RUN_FINISHED` + +It is for demos, story-like development, and tests. It is not a production transport. + +## Use the provider + +For Angular apps, use `provideFakeAgUiAgent()`. + +```ts +import { ApplicationConfig } from '@angular/core'; +import { provideFakeAgUiAgent } from '@ngaf/ag-ui'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideFakeAgUiAgent({ + tokens: ['Hello', ' from', ' a', ' fake', ' agent.'], + delayMs: 50, + }), + ], +}; +``` + +Your component stays the same as the real backend version: + +```ts +import { Component } from '@angular/core'; +import { ChatComponent } from '@ngaf/chat'; +import { injectAgUiAgent } from '@ngaf/ag-ui'; + +@Component({ + standalone: true, + imports: [ChatComponent], + template: ``, +}) +export class DemoChat { + protected readonly agent = injectAgUiAgent(); +} +``` + +Switching to a real backend is a provider change: + +```diff +- provideFakeAgUiAgent({ tokens: ['Offline', ' response.'] }) ++ provideAgUiAgent({ url: '/api/agent' }) +``` + +## Add reasoning + +Pass `reasoningTokens` when you need to exercise reasoning UI. + +```ts +provideFakeAgUiAgent({ + reasoningTokens: ['Reading policy. ', 'Checking account status.'], + tokens: ['The account is eligible.'], + delayMs: 40, +}); +``` + +Reasoning events are emitted before text events and use the same message id, so the reducer stores reasoning and final content on one assistant message. + +## Use the class directly + +Use `FakeAgent` directly when a test needs an AG-UI source rather than Angular DI. + +```ts +import { FakeAgent, toAgent } from '@ngaf/ag-ui'; + +const source = new FakeAgent({ + tokens: ['One', ' two', ' three.'], + delayMs: 1, +}); + +const agent = toAgent(source); +``` + +That gives you the same `Agent` contract as `provideFakeAgUiAgent()`. + +## Configuration + +| Option | Type | Default | Notes | +| --- | --- | --- | --- | +| `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. | + +## What it does not do + +`FakeAgent` does not call a model, execute tools, persist history, or simulate interrupts. + +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/troubleshooting.mdx b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx new file mode 100644 index 000000000..337b93be4 --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx @@ -0,0 +1,158 @@ +# 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). + +## Nothing renders + +Check the provider first. + +```ts +providers: [ + provideAgUiAgent({ url: '/api/agent' }), +] +``` + +Then make sure the component uses the provided agent: + +```ts +import { ChatComponent } from '@ngaf/chat'; +import { injectAgUiAgent } from '@ngaf/ag-ui'; + +@Component({ + standalone: true, + imports: [ChatComponent], + template: ``, +}) +export class ChatPage { + protected readonly agent = injectAgUiAgent(); +} +``` + +If you inject `AG_UI_AGENT` manually, the token must be provided in the same Angular injector tree. + +## User messages appear, but no assistant response + +`submit()` optimistically appends the user message before calling `source.runAgent()`. If the user message appears and the assistant does not, the UI path is working. The next place to inspect is the AG-UI event stream. + +The backend must emit at least: + +```ts +{ type: 'RUN_STARTED' } +{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' } +{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Hello' } +{ type: 'TEXT_MESSAGE_END', messageId: 'm1' } +{ type: 'RUN_FINISHED' } +``` + +If `TEXT_MESSAGE_CONTENT` uses a different `messageId` than `TEXT_MESSAGE_START`, the reducer cannot append content to the message. + +## Loading never stops + +The adapter sets `isLoading` to `true` on `RUN_STARTED`. + +It sets `isLoading` back to `false` on: + +- `RUN_FINISHED` +- `RUN_ERROR` +- `onRunFailed` + +If loading never stops, your backend likely did not emit a terminal event or the AG-UI source did not report a failure. + +## Errors are not visible + +Protocol errors should come through `RUN_ERROR`. + +```ts +{ type: 'RUN_ERROR', message: 'Model quota exceeded' } +``` + +Transport or runtime failures may arrive through the AG-UI subscriber `onRunFailed` callback. The adapter stores that thrown value in `agent.error()` and sets `status` to `error`. + +Your UI still needs to render error state. The base chat composition handles common error display, but custom layouts should read: + +```ts +agent.status(); +agent.error(); +``` + +## Tool call args are empty + +`TOOL_CALL_ARGS` parses the event `delta` as JSON. + +This works: + +```ts +{ type: 'TOOL_CALL_ARGS', toolCallId: 't1', delta: '{"query":"Angular"}' } +``` + +This becomes `{}`: + +```ts +{ type: 'TOOL_CALL_ARGS', toolCallId: 't1', delta: '{"query":' } +``` + +The current reducer replaces args with each parsed payload. It does not assemble partial JSON fragments across multiple `TOOL_CALL_ARGS` events. Emit complete JSON for each args event. + +## Citations do not show up + +Check three things: + +1. Citations must live under `state.citations`. +2. The citation key must match the assistant message id. +3. A `STATE_SNAPSHOT` or `STATE_DELTA` must arrive after citation data is available. + +Example: + +```ts +{ + type: 'STATE_SNAPSHOT', + snapshot: { + citations: { + m1: [{ title: 'Policy', url: 'https://example.com/policy' }], + }, + }, +} +``` + +If your backend sends citations before the message exists, send another state event after the message is created. See [Citations](/docs/ag-ui/guides/citations). + +## Stop does not cancel the run + +`agent.stop()` calls `source.abortRun()`. + +Cancellation depends on the AG-UI source implementation. `HttpAgent` supports abort behavior. A custom `AbstractAgent` subclass needs to implement cancellation itself. + +## Regenerate throws + +`regenerate(index)` throws when: + +- Another run is already loading. +- The target index is not an assistant message. +- There is no previous user message to rerun from. + +Pass the index of the assistant message you want to replace, not the user message. + +## Interrupts, subagents, or history do not work + +Those flows 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, and citations from state. + +Use `@ngaf/langgraph` for the richer LangGraph-specific surface, or write a custom adapter against the `@ngaf/chat` `Agent` contract if you need AG-UI plus product-specific behavior. + +## Isolate with FakeAgent + +When you are unsure whether the issue is UI wiring or backend events, swap in the fake provider: + +```ts +import { provideFakeAgUiAgent } from '@ngaf/ag-ui'; + +providers: [ + provideFakeAgUiAgent({ + tokens: ['Fake', ' response.'], + delayMs: 20, + }), +] +``` + +If the UI works with `FakeAgent`, the Angular wiring is fine. Focus on the backend event stream. diff --git a/apps/website/content/docs/ag-ui/reference/event-mapping.mdx b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx new file mode 100644 index 000000000..a0ff40573 --- /dev/null +++ b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx @@ -0,0 +1,173 @@ +# Event Mapping + +`@ngaf/ag-ui` reduces AG-UI protocol events into the `Agent` contract from `@ngaf/chat`. + +This page is the compatibility map. If your backend emits these events with the expected fields, the chat UI can render the run without knowing which runtime produced it. + +## Summary + +| AG-UI event | Agent field | Behavior | +| --- | --- | --- | +| `RUN_STARTED` | `status`, `isLoading`, `error` | Sets `status` to `running`, `isLoading` to `true`, and clears `error`. | +| `RUN_FINISHED` | `status`, `isLoading` | Sets `status` to `idle` and `isLoading` to `false`. | +| `RUN_ERROR` | `status`, `isLoading`, `error` | Sets `status` to `error`, stops loading, and stores the event message when present. | +| `TEXT_MESSAGE_START` | `messages` | Creates or reuses an assistant message slot. | +| `TEXT_MESSAGE_CONTENT` | `messages` | Appends `delta` to the message content. | +| `TEXT_MESSAGE_END` | `messages` | No-op today. Content is already accumulated from deltas. | +| `REASONING_MESSAGE_START` | `messages` | Creates or reuses an assistant message slot with `reasoning`. | +| `REASONING_MESSAGE_CONTENT` | `messages` | Appends `delta` to `message.reasoning`. | +| `REASONING_MESSAGE_CHUNK` | `messages` | Treated the same as `REASONING_MESSAGE_CONTENT`. | +| `REASONING_MESSAGE_END` | `messages` | Adds `reasoningDurationMs` when timing is available. | +| `TOOL_CALL_START` | `toolCalls` | Adds a running tool call. | +| `TOOL_CALL_ARGS` | `toolCalls` | Parses `delta` as JSON and replaces args on the matching tool call. | +| `TOOL_CALL_RESULT` | `toolCalls` | Stores the tool result on the matching tool call. | +| `TOOL_CALL_END` | `toolCalls` | Marks the matching tool call complete. | +| `STATE_SNAPSHOT` | `state`, `messages` | Replaces state and merges citations from `state.citations`. | +| `STATE_DELTA` | `state`, `messages` | Applies JSON Patch to state and merges citations from `state.citations`. | +| `MESSAGES_SNAPSHOT` | `messages` | Replaces the full message list. | +| `CUSTOM` | `events$` | Emits a runtime-neutral custom event. | +| unknown event | none | Ignored. Future protocol events do not crash the adapter. | + +## Run lifecycle + +The adapter exposes lifecycle through `status`, `isLoading`, and `error`. + +```ts +agent.status(); // 'idle' | 'running' | 'error' +agent.isLoading(); // boolean +agent.error(); // unknown +``` + +`RUN_STARTED` clears a previous error. `RUN_ERROR` and `onRunFailed` both put the agent into the `error` state. + +`onRunFailed` comes from the AG-UI subscriber API rather than a protocol event. The adapter treats it like a run failure: `status = 'error'`, `isLoading = false`, and `error` is the thrown value. + +## Messages + +Text messages are accumulated by `messageId`. + +```ts +{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' } +{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Hello' } +{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: ' there' } +{ type: 'TEXT_MESSAGE_END', messageId: 'm1' } +``` + +The resulting message is: + +```ts +{ id: 'm1', role: 'assistant', content: 'Hello there' } +``` + +`TEXT_MESSAGE_END` does not finalize anything separately. The content already lives in the signal. + +`MESSAGES_SNAPSHOT` replaces the full message array. Use it when the backend is authoritative for the whole conversation. + +## Reasoning + +Reasoning events write to the same assistant message when they share the same `messageId` as the final text response. + +```ts +{ type: 'REASONING_MESSAGE_START', messageId: 'm1', role: 'assistant' } +{ type: 'REASONING_MESSAGE_CONTENT', messageId: 'm1', delta: 'Checking policy.' } +{ type: 'REASONING_MESSAGE_END', messageId: 'm1' } +{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' } +{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Approved.' } +``` + +The message keeps both fields: + +```ts +{ + id: 'm1', + role: 'assistant', + reasoning: 'Checking policy.', + reasoningDurationMs: 12, + content: 'Approved.', +} +``` + +The duration is measured in the browser from start to end event. It is useful for display, not billing or tracing. + +## Tool calls + +Tool calls are stored independently from messages in `agent.toolCalls()`. + +```ts +{ type: 'TOOL_CALL_START', toolCallId: 'search-1', toolCallName: 'search' } +{ type: 'TOOL_CALL_ARGS', toolCallId: 'search-1', delta: '{"q":"Angular"}' } +{ type: 'TOOL_CALL_RESULT', toolCallId: 'search-1', content: { hits: 3 } } +{ type: 'TOOL_CALL_END', toolCallId: 'search-1' } +``` + +The resulting tool call is: + +```ts +{ + id: 'search-1', + name: 'search', + args: { q: 'Angular' }, + result: { hits: 3 }, + status: 'complete', +} +``` + +`TOOL_CALL_ARGS` expects the `delta` value to be parseable JSON. If parsing fails, args become `{}`. The current reducer replaces args with each parsed payload; it does not merge partial JSON fragments. + +## State + +`STATE_SNAPSHOT` replaces the full state object: + +```ts +{ type: 'STATE_SNAPSHOT', snapshot: { topic: 'billing' } } +``` + +`STATE_DELTA` applies JSON Patch operations to the current state: + +```ts +{ + type: 'STATE_DELTA', + delta: [ + { op: 'replace', path: '/topic', value: 'support' }, + { op: 'add', path: '/caseId', value: 'case-123' }, + ], +} +``` + +After either state event, the adapter also runs the citations bridge. If `state.citations` contains entries keyed by message id, those citations are copied onto the matching messages. + +See [Citations](/docs/ag-ui/guides/citations) for the expected shape. + +## Custom events + +`CUSTOM` events are exposed through `events$`. + +When the custom event name is `state_update` and the value is an object, the adapter emits: + +```ts +{ type: 'state_update', data: value } +``` + +For every other custom event name, it emits: + +```ts +{ type: 'custom', name, data: value } +``` + +Use `state` for durable UI state. Use `events$` for transient events, telemetry hooks, or UI side effects that should not be stored as conversation state. + +## Submit and stop + +`agent.submit({ message })` performs three steps: + +1. Builds a local user message. +2. Appends it to `messages` and calls `source.addMessage()`. +3. Calls `source.runAgent()`. + +If `message` is omitted, no user message is appended, but `runAgent()` still runs. + +`agent.stop()` calls `source.abortRun()`. Cancellation depends on the AG-UI source implementation. + +## Unsupported protocol areas + +The current adapter does not implement customer-facing flows for interrupts, subagents, history, or time-travel. Unknown protocol events are ignored rather than treated as errors. diff --git a/apps/website/content/docs/agent/concepts/agent-contract.mdx b/apps/website/content/docs/agent/concepts/agent-contract.mdx new file mode 100644 index 000000000..f1745269f --- /dev/null +++ b/apps/website/content/docs/agent/concepts/agent-contract.mdx @@ -0,0 +1,169 @@ +# Agent Contract + +The `Agent` contract is the spine between runtime adapters and chat UI. + +`@ngaf/chat` owns the contract. `@ngaf/langgraph` and `@ngaf/ag-ui` produce objects that satisfy it. Chat primitives and compositions consume it without knowing which runtime is behind the stream. + +This matters because the UI should not care whether a response came from LangGraph Platform, AG-UI, a local mock, or a custom HTTP service. The boundary is explicit: adapters translate runtime events into Angular signals and a small action surface. + +```text +LangGraph Platform -- @ngaf/langgraph --+ + +-- Agent -- @ngaf/chat +AG-UI backend ------ @ngaf/ag-ui -------+ + +custom backend ------ your adapter -----+ +``` + +## The Contract Surface + +Import the contract from `@ngaf/chat`: + +```ts +import type { Agent } from '@ngaf/chat'; +``` + +An `Agent` has state signals, actions, optional runtime capabilities, and one event stream. + +```ts +interface Agent { + messages: Signal; + status: Signal<'idle' | 'running' | 'error'>; + isLoading: Signal; + error: Signal; + toolCalls: Signal; + state: Signal>; + + submit(input: AgentSubmitInput, opts?: AgentSubmitOptions): Promise; + stop(): Promise; + regenerate(assistantMessageIndex: number): Promise; + + interrupt?: Signal; + subagents?: Signal>; + + events$: Observable; +} +``` + +The invariant is simple: durable UI state lives on signals. `events$` carries things that are not already derivable from those signals. + +## Submit + +`submit()` is the only runtime-neutral way to advance the agent. + +```ts +await agent.submit({ message: 'Explain this trace.' }); + +await agent.submit({ + resume: { approved: true }, + state: { reviewer: 'Ada' }, +}); +``` + +The input can carry a new user message, an interrupt resume payload, a state patch, or a combination. The adapter decides how that maps to the backend. + +This matters because chat UI can send user intent without learning backend protocol details. LangGraph can turn `resume` into a command. An AG-UI adapter can call `runAgent()`. A test mock can just record the call. + +The second argument is intentionally small at the contract layer: + +```ts +interface AgentSubmitOptions { + signal?: AbortSignal; +} +``` + +Runtime-specific adapters may accept richer options. `@ngaf/langgraph` exports `LangGraphSubmitOptions` for LangGraph run configuration such as checkpointing, durability, multitask strategy, and stream mode. Keep app-level code on the neutral shape unless it is intentionally using LangGraph-only behavior. + +## Signals + +Signals are the stable read model. + +| Signal | Meaning | +|---|---| +| `messages()` | Runtime-neutral message history. | +| `status()` | `'idle'`, `'running'`, or `'error'`. | +| `isLoading()` | Convenience boolean for active generation. | +| `error()` | Last error payload, or `null` by convention. | +| `toolCalls()` | Tool calls normalized for chat display. | +| `state()` | Backend-defined state snapshot as a plain object. | +| `interrupt?.()` | Current human-in-the-loop pause, if supported. | +| `subagents?.()` | Delegated work keyed by tool-call id, if supported. | + +The optional signals are important. A simple echo adapter should not fake interrupts or subagents. Components that need those concepts feature-detect the signal and render a neutral fallback when it is absent. + +## Events + +`events$` is required, but it can be `EMPTY`. + +```ts +import { EMPTY } from 'rxjs'; +import type { AgentEvent } from '@ngaf/chat'; + +const events$ = EMPTY as Observable; +``` + +Current event variants are deliberately narrow: + +| Event | Purpose | +|---|---| +| `state_update` | Sync state intended for render/generative UI stores. | +| `custom` | Runtime-specific escape hatch with `name` and `data`. | + +Do not mirror `messages`, `status`, `toolCalls`, `interrupt`, or `subagents` through `events$`. Put those on signals. Duplicating state creates ordering bugs and makes components guess which source wins. + +## Adapters And Transports + +`@ngaf/langgraph` exports `agent()`, `provideAgent()`, `FetchStreamTransport`, `MockAgentTransport`, and `AgentTransport`. + +`agent()` is the Angular adapter. It connects to LangGraph, consumes stream events through a transport, and returns a `LangGraphAgent`. That object satisfies the neutral `Agent` contract and also exposes LangGraph-specific signals such as raw LangGraph messages, history, queue, branch state, and checkpoint helpers. + +`AgentTransport` is lower level. Use it when the backend is LangGraph-compatible but the transport needs to change: custom fetch behavior, tests, local fixtures, or a nonstandard gateway. + +`@ngaf/ag-ui` exports `toAgent()`, `provideAgUiAgent()`, `injectAgUiAgent()`, `FakeAgent`, and `provideFakeAgUiAgent()`. + +`toAgent()` wraps an AG-UI `AbstractAgent` into the same neutral contract. The AG-UI adapter reduces AG-UI events into chat signals, appends user messages on submit, calls `runAgent()`, and maps stop to `abortRun()`. + +## Lifecycle + +The UI lifecycle is intentionally boring. + +1. User input calls `submit({ message })`. +2. Adapter marks the run active through `status()` and `isLoading()`. +3. Runtime events update `messages()`, `toolCalls()`, `state()`, and optional signals. +4. Chat components re-render from signals. +5. `stop()` aborts the active run when supported. +6. `regenerate(index)` rolls back from an assistant message and reruns from the preceding user message. + +LangGraph adds deeper lifecycle and history surfaces. `@ngaf/langgraph` exports `AGENT_LIFECYCLE`, `AgentLifecycle`, and `AgentLifecycleRegistry`. Those are useful for telemetry, debugging, persistence, and time-travel UI. They are not required by `@ngaf/chat`. + +## Testing And Mocks + +Use the closest mock to the boundary you are testing. + +For chat components, use `mockAgent()` from `@ngaf/chat`. It gives writable signals and records `submit()` calls. + +```ts +import { mockAgent } from '@ngaf/chat'; + +const agent = mockAgent({ + messages: [{ id: 'm1', role: 'assistant', content: 'Ready' }], + withInterrupt: true, +}); +``` + +For LangGraph-specific code, use `mockLangGraphAgent()` or `MockAgentTransport` from `@ngaf/langgraph`. That lets you test queue, checkpoint, raw SDK, and transport behavior without pretending those details exist on every adapter. + +For AG-UI integration, use `FakeAgent` or `provideFakeAgUiAgent()` from `@ngaf/ag-ui`. + +This matters because the contract is the seam you can test cheaply. Most component tests should not need a network, LangGraph server, or AG-UI runtime. + +## What It Is Not + +The `Agent` contract is not a backend protocol. It does not define SSE frames, AG-UI event names, LangGraph thread state, or tool execution semantics. + +It is not a message database. Persistence belongs to the runtime or application state, then gets projected back through signals. + +It is not a full orchestration API. LangGraph-specific operations such as branch selection, queued runs, checkpoint history, and raw SDK messages stay on `LangGraphAgent`. + +It is not a UI renderer. Rendering belongs to `@ngaf/chat`, `@ngaf/render`, and A2UI surfaces. The agent only exposes the state and events those renderers need. + +Keep that boundary sharp. It makes adapters replaceable, tests smaller, and chat components more predictable. diff --git a/apps/website/content/docs/agent/getting-started/installation.mdx b/apps/website/content/docs/agent/getting-started/installation.mdx index 8120b987b..d0cb89929 100644 --- a/apps/website/content/docs/agent/getting-started/installation.mdx +++ b/apps/website/content/docs/agent/getting-started/installation.mdx @@ -19,10 +19,23 @@ A running LangGraph agent accessible via HTTP. Can be local (langgraph dev) or d ## Install the package ```bash -npm install @ngaf/langgraph +npm install @ngaf/langgraph @ngaf/chat ``` -This installs the library and its peer dependencies including `@langchain/langgraph-sdk`. +`@ngaf/langgraph` provides `agent()` and `provideAgent()`. `@ngaf/chat` provides the runtime-neutral chat UI consumed by the LangGraph adapter. + +## Peer Dependencies + +`@ngaf/langgraph` declares the following peer dependencies: + +| Package | Version | +|---------|---------| +| `@ngaf/chat` | `*` | +| `@ngaf/licensing` | `*` | +| `@angular/core` | `^20.0.0 \|\| ^21.0.0` | +| `@langchain/core` | `^1.1.33` | +| `@langchain/langgraph-sdk` | `^1.7.4` | +| `rxjs` | `~7.8.0` | ## Configure the provider @@ -90,7 +103,7 @@ Create a minimal component to verify the setup works. `agent()` must be called i ```typescript // In a component field initializer (injection context) const test = agent({ assistantId: 'chat_agent' }); -console.log(test.status()); // 'idle' — setup is correct +console.log(test.status()); // 'idle' - setup is correct ``` ## Troubleshooting diff --git a/apps/website/content/docs/chat/api/chat-config.mdx b/apps/website/content/docs/chat/api/chat-config.mdx index 7e12512e3..7f7742a0c 100644 --- a/apps/website/content/docs/chat/api/chat-config.mdx +++ b/apps/website/content/docs/chat/api/chat-config.mdx @@ -1,21 +1,26 @@ # ChatConfig -`ChatConfig` is the configuration interface accepted by `provideChat()`. It defines global settings for chat composition components including the generative UI registry, avatar styling, and assistant naming. +`ChatConfig` is the configuration interface accepted by `provideChat()`. It defines global settings for chat composition components including the generative UI registry, assistant labels, and license token. **Import:** ```typescript import type { ChatConfig } from '@ngaf/chat'; +import type { AngularRegistry } from '@ngaf/render'; ``` ## Interface Definition ```typescript interface ChatConfig { + /** Default render registry for generative UI components. */ + renderRegistry?: AngularRegistry; /** Override the default AI avatar label (default: "A"). */ avatarLabel?: string; /** Override the default assistant display name (default: "Assistant"). */ assistantName?: string; + /** Signed license token from cacheplane.dev. Optional; omitted in dev. */ + license?: string; } ``` @@ -37,7 +42,7 @@ A short string (typically one or two characters) displayed in the AI avatar badg provideChat({ avatarLabel: 'AI' }); ``` -The avatar badge is a small square element styled with `--chat-avatar-bg` and `--chat-avatar-text` CSS variables. +There is no avatar-specific CSS token. Use `avatarLabel` for the badge text, and use the shared `--ngaf-chat-*` tokens such as `--ngaf-chat-surface`, `--ngaf-chat-text`, and `--ngaf-chat-text-muted` to align surrounding chat surfaces with your app theme. ### assistantName @@ -55,6 +60,34 @@ The display name for the AI assistant. Used in labels, ARIA attributes, and any provideChat({ assistantName: 'Code Copilot' }); ``` +### renderRegistry + +```typescript +renderRegistry?: AngularRegistry +``` + +The default render registry for generative UI components. + +**Example:** + +```typescript +provideChat({ renderRegistry }); +``` + +### license + +```typescript +license?: string +``` + +A signed license token from cacheplane.dev. It is optional in development and should be provided from your production configuration. + +**Example:** + +```typescript +provideChat({ license: environment.ngafLicense }); +``` + ## Accessing ChatConfig at Runtime Inject `CHAT_CONFIG` to read configuration values in your own components: @@ -84,7 +117,7 @@ export class ChatHeaderComponent { The `ChatConfig` interface is defined in two files within the library: - `libs/chat/src/lib/provide-chat.ts` -- The canonical definition with JSDoc comments, alongside the `provideChat()` function and `CHAT_CONFIG` token -- `libs/chat/src/lib/chat.types.ts` -- A simplified re-export for internal use +- `libs/chat/src/lib/chat.types.ts` -- A separate internal type used by lower-level chat types The public API exports `ChatConfig` as a type-only export: diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx index de77ac7f0..808d0bfa7 100644 --- a/apps/website/content/docs/chat/components/chat-debug.mdx +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -1,6 +1,6 @@ # ChatDebugComponent -`ChatDebugComponent` is a full debug cockpit that combines a chat interface with a side panel for inspecting LangGraph execution state. It renders the same chat UI as `ChatComponent` alongside a debug panel with a timeline, state inspector, diff viewer, and navigation controls. +`ChatDebugComponent` provides a docked development panel for inspecting an `AgentWithHistory`. It includes built-in Timeline and State tabs, emits replay/fork events from timeline checkpoints, and supports host-provided controls and inspector tabs. **Selector:** `chat-debug` @@ -10,19 +10,10 @@ import { ChatDebugComponent } from '@ngaf/chat'; ``` -## When to Use It - -Use `ChatDebugComponent` during development to understand what your LangGraph agent is doing at each step. The debug panel shows: - -- A timeline of execution checkpoints -- State values at each checkpoint -- A diff between consecutive checkpoints -- Navigation controls to step through the execution history - ## Basic Usage ```typescript -import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { agent } from '@ngaf/langgraph'; import { ChatDebugComponent } from '@ngaf/chat'; @@ -32,199 +23,140 @@ import { ChatDebugComponent } from '@ngaf/chat'; imports: [ChatDebugComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: ` -
- -
+ `, }) export class DebugPageComponent { - chatRef = agent({ + protected readonly replayCheckpointId = signal(null); + protected readonly forkSourceCheckpointId = signal(null); + + protected readonly chat = agent({ + apiUrl: '/api/langgraph', assistantId: 'chat_agent', - threadId: signal(null), + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), }); -} -``` - -## API - -### Inputs - -| Input | Type | Default | Description | -|-------|------|---------|-------------| -| `agent` | `AgentWithHistory` | **Required** | The agent providing streaming state and execution history | - -## Layout - -The component renders a two-column layout: - -- **Left**: The chat area (messages, typing indicator, error display, input) -- **Right**: The collapsible debug panel (320px wide) - -The debug panel can be toggled open/closed with a button on the divider. - -## Debug Panel Sections - -### Summary - -At the top, `DebugSummaryComponent` displays the total number of checkpoints and cumulative execution duration. - -### Controls - -`DebugControlsComponent` provides VCR-style navigation buttons: - -| Button | Action | Disabled When | -|--------|--------|---------------| -| `\|<` | Jump to start | Already at first checkpoint | -| `<` | Step back | Already at first checkpoint | -| `>` | Step forward | Already at last checkpoint | -| `>\|` | Jump to end | Already at last checkpoint | - -### Timeline - -`DebugTimelineComponent` renders a vertical timeline rail with one `DebugCheckpointCardComponent` per checkpoint. Each card shows: - -- The node name (from `state.next[0]` or `"Step N"`) -- Optional duration badge (milliseconds) -- Optional token count badge -- Visual selection state (highlighted dot and border) - -Click a checkpoint to select it and view its details below. - -### Detail - -When a checkpoint is selected, `DebugDetailComponent` renders two sections: - -1. **State Diff** (`DebugStateDiffComponent`): Shows what changed between the previous and current checkpoint as color-coded entries: - - Green (`+`): Added keys - - Red (`-`): Removed keys - - Yellow (`~`): Changed values (shows before and after) - -2. **Current State** (`DebugStateInspectorComponent`): Displays the full state values as formatted JSON. - -## Debug Sub-Components - -Each debug sub-component is exported individually for custom debug UIs: - -| Component | Selector | Purpose | -|-----------|----------|---------| -| `DebugTimelineComponent` | `chat-debug-timeline` | Vertical timeline of checkpoints | -| `DebugCheckpointCardComponent` | `chat-debug-checkpoint-card` | Single checkpoint card in the timeline | -| `DebugControlsComponent` | `chat-debug-controls` | VCR-style step navigation | -| `DebugSummaryComponent` | `chat-debug-summary` | Step count and total duration | -| `DebugDetailComponent` | `chat-debug-detail` | State diff + state inspector | -| `DebugStateDiffComponent` | `chat-debug-state-diff` | Color-coded state diff | -| `DebugStateInspectorComponent` | `chat-debug-state-inspector` | JSON state viewer | -## Debug Utilities + replay(checkpointId: string) { + this.replayCheckpointId.set(checkpointId); + // Route this into your app's replay workflow. + } -### toDebugCheckpoint() - -Converts a `ThreadState` entry to a `DebugCheckpoint`: - -```typescript -import { toDebugCheckpoint } from '@ngaf/chat'; - -const checkpoint = toDebugCheckpoint(threadState, index); -// { node: 'agent', checkpointId: 'abc123' } + fork(checkpointId: string) { + this.forkSourceCheckpointId.set(checkpointId); + // Route this into your app's thread fork workflow. + } +} ``` -### extractStateValues() - -Extracts the state values from a `ThreadState`, returning `{}` if unavailable: - -```typescript -import { extractStateValues } from '@ngaf/chat'; +## Inputs -const values = extractStateValues(threadState); -// { messages: [...], someKey: 'value' } -``` +| Input | Type | Default | Description | +| --- | --- | --- | --- | +| `agent` | `AgentWithHistory` | **Required** | Agent state and checkpoint history. | +| `dock` | `'right' \| 'bottom' \| 'left'` | `'right'` | Initial dock position. | +| `defaultOpen` | `boolean` | `false` | Initial open state when no persisted state exists. | +| `storageKey` | `string` | `'chat-debug'` | Local storage key prefix for persisted open/dock/tab state. | -### computeStateDiff() +## Outputs -Computes a recursive diff between two state objects: +| Output | Type | Description | +| --- | --- | --- | +| `replayRequested` | `string` | Emits a checkpoint id when the built-in Timeline tab requests replay. | +| `forkRequested` | `string` | Emits a checkpoint id when the built-in Timeline tab requests fork. | +| `openChange` | `boolean` | Emits when the panel opens or closes. | +| `dockChange` | `'right' \| 'bottom' \| 'left'` | Emits when the dock position changes. | -```typescript -import { computeStateDiff } from '@ngaf/chat'; -import type { DiffEntry } from '@ngaf/chat'; +`replayRequested` and `forkRequested` are integration hooks. The debug panel does not mutate the agent by itself; the host app decides whether a checkpoint opens a replay view, starts a forked thread, or maps to a backend-specific time travel operation. -const diff: DiffEntry[] = computeStateDiff(beforeState, afterState); -``` +## Host Controls -### DebugCheckpoint Type +Use `ChatDebugControlsDirective` to pin app-specific controls at the top of the debug panel. ```typescript -interface DebugCheckpoint { - node?: string; - duration?: number; - tokenCount?: number; - checkpointId?: string; -} -``` +import { + ChatDebugComponent, + ChatDebugControlsDirective, + ChatDebugActionComponent, +} from '@ngaf/chat'; -### DiffEntry Type +@Component({ + standalone: true, + imports: [ + ChatDebugComponent, + ChatDebugControlsDirective, + ChatDebugActionComponent, + ], + template: ` + + + + + + `, +}) +export class DebugControlsExample { + // chat = agent(...) -```typescript -interface DiffEntry { - path: string; - type: 'added' | 'removed' | 'changed'; - before?: unknown; - after?: unknown; + resetThread() { + // App-specific reset behavior. + } } ``` -## Building a Custom Debug Panel +## Custom Inspector Tabs -You can use the sub-components individually to build a custom debug layout: +Use `ChatDebugInspectorDirective` to add tabs after the built-in Timeline and State tabs. ```typescript -import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core'; import { - DebugTimelineComponent, - DebugDetailComponent, - toDebugCheckpoint, - extractStateValues, + ChatDebugComponent, + ChatDebugInspectorDirective, + ChatDebugSectionComponent, } from '@ngaf/chat'; -import type { DebugCheckpoint } from '@ngaf/chat'; @Component({ - selector: 'app-custom-debug', standalone: true, - imports: [DebugTimelineComponent, DebugDetailComponent], - changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ChatDebugComponent, + ChatDebugInspectorDirective, + ChatDebugSectionComponent, + ], template: ` -
- - - @if (selectedIndex() >= 0) { - - } -
+ + + +
Environment: {{ environment }}
+
+
+
`, }) -export class CustomDebugComponent { - // chatRef = agent(...) - - selectedIndex = signal(-1); +export class RuntimeInspectorExample { + // chat = agent(...) + protected readonly environment = 'local'; +} +``` - checkpoints = computed((): DebugCheckpoint[] => - this.chatRef.history().map((state, i) => toDebugCheckpoint(state, i)) - ); +## Public Debug Primitives - currentState = computed(() => - extractStateValues(this.chatRef.history()[this.selectedIndex()]) - ); +These debug primitives are exported for host-provided controls and inspectors: - previousState = computed(() => { - const idx = this.selectedIndex(); - if (idx <= 0) return {}; - return extractStateValues(this.chatRef.history()[idx - 1]); - }); -} -``` +| Export | Selector | Purpose | +| --- | --- | --- | +| `ChatDebugControlsDirective` | `ng-template[chatDebugControls]` | Registers the pinned controls slot. | +| `ChatDebugInspectorDirective` | `ng-template[chatDebugInspector]` | Registers a host inspector tab with a label. | +| `ChatDebugSectionComponent` | `chat-debug-section` | Section container for custom inspector content. | +| `ChatDebugSegmentedComponent` | `chat-debug-segmented` | Segmented control primitive. | +| `ChatDebugSelectComponent` | `chat-debug-select` | Select primitive. | +| `ChatDebugSwitchComponent` | `chat-debug-switch` | Switch primitive. | +| `ChatDebugActionComponent` | `chat-debug-action` | Full-width action button primitive. | diff --git a/apps/website/content/docs/chat/concepts/message-model.mdx b/apps/website/content/docs/chat/concepts/message-model.mdx new file mode 100644 index 000000000..74ace1888 --- /dev/null +++ b/apps/website/content/docs/chat/concepts/message-model.mdx @@ -0,0 +1,192 @@ +# Message Model + +`@ngaf/chat` renders a runtime-neutral message model. Adapters translate backend-specific messages into this shape, then components render from it. + +This matters because provider message formats are not stable enough to build customer UI against directly. LangGraph, AG-UI, OpenAI, Anthropic, and custom backends all have different event and content shapes. The chat layer needs one model. + +## Message Shape + +```ts +interface Message { + id: string; + role: 'user' | 'assistant' | 'system' | 'tool'; + content: string | ContentBlock[]; + toolCallId?: string; + name?: string; + reasoning?: string; + reasoningDurationMs?: number; + extra?: Record; + citations?: Citation[]; +} +``` + +The fields are intentionally small. Put portable UI state in known fields. Put runtime-specific data in `extra`. + +## Roles + +Runtime roles are: + +| Role | Meaning | +|---|---| +| `user` | Human input submitted through the UI or adapter. | +| `assistant` | Model output. May include markdown, reasoning, tool calls, citations, or generated UI payloads. | +| `tool` | Tool result associated with a tool call. | +| `system` | System or runtime message shown as context. | + +You will also see the words `human`, `ai`, and `function` in template APIs: + +```html + + + + + +``` + +`chatMessageTemplate` names are UI template names, not runtime roles. + +`user` maps to `human`. `assistant` maps to `ai`. `tool` and `system` keep their names. + +`function` is present for compatibility with older function-call vocabulary. The current runtime-neutral `Role` type does not include `function`. New adapters should normalize function results into `tool` messages or carry backend-specific function details in `extra`. + +## Content + +`content` is either plain text or structured blocks. + +```ts +type ContentBlock = + | { type: 'text'; text: string } + | { type: 'image'; url: string; alt?: string } + | { type: 'tool_use'; id: string; name: string; args: unknown } + | { type: 'tool_result'; toolCallId: string; result: unknown; isError?: boolean }; +``` + +Plain text is the common case. `ChatComponent` treats assistant text as streamable content. Markdown, json-render specs, and A2UI payloads are detected from that text. + +Structured blocks are useful when the adapter can preserve more shape than a string. Custom templates should check whether `content` is a string before assuming it can be rendered directly. + +```ts +function textOf(message: Message): string { + return typeof message.content === 'string' + ? message.content + : message.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join(''); +} +``` + +## Markdown + +Assistant text is rendered as markdown by the `` composition. The markdown renderer also resolves citation references when citations are present. + +With primitives, markdown is your decision. You can use `ChatStreamingMdComponent`, `renderMarkdown()`, or your own renderer. + +This matters because markdown is presentation policy. A customer support chat, a code assistant, and an audit trail may need different rules for links, tables, code blocks, and images. + +## Reasoning + +Reasoning is separate from visible answer content. + +```ts +message.reasoning; +message.reasoningDurationMs; +``` + +Adapters populate `reasoning` from provider-specific reasoning or thinking blocks. AG-UI adapters can populate it from reasoning events. The surfaced value is always a plain string. Provider-specific encrypted blocks, summaries, and step metadata should not leak into portable UI code. + +`ChatComponent` renders reasoning with `` above the assistant response. If you build from primitives, decide where reasoning belongs and whether it should be visible by default. + +## Tool Calls + +Tool calls have their own normalized signal: + +```ts +interface ToolCall { + id: string; + name: string; + args: unknown; + status: 'pending' | 'running' | 'complete' | 'error'; + result?: unknown; + error?: unknown; +} +``` + +Tool result messages can also carry `toolCallId`. + +Use `toolCalls()` when rendering cross-message tool status. Use `tool` messages when rendering tool output inside the transcript. + +This matters because tool calls stream. Arguments may be partial while status is not `complete`. UI should treat `args` as unknown until the adapter marks the call complete or your tool template can handle partial data. + +## Citations + +Messages can carry provider-agnostic citations: + +```ts +message.citations; +``` + +`@ngaf/langgraph` exports `extractCitations()` for advanced adapters that need to normalize nonstandard citation payloads. Chat markdown views can render citation markers against the message citation list. + +Keep citation data on the message that owns the content. Avoid a separate global citation store unless your product has a cross-message source panel. + +## Interrupts + +Interrupts are not messages. They are optional agent state: + +```ts +agent.interrupt?.(); +``` + +An interrupt means the runtime paused and needs human input. Resume it through `submit({ resume })`. + +```ts +await agent.submit({ resume: { approved: true } }); +``` + +This matters because an interrupt is lifecycle state, not transcript content. You can render it as a banner, dialog, or approval panel, but the canonical state should stay on `agent.interrupt`. + +## Subagents + +Subagents are also optional agent state: + +```ts +agent.subagents?.(); // Map +``` + +Each `Subagent` has a tool-call id, optional name, status signal, message signal, and state signal. + +Use subagents when the backend delegates work to child graphs or specialized workers. Do not encode subagent progress as fake assistant messages if the runtime can expose structured subagent state. Structured state lets UI render progress, nested transcripts, and failure states without parsing prose. + +## A2UI And Generated Surfaces + +Generated UI can arrive through assistant content. + +`ChatComponent` classifies assistant text: + +- Markdown when the content is normal text. +- json-render when the first non-whitespace character is `{`. +- A2UI when the content starts with `---a2ui_JSON---`. + +json-render uses a single spec object. A2UI uses a stream of JSONL envelopes that update surfaces, data models, and rendering state. + +The message still remains an assistant message. The generated UI is a rendering interpretation of its content, not a new message role. + +## Practical Rules + +Normalize at the adapter boundary. Components should not inspect raw LangGraph SDK messages or AG-UI events unless they are adapter-specific tools. + +Treat `content` as `string | ContentBlock[]`. Do not assume every message can be interpolated directly. + +Use the exported type guards when role-specific code helps: + +```ts +import { + isUserMessage, + isAssistantMessage, + isToolMessage, + isSystemMessage, +} from '@ngaf/chat'; +``` + +Put backend-specific fields in `extra`, and document them in your adapter. Portable UI should survive when `extra` is absent. diff --git a/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx b/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx new file mode 100644 index 000000000..c16866ad2 --- /dev/null +++ b/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx @@ -0,0 +1,168 @@ +# Primitives vs Compositions + +`@ngaf/chat` gives you two levels of UI. + +Use compositions when the default product shape is close to what you need. Use primitives when the layout, interaction model, or rendering policy is part of your application. + +This matters because chat UI gets complicated quickly. Message streaming, input state, tool calls, reasoning, interrupts, subagents, auto-scroll, markdown, and generative UI all need to agree on the same `Agent`. The fastest path is usually to start with a composition and drop down only where the product needs control. + +## Compositions + +The main composition is ``. + +```ts +import { ChatComponent } from '@ngaf/chat'; +``` + +```html + +``` + +`ChatComponent` composes the common pieces: + +- `chat-message-list` for messages. +- `chat-input` for text entry and stop behavior. +- Markdown rendering for assistant content. +- Reasoning display. +- Tool call cards. +- Subagent display. +- A2UI and json-render surfaces when `views` are provided. +- Optional thread sidebar. +- Auto-scroll and scroll-to-bottom behavior. + +Use `` when you want the library to own the chat shell. + +```html + +``` + +`` is also a composition. It is built for development and inspection, not end-user chat layout. + +```ts +import { ChatDebugComponent } from '@ngaf/chat'; +``` + +```html + + +``` + +Use `chat-debug` when you need timeline and state inspection while building. Do not treat it as a customer-facing control surface unless you intentionally design around that. + +## Primitives + +Primitives are standalone Angular components and directives. They let you assemble your own shell while keeping the tested chat pieces. + +The core primitive set usually starts with `chat-message-list` and `chat-input`: + +```ts +import { + ChatMessageListComponent, + MessageTemplateDirective, + ChatInputComponent, +} from '@ngaf/chat'; +``` + +```html +
+
Support
+ + + + + + + + + + + + + + + + + + + + +
+``` + +Use primitives when you need: + +- A custom shell, sidebar, header, footer, or mobile layout. +- Product-specific message rendering. +- Custom markdown policy. +- A different input placement or composer. +- Separate handling for tool calls, citations, interrupts, or subagents. +- A chat view embedded inside an existing operational screen. + +## Choosing The Level + +| Need | Use | +|---|---| +| A complete chat UI quickly | `` | +| Default chat plus projected tool-call templates | `` | +| Built-in thread list and auto-scroll | `` | +| Development inspection | `` | +| Custom message layout | `chat-message-list` | +| Custom composer around the same agent | `chat-input` | +| Full product-specific shell | primitives | +| You are building a design system wrapper | primitives | + +The practical tradeoff is ownership. A composition owns more behavior for you. A primitive gives you more control and more responsibility. + +## Template Role Names + +Runtime messages use these roles: + +```ts +type Role = 'user' | 'assistant' | 'system' | 'tool'; +``` + +Message templates use UI names: + +```ts +type MessageTemplateType = 'human' | 'ai' | 'tool' | 'system' | 'function'; +``` + +The mapping is: + +| Message role | `chatMessageTemplate` | +|---|---| +| `user` | `human` | +| `assistant` | `ai` | +| `tool` | `tool` | +| `system` | `system` | + +`function` exists as a template type for compatibility with older function-message vocabulary. The current runtime-neutral `Role` type does not include `function`, so do not build new adapters around a separate function role. Normalize function output into tool messages or adapter-specific `extra` data. + +## Gotchas + +Give `` a parent with height. The composition uses flex layout and `height: 100%`; without a constrained parent, it can collapse. + +Do not pass a raw backend client to chat primitives. They need the `Agent` contract from `@ngaf/chat`, not a LangGraph SDK client or AG-UI `AbstractAgent`. + +Do not mix two agents in one shell unless you mean it. `chat-message-list`, `chat-input`, tool-call UI, and debug UI should usually point at the same `Agent` instance. + +If you use primitives, you own the behaviors the composition normally provides: auto-scroll, empty state, error display, reasoning placement, tool-call placement, subagent placement, and interrupt display. + +If you use `` and project custom pieces, stay inside the supported projection points. For example, project `chatToolCallTemplate` for tool-call rendering and `[chatInputModelSelect]` content for the model picker. If the whole structure needs to move, switch to primitives. + +## A Conservative Path + +Start with `` while wiring the runtime. + +Move individual surfaces to templates when the product needs customization. + +Move to primitives when layout becomes the requirement. + +That keeps early integration focused on the agent contract and delays UI ownership until there is a concrete reason to take it on. diff --git a/apps/website/content/docs/chat/getting-started/installation.mdx b/apps/website/content/docs/chat/getting-started/installation.mdx index 5aece0734..1df1a645e 100644 --- a/apps/website/content/docs/chat/getting-started/installation.mdx +++ b/apps/website/content/docs/chat/getting-started/installation.mdx @@ -30,10 +30,13 @@ npm install @ngaf/chat marked | `@angular/core` | `^20.0.0 \|\| ^21.0.0` | Yes | | `@angular/common` | `^20.0.0 \|\| ^21.0.0` | Yes | | `@angular/forms` | `^20.0.0 \|\| ^21.0.0` | Yes | -| `@ngaf/render` | `^0.0.1` | Yes | -| `@ngaf/a2ui` | `^0.0.1` | Yes | +| `@angular/platform-browser` | `^20.0.0 \|\| ^21.0.0` | Yes | +| `@ngaf/licensing` | `*` | Yes | +| `@ngaf/render` | `*` | Yes | +| `@ngaf/a2ui` | `*` | Yes | | `@json-render/core` | `^0.16.0` | Yes | | `@langchain/core` | `^1.1.33` | Yes | +| `rxjs` | `~7.8.0` | Yes | | `marked` | `^15.0.0 \|\| ^16.0.0` | Yes | `@cacheplane/partial-json` is installed by `@ngaf/chat` for streaming JSON parsing. @@ -58,7 +61,7 @@ export const appConfig: ApplicationConfig = { }; ``` -`provideChat()` is optional — chat components fall back to sensible defaults. +`provideChat()` is optional - chat components fall back to sensible defaults. ## Theming diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index 43f5a197d..8806bf8d0 100644 --- a/apps/website/content/docs/chat/getting-started/introduction.mdx +++ b/apps/website/content/docs/chat/getting-started/introduction.mdx @@ -16,7 +16,7 @@ Primitives are low-level, headless components that read from an `Agent` and expo | Primitive | Selector | Purpose | |-----------|----------|---------| -| `ChatMessagesComponent` | `chat-messages` | Iterates messages and renders via template directives | +| `ChatMessageListComponent` | `chat-message-list` | Iterates messages and renders via template directives | | `MessageTemplateDirective` | `ng-template[chatMessageTemplate]` | Declares a template for a specific message type | | `ChatInputComponent` | `chat-input` | Text input with submit handling and keyboard shortcuts | | `ChatTypingIndicatorComponent` | `chat-typing-indicator` | Animated dots shown while the agent is streaming | @@ -29,7 +29,7 @@ Primitives are low-level, headless components that read from an `Agent` and expo ### Compositions -Compositions are opinionated, styled components that combine primitives into ready-to-use UI blocks. They include `CHAT_THEME_STYLES` and `CHAT_MARKDOWN_STYLES` out of the box. +Compositions are opinionated, styled components that combine primitives into ready-to-use UI blocks. They apply the `--ngaf-chat-*` token system automatically and include markdown styling where needed. | Composition | Selector | Purpose | |-------------|----------|---------| @@ -60,7 +60,7 @@ LangGraph Platform - **`@ngaf/langgraph`** provides the `agent()` function and returns a `LangGraphAgent`, which satisfies the `Agent` contract consumed by `@ngaf/chat`. It exposes reactive Signals for `messages()`, `isLoading()`, `error()`, `interrupt()`, `toolCalls()`, `history()`, and more. -- **`@ngaf/render`** provides `RenderSpecComponent` and view registries for rendering JSON UI specs as Angular components. The `ChatComponent` auto-detects JSON specs in AI messages and renders them through `@ngaf/render` — pass a view registry via the `[views]` input. The `ChatComponent` also auto-detects A2UI v0.9 payloads and renders them using a built-in 12-component catalog. +- **`@ngaf/render`** provides `RenderSpecComponent` and view registries for rendering JSON UI specs as Angular components. The `ChatComponent` auto-detects JSON specs in AI messages and renders them through `@ngaf/render` - pass a view registry via the `[views]` input. The `ChatComponent` also auto-detects A2UI v0.9 payloads and renders them using a built-in 12-component catalog. ## When to Use `ChatComponent` vs. Custom Assembly @@ -73,14 +73,14 @@ LangGraph Platform **Assemble primitives** when you need to customize layout, add components between sections, change the message rendering logic, or integrate chat into a larger dashboard. Primitives give you building blocks without opinions. ```html - + - + - + - +
``` diff --git a/apps/website/content/docs/chat/guides/markdown.mdx b/apps/website/content/docs/chat/guides/markdown.mdx index 7f58ec28e..c314a7959 100644 --- a/apps/website/content/docs/chat/guides/markdown.mdx +++ b/apps/website/content/docs/chat/guides/markdown.mdx @@ -65,8 +65,8 @@ The stylesheet covers the following markdown elements: | Element | Styling | |---------|---------| | `p` | Bottom margin of `0.75em`, last child has no bottom margin | -| `code` (inline) | Background `var(--chat-bg-alt)`, padding, `4px` radius, monospace font | -| `pre` | Background `var(--chat-bg-alt)`, `12px 16px` padding, horizontal scroll | +| `code` (inline) | Background `var(--ngaf-chat-surface-alt)`, padding, `4px` radius, monospace font | +| `pre` | Background `var(--ngaf-chat-surface-alt)`, `12px 16px` padding, horizontal scroll | | `pre code` | No extra background or padding (inherits from `pre`) | | `ul`, `ol` | `0.5em` vertical margin, `1.5em` left padding | | `li` | `0.25em` vertical margin | @@ -80,7 +80,7 @@ The stylesheet covers the following markdown elements: | `th` | Alt background, bold, `0.875em` | | `td` | Standard border and padding | -All colors reference CSS custom properties from `CHAT_THEME_STYLES`, so markdown elements automatically respect your theme. +All colors reference `--ngaf-chat-*` CSS custom properties, so markdown elements automatically respect the active chat theme. ## Using Markdown in Custom Components @@ -90,7 +90,7 @@ To render markdown in a custom message template, apply both the styles and the ` import { Component, inject } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { - ChatMessagesComponent, + ChatMessageListComponent, MessageTemplateDirective, CHAT_MARKDOWN_STYLES, renderMarkdown, @@ -99,17 +99,17 @@ import { @Component({ selector: 'app-chat-view', standalone: true, - imports: [ChatMessagesComponent, MessageTemplateDirective], + imports: [ChatMessageListComponent, MessageTemplateDirective], styles: [CHAT_MARKDOWN_STYLES], template: ` - +
-
+ `, }) export class ChatViewComponent { diff --git a/apps/website/content/docs/chat/guides/theming.mdx b/apps/website/content/docs/chat/guides/theming.mdx index 2f8174f09..499b1546e 100644 --- a/apps/website/content/docs/chat/guides/theming.mdx +++ b/apps/website/content/docs/chat/guides/theming.mdx @@ -6,7 +6,7 @@ Chat components define all design tokens using CSS custom properties on the `:host` element. The token system supports automatic light/dark switching via `prefers-color-scheme` and explicit override via a `[data-ngaf-chat-theme]` attribute. -No imported stylesheet is needed — tokens are applied automatically by each component's encapsulated styles. An optional global stylesheet (`@ngaf/chat/chat.css`) is available if you want to customize tokens at the `:root` level. +No imported stylesheet is needed - tokens are applied automatically by each component's encapsulated styles. An optional global stylesheet (`@ngaf/chat/chat.css`) is available if you want to customize tokens at the `:root` level. ## CSS Custom Properties Reference @@ -33,24 +33,24 @@ No imported stylesheet is needed — tokens are applied automatically by each co | Token | Light | Dark | Purpose | |---|---|---|---| -| `--ngaf-chat-radius-bubble` | `15px` | — | User message bubble radius | -| `--ngaf-chat-radius-input` | `20px` | — | Input pill radius | -| `--ngaf-chat-radius-card` | `8px` | — | Card / trace radius | -| `--ngaf-chat-radius-button` | `8px` | — | Button radius | -| `--ngaf-chat-radius-launcher` | `9999px` | — | Circular launcher button | -| `--ngaf-chat-max-width` | `48rem` | — | Message column max width | +| `--ngaf-chat-radius-bubble` | `15px` | - | User message bubble radius | +| `--ngaf-chat-radius-input` | `20px` | - | Input pill radius | +| `--ngaf-chat-radius-card` | `8px` | - | Card / trace radius | +| `--ngaf-chat-radius-button` | `8px` | - | Button radius | +| `--ngaf-chat-radius-launcher` | `9999px` | - | Circular launcher button | +| `--ngaf-chat-max-width` | `48rem` | - | Message column max width | ### Typography | Token | Light | Dark | Purpose | |---|---|---|---| -| `--ngaf-chat-font-family` | system stack | — | Default font | -| `--ngaf-chat-font-mono` | mono stack | — | Code blocks | -| `--ngaf-chat-font-size` | `1rem` | — | Message text | -| `--ngaf-chat-font-size-sm` | `0.875rem` | — | UI controls | -| `--ngaf-chat-font-size-xs` | `0.75rem` | — | Labels, metadata | -| `--ngaf-chat-line-height` | `1.6` | — | Assistant text | -| `--ngaf-chat-line-height-tight` | `1.5` | — | User bubble | +| `--ngaf-chat-font-family` | system stack | - | Default font | +| `--ngaf-chat-font-mono` | mono stack | - | Code blocks | +| `--ngaf-chat-font-size` | `1rem` | - | Message text | +| `--ngaf-chat-font-size-sm` | `0.875rem` | - | UI controls | +| `--ngaf-chat-font-size-xs` | `0.75rem` | - | Labels, metadata | +| `--ngaf-chat-line-height` | `1.6` | - | Assistant text | +| `--ngaf-chat-line-height-tight` | `1.5` | - | User bubble | ## Light and Dark Mode @@ -135,5 +135,5 @@ If you previously customized `--chat-*` tokens, rename them to `--ngaf-chat-*`. | `--chat-success` | `--ngaf-chat-success` | -The `CHAT_THEME_STYLES` and `CHAT_MARKDOWN_STYLES` named exports no longer exist in `@ngaf/chat`. Remove any imports of those constants — theme tokens are now applied automatically by each component. +The `CHAT_THEME_STYLES` named export no longer exists in `@ngaf/chat`. Remove imports of that constant - theme tokens are now applied automatically by each component. `CHAT_MARKDOWN_STYLES` remains available for custom markdown renderers. diff --git a/apps/website/content/docs/getting-started.mdx b/apps/website/content/docs/getting-started.mdx index 471e916f1..28a6a5c4f 100644 --- a/apps/website/content/docs/getting-started.mdx +++ b/apps/website/content/docs/getting-started.mdx @@ -1,19 +1,22 @@ # Getting Started -Add angular to your Angular 20+ application. +Add Angular Agent Framework to an Angular 20+ application with a LangGraph Platform endpoint. ## Installation ```bash -npm install angular@latest +npm install @ngaf/langgraph @ngaf/chat ``` +`@ngaf/langgraph` provides `agent()` and `provideAgent()`. `@ngaf/chat` provides the ready-to-use `` composition and lower-level primitives. + ## Setup -In `app.config.ts`, add `provideAgent` to your providers: +In `app.config.ts`, add `provideAgent()` to your providers: ```typescript -import { provideAgent } from 'angular'; +import type { ApplicationConfig } from '@angular/core'; +import { provideAgent } from '@ngaf/langgraph'; export const appConfig: ApplicationConfig = { providers: [ @@ -25,23 +28,23 @@ export const appConfig: ApplicationConfig = { ## Your first component ```typescript -import { agent } from 'angular'; +import { Component, signal } from '@angular/core'; +import { agent } from '@ngaf/langgraph'; +import { ChatComponent as NgafChatComponent } from '@ngaf/chat'; @Component({ - template: ` - @for (msg of chat.messages(); track $index) { -

{{ msg.content }}

- } - - `, + selector: 'app-chat', + standalone: true, + imports: [NgafChatComponent], + template: ``, }) export class ChatComponent { chat = agent({ assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), }); } ``` -`agent()` MUST be called in an Angular injection context (component field initializer or constructor). Never call it in `ngOnInit`. +`agent()` must be called in an Angular injection context. Use a component field initializer or constructor; do not create it in `ngOnInit` or an async callback. diff --git a/apps/website/content/docs/introduction.mdx b/apps/website/content/docs/introduction.mdx index 7a55ed4ee..3c6f37a37 100644 --- a/apps/website/content/docs/introduction.mdx +++ b/apps/website/content/docs/introduction.mdx @@ -1,22 +1,49 @@ # Introduction -Agent is the Angular streaming library for LangChain and LangGraph. +Angular Agent Framework provides Angular-native agent UI primitives, LangGraph streaming adapters, and runtime-neutral contracts for building agent experiences in Angular apps. -## Installation +The core packages are: + +- `@ngaf/langgraph` for `agent()`, `provideAgent()`, and LangGraph Platform streaming. +- `@ngaf/chat` for chat compositions, primitives, markdown rendering, tool calls, interrupts, thread UI, and generative UI surfaces. +- `@ngaf/render` for rendering JSON Render specs with Angular components. +- `@ngaf/ag-ui` for adapting AG-UI agents into the same runtime-neutral chat contract. + +## Install ```bash -npm install angular +npm install @ngaf/langgraph @ngaf/chat ``` -## Quick start +## Minimal usage + +```typescript +import type { ApplicationConfig } from '@angular/core'; +import { provideAgent } from '@ngaf/langgraph'; + +export const appConfig: ApplicationConfig = { + providers: [provideAgent({ apiUrl: 'http://localhost:2024' })], +}; +``` ```typescript -import { agent } from 'angular'; +import { Component, signal } from '@angular/core'; +import { agent } from '@ngaf/langgraph'; +import { ChatComponent as NgafChatComponent } from '@ngaf/chat'; -const chat = agent({ - assistantId: 'chat_agent', - apiUrl: 'http://localhost:2024', -}); +@Component({ + selector: 'app-chat', + standalone: true, + imports: [NgafChatComponent], + template: ``, +}) +export class ChatComponent { + chat = agent({ + assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), + }); +} ``` -More documentation is generated automatically — run `npm run generate-docs` to populate this directory. +`agent()` must run in an Angular injection context, such as a component field initializer or constructor. diff --git a/apps/website/content/docs/licensing/getting-started/introduction.mdx b/apps/website/content/docs/licensing/getting-started/introduction.mdx new file mode 100644 index 000000000..95b572cfe --- /dev/null +++ b/apps/website/content/docs/licensing/getting-started/introduction.mdx @@ -0,0 +1,72 @@ +# Introduction + +`@ngaf/licensing` is the shared license-check helper used by the framework packages. It verifies compact Ed25519-signed tokens offline, evaluates the result into a small status set, emits non-blocking warnings, and sends non-blocking license telemetry when asked. + +The package itself is MIT licensed. `COMMERCIAL.md` states that the libraries in this repository are free to use, modify, and distribute in commercial and noncommercial projects. The proprietary part called out there is the internal minting service, not this package. + +## Public API shape + +The main entry point exports: + +| API | Purpose | +|-----|---------| +| `verifyLicense()` | verifies token signature against a public key | +| `evaluateLicense()` | turns a verify result and current time into a status | +| `runLicenseCheck()` | verifies, evaluates, warns once, and sends non-blocking telemetry | +| `emitNag()` | emits the warning for non-licensed statuses | +| `signLicense()` | signs claims with an Ed25519 private key | +| `inferNoncommercial()` | returns a default noncommercial hint from `NODE_ENV` | +| `createTelemetryClient()` | sends a license telemetry POST | +| `LICENSE_PUBLIC_KEY` | bundled public key | + +`@noble/ed25519` is the only peer dependency. + +## Token model + +A token is: + +```text +. +``` + +The payload must match `LicenseClaims`: + +```ts +type LicenseTier = 'developer-seat' | 'app-deployment' | 'enterprise'; + +interface LicenseClaims { + sub: string; + tier: LicenseTier; + iat: number; + exp: number; + seats: number; +} +``` + +`seats` must be a number greater than or equal to `1`. + +## Status model + +`evaluateLicense()` returns one of: + +| Status | Meaning | +|--------|---------| +| `licensed` | signature verified and `nowSec <= exp` | +| `grace` | signature verified, expired, but still inside the grace window | +| `expired` | signature verified and outside the grace window | +| `missing` | no token and not marked noncommercial | +| `tampered` | token was malformed or failed signature verification | +| `noncommercial` | no token and `isNoncommercial` was true | + +The default grace window is 14 days. + +## Runtime posture + +The higher-level check is designed not to block app startup: + +- signature verification is local; +- warning output goes through `console.warn` unless a custom `warn` function is supplied; +- telemetry is fire-and-forget; +- telemetry send failures are swallowed by the telemetry client. + +The code returns statuses instead of throwing for normal license states. Consumers can choose what to do with the status, but the framework packages use it as a warning and visibility mechanism, not as an app kill switch. diff --git a/apps/website/content/docs/licensing/guides/ci-and-offline.mdx b/apps/website/content/docs/licensing/guides/ci-and-offline.mdx new file mode 100644 index 000000000..92c544dea --- /dev/null +++ b/apps/website/content/docs/licensing/guides/ci-and-offline.mdx @@ -0,0 +1,85 @@ +# CI and Offline Use + +License verification is offline. `verifyLicense(token, publicKey)` parses the compact token, checks the Ed25519 signature, and returns claims when the signature is valid. + +No network call is needed to know whether a token is signed correctly. + +## CI + +For CI builds, treat the license token like any other secret: + +```bash +NGAF_LICENSE=... npm test +``` + +Then pass it through the framework provider that owns the package you use. + +```ts +provideAgent({ + apiUrl: process.env['LANGGRAPH_API_URL'], + license: process.env['NGAF_LICENSE'], +}); +``` + +If you call `runLicenseCheck()` directly, inject the current time only when you need deterministic tests: + +```ts +await runLicenseCheck({ + package: '@ngaf/example', + version: '1.0.0', + token, + publicKey, + telemetryEndpoint, + nowSec: 1_735_689_600, +}); +``` + +## Offline verification + +Use `verifyLicense()` and `evaluateLicense()` directly when network access is unavailable or unwanted. + +```ts +import { evaluateLicense, verifyLicense } from '@ngaf/licensing'; + +const verified = await verifyLicense(token, publicKey); +const result = evaluateLicense(verified, { + nowSec: Math.floor(Date.now() / 1000), +}); +``` + +This does not emit warnings and does not send telemetry. + +## Telemetry in licensing checks + +`runLicenseCheck()` creates a telemetry client and calls `send()` without awaiting it. The telemetry body contains: + +- package name; +- package version; +- license id from `claims.sub`, when present; +- an anonymous instance id; +- epoch-second timestamp. + +The licensing telemetry helper opts out when either of these is set: + +```bash +CACHEPLANE_TELEMETRY=0 +CACHEPLANE_TELEMETRY=false +``` + +It also checks `globalThis.CACHEPLANE_TELEMETRY` and treats `false`, `0`, or `"0"` as opt-out values. + +If `fetch` is not available, the telemetry send is a no-op. If the request fails, the error is swallowed. + +## Signing tokens + +`signLicense()` is exported for token minting and tests: + +```ts +import { signLicense } from '@ngaf/licensing'; + +const token = await signLicense(claims, privateKey); +``` + +It signs the UTF-8 JSON payload with Ed25519 and returns the compact token format expected by `verifyLicense()`. + +Do not ship private keys in application code. Verification needs only the public key. diff --git a/apps/website/content/docs/licensing/guides/setup.mdx b/apps/website/content/docs/licensing/guides/setup.mdx new file mode 100644 index 000000000..71394a288 --- /dev/null +++ b/apps/website/content/docs/licensing/guides/setup.mdx @@ -0,0 +1,86 @@ +# Setup + +Most applications do not import `@ngaf/licensing` directly. Framework packages call `runLicenseCheck()` from their providers when you pass a license token. + +For example, higher-level packages expose a `license` option: + +```ts +provideAgent({ + apiUrl: 'https://api.example.com', + license: environment.ngafLicense, +}); +``` + +When no token is supplied, the licensing helper can evaluate the environment as `noncommercial` when the package passes `isNoncommercial: true`. + +## Direct use + +Use the direct API when you are building a package inside the framework or a custom integration that needs the same behavior. + +```ts +import { + LICENSE_PUBLIC_KEY, + inferNoncommercial, + runLicenseCheck, +} from '@ngaf/licensing'; + +const status = await runLicenseCheck({ + package: '@ngaf/example', + version: '1.0.0', + token: process.env.NGAF_LICENSE, + publicKey: LICENSE_PUBLIC_KEY, + telemetryEndpoint: 'https://telemetry.example.com/v1/ping', + isNoncommercial: inferNoncommercial(), +}); +``` + +`runLicenseCheck()` returns the evaluated `LicenseStatus`. + +## Warnings + +`emitNag()` is silent for: + +- `licensed`; +- `noncommercial`. + +It warns once per package and status for: + +- `missing`; +- `grace`; +- `expired`; +- `tampered`. + +The warning prefix is `[cacheplane]`, and the package keeps running. + +You can inject a custom warning sink: + +```ts +await runLicenseCheck({ + package: '@ngaf/example', + version: '1.0.0', + token, + publicKey, + telemetryEndpoint, + warn: (message) => logger.warn(message), +}); +``` + +## Noncommercial hint + +`inferNoncommercial()` checks `globalThis.process?.env.NODE_ENV`. + +It returns: + +- `true` when `NODE_ENV` exists and is anything other than `"production"`; +- `false` when `NODE_ENV` is `"production"`; +- `false` when there is no `process` global. + +This is only a default hint. Callers can pass `isNoncommercial` explicitly. + +## Gotchas + +`runLicenseCheck()` is idempotent for identical `package` and `token` values. A repeated call with the same key returns `licensed` without re-running the check. + +That keeps repeated provider initialization quiet, but it means package authors should not use repeated calls with identical inputs as a status polling mechanism. + +`verifyLicense()` does not check time. Pair it with `evaluateLicense()` when you need expiration and grace behavior. diff --git a/apps/website/content/docs/licensing/reference/api.mdx b/apps/website/content/docs/licensing/reference/api.mdx new file mode 100644 index 000000000..55393eadc --- /dev/null +++ b/apps/website/content/docs/licensing/reference/api.mdx @@ -0,0 +1,172 @@ +# Licensing API + +This page documents the public exports from `@ngaf/licensing`. + +## Types + +```ts +type LicenseTier = 'developer-seat' | 'app-deployment' | 'enterprise'; + +interface LicenseClaims { + sub: string; + tier: LicenseTier; + iat: number; + exp: number; + seats: number; +} +``` + +```ts +type VerifyReason = 'malformed' | 'tampered'; + +type VerifyResult = + | { ok: true; claims: LicenseClaims } + | { ok: false; reason: VerifyReason }; +``` + +```ts +type LicenseStatus = + | 'licensed' + | 'grace' + | 'expired' + | 'missing' + | 'tampered' + | 'noncommercial'; +``` + +## verifyLicense() + +```ts +function verifyLicense( + token: string, + publicKey: Uint8Array, +): Promise; +``` + +Parses and verifies the compact token against an Ed25519 public key. + +It does not check `iat`, `exp`, grace windows, or commercial context. It only answers whether the token shape and signature are valid. + +Failure reasons: + +| Reason | Meaning | +|--------|---------| +| `malformed` | token format, base64url payload, JSON, or claim shape failed | +| `tampered` | signature verification failed or threw | + +## evaluateLicense() + +```ts +interface EvaluateOptions { + nowSec: number; + graceSec?: number; + isNoncommercial?: boolean; +} + +function evaluateLicense( + verifyResult: VerifyResult | undefined, + options: EvaluateOptions, +): EvaluateResult; +``` + +Turns a verify result into a `LicenseStatus`. + +`verifyResult` can be `undefined` when no token exists. In that case the result is `noncommercial` only when `isNoncommercial` is true; otherwise it is `missing`. + +`graceSec` defaults to 14 days. + +## runLicenseCheck() + +```ts +interface RunLicenseCheckOptions { + package: string; + version: string; + token?: string; + publicKey: Uint8Array; + telemetryEndpoint: string; + nowSec?: number; + isNoncommercial?: boolean; + warn?: (message: string) => void; + fetch?: typeof fetch; +} + +function runLicenseCheck( + options: RunLicenseCheckOptions, +): Promise; +``` + +Runs the full check: + +1. verifies the token when present; +2. evaluates the license status; +3. emits a warning when appropriate; +4. sends non-blocking telemetry. + +Repeated calls with the same package and token are treated as already handled and return `licensed`. + +## emitNag() + +```ts +interface EmitNagOptions { + package: string; + warn?: (message: string) => void; +} + +function emitNag( + result: Pick, + options: EmitNagOptions, +): void; +``` + +Emits one warning per package/status pair. Silent statuses are `licensed` and `noncommercial`. + +## inferNoncommercial() + +```ts +function inferNoncommercial(): boolean; +``` + +Returns `true` when `process.env.NODE_ENV` exists and is not `"production"`. Returns `false` for production and for browser-like environments without a `process` global. + +## signLicense() + +```ts +function signLicense( + claims: LicenseClaims, + privateKey: Uint8Array, +): Promise; +``` + +Signs claims with an Ed25519 private key and returns the compact token consumed by `verifyLicense()`. + +## createTelemetryClient() + +```ts +interface TelemetryEvent { + package: string; + version: string; + licenseId?: string; +} + +interface CreateTelemetryClientOptions { + endpoint: string; + fetch?: typeof fetch; + generateInstanceId?: () => string; +} +``` + +`createTelemetryClient(options).send(event)` POSTs JSON to the configured endpoint unless telemetry is opted out or `fetch` is unavailable. + +The request body uses snake-case fields: + +```json +{ + "package": "@ngaf/example", + "version": "1.0.0", + "license_id": "cus_123", + "anon_instance_id": "generated-id", + "ts": 1735689600 +} +``` + +Send failures are swallowed. diff --git a/apps/website/content/docs/render/a2ui/overview.mdx b/apps/website/content/docs/render/a2ui/overview.mdx index 28a6c650a..069a0b88a 100644 --- a/apps/website/content/docs/render/a2ui/overview.mdx +++ b/apps/website/content/docs/render/a2ui/overview.mdx @@ -1,301 +1,113 @@ # A2UI Overview -A2UI is an open standard for agent-driven user interfaces. It lets an AI agent describe a structured UI — components, layout, and live data — using a simple JSON protocol, and have that UI rendered automatically inside a chat session. +A2UI is the structured UI path for agent-built surfaces in chat. -When an agent response begins with the `---a2ui_JSON---` sentinel, the chat switches to A2UI rendering mode automatically — provided you supply a component catalog via the `views` input. +The important boundary is simple: the agent streams declarative messages, the client owns rendering, and Angular handlers stay inside your application. The model does not ship code. It emits a constrained surface description that `@ngaf/chat`, `@ngaf/a2ui`, and `@ngaf/render` turn into Angular UI. - -The implementation follows the A2UI v0.9 specification. For the full protocol reference, see [a2ui.org](https://a2ui.org). - +When an assistant message starts with `---a2ui_JSON---`, the chat streaming pipeline treats the rest of the content as newline-delimited A2UI JSON. -## How It Works in ChatComponent +## Runtime Flow -When content arrives with the `---a2ui_JSON---` prefix, the streaming pipeline switches to JSONL mode. Each newline-delimited JSON object is parsed as an A2UI message and handed to the surface store. - -``` -AI response starts with ---a2ui_JSON--- - → ContentClassifier sets type = 'a2ui' - → createA2uiMessageParser() parses JSONL line-by-line - → Each A2uiMessage is applied to A2uiSurfaceStore - → Surfaces signal updates reactively - → A2uiSurfaceComponent renders each surface via json-render +```text +assistant text starts with ---a2ui_JSON--- + -> content classifier switches to A2UI mode + -> createA2uiMessageParser() parses JSONL messages + -> createA2uiSurfaceStore() applies those messages by surface id + -> ChatComponent passes surface + state into A2uiSurfaceComponent + -> A2uiSurfaceComponent renders progressive state through your catalog ``` -The surface store maintains a `Map` keyed by surface ID. Each surface holds a flat component map, a data model, theme metadata, and the catalog ID used to resolve components. - -To render A2UI surfaces, pass a `ViewRegistry` to the `ChatComponent` via the `views` input. The `a2uiBasicCatalog()` function provides the 18 built-in components. +This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. The normal chat path uses progressive surface state and `a2uiSlot` so components can appear as their required data arrives. -## The Four Message Types +There is also a compatibility path: when `A2uiSurfaceComponent` receives a `surface` without a `state`, it calls `surfaceToSpec()` and renders through `@ngaf/render`. That fallback is where render-spec handlers, render events, and json-render state bindings apply. -The A2UI protocol is built on four message types. Agents compose entire interfaces by sending sequences of these messages. +## Message Envelopes -| Message | Purpose | -|---------|---------| -| `createSurface` | Creates a new named surface with a catalog ID and optional theme | -| `updateComponents` | Adds or replaces components on a surface (merged by component ID) | -| `updateDataModel` | Sets or patches a value in the surface data model via JSON Pointer | -| `deleteSurface` | Removes a surface and all its components | +The parser recognizes four envelope keys: -### createSurface +| Envelope | Purpose | +| --- | --- | +| `surfaceUpdate` | Adds or replaces components on a surface. | +| `dataModelUpdate` | Updates the surface data model. | +| `beginRendering` | Marks the surface root and optional style hints. | +| `deleteSurface` | Removes a surface. | -Declares a new surface. Must be sent before any other message for that surface ID. +Unknown envelopes are ignored. Malformed JSONL lines are skipped. Incomplete JSON waits in the parser buffer until a newline arrives. -```json -{"createSurface": {"surfaceId": "order-status", "catalogId": "basic"}} -``` +That behavior is deliberate. Agent streams are partial. The parser should not crash the UI because one line is unfinished mid-token. -### updateComponents +## Minimal Protocol Stream -Sends one or more component definitions. Each component has a unique `id`, a `component` type name, and any component-specific props. +A surface usually needs three messages: components, data, and a root. This is the wire format accepted by the parser and surface store. -```json -{"updateComponents": { - "surfaceId": "order-status", - "components": [ - {"id": "root", "component": "Column", "children": ["title", "status"]}, - {"id": "title", "component": "Text", "text": "Your Order"}, - {"id": "status", "component": "Text", "text": {"path": "/state"}} - ] -}} +```text +---a2ui_JSON--- +{"surfaceUpdate":{"surfaceId":"contact","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","name","submit"]},"gap":12}}},{"id":"title","component":{"Text":{"text":{"literalString":"Contact us"},"usageHint":"h2"}}},{"id":"name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"}}}},{"id":"submit_label","component":{"Text":{"text":{"literalString":"Send"}}}},{"id":"submit","component":{"Button":{"child":"submit_label","primary":true,"action":{"name":"formSubmit","context":[{"key":"name","value":{"path":"/name"}}]}}}}]}} +{"dataModelUpdate":{"surfaceId":"contact","contents":[{"key":"name","valueString":""}]}} +{"beginRendering":{"surfaceId":"contact","root":"root"}} ``` -### updateDataModel - -Sets values at a JSON Pointer path in the surface's data model. Components whose props reference these paths re-render automatically. +Components use keyed union definitions: ```json -{"updateDataModel": {"surfaceId": "order-status", "path": "/state", "value": "Shipped"}} -``` - -Omit `path` (or use `/`) to replace the entire data model at once. - -### deleteSurface - -Removes a surface from the store and dismisses its rendered output. - -```json -{"deleteSurface": {"surfaceId": "order-status"}} -``` - -## Action-Event Bridge - -A2UI actions (button clicks, form events) now map directly to render-spec `on` bindings. When `surfaceToSpec()` converts a surface to a spec, each component's `action` prop is translated into an `on` binding on the corresponding spec element. This means A2UI actions flow through the render-lib event system rather than being handled ad-hoc by individual components. - -The bridge provides two default handlers: - -- `a2ui:event` -- dispatches named events (e.g., `submit`, `cancel`) back to the agent -- `a2ui:localAction` -- executes local function calls (e.g., `openUrl`) - -This unified event flow means consumers can observe all A2UI interactions through the `RenderEvent` stream on `RenderSpecComponent`, including handler execution, state changes, and lifecycle signals. See the [Render Events](/docs/render/events) page for the full event type reference. - -## A2UI vs json-render - -Both A2UI and json-render produce rendered UIs inside chat messages, but they serve different purposes. - -| | A2UI | json-render | -|-|------|-------------| -| Protocol | Multi-message JSONL stream | Single JSON object | -| State | Surfaces with live data models | Stateless spec | -| Interactivity | Two-way bindings, button actions, validation | Read-only display | -| Detection | `---a2ui_JSON---` prefix | First character is `{` | -| Use case | Agent-driven dynamic UIs | Rich content cards | - -Use **A2UI** when the agent needs to build an interface incrementally, bind it to live data, or respond to user interactions. Use **json-render** for rich one-shot content cards — formatted results, structured data displays, and similar read-only output. - -## Quick Setup - -Pass `a2uiBasicCatalog()` to the `views` input to enable A2UI rendering: - -```typescript -import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; - -@Component({ - template: ``, - imports: [ChatComponent], -}) -export class AppComponent { - catalog = a2uiBasicCatalog(); - agentRef = agent({ /* your agent options */ }); +{ + "id": "title", + "component": { + "Text": { + "text": { "literalString": "Contact us" }, + "usageHint": "h2" + } + } } ``` -When the AI response starts with `---a2ui_JSON---`, the chat renders the surfaces using the provided catalog. +This is different from a flat `component: "Text"` shape. The current source expects the keyed union form. - -To extend or replace the built-in components, compose catalogs with `withViews()` or `views()`. See the [Custom Catalogs guide](/docs/chat/guides/custom-catalogs) for details. - +This stream demonstrates the protocol boundary, not a guarantee that every built-in catalog component is fully wired in the progressive chat renderer. The current chat path renders surface state first and pushes resolved `A2uiComponentView.props` directly into Angular components. The richer projection work - unwrapping component definitions, mapping children, creating render state bindings, and turning actions into handlers - lives in the render-spec compatibility path. -## Automatic Event Routing +## Data Model -When an A2UI button fires an `a2ui:event` action, `ChatComponent` automatically -routes it back to the agent as a human message. No manual `(renderEvent)` wiring -is needed for standard A2UI event flows. - -The event is sent as a JSON-encoded human message: +A2UI component props can point at the surface data model. ```json { - "type": "a2ui_event", - "surfaceId": "contact", - "name": "formSubmit", - "context": { "formId": "contact" } -} -``` - -The `renderEvent` output still fires for all events, so consumers can observe -or log events without intercepting the routing. - -### Minimal Consumer Setup - -```typescript -import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; -import { agent } from '@ngaf/langgraph'; - -@Component({ - template: ``, -}) -export class MyComponent { - agentRef = agent({ apiUrl: '/api', assistantId: 'my-agent' }); - catalog = a2uiBasicCatalog(); + "id": "name", + "component": { + "TextField": { + "label": { "literalString": "Name" }, + "text": { "path": "/name" } + } + } } ``` -## Custom Function Call Handlers +In the render-spec compatibility path, `surfaceToSpec()` converts path references into json-render state bindings. Catalog input components can use `emitBinding()` to write back through the render event pipeline. -When an A2UI button has a `functionCall` action, the `call` name is looked up in the `[handlers]` map on `ChatComponent`. This lets you define client-side behavior triggered by agent-built UI: +```ts +import { emitBinding } from '@ngaf/chat'; -```typescript -@Component({ - template: ``, -}) -export class MyComponent { - agentRef = agent({ apiUrl: '/api', assistantId: 'my-agent' }); - catalog = a2uiBasicCatalog(); - - handlers = { - addToCart: async (args: Record) => { - const cart = inject(CartService); - return cart.add(args['sku'] as string); - }, - }; +onInput(event: Event): void { + const value = (event.target as HTMLInputElement).value; + emitBinding(this.emit(), this._bindings(), 'text', value); } ``` -The agent sends a button with `{"action": {"functionCall": {"call": "addToCart", "args": {"sku": "ABC"}}}}`. When clicked, the `addToCart` handler runs in Angular's injection context — `inject()` works for accessing services. - -If no consumer handler matches the `call` name, built-in handlers are used as fallbacks (e.g., `openUrl` opens a URL in a new tab). +The write-back protocol is client-side state. The current action-message builder does not re-read component state at click time. It serializes the action params it receives. If a caller provides an internal `A2uiSurface` with `sendDataModel: true`, the builder also includes the current surface data model snapshot under `metadata.a2uiClientDataModel`; the JSONL wire envelopes do not expose a `sendDataModel` field. -Handler return values are emitted on the `RenderHandlerEvent` — observe them via the `renderEvent` output on `ChatComponent`. +## Actions -## Validation - -A2UI v0.9 uses `CheckRule` objects for client-side validation. Input components and buttons can define a `checks` array — each check has a `condition` (a `DynamicBoolean`) and an error `message`. - -### CheckRule Shape +Buttons carry an `A2uiAction`: ```json { - "checks": [ - { - "condition": { "call": "required", "args": { "value": { "path": "/name" } } }, - "message": "Name is required" - } + "name": "formSubmit", + "context": [ + { "key": "name", "value": { "path": "/name" } } ] } ``` -The `condition` can be: -- A **boolean literal**: `true` or `false` -- A **path reference**: `{ "path": "/agreed" }` — resolves to a data model value -- A **FunctionCall**: `{ "call": "required", "args": { ... } }` — invokes a named function -- A **composite**: `{ "call": "and", "args": { "values": [...] } }` — combines multiple conditions - -### Built-in Functions - -| Category | Functions | -|----------|-----------| -| Validation | `required`, `regex`, `length`, `numeric`, `email` | -| Logic | `and`, `or`, `not` | -| Formatting | `formatString`, `formatNumber`, `formatCurrency`, `formatDate`, `pluralize` | -| Navigation | `openUrl` | - -### Input Component Behavior - -Input components (`TextField`, `CheckBox`, `ChoicePicker`, `Slider`, `DateTimeInput`) validate continuously — errors display inline as the user interacts. The input border changes color to indicate validation state. - -### Button Behavior - -Per the v0.9 spec: if any check fails, the button is automatically disabled. Error messages display below the button. - -### Composite Conditions - -Use `and`, `or`, and `not` to compose complex validation rules: - -```json -{ - "condition": { - "call": "and", - "args": { - "values": [ - { "call": "required", "args": { "value": { "path": "/name" } } }, - { "call": "or", "args": { "values": [ - { "call": "required", "args": { "value": { "path": "/email" } } }, - { "call": "required", "args": { "value": { "path": "/phone" } } } - ]}} - ] - } - }, - "message": "Name required, plus email or phone" -} -``` - -### Custom Catalog Components - -Custom catalog components receive a pre-computed `validationResult` prop: - -```typescript -interface A2uiValidationResult { - valid: boolean; - errors: string[]; -} -``` - -Use the shared `A2uiValidationErrorsComponent` for consistent error display: - -```typescript -import { A2uiValidationErrorsComponent } from '@ngaf/chat'; - -@Component({ - imports: [A2uiValidationErrorsComponent], - template: ` - - - `, -}) -export class MyCustomInputComponent { - readonly value = input(''); - readonly validationResult = input({ valid: true, errors: [] }); -} -``` - -### Theming - -Validation styling uses CSS custom properties: - -| Property | Default | Usage | -|----------|---------|-------| -| `--a2ui-error` | `#ef4444` | Error text and invalid border color | -| `--a2ui-border` | `rgba(255,255,255,0.1)` | Default input border | -| `--a2ui-input-bg` | `rgba(255,255,255,0.05)` | Input background | -| `--a2ui-label` | `rgba(255,255,255,0.6)` | Label text color | - -## Events & Data Model Transport - -When a user triggers an event action (e.g., clicking a button with `action.event`), the Angular renderer builds a v0.9-compliant action message and sends it back to the agent. Local actions (`action.functionCall`) execute client-side only — the agent never sees them. - -### Action Message Shape - -The outbound message follows the [v0.9 spec](https://a2ui.org): +In the render-spec compatibility path, `surfaceToSpec()` turns this into a render `click` binding that calls the built-in `a2ui:event` handler. `A2uiSurfaceComponent` then emits an `A2uiActionMessage`. ```json { @@ -303,132 +115,92 @@ The outbound message follows the [v0.9 spec](https://a2ui.org): "action": { "name": "formSubmit", "surfaceId": "contact", - "sourceComponentId": "submit-btn", + "sourceComponentId": "submit", "timestamp": "2026-04-10T14:30:00.000Z", "context": { - "name": "Alice", - "email": "alice@example.com" - } - } -} -``` - -| Field | Description | -|-------|-------------| -| `version` | Always `"v0.9"` | -| `action.name` | The event name from the component's `action.event.name` | -| `action.surfaceId` | The surface that owns this component | -| `action.sourceComponentId` | The `id` of the component that triggered the event | -| `action.timestamp` | ISO 8601 timestamp of when the action was dispatched | -| `action.context` | Resolved values from `action.event.context` — path refs and function calls are evaluated against the current data model | - -### Context Resolution - -Context values in `action.event.context` are `DynamicValue`s — they can be literals, path references, or function calls. They are resolved at dispatch time against the current data model: - -```json -{ - "action": { - "event": { - "name": "formSubmit", - "context": { - "name": {"path": "/name"}, - "email": {"path": "/email"}, - "total": {"call": "formatCurrency", "args": {"value": {"path": "/amount"}}} - } - } - } -} -``` - -When the user clicks the button, the renderer resolves `/name` and `/email` from the data model and calls `formatCurrency` on `/amount`, producing a flat `context` object with concrete values. - -### sendDataModel - -Set `sendDataModel: true` on `createSurface` to attach the full data model snapshot to every outbound action: - -```json -{"type": "createSurface", "surfaceId": "contact", "catalogId": "basic", "sendDataModel": true} -``` - -When enabled, the action message includes a `metadata` field: - -```json -{ - "version": "v0.9", - "action": { "..." : "..." }, - "metadata": { - "a2uiClientDataModel": { - "version": "v0.9", - "surfaces": { - "contact": { - "name": "Alice", - "email": "alice@example.com", - "department": "Engineering" - } - } + "name": { "literalString": "Alice" } } } } ``` -The data model is only sent with event actions — there are no passive change notifications on input changes. This matches the v0.9 spec requirement that the data model piggybacks on outbound messages. +If the internal surface object has `sendDataModel: true`, the emitted message also includes `metadata.a2uiClientDataModel` with the current surface data model snapshot. Streamed protocol surfaces created by the current surface store do not set that flag. -### Angular Integration +In the progressive chat path, `A2uiSurfaceComponent` renders the surface state first. Catalog components receive resolved props as Angular inputs. The current `a2uiSlot` implementation does not wire render-spec handlers, child spec context, or render events into those mounted components. Treat agent-bound action messages as a render-spec compatibility behavior unless your catalog explicitly handles its own event wiring. -`A2uiSurfaceComponent` exposes two outputs: +## Local Handlers -| Output | Type | Description | -|--------|------|-------------| -| `(action)` | `A2uiActionMessage` | Agent-bound action messages — the complete v0.9 envelope | -| `(events)` | `RenderEvent` | All render events (state changes, handler calls, lifecycle) for observation | +In the render-spec compatibility path, `A2uiSurfaceComponent` also registers an `a2ui:localAction` handler. Consumer handlers take priority, and the built-in fallback currently supports `openUrl`. -`ChatComponent` auto-routes `(action)` events to the agent as human messages. For standalone usage, bind `(action)` directly: +Use local handlers for client-owned behavior. Use A2UI actions for agent-bound events. ```html ``` -## Data Model Bindings +```ts +handlers = { + openDetails: async (args: Record) => { + await this.router.navigate(['/orders', args['orderId']]); + }, +}; +``` -When the agent sets component properties using path references (`{ "path": "/name" }`), the surface component -tracks these as **bindings** — a mapping from prop name to JSON Pointer path. These bindings are passed to -catalog components as the `_bindings` prop. +## A2UI vs json-render -### How Bindings Work +Both paths render structured UI, but they optimize for different jobs. -1. **Agent sends components** with path references: `{ "value": { "path": "/form/name" } }` -2. **`surfaceToSpec`** resolves the path to a current value AND records the binding in `_bindings` -3. **Catalog component** reads the resolved value normally. When the user changes the value, it emits an `a2ui:datamodel` event via the `emit` callback -4. **The event format** is `a2ui:datamodel:{path}:{value}` +| | A2UI | json-render | +| --- | --- | --- | +| Wire shape | JSONL message stream | Single JSON spec | +| State | Surface data model | Spec state | +| Best fit | Incremental agent-owned surfaces; protocol-level A2UI streams | One-shot rendered content | +| Detection | `---a2ui_JSON---` prefix | JSON object content | +| Rendering | Progressive surface state in chat; render-spec fallback when no state is supplied | json-render spec directly | -### Using `emitBinding` +Use A2UI when the agent needs to keep updating a surface and you are working at the protocol boundary. Use json-render when the agent needs a stable, directly rendered structured result. For production interaction that depends on component handlers, state bindings, and child projection, verify the exact A2UI rendering path you are using. -Custom catalog components can use the `emitBinding` utility for consistent binding emission: +## Setup -```typescript -import { emitBinding } from '@ngaf/chat'; +Pass the built-in A2UI catalog to chat: -// In your component's change handler: -onInput(event: Event): void { - const val = (event.target as HTMLInputElement).value; - emitBinding(this.emit(), this._bindings(), 'value', val); +```ts +import { Component } from '@angular/core'; +import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; +import { agent } from '@ngaf/langgraph'; + +@Component({ + standalone: true, + imports: [ChatComponent], + template: ``, +}) +export class SupportChatComponent { + protected readonly chat = agent({ + apiUrl: '/api/langgraph', + assistantId: 'support', + }); + + protected readonly catalog = a2uiBasicCatalog(); } ``` -### Known Limitations +For custom component sets, build a catalog with the same view registry tools used by `@ngaf/render`. + +## Gotchas + +The A2UI parser is not a full schema validator. It recognizes envelope keys and leaves deeper validation to typed code, tests, and your runtime boundary. + +Schema-valid messages are not enough to make UI executable. Your catalog must contain components for the emitted types, and your handlers must exist for the actions you expect users to take. -The current binding mechanism is client-side only — the `a2ui:datamodel` events are emitted -but do not yet flow through the render lib's `StateStore`. Data model updates from user input -are not reflected back to other components in real time. Full `StateStore` integration is planned -for a future release. +Do not use old envelope names such as `createSurface`, `updateComponents`, or `updateDataModel` with the current parser. They are ignored because the source recognizes `surfaceUpdate`, `dataModelUpdate`, `beginRendering`, and `deleteSurface`. -Data model state is refreshed when the agent sends an `updateDataModel` message. +Do not assume the progressive chat renderer and the render-spec compatibility path have identical capabilities. The compatibility path projects a surface through `surfaceToSpec()`. The progressive path mounts catalog components from `A2uiComponentView` state and pushes Angular inputs directly. ## What's Next @@ -438,27 +210,27 @@ Data model state is refreshed when the agent sends an `updateDataModel` message. icon="layers" href="/docs/render/a2ui/surface-component" > - API reference for A2uiSurfaceComponent — render a surface outside ChatComponent. + Render a surface outside the full ChatComponent composition. - API reference for createA2uiSurfaceStore() and the apply/surfaces/surface interface. + Understand how messages update surfaces and data models. - Reference for all 18 built-in components and their props. + See the built-in catalog components and their props. - How the streaming pipeline detects and routes A2UI content. + Read the protocol package docs for parser, schema, and pointer helpers. diff --git a/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx b/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx new file mode 100644 index 000000000..17d06ec27 --- /dev/null +++ b/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx @@ -0,0 +1,202 @@ +# json-render vs A2UI + +`@ngaf/render` and `@ngaf/a2ui` both render structured UI, but they solve different problems. + +`@ngaf/render` renders a fixed json-render spec into Angular components. + +`@ngaf/a2ui` defines an agent-to-UI protocol: surfaces, components, dynamic values, data model updates, and actions. In `@ngaf/chat`, A2UI surfaces are rendered through the same render infrastructure where possible. + +This matters because the tradeoff is not "which renderer is better." The tradeoff is contract shape. A fixed spec is easier to validate and reason about. A2UI is better when the agent needs to update a UI over time or receive structured user actions back. + +## The Layers + +```text +@ngaf/render + renders a Spec using an Angular registry, state store, functions, and handlers + +@ngaf/a2ui + defines A2UI v0.9 message and component types + +@ngaf/chat + detects assistant content, manages streaming state, and mounts render or A2UI surfaces +``` + +Use `@ngaf/render` directly when your application already has a spec. + +Use A2UI through `@ngaf/chat` when the agent is producing UI as part of a conversation. + +## json-render + +json-render takes a single spec with a root and an element map. + +```html + +``` + +The registry maps element types to Angular components. Components receive resolved props, child keys, an `emit()` function, loading state, and the full spec. + +```ts +import { + RenderSpecComponent, + defineAngularRegistry, + signalStateStore, +} from '@ngaf/render'; +``` + +Choose json-render when: + +- The UI can be represented as one spec. +- You want a fixed contract that is easy to validate before rendering. +- The application, not the agent protocol, owns event semantics. +- You need custom Angular components with explicit inputs. +- You are rendering structured results, cards, forms, or dashboards outside chat. + +The practical advantage is control. You decide the schema, registry, handlers, and state store. + +The practical cost is that streaming and progressive surface updates are not the protocol. Chat can parse a streaming JSON object incrementally, but the model is still "one spec becomes one component tree." + +## A2UI + +A2UI is a wire protocol for agent-built interfaces. + +The current exported message union is: + +```ts +type A2uiMessage = + | { surfaceUpdate: A2uiSurfaceUpdate } + | { dataModelUpdate: A2uiDataModelUpdate } + | { beginRendering: A2uiBeginRendering } + | { deleteSurface: A2uiDeleteSurface }; +``` + +A surface has an id, catalog id, component map, data model, optional theme, optional styles, and optional `sendDataModel` behavior. + +```ts +interface A2uiSurface { + surfaceId: string; + catalogId: string; + components: Map; + dataModel: Record; + styles?: { font?: string; primaryColor?: string }; +} +``` + +Choose A2UI when: + +- The agent needs to build UI incrementally. +- Data can arrive separately from component structure. +- Components bind to live data model paths. +- User actions should be sent back as structured action messages. +- Fallbacks matter while some bound data is still unresolved. +- The UI is part of a conversational agent response. + +The practical advantage is that A2UI models an ongoing surface, not just a rendered object. + +The practical cost is protocol discipline. The agent must emit valid envelopes in the right order, the catalog must support the component types, and action semantics must be designed. + +## Chat Detection + +`ChatComponent` classifies assistant message content as it streams. + +| Content | Rendering path | +|---|---| +| Normal text | Markdown | +| First non-whitespace character is `{` | json-render | +| Starts with `---a2ui_JSON---` | A2UI JSONL | + +For json-render, chat parses the JSON into a spec and renders it with the `views` registry. + +For A2UI, chat parses newline-delimited A2UI messages, applies them to an `A2uiSurfaceStore`, and renders each surface with ``. + +```html + +``` + +The same `views` input is used by both paths. For A2UI, `a2uiBasicCatalog()` provides the built-in catalog: + +```ts +import { a2uiBasicCatalog } from '@ngaf/chat'; + +catalog = a2uiBasicCatalog(); +``` + +## Registries And Catalogs + +`@ngaf/render` uses a view registry: + +```ts +import { views, withViews } from '@ngaf/render'; + +const registry = views({ + OrderSummary: OrderSummaryComponent, +}); +``` + +A2UI catalogs use the same underlying registry shape in chat. Component names such as `Text`, `Button`, `Card`, `Column`, `Row`, `TextField`, `CheckBox`, `MultipleChoice`, and `Slider` resolve to Angular components. + +You can compose registries with `withViews()` and pass the result to chat. + +This matters because the registry is the allowlist. If the agent emits a component name that is not registered, the renderer should fall back instead of executing unknown UI. + +## State And Validation + +json-render can use a `StateStore`, computed functions, and handlers: + +```html + +``` + +That is application-owned state. You can validate the spec before rendering, restrict component names, and decide which handlers exist. + +A2UI carries a client data model in protocol messages. Components use dynamic values such as literal strings, numbers, booleans, arrays, or paths into the data model. The chat-side surface store applies data model updates and gates component readiness until bound values resolve. + +Validation in A2UI is part of the component/action protocol. It is not the same as validating a json-render spec before mount. Treat it as user-interaction validation, not as a substitute for protocol validation at the boundary. + +## Actions And Handlers + +json-render actions are local render events. A rendered component calls `emit()`, and `RenderSpecComponent` invokes the matching handler from `[handlers]`. + +A2UI actions produce `A2uiActionMessage` objects: + +```ts +interface A2uiActionMessage { + version: 'v0.9'; + action: { + name: string; + surfaceId: string; + sourceComponentId: string; + timestamp: string; + context: Record; + }; +} +``` + +If a surface asks to send the client data model, the action message can include `metadata.a2uiClientDataModel`. + +In chat, standard A2UI action messages are routed back through the agent as structured user input. Local handlers still exist for client-side behavior, but executable behavior is explicit: only registered handlers run. + +This matters because rendering JSON should not mean executing arbitrary model instructions. The model can request an action by name. Your registry and handlers decide what that name can do. + +## Fallbacks + +`@ngaf/render` has a default fallback for registered views that are not ready and supports per-view fallback entries. + +A2UI uses the same idea at the catalog level. A catalog entry can be a component type or an object with a component and fallback. While data-bound props are unresolved, the fallback renders. Once the real component mounts, readiness is monotonic for that element. + +Fallbacks are not decoration. They are the safety behavior for streaming UI. Use them when a component needs required data that may arrive after structure. + +## Choosing + +Use json-render when you want a fixed, application-owned spec. + +Use A2UI when you want an agent-owned surface that changes over time and sends structured actions back. + +Use markdown when the best UI is text. + +A good rule: if you can validate the whole UI before showing it, start with json-render. If the UI is a live conversation artifact with partial data, actions, and updates, use A2UI. diff --git a/apps/website/content/docs/render/getting-started/installation.mdx b/apps/website/content/docs/render/getting-started/installation.mdx index eb295424c..bf8597e5a 100644 --- a/apps/website/content/docs/render/getting-started/installation.mdx +++ b/apps/website/content/docs/render/getting-started/installation.mdx @@ -28,6 +28,7 @@ The library requires the following peer dependencies: | `@angular/core` | `^20.0.0` or `^21.0.0` | | `@angular/common` | `^20.0.0` or `^21.0.0` | | `@json-render/core` | `^0.16.0` | +| `@ngaf/licensing` | `*` | `@angular/core` and `@angular/common` are already part of any Angular 20+ project. You only need to install `@json-render/core` as an additional dependency if your package manager does not install peer dependencies automatically. diff --git a/apps/website/content/docs/telemetry/getting-started/introduction.mdx b/apps/website/content/docs/telemetry/getting-started/introduction.mdx new file mode 100644 index 000000000..0914bb760 --- /dev/null +++ b/apps/website/content/docs/telemetry/getting-started/introduction.mdx @@ -0,0 +1,50 @@ +# Introduction + +`@ngaf/telemetry` is the shared telemetry package for the framework. It has separate surfaces for browser applications, Node adapters, and shared utilities. + +The important boundary is simple: + +- browser telemetry is opt-in through Angular DI; +- Node telemetry is opt-out and used by package lifecycle or server adapter code; +- shared utilities expose event and environment helpers. + +Do not treat the browser and Node entry points as interchangeable. They have different runtime assumptions and different privacy defaults. + +## Entry points + +```ts +// Shared utilities +import { isTelemetryDisabled, getDisableReason } from '@ngaf/telemetry'; + +// Browser Angular provider and service +import { provideNgafTelemetry } from '@ngaf/telemetry/browser'; + +// Node/server helpers +import { disableTelemetry, capturePostinstall } from '@ngaf/telemetry/node'; +``` + +The package also exports `@ngaf/telemetry/node/postinstall` for the package postinstall executable. + +## Browser posture + +The browser package does nothing unless the application explicitly provides telemetry config with `enabled: true`. + +When enabled, delivery is chosen in this order: + +1. `sink`; +2. `endpoint`; +3. `posthogKey`. + +`sink` and `endpoint` are the preferred app-owned paths. `posthogKey` and `posthogHost` still exist for older integrations and are marked deprecated in source comments. + +## Node posture + +Node helpers check opt-out environment variables before sending. They use a per-process anonymous ID and deterministic sampling based on that ID. + +Node capture failures return a failure result or are swallowed by adapter helpers. Telemetry should not break install, startup, or request handling. + +## What this package does not promise + +The source does not contain content capture for prompts, completions, tool inputs, or tool outputs. It also does not persist browser IDs to local storage or cookies. + +It does collect runtime metadata when the corresponding Node or browser capture APIs are called. Keep event properties short, operational, and free of application data. diff --git a/apps/website/content/docs/telemetry/guides/browser.mdx b/apps/website/content/docs/telemetry/guides/browser.mdx new file mode 100644 index 000000000..905b10109 --- /dev/null +++ b/apps/website/content/docs/telemetry/guides/browser.mdx @@ -0,0 +1,85 @@ +# Browser Telemetry + +Browser telemetry is opt-in. If your Angular app never calls `provideNgafTelemetry()`, the service has no enabled config and `capture()` returns without sending. + +## Configure + +```ts +import type { ApplicationConfig } from '@angular/core'; +import { provideNgafTelemetry } from '@ngaf/telemetry/browser'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideNgafTelemetry({ + enabled: true, + endpoint: '/api/telemetry', + sampleRate: 1, + }), + ], +}; +``` + +The endpoint receives: + +```json +{ + "event": "ngaf:stream_started", + "distinctId": "browser:", + "properties": { + "transport": "langgraph", + "sample_weight": 1 + } +} +``` + +The browser distinct ID is generated per service instance. The source does not write it to storage. + +## Prefer a sink for app-owned analytics + +Use `sink` when your app already has an analytics boundary. + +```ts +provideNgafTelemetry({ + enabled: true, + sink: async ({ event, properties }) => { + await analytics.track(event, properties); + }, +}); +``` + +When `sink` is present, the service does not use `endpoint` or PostHog. + +## Sampling + +`sampleRate` is normalized: + +- missing, invalid, or non-finite values become `1`; +- values less than or equal to `0` disable capture; +- values greater than or equal to `1` capture every event; +- values between `0` and `1` use `Math.random()`. + +Captured events include `sample_weight`. When the sample rate is `0.25`, the default weight is `4`. + +## Events captured by the service + +The service exposes convenience methods: + +```ts +telemetry.captureRuntimeInstanceCreated({ + transport: 'langgraph', + provider: 'openai', + model: 'gpt-4.1', +}); + +telemetry.captureStreamStarted({ transport: 'langgraph', provider: 'openai', model: 'gpt-4.1' }); +telemetry.captureStreamEnded({ transport: 'langgraph', provider: 'openai', model: 'gpt-4.1', durationMs: 1200 }); +telemetry.captureStreamErrored({ transport: 'langgraph', provider: 'openai', model: 'gpt-4.1', error }); +``` + +`captureStreamErrored()` sends `errorClass`, not the raw error object. + +## Delivery failures + +Browser capture is wrapped in a `try/catch`. A sink error, fetch failure, or dynamic import failure is swallowed. + +That keeps telemetry from becoming part of your application control flow. diff --git a/apps/website/content/docs/telemetry/guides/node.mdx b/apps/website/content/docs/telemetry/guides/node.mdx new file mode 100644 index 000000000..bf6463880 --- /dev/null +++ b/apps/website/content/docs/telemetry/guides/node.mdx @@ -0,0 +1,95 @@ +# Node Telemetry + +Node telemetry lives under `@ngaf/telemetry/node`. It is intended for package lifecycle hooks and server-side adapters. + +```ts +import { + captureRuntimeInstanceCreated, + captureStreamStarted, + captureStreamEnded, + captureStreamErrored, + disableTelemetry, +} from '@ngaf/telemetry/node'; +``` + +## Opt out programmatically + +Call `disableTelemetry()` before capture helpers run. + +```ts +import { disableTelemetry } from '@ngaf/telemetry/node'; + +disableTelemetry(); +``` + +This sets an in-process flag. It does not mutate environment variables. + +## Capture runtime lifecycle + +```ts +await captureRuntimeInstanceCreated({ + transport: 'langgraph', + provider: 'openai', + model: 'gpt-4.1', + angularVersion: '21.1.0', +}); +``` + +The `RuntimeInstanceTelemetry` type includes `apiKey`, but the adapter strips it before sending. Do not pass secrets as telemetry properties anyway. + +## Capture streams + +```ts +await captureStreamStarted({ + provider: 'openai', + model: 'gpt-4.1', +}); + +await captureStreamEnded({ + provider: 'openai', + model: 'gpt-4.1', + durationMs: 1200, +}); + +await captureStreamErrored({ + provider: 'openai', + model: 'gpt-4.1', + error, +}); +``` + +`captureStreamErrored()` records an error class. It does not send the full error object. + +## Ingest and sampling + +`captureEvent()` sends to: + +```text +https://cacheplane.ai/api/ingest +``` + +unless `NGAF_TELEMETRY_INGEST_URL` is set. + +Sampling uses `NGAF_TELEMETRY_SAMPLE_RATE`. Invalid values fall back to `1`. Values are clamped to the range `0` to `1`. + +Every sent event includes `sample_weight`. + +## Postinstall script + +The postinstall entry point reads package name and version from npm lifecycle environment variables or package.json, skips disabled environments, and sends `ngaf:postinstall`. + +It skips local top-level installs by default. Dependency installs under `node_modules` and global installs can be eligible unless disabled. + +When `DEBUG` includes `ngaf:telemetry`, `ngaf:*`, or `*`, the script prints the payload shape it attempted to send. It prints the normal install telemetry notice only when the ingest endpoint accepted the event. + +## Failure behavior + +The Node adapter helpers catch errors and return without throwing. `captureEvent()` returns: + +```ts +type CaptureResult = + | { sent: true } + | { sent: false; reason: 'disabled' | 'sampled' | 'failed' }; +``` + +Use the result in tests or diagnostics. Do not make application correctness depend on telemetry delivery. diff --git a/apps/website/content/docs/telemetry/guides/privacy-and-opt-out.mdx b/apps/website/content/docs/telemetry/guides/privacy-and-opt-out.mdx new file mode 100644 index 000000000..f6080cb14 --- /dev/null +++ b/apps/website/content/docs/telemetry/guides/privacy-and-opt-out.mdx @@ -0,0 +1,77 @@ +# Privacy and Opt-Out + +The telemetry source has two different privacy postures. + +Browser telemetry is off unless the app opts in. Node telemetry can send from package lifecycle hooks or server adapters unless the environment or process disables it. + +## Node opt-out + +Any of these environment signals disables Node telemetry: + +```bash +DO_NOT_TRACK=1 +npm_config_do_not_track=true +NPM_CONFIG_DO_NOT_TRACK=true +NGAF_TELEMETRY_DISABLED=1 +CI=true +GITHUB_ACTIONS=true +CONTINUOUS_INTEGRATION=true +BUILDKITE=true +CIRCLECI=true +``` + +The boolean parser treats `1`, `true`, `TRUE`, and `yes` as true values. + +You can also disable telemetry in process: + +```ts +import { disableTelemetry } from '@ngaf/telemetry/node'; + +disableTelemetry(); +``` + +`getDisableReason()` reports one of: + +```ts +'DO_NOT_TRACK' | 'NGAF_TELEMETRY_DISABLED' | 'CI' | null +``` + +## Browser opt-in + +Browser capture requires an enabled config: + +```ts +provideNgafTelemetry({ enabled: true, sink }); +``` + +With `enabled: false` or no provider, browser capture no-ops. + +The browser service sends only when application code or framework browser code calls its capture methods. It does not install a global listener. + +## Anonymous IDs + +Node uses `anon_` from `node:crypto` and caches it for the current process. + +Browser endpoint delivery uses `browser:` when `crypto.randomUUID()` is available, with a `Math.random()` fallback. The value is kept in the service instance. + +The source does not persist either ID across process restarts or browser sessions. + +## Data minimization from source + +The Node postinstall payload includes package/runtime installation metadata such as package name, version, Node version, OS, architecture, package manager details, and global/workspace flags when npm exposes them. + +Runtime lifecycle helpers send transport/provider/model style metadata. Stream error helpers send an error class, not the raw error object. + +The source strips `apiKey` in the Node runtime adapter before sending. + +There is no source path that sends prompts, completions, message content, tool call arguments, tool call outputs, or environment variable dumps. + +## Custom ingest + +Set a custom Node ingest URL with: + +```bash +NGAF_TELEMETRY_INGEST_URL=https://telemetry.example.com/api/ingest +``` + +For browser apps, prefer `sink` or `endpoint` so telemetry remains inside your application's analytics boundary. diff --git a/apps/website/content/docs/telemetry/reference/events.mdx b/apps/website/content/docs/telemetry/reference/events.mdx new file mode 100644 index 000000000..1ad63525e --- /dev/null +++ b/apps/website/content/docs/telemetry/reference/events.mdx @@ -0,0 +1,91 @@ +# Telemetry Events + +The shared event union is: + +```ts +type NgafEvent = NgafNodeEvent | NgafBrowserEvent; +``` + +## Node events + +```ts +type NgafNodeEvent = + | 'ngaf:postinstall' + | 'ngaf:runtime_instance_created' + | 'ngaf:stream_started' + | 'ngaf:stream_ended' + | 'ngaf:stream_errored'; +``` + +| Event | Source | Properties from source | +|-------|--------|------------------------| +| `ngaf:postinstall` | package postinstall script | `pkg`, `version`, `node`, `node_version`, `os`, `arch`, `global_install`, package-manager fields when npm exposes them | +| `ngaf:runtime_instance_created` | Node adapter helper | `transport`, `provider`, `model`, `angularVersion`; `apiKey` is removed | +| `ngaf:stream_started` | Node adapter helper | `provider`, `model`, optional fields in the input object | +| `ngaf:stream_ended` | Node adapter helper | `provider`, `model`, `durationMs` when supplied | +| `ngaf:stream_errored` | Node adapter helper | stream properties plus `errorClass` | + +`captureEvent()` also adds `sample_weight` to sent event properties. + +## Browser events + +The shared event file lists these browser-only events: + +```ts +type NgafBrowserEvent = + | 'ngaf:browser_provided' + | 'ngaf:browser_chat_init'; +``` + +The browser Angular token broadens the local service event type to: + +```ts +type NgafTelemetryEvent = + | 'ngaf:browser_provided' + | 'ngaf:browser_chat_init' + | 'ngaf:runtime_instance_created' + | 'ngaf:stream_started' + | 'ngaf:stream_ended' + | 'ngaf:stream_errored'; +``` + +That means browser code can capture the browser-specific events plus runtime lifecycle events when telemetry is enabled. + +## Browser payloads + +Endpoint delivery sends: + +```json +{ + "event": "ngaf:stream_ended", + "distinctId": "browser:", + "properties": { + "transport": "langgraph", + "provider": "openai", + "model": "gpt-4.1", + "durationMs": 1200, + "sample_weight": 1 + } +} +``` + +When using `sink`, the sink receives the same `event` and `properties` values before endpoint formatting. + +## Node payloads + +Node delivery sends: + +```json +{ + "key": "phc_public_cacheplane_telemetry", + "distinctId": "anon_", + "event": "ngaf:stream_started", + "properties": { + "provider": "openai", + "model": "gpt-4.1", + "sample_weight": 1 + } +} +``` + +The public ingest key is a routing identifier accepted by the Cacheplane ingest proxy. It is not a secret. diff --git a/apps/website/public/AGENTS.md b/apps/website/public/AGENTS.md index 267feb599..e84f1c29a 100644 --- a/apps/website/public/AGENTS.md +++ b/apps/website/public/AGENTS.md @@ -1,4 +1,4 @@ -# Angular Agent Framework v0.0.29 +# Angular Agent Framework v0.0.32 Agent UI primitives and LangGraph streaming adapters for Angular. @@ -37,7 +37,7 @@ export class ChatComponent { - Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` - Global config: `provideAgent({ apiUrl })` in app.config.ts - Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` — never mock `agent()` itself +- Testing: use `MockAgentTransport` - never mock `agent()` itself ## Version check If this file is stale, fetch the latest: https://cacheplane.ai/llms-full.txt diff --git a/apps/website/public/CLAUDE.md b/apps/website/public/CLAUDE.md index 267feb599..e84f1c29a 100644 --- a/apps/website/public/CLAUDE.md +++ b/apps/website/public/CLAUDE.md @@ -1,4 +1,4 @@ -# Angular Agent Framework v0.0.29 +# Angular Agent Framework v0.0.32 Agent UI primitives and LangGraph streaming adapters for Angular. @@ -37,7 +37,7 @@ export class ChatComponent { - Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` - Global config: `provideAgent({ apiUrl })` in app.config.ts - Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` — never mock `agent()` itself +- Testing: use `MockAgentTransport` - never mock `agent()` itself ## Version check If this file is stale, fetch the latest: https://cacheplane.ai/llms-full.txt diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index 5d41acff9..1a2a69f0e 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -1,4 +1,11 @@ -export type LibraryId = 'agent' | 'render' | 'chat' | 'ag-ui'; +export type LibraryId = + | 'agent' + | 'render' + | 'chat' + | 'ag-ui' + | 'a2ui' + | 'licensing' + | 'telemetry'; export interface DocsPage { title: string; @@ -57,6 +64,7 @@ export const docsConfig: DocsLibrary[] = [ id: 'concepts', color: 'red', pages: [ + { title: 'Agent Contract', slug: 'agent-contract', section: 'concepts' }, { title: 'Angular Signals', slug: 'angular-signals', section: 'concepts' }, { title: 'LangGraph Basics', slug: 'langgraph-basics', section: 'concepts' }, { title: 'Agent Architecture', slug: 'agent-architecture', section: 'concepts' }, @@ -103,6 +111,14 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Lifecycle Signals', slug: 'lifecycle', section: 'guides' }, ], }, + { + title: 'Concepts', + id: 'concepts', + color: 'red', + pages: [ + { title: 'JSON Render vs A2UI', slug: 'json-render-vs-a2ui', section: 'concepts' }, + ], + }, { title: 'A2UI', id: 'a2ui', @@ -157,6 +173,15 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Lifecycle Signals', slug: 'lifecycle', section: 'guides' }, ], }, + { + title: 'Concepts', + id: 'concepts', + color: 'red', + pages: [ + { title: 'Primitives vs Compositions', slug: 'primitives-vs-compositions', section: 'concepts' }, + { title: 'Message Model', slug: 'message-model', section: 'concepts' }, + ], + }, { title: 'Components', id: 'components', @@ -203,6 +228,121 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Installation', slug: 'installation', section: 'getting-started' }, ], }, + { + title: 'Concepts', + id: 'concepts', + color: 'red', + pages: [ + { title: 'Architecture', slug: 'architecture', section: 'concepts' }, + ], + }, + { + title: 'Guides', + id: 'guides', + color: 'blue', + pages: [ + { title: 'Fake Agent', slug: 'fake-agent', section: 'guides' }, + { title: 'Citations', slug: 'citations', section: 'guides' }, + { title: 'Troubleshooting', slug: 'troubleshooting', section: 'guides' }, + ], + }, + { + title: 'Reference', + id: 'reference', + color: 'blue', + pages: [ + { title: 'Event Mapping', slug: 'event-mapping', section: 'reference' }, + ], + }, + ], + }, + { + id: 'a2ui', + title: 'A2UI', + description: 'Protocol types and helpers for agent-driven UI surfaces', + sections: [ + { + title: 'Getting Started', + id: 'getting-started', + color: 'blue', + pages: [ + { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, + ], + }, + { + title: 'Reference', + id: 'reference', + color: 'blue', + pages: [ + { title: 'Schema', slug: 'schema', section: 'reference' }, + { title: 'Parser, Resolver, and Guards', slug: 'parser-resolver-guards', section: 'reference' }, + ], + }, + ], + }, + { + id: 'licensing', + title: 'Licensing', + description: 'License token verification and package check behavior', + sections: [ + { + title: 'Getting Started', + id: 'getting-started', + color: 'blue', + pages: [ + { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, + ], + }, + { + title: 'Guides', + id: 'guides', + color: 'blue', + pages: [ + { title: 'Setup', slug: 'setup', section: 'guides' }, + { title: 'CI and Offline', slug: 'ci-and-offline', section: 'guides' }, + ], + }, + { + title: 'Reference', + id: 'reference', + color: 'blue', + pages: [ + { title: 'API', slug: 'api', section: 'reference' }, + ], + }, + ], + }, + { + id: 'telemetry', + title: 'Telemetry', + description: 'Browser and Node telemetry setup, privacy controls, and events', + sections: [ + { + title: 'Getting Started', + id: 'getting-started', + color: 'blue', + pages: [ + { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, + ], + }, + { + title: 'Guides', + id: 'guides', + color: 'blue', + pages: [ + { title: 'Browser', slug: 'browser', section: 'guides' }, + { title: 'Node', slug: 'node', section: 'guides' }, + { title: 'Privacy and Opt-Out', slug: 'privacy-and-opt-out', section: 'guides' }, + ], + }, + { + title: 'Reference', + id: 'reference', + color: 'blue', + pages: [ + { title: 'Events', slug: 'events', section: 'reference' }, + ], + }, ], }, ]; diff --git a/apps/website/src/lib/docs.spec.ts b/apps/website/src/lib/docs.spec.ts index 8b292eec9..25d5782d9 100644 --- a/apps/website/src/lib/docs.spec.ts +++ b/apps/website/src/lib/docs.spec.ts @@ -10,6 +10,16 @@ describe('website docs bindings', () => { expect(slugs).toContainEqual({ library: 'agent', section: 'guides', slug: 'streaming' }); expect(slugs).toContainEqual({ library: 'render', section: 'getting-started', slug: 'introduction' }); expect(slugs).toContainEqual({ library: 'chat', section: 'getting-started', slug: 'introduction' }); + expect(slugs).toContainEqual({ library: 'ag-ui', section: 'concepts', slug: 'architecture' }); + expect(slugs).toContainEqual({ library: 'a2ui', section: 'getting-started', slug: 'introduction' }); + expect(slugs).toContainEqual({ library: 'licensing', section: 'guides', slug: 'setup' }); + expect(slugs).toContainEqual({ library: 'telemetry', section: 'guides', slug: 'privacy-and-opt-out' }); + }); + + it('loads every configured doc page', () => { + for (const { library, section, slug } of getAllDocSlugs()) { + expect(getDocBySlug(library, section, slug)).not.toBeNull(); + } }); it('loads a doc by library, section and slug', () => { diff --git a/cockpit/ag-ui/streaming/angular/prompts/streaming.md b/cockpit/ag-ui/streaming/angular/prompts/streaming.md index d8a931707..284a1e458 100644 --- a/cockpit/ag-ui/streaming/angular/prompts/streaming.md +++ b/cockpit/ag-ui/streaming/angular/prompts/streaming.md @@ -1,7 +1,7 @@ # AG-UI Streaming (Angular) -This capability demonstrates real-time token streaming from an AG-UI compatible agent using the `@ngaf/chat` Angular component library. The example shows how to wire the `AG_UI_AGENT` injection token (provided by `provideAgUiAgent`) into the `` host component and compose ``, ``, and `` to deliver a responsive, streaming chat experience. +This capability demonstrates real-time token streaming from an AG-UI compatible agent using the `@ngaf/chat` Angular component library. The example shows how to wire the `AG_UI_AGENT` injection token (provided by `provideAgUiAgent`) into the `` host component and compose ``, ``, and `` to deliver a responsive, streaming chat experience. -Key components used: ``, ``, ``, ``. The `provideAgUiAgent` provider handles SSE event processing from the AG-UI streaming endpoint, and the chat components subscribe reactively without any manual subscription management. +Key components used: ``, ``, ``, ``. The `provideAgUiAgent` provider handles SSE event processing from the AG-UI streaming endpoint, and the chat components subscribe reactively without any manual subscription management. -The demo illustrates the chat-runtime decoupling: the same `` composition works with any agent runtime — LangGraph, AG-UI, or others — by conforming to the runtime-neutral `Agent` contract from `@ngaf/chat`. +The demo illustrates the chat-runtime decoupling: the same `` composition works with any agent runtime - LangGraph, AG-UI, or others - by conforming to the runtime-neutral `Agent` contract from `@ngaf/chat`. diff --git a/cockpit/chat/a2ui/python/docs/guide.md b/cockpit/chat/a2ui/python/docs/guide.md index a77ed5152..b102f3947 100644 --- a/cockpit/chat/a2ui/python/docs/guide.md +++ b/cockpit/chat/a2ui/python/docs/guide.md @@ -3,7 +3,7 @@ Render agent-driven interactive UI using the A2UI (Agent-to-UI) protocol. The agent streams JSONL messages that build surfaces from the built-in -18-component catalog — no custom view components needed. +18-component catalog - no custom view components needed. @@ -25,7 +25,7 @@ import { agent } from '@ngaf/langgraph'; selector: 'app-a2ui', standalone: true, imports: [ChatComponent], - template: ``, + template: ``, }) export class A2uiComponent { protected readonly agentRef = agent({ @@ -36,7 +36,7 @@ export class A2uiComponent { } ``` -No event handler wiring needed — A2UI button events route back to the +No event handler wiring needed - A2UI button events route back to the agent automatically. @@ -53,9 +53,9 @@ newline-delimited JSON messages: ``` Three message types build a surface: -1. `createSurface` — initializes the surface -2. `updateDataModel` — sets the initial data model state -3. `updateComponents` — defines the component tree +1. `createSurface` - initializes the surface +2. `updateDataModel` - sets the initial data model state +3. `updateComponents` - defines the component tree @@ -79,7 +79,7 @@ Components bind to the data model using path references: ``` The `surfaceToSpec` function auto-detects path references and populates -`_bindings` for each input component — agents do not write `_bindings` +`_bindings` for each input component - agents do not write `_bindings` directly. When the user changes a bound input, the component emits a data model update event. diff --git a/cockpit/chat/debug/python/docs/guide.md b/cockpit/chat/debug/python/docs/guide.md index 0f20d4dce..fdefd85aa 100644 --- a/cockpit/chat/debug/python/docs/guide.md +++ b/cockpit/chat/debug/python/docs/guide.md @@ -28,7 +28,7 @@ import { ChatDebugComponent } from '@ngaf/chat'; Place the debug component in your template: ```html - + ``` This renders the full debug panel with timeline, state inspector, diff --git a/cockpit/chat/generative-ui/python/docs/guide.md b/cockpit/chat/generative-ui/python/docs/guide.md index c9d918e86..a391a01e7 100644 --- a/cockpit/chat/generative-ui/python/docs/guide.md +++ b/cockpit/chat/generative-ui/python/docs/guide.md @@ -40,14 +40,14 @@ const myViews = views({ Pass the view map to `ChatComponent` via the `[views]` input: ```html - + ``` Instruct the LLM to respond with raw JSON following the Spec schema. -No code fences or markdown — just valid JSON so the streaming pipeline +No code fences or markdown - just valid JSON so the streaming pipeline can detect and parse it incrementally. @@ -55,19 +55,19 @@ can detect and parse it incrementally. ## How Streaming Auto-Detection Works -1. **Token streaming** — The LLM streams response tokens to the client. -2. **ContentClassifier** — Inspects the incoming token buffer and detects +1. **Token streaming** - The LLM streams response tokens to the client. +2. **ContentClassifier** - Inspects the incoming token buffer and detects when the content is JSON rather than plain text or markdown. -3. **Partial JSON parser** — As JSON tokens arrive, a partial parser +3. **Partial JSON parser** - As JSON tokens arrive, a partial parser builds an incremental parse tree without waiting for the full payload. -4. **ParseTreeStore** — Materializes the partial parse tree into a live +4. **ParseTreeStore** - Materializes the partial parse tree into a live `Spec` object (elements map + root key) that updates on every chunk. -5. **Component rendering** — The `[views]` registry resolves each element +5. **Component rendering** - The `[views]` registry resolves each element type to an Angular component, which renders incrementally as the spec grows. Because detection and parsing happen on every streamed chunk, the user -sees UI components materialize progressively — cards appear and fill in +sees UI components materialize progressively - cards appear and fill in as the LLM generates the JSON structure. diff --git a/cockpit/chat/input/angular/src/app/input.component.ts b/cockpit/chat/input/angular/src/app/input.component.ts index e36f619a8..142f42d2c 100644 --- a/cockpit/chat/input/angular/src/app/input.component.ts +++ b/cockpit/chat/input/angular/src/app/input.component.ts @@ -25,7 +25,7 @@ import { environment } from '../environments/environment';
- +
diff --git a/cockpit/chat/input/python/docs/guide.md b/cockpit/chat/input/python/docs/guide.md index 8548d2cd2..b62364647 100644 --- a/cockpit/chat/input/python/docs/guide.md +++ b/cockpit/chat/input/python/docs/guide.md @@ -27,27 +27,27 @@ import { ChatInputComponent } from '@ngaf/chat'; Set a custom placeholder via the component input: ```html - + ``` ChatInputComponent supports Enter to send and Shift+Enter for newlines -out of the box. Listen for the `send` event: +out of the box. Listen for the `submitted` event: ```html - + ``` -The input automatically disables while the stream is active. Access -loading state via the agent ref: +The send action is disabled while the agent is loading. Access +loading state from the agent: ```typescript -protected readonly isLoading = computed(() => this.stream.status() === 'streaming'); +protected readonly isLoading = computed(() => this.agent.isLoading()); ``` @@ -56,10 +56,11 @@ protected readonly isLoading = computed(() => this.stream.status() === 'streamin Customize input appearance using CSS custom properties: ```css -chat-input { - --chat-input-bg: #1a1a2e; - --chat-input-border: #333; - --chat-input-text: #e0e0e0; +:root { + --ngaf-chat-surface: #1a1a2e; + --ngaf-chat-separator: #333; + --ngaf-chat-text: #e0e0e0; + --ngaf-chat-text-muted: #9ca3af; } ``` diff --git a/cockpit/chat/interrupts/python/docs/guide.md b/cockpit/chat/interrupts/python/docs/guide.md index acf526111..9ed5a28f6 100644 --- a/cockpit/chat/interrupts/python/docs/guide.md +++ b/cockpit/chat/interrupts/python/docs/guide.md @@ -43,7 +43,7 @@ protected readonly isInterrupted = computed( Use `ChatInterruptPanelComponent` to display the approval UI: ```html - + ``` The panel shows the interrupt payload, draft content, and action buttons. @@ -56,7 +56,7 @@ to resume or cancel the graph execution: ```html ``` diff --git a/cockpit/chat/messages/angular/src/app/messages.component.ts b/cockpit/chat/messages/angular/src/app/messages.component.ts index 59c2a4b69..516d7f0d0 100644 --- a/cockpit/chat/messages/angular/src/app/messages.component.ts +++ b/cockpit/chat/messages/angular/src/app/messages.component.ts @@ -12,7 +12,7 @@ import { environment } from '../environments/environment'; /** * MessagesComponent demonstrates the chat message primitives from @ngaf/chat. * - * Uses ChatMessagesComponent, ChatInputComponent, and ChatTypingIndicatorComponent + * Uses ChatMessageListComponent, ChatInputComponent, and ChatTypingIndicatorComponent * individually rather than the composed ChatComponent, giving full control * over layout and message rendering. */ @@ -31,7 +31,7 @@ import { environment } from '../environments/environment';
- +
diff --git a/cockpit/chat/messages/python/docs/guide.md b/cockpit/chat/messages/python/docs/guide.md index d8e59123b..66d39c366 100644 --- a/cockpit/chat/messages/python/docs/guide.md +++ b/cockpit/chat/messages/python/docs/guide.md @@ -1,14 +1,14 @@ # Chat Messages with @ngaf/chat -Render chat messages using the primitive components ChatMessagesComponent, +Render chat messages using the primitive components ChatMessageListComponent, ChatInputComponent, and ChatTypingIndicatorComponent. These building blocks give full control over message layout, input handling, and loading states. Build a chat interface using the individual message primitives from -`@ngaf/chat`. Import `ChatMessagesComponent`, `ChatInputComponent`, +`@ngaf/chat`. Import `ChatMessageListComponent`, `ChatInputComponent`, and `ChatTypingIndicatorComponent` separately instead of the composed `ChatComponent`. @@ -20,7 +20,7 @@ Import the individual chat primitives instead of the composed `ChatComponent`: ```typescript import { - ChatMessagesComponent, + ChatMessageListComponent, ChatInputComponent, ChatTypingIndicatorComponent, } from '@ngaf/chat'; @@ -29,10 +29,10 @@ import { -Use `ChatMessagesComponent` to display the conversation history: +Use `ChatMessageListComponent` to display the conversation history: ```html - + ``` The component renders human and AI messages with appropriate styling @@ -45,8 +45,8 @@ Place `ChatInputComponent` and `ChatTypingIndicatorComponent` below the messages: ```html - - + + ``` @@ -56,7 +56,7 @@ Create a `submitMessage()` method that sends user input to the stream: ```typescript submitMessage(content: string) { - this.stream.submit([{ role: 'human', content }]); + this.agent.submit({ message: content }); } ``` diff --git a/cockpit/chat/messages/python/prompts/messages.md b/cockpit/chat/messages/python/prompts/messages.md index 02977840f..e70cff93e 100644 --- a/cockpit/chat/messages/python/prompts/messages.md +++ b/cockpit/chat/messages/python/prompts/messages.md @@ -4,9 +4,9 @@ You are an assistant that demonstrates the chat message primitives from @ngaf/ch Your role is to showcase different message types and rendering styles. Use varied response formats including short answers, longer explanations, -bulleted lists, and code snippets to demonstrate how ChatMessagesComponent +bulleted lists, and code snippets to demonstrate how ChatMessageListComponent renders different content. -When greeting the user, explain that this demo showcases ChatMessagesComponent, +When greeting the user, explain that this demo showcases ChatMessageListComponent, ChatInputComponent, and ChatTypingIndicatorComponent working together as individual primitives rather than the composed ChatComponent. diff --git a/cockpit/chat/messages/python/src/graph.py b/cockpit/chat/messages/python/src/graph.py index 0b9f3df25..518225fcf 100644 --- a/cockpit/chat/messages/python/src/graph.py +++ b/cockpit/chat/messages/python/src/graph.py @@ -18,7 +18,7 @@ def build_messages_graph(): Constructs a conversational graph that demonstrates message rendering. The agent responds with various message styles to showcase - ChatMessagesComponent, ChatInputComponent, and ChatTypingIndicatorComponent. + ChatMessageListComponent, ChatInputComponent, and ChatTypingIndicatorComponent. """ llm = ChatOpenAI(model="gpt-5-mini", streaming=True) diff --git a/cockpit/chat/subagents/python/docs/guide.md b/cockpit/chat/subagents/python/docs/guide.md index f6620f4b7..53bd3a44a 100644 --- a/cockpit/chat/subagents/python/docs/guide.md +++ b/cockpit/chat/subagents/python/docs/guide.md @@ -43,7 +43,7 @@ protected readonly stream = agent({ Use `ChatSubagentsComponent` to display all active subagents: ```html - + ``` @@ -52,7 +52,9 @@ Use `ChatSubagentsComponent` to display all active subagents: Use `ChatSubagentCardComponent` for detailed views of each subagent: ```html - +@for (subagent of stream.subagents().values(); track subagent.toolCallId) { + +} ``` diff --git a/cockpit/chat/theming/angular/e2e/theming.spec.ts b/cockpit/chat/theming/angular/e2e/theming.spec.ts index 8e0cc5ab8..82c09e964 100644 --- a/cockpit/chat/theming/angular/e2e/theming.spec.ts +++ b/cockpit/chat/theming/angular/e2e/theming.spec.ts @@ -21,7 +21,7 @@ test.describe('Chat Theming Example', () => { }); test('displays CSS variable list', async ({ page }) => { - await expect(page.locator('aside')).toContainText('--chat-bg'); - await expect(page.locator('aside')).toContainText('--chat-accent'); + await expect(page.locator('aside')).toContainText('--ngaf-chat-bg'); + await expect(page.locator('aside')).toContainText('--ngaf-chat-accent'); }); }); diff --git a/cockpit/chat/theming/python/docs/guide.md b/cockpit/chat/theming/python/docs/guide.md index 36336b7a7..2dde0d4b9 100644 --- a/cockpit/chat/theming/python/docs/guide.md +++ b/cockpit/chat/theming/python/docs/guide.md @@ -1,38 +1,25 @@ # Chat Theming with @ngaf/chat -Customize chat appearance using CSS custom properties and -CHAT_THEME_STYLES. Create theme presets and build a theme picker -for runtime theme switching. +Customize chat appearance using `--ngaf-chat-*` CSS custom properties. Create theme presets and build a theme picker for runtime theme switching. -Add theming to your chat interface using CSS custom properties and -`CHAT_THEME_STYLES` from `@ngaf/chat`. Create theme presets -and a theme picker for switching themes at runtime. +Add theming to your chat interface using `--ngaf-chat-*` CSS custom properties. Create theme presets and a theme picker for switching themes at runtime. -Chat components use CSS custom properties for all visual styling: +Chat components use CSS custom properties for visual styling: ```css ---chat-bg: #171717; ---chat-text: #e0e0e0; ---chat-accent: #3b82f6; ---chat-surface: #222; ---chat-border: #333; ---chat-text-muted: #777; -``` - - - - -Use `CHAT_THEME_STYLES` to apply a complete theme: - -```typescript -import { CHAT_THEME_STYLES } from '@ngaf/chat'; +--ngaf-chat-bg: #171717; +--ngaf-chat-text: #e0e0e0; +--ngaf-chat-accent: #3b82f6; +--ngaf-chat-surface-alt: #222; +--ngaf-chat-separator: #333; +--ngaf-chat-text-muted: #777; ``` @@ -42,16 +29,25 @@ Define theme presets as objects mapping CSS custom properties: ```typescript const themes = { - dark: { '--chat-bg': '#171717', '--chat-text': '#e0e0e0' }, - light: { '--chat-bg': '#ffffff', '--chat-text': '#1a1a1a' }, - ocean: { '--chat-bg': '#0c1426', '--chat-text': '#c8d6e5' }, + dark: { + '--ngaf-chat-bg': '#171717', + '--ngaf-chat-text': '#e0e0e0', + }, + light: { + '--ngaf-chat-bg': '#ffffff', + '--ngaf-chat-text': '#1a1a1a', + }, + ocean: { + '--ngaf-chat-bg': '#0c1426', + '--ngaf-chat-text': '#c8d6e5', + }, }; ``` -Create buttons that swap CSS variables on the host element: +Create controls that swap CSS variables on the document root or a chat container: ```typescript setTheme(name: string) { @@ -63,13 +59,13 @@ setTheme(name: string) { ``` - + Override specific component styles without affecting the global theme: ```css chat-input { - --chat-input-bg: #1a1a2e; + --ngaf-chat-input-bg: #1a1a2e; } ``` @@ -77,6 +73,5 @@ chat-input { -CHAT_THEME_STYLES provides sensible defaults. Override only the -properties you need to change for your brand. +Chat components provide sensible defaults. Override only the `--ngaf-chat-*` properties you need to change for your brand. diff --git a/cockpit/chat/theming/python/prompts/theming.md b/cockpit/chat/theming/python/prompts/theming.md index 2503d981a..c706045d1 100644 --- a/cockpit/chat/theming/python/prompts/theming.md +++ b/cockpit/chat/theming/python/prompts/theming.md @@ -4,9 +4,9 @@ You are an assistant that demonstrates chat theming and CSS custom property customization in @ngaf/chat. The chat UI supports extensive theming via CSS custom properties like -`--chat-bg`, `--chat-text`, `--chat-accent`, `--chat-surface`, and more. -These can be swapped at runtime using CHAT_THEME_STYLES or by setting -CSS variables on a parent element. +`--ngaf-chat-bg`, `--ngaf-chat-text`, `--ngaf-chat-accent`, +`--ngaf-chat-surface-alt`, and more. These can be swapped at runtime by +setting CSS variables on a parent element. Explain the theming system when asked, and demonstrate how different themes change the appearance of the chat interface. The sidebar contains diff --git a/cockpit/chat/threads/python/docs/guide.md b/cockpit/chat/threads/python/docs/guide.md index 0294e9bd2..001a010b8 100644 --- a/cockpit/chat/threads/python/docs/guide.md +++ b/cockpit/chat/threads/python/docs/guide.md @@ -31,7 +31,7 @@ protected readonly stream = agent({ Use `ChatThreadListComponent` in a sidebar to show all threads: ```html - + ``` @@ -68,6 +68,6 @@ restores the thread list and conversation history. -Threads are ideal for keeping separate contexts — e.g., one thread +Threads are ideal for keeping separate contexts - e.g., one thread for code review and another for brainstorming. diff --git a/cockpit/chat/timeline/python/docs/guide.md b/cockpit/chat/timeline/python/docs/guide.md index 90804afc2..f46ea53e1 100644 --- a/cockpit/chat/timeline/python/docs/guide.md +++ b/cockpit/chat/timeline/python/docs/guide.md @@ -32,7 +32,7 @@ Use `ChatTimelineSliderComponent` to display a scrubber for navigating conversation checkpoints: ```html - + ``` @@ -48,7 +48,7 @@ Position the slider below the chat or in a sidebar for easy access: ```html ``` diff --git a/cockpit/chat/tool-calls/python/docs/guide.md b/cockpit/chat/tool-calls/python/docs/guide.md index eef7d8928..cb918933a 100644 --- a/cockpit/chat/tool-calls/python/docs/guide.md +++ b/cockpit/chat/tool-calls/python/docs/guide.md @@ -51,7 +51,7 @@ tool_node = ToolNode(tools) Use `ChatToolCallsComponent` to display active tool calls: ```html - + ``` @@ -61,13 +61,15 @@ Use `ChatToolCallCardComponent` for detailed tool call views showing the tool name, arguments, and result: ```html - +@for (toolCall of stream.toolCalls(); track toolCall.id) { + +} ``` -Tool calls execute in a loop — the agent generates a tool call, the tool +Tool calls execute in a loop - the agent generates a tool call, the tool node executes it, and the result feeds back into the agent for the next step. diff --git a/cockpit/langgraph/deployment-runtime/python/docs/guide.md b/cockpit/langgraph/deployment-runtime/python/docs/guide.md index f1853e4da..1a3d70f0c 100644 --- a/cockpit/langgraph/deployment-runtime/python/docs/guide.md +++ b/cockpit/langgraph/deployment-runtime/python/docs/guide.md @@ -91,7 +91,7 @@ export class DeploymentRuntimeComponent { currentThreadId = ''; send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); + void this.stream.submit({ message: text }); } } ``` diff --git a/cockpit/langgraph/durable-execution/python/docs/guide.md b/cockpit/langgraph/durable-execution/python/docs/guide.md index b0d7e5d08..647277449 100644 --- a/cockpit/langgraph/durable-execution/python/docs/guide.md +++ b/cockpit/langgraph/durable-execution/python/docs/guide.md @@ -47,7 +47,7 @@ export class DurableExecutionComponent { }); send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); + void this.stream.submit({ message: text }); } } ``` @@ -106,7 +106,7 @@ Render a "Retry" button when `stream.error()` is set. Call `stream.reload()` to - + The backend uses a three-node graph with `MemorySaver` checkpointing: diff --git a/cockpit/langgraph/memory/python/docs/guide.md b/cockpit/langgraph/memory/python/docs/guide.md index 23d716c27..7c62e8c83 100644 --- a/cockpit/langgraph/memory/python/docs/guide.md +++ b/cockpit/langgraph/memory/python/docs/guide.md @@ -100,7 +100,7 @@ Facts appear in the sidebar in real time as the agent learns them. Define a custom `MemoryState` that extends messages with a `memory` dict, then -wire two nodes — `generate` and `extract_memory` — in sequence: +wire two nodes - `generate` and `extract_memory` - in sequence: ```python # graph.py @@ -151,7 +151,7 @@ survives server restarts and scales across workers. The `memory` dict is part of graph state and is streamed back to the client on -every state update. There is no separate API call needed — just read `stream.value()`. +every state update. There is no separate API call needed - just read `stream.value()`. @@ -160,6 +160,6 @@ variables or a proxy. -- [Chat Messages](/chat/core-capabilities/messages/overview/python) — Learn how ChatMessagesComponent renders messages -- [Chat Threads](/chat/core-capabilities/threads/overview/python) — Learn how ChatThreadsComponent manages conversation threads +- [Chat Messages](/chat/core-capabilities/messages/overview/python) - Learn how ChatMessageListComponent renders messages +- [Chat Threads](/chat/core-capabilities/threads/overview/python) - Learn how ChatThreadsComponent manages conversation threads diff --git a/cockpit/langgraph/streaming/angular/prompts/streaming.md b/cockpit/langgraph/streaming/angular/prompts/streaming.md index 504ba93d6..3f14a2393 100644 --- a/cockpit/langgraph/streaming/angular/prompts/streaming.md +++ b/cockpit/langgraph/streaming/angular/prompts/streaming.md @@ -1,5 +1,5 @@ # LangGraph Streaming (Angular) -This capability demonstrates real-time token streaming from a LangGraph agent using the `@ngaf/chat` Angular component library. The example shows how to wire a `agent` ref into the `` host component and compose ``, ``, and `` to deliver a responsive, streaming chat experience. +This capability demonstrates real-time token streaming from a LangGraph agent using the `@ngaf/chat` Angular component library. The example shows how to wire an `agent` ref into the `` host component and compose ``, ``, and `` to deliver a responsive, streaming chat experience. -Key components used: ``, ``, ``, ``. The `agent` signal handles SSE fan-out from the LangGraph streaming endpoint, and the chat components subscribe reactively without any manual subscription management. +Key components used: ``, ``, ``, ``. The `agent` signal handles SSE fan-out from the LangGraph streaming endpoint, and the chat components subscribe reactively without any manual subscription management. diff --git a/cockpit/langgraph/streaming/python/docs/guide.md b/cockpit/langgraph/streaming/python/docs/guide.md index 11c196c3a..1e0c96a33 100644 --- a/cockpit/langgraph/streaming/python/docs/guide.md +++ b/cockpit/langgraph/streaming/python/docs/guide.md @@ -6,7 +6,7 @@ Build a real-time streaming chat interface using `agent()` from -Add real-time LLM streaming to this Angular component using `agent()` from `@ngaf/langgraph`. Configure `provideAgent({ apiUrl })` in the app config, then call `stream.submit()` to send messages. Bind `stream.messages()` in the template using `@for` — all Signals, no subscriptions needed. +Add real-time LLM streaming to this Angular component using `agent()` from `@ngaf/langgraph`. Configure `provideAgent({ apiUrl })` in the app config, then call `stream.submit()` to send messages. Bind `stream.messages()` in the template using `@for` - all Signals, no subscriptions needed. @@ -47,7 +47,7 @@ export class StreamingComponent { ``` -`agent()` must be called within an Angular injection context — a component field initializer or constructor body. +`agent()` must be called within an Angular injection context - a component field initializer or constructor body. @@ -63,7 +63,7 @@ Use Angular's control flow to render messages reactively: } ``` -The template re-renders automatically as tokens arrive — no manual subscriptions or change detection needed. +The template re-renders automatically as tokens arrive - no manual subscriptions or change detection needed. @@ -76,9 +76,7 @@ send(): void { const text = this.prompt().trim(); if (!text || this.stream.isLoading()) return; this.prompt.set(''); - this.stream.submit({ - messages: [{ role: 'human', content: text }], - }); + void this.stream.submit({ message: text }); } ``` @@ -114,7 +112,7 @@ Deploy with `langgraph deploy` from `langgraph-cli`. The `assistantId` in your A -No service layer needed — `agent()` replaces wrapper services entirely. It handles connection lifecycle, state management, and error recovery. +No service layer needed - `agent()` replaces wrapper services entirely. It handles connection lifecycle, state management, and error recovery. @@ -122,6 +120,6 @@ Never expose your LangSmith API key in client-side code. Use server-side environ -- [Chat Messages](/chat/core-capabilities/messages/overview/python) — Learn how ChatMessagesComponent renders messages -- [Chat Input](/chat/core-capabilities/input/overview/python) — Explore ChatInputComponent for message submission +- [Chat Messages](/chat/core-capabilities/messages/overview/python) - Learn how ChatMessageListComponent renders messages +- [Chat Input](/chat/core-capabilities/input/overview/python) - Explore ChatInputComponent for message submission diff --git a/cockpit/langgraph/time-travel/python/docs/guide.md b/cockpit/langgraph/time-travel/python/docs/guide.md index 542343e21..3b79380d7 100644 --- a/cockpit/langgraph/time-travel/python/docs/guide.md +++ b/cockpit/langgraph/time-travel/python/docs/guide.md @@ -34,7 +34,7 @@ export const appConfig: ApplicationConfig = { In your component, call `agent()`. The history and branch signals are -available automatically — no extra config needed: +available automatically - no extra config needed: ```typescript // time-travel.component.ts @@ -85,7 +85,7 @@ selectCheckpoint(state: { checkpoint_id?: string }): void { } send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); + void this.stream.submit({ message: text }); } ``` @@ -98,7 +98,7 @@ The original timeline remains accessible in the history sidebar. The backend uses `MemorySaver` to persist checkpoint history. Time travel is a -client-side feature — the graph itself requires only the checkpointer: +client-side feature - the graph itself requires only the checkpointer: ```python # graph.py @@ -141,6 +141,6 @@ submitting does not modify the thread state. -- [Chat Timeline](/chat/core-capabilities/timeline/overview/python) — Explore ChatTimelineComponent for visualizing thread history -- [Chat Debug](/chat/core-capabilities/debug/overview/python) — Learn how ChatDebugComponent aids in debugging agent behavior +- [Chat Timeline](/chat/core-capabilities/timeline/overview/python) - Explore ChatTimelineComponent for visualizing thread history +- [Chat Debug](/chat/core-capabilities/debug/overview/python) - Learn how ChatDebugComponent aids in debugging agent behavior