diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bd67fbc87..40568a2b5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,7 +35,8 @@ jobs:
- run: npm ci
- run: npx nx lint website
- run: npm run generate-api-docs
- # nx build website triggers demo:build first (dependsOn in project.json)
+ - name: Verify generated API docs are committed
+ run: git diff --exit-code -- apps/website/content/docs/*/api/api-docs.json
- run: npx nx build website
cockpit:
diff --git a/README.md b/README.md
index 9d96556ec..d622f51f0 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
- The Angular Agent Framework for LangChain
+ Agent UI primitives and LangGraph streaming adapters for Angular
@@ -27,14 +27,14 @@
---
-`agent()` is the Angular equivalent of LangGraph's React `useStream()` hook, built on Angular Signals and the Angular Resource API. It gives enterprise Angular teams production-grade streaming primitives for LangChain. Drop it into any Angular 20+ component, point it at your LangGraph Platform endpoint, and get reactive, signal-driven access to streaming state, messages, tool calls, interrupts, and thread history.
+`agent()` is the Angular equivalent of LangGraph's React `useStream()` hook, projected into a runtime-neutral `Agent` contract consumed by `@ngaf/chat`. Drop it into any Angular 20+ component, point it at your LangGraph Platform endpoint, and get signal-driven access to messages, status, tool calls, interrupts, subagents, regenerate, and thread history.
---
## Install
```bash
-npm install @ngaf/langgraph
+npm install @ngaf/langgraph @ngaf/chat
```
**Peer dependencies:** `@angular/core ^20.0.0 || ^21.0.0`, `@langchain/core ^1.1.0`, `@langchain/langgraph-sdk ^1.7.0`, `rxjs ~7.8.0`
@@ -45,17 +45,14 @@ npm install @ngaf/langgraph
```typescript
import { Component } from '@angular/core';
+import { ChatComponent as NgafChatComponent } from '@ngaf/chat';
import { agent } from '@ngaf/langgraph';
-import type { BaseMessage } from '@langchain/core/messages';
@Component({
- selector: 'app-chat',
+ selector: 'app-support-chat',
+ imports: [NgafChatComponent],
template: `
-
- @for (msg of chat.messages(); track $index) {
-
{{ msg.content }}
- }
-
+
@if (chat.isLoading()) {
Streaming…
@@ -64,20 +61,19 @@ import type { BaseMessage } from '@langchain/core/messages';
`,
})
-export class ChatComponent {
- chat = agent<{ messages: BaseMessage[] }>({
+export class SupportChatComponent {
+ chat = agent({
apiUrl: 'https://your-langgraph-platform.com',
assistantId: 'my-agent',
- messagesKey: 'messages',
});
send() {
- this.chat.submit({ messages: [{ role: 'human', content: 'Hello' }] });
+ void this.chat.submit({ message: 'Hello' });
}
}
```
-That's it. `chat.messages()` is an Angular Signal. Bind it directly in your template — no subscriptions, no `async` pipe, no zone.js required.
+That's it. `chat.messages()` and `chat.status()` are Angular Signals. Bind them directly in your template — no subscriptions, no `async` pipe, no zone.js required.
---
@@ -89,7 +85,7 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
| Messages signal | `messages()` | `messages` |
| Loading state | `isLoading()` | `isLoading` |
| Error state | `error()` | — |
-| Resource status (idle/loading/resolved/error) | `status()` — full `ResourceStatus` | partial |
+| Runtime-neutral status | `status()` — `'idle' \| 'running' \| 'error'` | partial |
| Interrupt / human-in-the-loop | `interrupt()` / `interrupts()` | `interrupt` / `interrupts` |
| Tool call progress | `toolProgress()` | `toolProgress` |
| Tool calls with results | `toolCalls()` | `toolCalls` |
@@ -99,8 +95,9 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
| Reactive thread switching | `Signal` input | prop |
| Submit | `submit(values, opts?)` | `submit(values, opts?)` |
| Stop | `stop()` | `stop()` |
+| Regenerate response | `regenerate(assistantMessageIndex)` | — |
| Reload last submission | `reload()` | — |
-| Custom transport (for testing) | `MockStreamTransport` | mock fetch |
+| Custom transport (for testing) | `MockAgentTransport` | mock fetch |
| Angular `ResourceRef` compatibility | Full duck-type parity | N/A |
| Angular 20+ Signals API | Native | N/A |
| SSR / Server Components | Client-side only | React Server Components (React) |
@@ -123,11 +120,11 @@ That's it. `chat.messages()` is an Angular Signal. Bind it directly in your temp
## Documentation
-- [Getting Started](https://cacheplane.ai/docs/getting-started)
-- [API Reference](https://cacheplane.ai/api-reference)
-- [Testing with MockStreamTransport](https://cacheplane.ai/docs/testing)
-- [Human-in-the-Loop / Interrupts](https://cacheplane.ai/docs/interrupts)
-- [Subagent Streaming](https://cacheplane.ai/docs/subagents)
+- [Agent Quickstart](https://cacheplane.ai/docs/agent/getting-started/quickstart)
+- [agent() API](https://cacheplane.ai/docs/agent/api/agent)
+- [Chat Introduction](https://cacheplane.ai/docs/chat/getting-started/introduction)
+- [Human-in-the-Loop / Interrupts](https://cacheplane.ai/docs/agent/guides/interrupts)
+- [Subgraph and Subagent Streaming](https://cacheplane.ai/docs/agent/guides/subgraphs)
---
diff --git a/apps/website/content/docs/ag-ui/api/api-docs.json b/apps/website/content/docs/ag-ui/api/api-docs.json
index 2e17267c0..114894dba 100644
--- a/apps/website/content/docs/ag-ui/api/api-docs.json
+++ b/apps/website/content/docs/ag-ui/api/api-docs.json
@@ -407,6 +407,12 @@
"description": "Milliseconds between successive token emissions.",
"optional": true
},
+ {
+ "name": "reasoningTokens",
+ "type": "string[]",
+ "description": "Optional reasoning chunks emitted before the text reply.",
+ "optional": true
+ },
{
"name": "tokens",
"type": "string[]",
@@ -416,6 +422,31 @@
],
"examples": []
},
+ {
+ "name": "bridgeCitationsState",
+ "kind": "function",
+ "description": "",
+ "signature": "bridgeCitationsState(thread: ThreadStateLike, messages: Message[]): Message[]",
+ "params": [
+ {
+ "name": "thread",
+ "type": "ThreadStateLike",
+ "description": "",
+ "optional": false
+ },
+ {
+ "name": "messages",
+ "type": "Message[]",
+ "description": "",
+ "optional": false
+ }
+ ],
+ "returns": {
+ "type": "Message[]",
+ "description": ""
+ },
+ "examples": []
+ },
{
"name": "injectAgUiAgent",
"kind": "function",
diff --git a/apps/website/content/docs/agent/api/agent.mdx b/apps/website/content/docs/agent/api/agent.mdx
index e388e3341..4bf7a64c5 100644
--- a/apps/website/content/docs/agent/api/agent.mdx
+++ b/apps/website/content/docs/agent/api/agent.mdx
@@ -1,46 +1,88 @@
# agent()
-`agent` is the core primitive of the library. It creates a reactive resource that opens a server-sent event stream, tracks loading and error states, and exposes the latest emitted value — all within Angular's signal-based reactivity model.
+`agent()` is the LangGraph adapter for Angular. It connects to a LangGraph Platform assistant, consumes the LangGraph SDK event stream, and projects the result into the runtime-neutral `Agent` contract used by `@ngaf/chat`.
-When the `url` signal changes, the resource tears down the previous connection and opens a fresh one automatically. You never write subscription management or cleanup logic yourself.
+Call it in an Angular injection context, usually as a component field initializer. The returned object exposes Angular Signals for UI state and async methods for user actions.
```ts
import { agent } from '@ngaf/langgraph';
-// Inside a component or service with injection context
-const repo = agent({
- url: () => `/api/repos/${this.repoId()}`,
- transport: inject(FetchStreamTransport),
+readonly chat = agent({
+ apiUrl: 'http://localhost:2024',
+ assistantId: 'my-agent',
+ threadId: this.threadId,
+ onThreadId: (id) => localStorage.setItem('threadId', id),
});
-// Use in template
-// repo.value() — latest emitted value (or undefined)
-// repo.status() — 'idle' | 'loading' | 'streaming' | 'error'
-// repo.error() — the thrown error, when status is 'error'
-// repo.interrupt() — call to cancel the stream immediately
+await this.chat.submit({ message: 'Hello' });
```
-## Key signals
+## Runtime-neutral surface
-| Signal | Type | Description |
+These fields are stable across runtime adapters and are what chat components consume.
+
+| Field | Type | Description |
|--------|------|-------------|
-| `value()` | `T \| undefined` | The latest value emitted by the stream. Starts as `undefined` and updates with each SSE event. |
-| `status()` | `'idle' \| 'loading' \| 'streaming' \| 'error'` | Lifecycle state of the current connection. |
-| `error()` | `unknown` | The error thrown when `status()` is `'error'`. `undefined` otherwise. |
-| `interrupt()` | `() => void` | Closes the active stream without an error — useful for user-initiated cancellation. |
+| `messages()` | `Message[]` | Runtime-neutral chat messages with `role`, `content`, optional `toolCallId`, citations, reasoning, and raw extras. |
+| `status()` | `'idle' \| 'running' \| 'error'` | UI lifecycle status. LangGraph loading and reloading both map to `running`. |
+| `isLoading()` | `boolean` | Convenience signal for active streaming. |
+| `error()` | `unknown` | Latest runtime error, when present. |
+| `toolCalls()` | `ToolCall[]` | Tool calls projected into the chat contract. |
+| `state()` | `Record` | Latest state values projected as a plain object. |
+| `submit(input, opts?)` | `Promise` | Submit a user message, resume payload, and/or state patch. |
+| `stop()` | `Promise` | Stop the active run. |
+| `regenerate(index)` | `Promise` | Remove the assistant message at `index` and all following messages, then rerun from the preceding user message. |
+| `interrupt()` | `AgentInterrupt \| undefined` | Optional current interrupt, when the backend pauses for human input. |
+| `subagents()` | `Map` | Optional subagent streams keyed by tool-call id. |
+
+## LangGraph-specific surface
+
+`agent()` also returns LangGraph-specific signals and helpers for apps that need the raw platform model:
+
+- `value()` and `hasValue()` for raw state values.
+- `langGraphMessages()`, `langGraphToolCalls()`, and `langGraphHistory()` for raw SDK-shaped data.
+- `history()`, `branch()`, `setBranch()`, and `experimentalBranchTree()` for checkpoint and time-travel UIs.
+- `queue()`, `joinStream()`, and `switchThread()` for persisted threads and queued runs.
+- `getSubagent()`, `getSubagentsByType()`, and `getSubagentsByMessage()` for subgraph/subagent inspection.
+
+## Submit and resume
+
+Use the runtime-neutral submit shape for normal chat input:
+
+```ts
+await chat.submit({ message: 'Explain streaming in Angular' });
+```
+
+Resume an interrupt with `resume`; combine it with `state` when the resume action also needs to update graph state:
-## When to use
+```ts
+await chat.submit({
+ resume: { approved: true },
+ state: { reviewer: 'Ada' },
+});
+```
+
+LangGraph run options are still accepted as the second argument:
+
+```ts
+await chat.submit(
+ { message: 'Queue this run' },
+ { multitaskStrategy: 'enqueue' },
+);
+```
+
+## Regenerate semantics
-Use `agent` whenever your UI needs to react to a live data stream from the server:
+`regenerate(assistantMessageIndex)` has replace semantics. It keeps the user message before the selected assistant message, removes the selected assistant message and every later message, synchronizes that rollback with LangGraph state when possible, then reruns with no new user message appended.
-- **AI / LLM responses** — stream tokens into a chat bubble as they arrive
-- **Live feeds** — stock tickers, activity logs, or progress updates
-- **Long-running jobs** — subscribe to backend task progress over SSE
+```ts
+await chat.regenerate(3);
+```
-For plain HTTP requests that return a single value and complete, Angular's built-in `resource()` or `httpResource()` is a better fit.
+The method throws when the selected index is not an assistant message, when no preceding user message exists, or while another response is already loading.
- `agent` must be called during construction, inside an injection
+ `agent()` must be called during construction, inside an injection
context (e.g. a component constructor, field initializer, or a function
passed to `runInInjectionContext`). Calling it outside an injection context
will throw.
@@ -68,7 +110,7 @@ For plain HTTP requests that return a single value and complete, Angular's built
icon="bolt"
href="/docs/agent/concepts/angular-signals"
>
- Understand how angular integrates with Angular's reactivity model.
+ Understand how `agent()` integrates with Angular's reactivity model.
diff --git a/apps/website/content/docs/agent/api/api-docs.json b/apps/website/content/docs/agent/api/api-docs.json
index f026a599e..7974a5992 100644
--- a/apps/website/content/docs/agent/api/api-docs.json
+++ b/apps/website/content/docs/agent/api/api-docs.json
@@ -170,6 +170,31 @@
"optional": true
}
]
+ },
+ {
+ "name": "updateState",
+ "signature": "updateState(threadId: string, values: Record, _signal: AbortSignal)",
+ "description": "Update server-side thread state, e.g. to remove messages for regenerate rollback.",
+ "params": [
+ {
+ "name": "threadId",
+ "type": "string",
+ "description": "",
+ "optional": false
+ },
+ {
+ "name": "values",
+ "type": "Record",
+ "description": "",
+ "optional": false
+ },
+ {
+ "name": "_signal",
+ "type": "AbortSignal",
+ "description": "",
+ "optional": false
+ }
+ ]
}
]
},
@@ -529,7 +554,7 @@
{
"name": "AgentOptions",
"kind": "interface",
- "description": "Options for creating a streaming resource via agent.",
+ "description": "Options for creating a LangGraph-backed agent via agent.",
"properties": [
{
"name": "apiUrl",
@@ -555,12 +580,6 @@
"description": "Initial state values before the first stream response arrives.",
"optional": true
},
- {
- "name": "messagesKey",
- "type": "string",
- "description": "Key in the state object that contains the messages array. Defaults to `'messages'`.",
- "optional": true
- },
{
"name": "onThreadId",
"type": "object",
@@ -704,6 +723,12 @@
"type": "unknown",
"description": "",
"optional": false
+ },
+ {
+ "name": "updateState",
+ "type": "unknown",
+ "description": "",
+ "optional": true
}
],
"examples": []
@@ -909,6 +934,12 @@
"description": "Pending server-side runs created via `multitaskStrategy: 'enqueue'`.",
"optional": false
},
+ {
+ "name": "regenerate",
+ "type": "object",
+ "description": "Discards the assistant message at the given index AND all messages after\nit, then re-runs the agent against the trimmed conversation tail. The\npreceding user message (at index - 1) is preserved and re-submitted as\nthe agent's input. No new user message is added to the history.\n\nThrows if the message at `index` is not 'assistant' role, or if the\nagent is currently loading another response.",
+ "optional": false
+ },
{
"name": "reload",
"type": "object",
@@ -1267,6 +1298,12 @@
"description": "Pending server-side runs created via `multitaskStrategy: 'enqueue'`.",
"optional": false
},
+ {
+ "name": "regenerate",
+ "type": "object",
+ "description": "Discards the assistant message at the given index AND all messages after\nit, then re-runs the agent against the trimmed conversation tail. The\npreceding user message (at index - 1) is preserved and re-submitted as\nthe agent's input. No new user message is added to the history.\n\nThrows if the message at `index` is not 'assistant' role, or if the\nagent is currently loading another response.",
+ "optional": false
+ },
{
"name": "reload",
"type": "object",
@@ -1609,13 +1646,13 @@
{
"name": "agent",
"kind": "function",
- "description": "Creates a streaming resource connected to a LangGraph agent.\n\nMust be called within an Angular injection context (component constructor,\nfield initializer, or `runInInjectionContext`). Returns a unified\nLangGraphAgent whose properties are Angular Signals that update\nin real-time as the agent streams.",
+ "description": "Creates a LangGraph-backed Angular agent.\n\nMust be called within an Angular injection context (component constructor,\nfield initializer, or `runInInjectionContext`). Returns a unified\nLangGraphAgent whose properties are Angular Signals that update\nin real time as LangGraph streams messages, values, tool calls, interrupts,\nsubagent state, and checkpoint history.",
"signature": "agent(options: AgentOptions>): LangGraphAgent>",
"params": [
{
"name": "options",
"type": "AgentOptions>",
- "description": "Configuration for the streaming resource",
+ "description": "Configuration for the LangGraph agent",
"optional": false
}
],
@@ -1627,6 +1664,25 @@
"```typescript\n// In a component field initializer\nconst chat = agent<{ messages: BaseMessage[] }>({\n assistantId: 'chat_agent',\n apiUrl: 'http://localhost:2024',\n threadId: signal(this.savedThreadId),\n onThreadId: (id) => localStorage.setItem('threadId', id),\n});\n\n// Access signals in template\n// chat.messages(), chat.status(), chat.error()\n```"
]
},
+ {
+ "name": "extractCitations",
+ "kind": "function",
+ "description": "",
+ "signature": "extractCitations(msg: KwargsLike): Citation[] | undefined",
+ "params": [
+ {
+ "name": "msg",
+ "type": "KwargsLike",
+ "description": "",
+ "optional": false
+ }
+ ],
+ "returns": {
+ "type": "Citation[] | undefined",
+ "description": ""
+ },
+ "examples": []
+ },
{
"name": "mockLangGraphAgent",
"kind": "function",
diff --git a/apps/website/content/docs/agent/api/fetch-stream-transport.mdx b/apps/website/content/docs/agent/api/fetch-stream-transport.mdx
index be5639995..52c418f60 100644
--- a/apps/website/content/docs/agent/api/fetch-stream-transport.mdx
+++ b/apps/website/content/docs/agent/api/fetch-stream-transport.mdx
@@ -1,39 +1,46 @@
# FetchStreamTransport
-`FetchStreamTransport` is the production-ready transport that opens a real server-sent event connection using the browser's `fetch` API and reads a `ReadableStream` response body. It is the default transport you register with `provideAgent` in production builds.
+`FetchStreamTransport` is the production transport for LangGraph Platform. It wraps the LangGraph SDK client, creates threads when needed, starts streaming runs, joins queued runs, cancels runs, loads history, and updates thread state for operations such as regenerate rollback.
## When you interact with it directly
-In most apps you will never import or inject `FetchStreamTransport` by name — you register it once in `provideAgent` and forget about it. The two cases where you reach for it explicitly are:
+In most apps you never instantiate `FetchStreamTransport` directly. When no custom transport is configured, `agent()` creates one from the resolved `apiUrl` and `onThreadId` callback.
-1. **Per-resource override** — you want one resource to use a different transport than the global default while everything else stays on `FetchStreamTransport`.
-2. **Outside the DI tree** — you are constructing a resource in a context where global providers are not available and you need to supply the transport manually.
+You reach for it explicitly when you want to share a transport instance, inspect behavior in tests, or implement a custom transport by matching the same `AgentTransport` interface.
```ts
-import { inject } from '@angular/core';
import { agent, FetchStreamTransport } from '@ngaf/langgraph';
-// Override transport for a single resource
-const events = agent({
- url: () => '/api/events',
- transport: inject(FetchStreamTransport),
+const transport = new FetchStreamTransport(
+ 'http://localhost:2024',
+ (threadId) => localStorage.setItem('threadId', threadId),
+);
+
+const chat = agent({
+ apiUrl: 'http://localhost:2024',
+ assistantId: 'support-agent',
+ transport,
});
```
## How it works
-`FetchStreamTransport` makes a `fetch` call to the given URL and expects the server to respond with `Content-Type: text/event-stream`. It then reads the `ReadableStream` body line-by-line, parses SSE `data:` fields, and emits each parsed JSON value into the resource signal.
+`FetchStreamTransport` creates a LangGraph SDK `Client` for the configured API URL. On `stream()`, it creates a thread when `threadId` is missing, calls `client.runs.stream(...)`, normalizes SDK events into `StreamEvent` objects, and yields them to the stream manager.
The transport handles:
-- **Backpressure** — reads chunks at the pace the browser delivers them
-- **Cancellation** — aborts the underlying `fetch` when `interrupt()` is called or the resource is destroyed
-- **Error propagation** — network errors and non-2xx responses surface through `resource.error()`
+- **Thread creation** — calls `onThreadId` when the SDK creates a new thread.
+- **Streaming modes** — defaults to `values`, `messages-tuple`, `updates`, and `custom`.
+- **Cancellation** — forwards `AbortSignal` to LangGraph run APIs.
+- **Queued runs** — supports create, cancel, and join flows when the backend supports them.
+- **History and state update** — loads checkpoint history and calls `threads.updateState()` for regenerate rollback.
+- **Error propagation** — SDK and network errors surface through `agent.error()`.
- `FetchStreamTransport` implements the `StreamTransport` interface. You can
- create custom transports (e.g. WebSocket-backed) by implementing the same
- interface and providing them in place of this class.
+ `FetchStreamTransport` implements `AgentTransport`. Custom transports must
+ implement `stream(assistantId, threadId, payload, signal, options?)`; optional
+ methods such as `joinStream`, `createQueuedRun`, `cancelRun`, `getHistory`,
+ and `updateState` enable advanced LangGraph features.
## What's Next
diff --git a/apps/website/content/docs/agent/api/mock-stream-transport.mdx b/apps/website/content/docs/agent/api/mock-stream-transport.mdx
index 69ae82238..078fb8da3 100644
--- a/apps/website/content/docs/agent/api/mock-stream-transport.mdx
+++ b/apps/website/content/docs/agent/api/mock-stream-transport.mdx
@@ -1,13 +1,13 @@
# MockAgentTransport
-`MockAgentTransport` is a test-friendly transport that replaces real network calls with an in-memory event emitter. Use it in unit and component tests to push values on demand and assert against your component's reactive state without a running server.
+`MockAgentTransport` is a deterministic `AgentTransport` implementation for tests. It records stream calls, lets tests emit LangGraph stream events manually, and avoids a running LangGraph server.
## Complete test example
-The pattern below covers the full lifecycle: configure the transport in `TestBed`, create the component, emit values, and assert signal state.
+The pattern below covers the full lifecycle: provide a transport instance, create the component, emit stream events, and assert signal state.
```ts
-import { Component, inject } from '@angular/core';
+import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
provideAgent,
@@ -16,44 +16,59 @@ import {
} from '@ngaf/langgraph';
@Component({ template: '' })
-class RepoComponent {
- readonly repo = agent<{ name: string }>({
- url: () => '/api/repos/42',
+class ChatComponent {
+ readonly chat = agent({
+ apiUrl: '',
+ assistantId: 'test-agent',
+ threadId: 'thread-1',
});
}
-describe('RepoComponent', () => {
+describe('ChatComponent', () => {
let transport: MockAgentTransport;
beforeEach(() => {
+ transport = new MockAgentTransport();
TestBed.configureTestingModule({
- imports: [RepoComponent],
- providers: [provideAgent({ transport: MockAgentTransport })],
+ imports: [ChatComponent],
+ providers: [provideAgent({ apiUrl: '', transport })],
});
- transport = TestBed.inject(MockAgentTransport);
});
- it('reflects the streamed value', () => {
- const fixture = TestBed.createComponent(RepoComponent);
+ it('reflects streamed messages', async () => {
+ const fixture = TestBed.createComponent(ChatComponent);
fixture.detectChanges();
- // Push a value into the stream — synchronous, no fakeAsync needed
- transport.emit('/api/repos/42', { name: 'my-repo' });
+ const stream = fixture.componentInstance.chat.submit({ message: 'Hello' });
+ transport.emit([
+ {
+ type: 'values',
+ values: {
+ messages: [{ type: 'ai', id: 'a1', content: 'Hi there' }],
+ },
+ },
+ ]);
+ transport.close();
+ await stream;
fixture.detectChanges();
- expect(fixture.componentInstance.repo.value()).toEqual({ name: 'my-repo' });
- expect(fixture.componentInstance.repo.status()).toBe('streaming');
+ expect(fixture.componentInstance.chat.messages()).toEqual([
+ expect.objectContaining({ role: 'assistant', content: 'Hi there' }),
+ ]);
+ expect(fixture.componentInstance.chat.status()).toBe('idle');
});
- it('surfaces errors through the error signal', () => {
- const fixture = TestBed.createComponent(RepoComponent);
+ it('surfaces errors through the error signal', async () => {
+ const fixture = TestBed.createComponent(ChatComponent);
fixture.detectChanges();
- transport.error('/api/repos/42', new Error('not found'));
+ const stream = fixture.componentInstance.chat.submit({ message: 'Hello' });
+ transport.emitError(new Error('not found'));
+ await expect(stream).rejects.toThrow('not found');
fixture.detectChanges();
- expect(fixture.componentInstance.repo.status()).toBe('error');
- expect(fixture.componentInstance.repo.error()).toBeInstanceOf(Error);
+ expect(fixture.componentInstance.chat.status()).toBe('error');
+ expect(fixture.componentInstance.chat.error()).toBeInstanceOf(Error);
});
});
```
@@ -62,14 +77,18 @@ describe('RepoComponent', () => {
| Method | Description |
|--------|-------------|
-| `emit(url, value)` | Pushes a single value into the stream at the given URL path. |
-| `error(url, err)` | Triggers an error on the stream at the given URL path. |
-| `complete(url)` | Closes the stream cleanly, as if the server sent the final event. |
+| `constructor(script?)` | Optionally seeds scripted event batches for manual stepping. |
+| `nextBatch()` | Returns the next scripted event batch. |
+| `emit(events)` | Pushes one or more `StreamEvent` objects into the active stream. |
+| `emitError(err)` | Causes the active stream to reject with `err`. |
+| `close()` | Closes the active stream after queued events drain. |
+| `isStreaming()` | Returns whether a stream is currently active. |
+
+The transport also records calls in `streams`, `createdQueuedRuns`, `cancelledRuns`, `joinedRuns`, and `historyCalls`, which is useful for asserting payloads and options.
- Because `MockAgentTransport` is synchronous by default, you can emit values
- and assert state changes in the same test tick — no `fakeAsync` or `tick`
- required.
+ `MockAgentTransport` emits only when your test calls `emit()`, `emitError()`,
+ or `close()`. This makes stream state and payload assertions deterministic.
## What's Next
@@ -92,7 +111,7 @@ describe('RepoComponent', () => {
Full reference for the primitive you are testing against.
diff --git a/apps/website/content/docs/agent/api/provide-agent.mdx b/apps/website/content/docs/agent/api/provide-agent.mdx
index c632fc6ad..78bba7b34 100644
--- a/apps/website/content/docs/agent/api/provide-agent.mdx
+++ b/apps/website/content/docs/agent/api/provide-agent.mdx
@@ -1,21 +1,25 @@
# provideAgent()
-`provideAgent` is the provider factory that registers `angular` in Angular's dependency injection system. Call it once inside `bootstrapApplication` (or an `ApplicationConfig`) to configure the transport and any global defaults used by every `agent` in your app.
+`provideAgent()` registers global defaults for every `agent()` call in an Angular application. Call it once in `bootstrapApplication` or an `ApplicationConfig` when multiple agents share the same LangGraph API URL, transport, or license token.
-This is the single configuration point for the entire library. Rather than configuring each resource individually, you declare your transport here and every `agent` call throughout the app inherits it automatically.
+Per-call options still win over provider defaults, so you can configure the common case globally and override individual agents when needed.
```ts
import { bootstrapApplication } from '@angular/platform-browser';
import {
provideAgent,
- FetchStreamTransport,
+ MockAgentTransport,
} from '@ngaf/langgraph';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideAgent({
- transport: FetchStreamTransport,
+ apiUrl: 'http://localhost:2024',
+ license: environment.ngafLicense,
+ transport: environment.testMode
+ ? new MockAgentTransport()
+ : undefined,
}),
],
});
@@ -25,24 +29,48 @@ bootstrapApplication(AppComponent, {
| Option | Type | Description |
|--------|------|-------------|
-| `transport` | `Type` | The transport class to inject when resources request `StreamTransport`. Required. |
+| `apiUrl` | `string` | Default LangGraph Platform API base URL. Individual `agent()` calls may override it. |
+| `transport` | `AgentTransport` | Optional transport instance. Defaults to `FetchStreamTransport` when omitted. |
+| `license` | `string` | Optional signed license token. Development and noncommercial use can omit it. |
-## Swapping transports by environment
+## Defaults and overrides
-Because `provideAgent` accepts a class token, you can vary the transport based on your environment without touching any component code:
+Provider defaults are merged with call-site options. The call site takes precedence.
```ts
-// main.ts — production
-provideAgent({ transport: FetchStreamTransport })
+provideAgent({ apiUrl: 'https://api.example.com' });
-// main.spec.ts / TestBed — tests
-provideAgent({ transport: MockAgentTransport })
+const productionAgent = agent({
+ assistantId: 'support-agent',
+});
+
+const localAgent = agent({
+ apiUrl: 'http://localhost:2024',
+ assistantId: 'dev-agent',
+});
+```
+
+## Test transports
+
+`transport` is an object that implements `AgentTransport`, not an Angular class token. Create an instance before passing it to `provideAgent()` or to an individual `agent()` call.
+
+```ts
+const transport = new MockAgentTransport();
+
+TestBed.configureTestingModule({
+ providers: [
+ provideAgent({
+ apiUrl: '',
+ transport,
+ }),
+ ],
+});
```
-
- Swap `FetchStreamTransport` for `MockAgentTransport` (or any custom class
- implementing the `StreamTransport` interface) to change the transport for all
- resources at once — useful for testing or SSR.
+
+ You do not need `provideAgent()` for one-off tests. Passing
+ `transport: new MockAgentTransport()` directly to `agent()` is often simpler
+ and keeps the test fixture explicit.
## What's Next
@@ -65,7 +93,7 @@ provideAgent({ transport: MockAgentTransport })
Full reference for the core primitive you configure here.
diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json
index 767973347..3119b1690 100644
--- a/apps/website/content/docs/chat/api/api-docs.json
+++ b/apps/website/content/docs/chat/api/api-docs.json
@@ -826,6 +826,72 @@
],
"methods": []
},
+ {
+ "name": "ChatCitationCardTemplateDirective",
+ "kind": "class",
+ "description": "ContentChild template directive for custom citation card rendering.\nUsage: ...",
+ "params": [],
+ "examples": [],
+ "properties": [
+ {
+ "name": "tpl",
+ "type": "TemplateRef