diff --git a/apps/website/content/docs/ag-ui/api/fake-agent.mdx b/apps/website/content/docs/ag-ui/api/fake-agent.mdx
new file mode 100644
index 000000000..1fe1776f7
--- /dev/null
+++ b/apps/website/content/docs/ag-ui/api/fake-agent.mdx
@@ -0,0 +1,123 @@
+# FakeAgent
+
+`FakeAgent` is an in-process AG-UI test double that emits a canned streaming response without a real backend. Use it for offline development, CI, and component tests.
+
+## provideFakeAgent()
+
+`provideFakeAgent()` is the DI-friendly entry point. It is a drop-in replacement for `provideAgent({ url })` when no backend is available.
+
+```ts
+import { bootstrapApplication } from '@angular/platform-browser';
+import { provideFakeAgent } from '@threadplane/ag-ui';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideFakeAgent(),
+ ],
+});
+```
+
+Pass a `FakeAgentConfig` to customize the canned response:
+
+```ts
+provideFakeAgent({
+ tokens: ['Hello', ' world', '!'],
+ delayMs: 40,
+})
+```
+
+### FakeAgentConfig
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `tokens` | `string[]` | Assistant reply streamed token-by-token. Defaults to a fixed placeholder message. |
+| `reasoningTokens` | `string[]` | Optional reasoning chunks emitted before the text reply. |
+| `delayMs` | `number` | Milliseconds between successive token emissions. Defaults to `60`. |
+
+## FakeAgent class
+
+Construct a `FakeAgent` directly when you need lower-level control, for example to pass it to `toAgent()` in a test harness.
+
+```ts
+import { FakeAgent } from '@threadplane/ag-ui';
+import { toAgent } from '@threadplane/ag-ui';
+
+const agent = toAgent(new FakeAgent({
+ tokens: ['Thinking', '...', ' done.'],
+ reasoningTokens: ['Step 1', ': evaluate'],
+ delayMs: 30,
+}));
+```
+
+`FakeAgent` extends `AbstractAgent` from `@ag-ui/client`. Its `run()` method returns an `Observable` that emits the full event sequence — `RUN_STARTED`, optional reasoning events, `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` tokens, `TEXT_MESSAGE_END`, `RUN_FINISHED` — then completes.
+
+## TestBed example
+
+```ts
+import { TestBed } from '@angular/core/testing';
+import { provideFakeAgent, injectAgent } from '@threadplane/ag-ui';
+import { Component } from '@angular/core';
+
+@Component({ template: '' })
+class ChatComponent {
+ readonly chat = injectAgent();
+}
+
+describe('ChatComponent', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ChatComponent],
+ providers: [
+ provideFakeAgent({ tokens: ['Hello', ' from', ' fake'] }),
+ ],
+ });
+ });
+
+ it('streams a canned reply', async () => {
+ const fixture = TestBed.createComponent(ChatComponent);
+ fixture.detectChanges();
+
+ await fixture.componentInstance.chat.submit({ message: 'Hi' });
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.chat.messages().at(-1)?.content)
+ .toBe('Hello from fake');
+ });
+});
+```
+
+
+ `FakeAgent` and `provideFakeAgent()` are intended for development and tests
+ only. Do not use them in production builds.
+
+
+See also: [Fake Agent guide](/docs/ag-ui/guides/fake-agent) for practical offline-development patterns, and [Testing guide](/docs/ag-ui/guides/testing) for full component-testing recipes.
+
+## What's Next
+
+
+
+ Practical patterns for offline demos and rapid prototyping with FakeAgent.
+
+
+ Full testing patterns for components that use `injectAgent()`.
+
+
+ The production provider `provideFakeAgent()` replaces in tests.
+
+
+
+{/* Auto-rendered from api-docs.json — see page component */}
diff --git a/apps/website/content/docs/ag-ui/api/inject-agent.mdx b/apps/website/content/docs/ag-ui/api/inject-agent.mdx
new file mode 100644
index 000000000..cd437ff88
--- /dev/null
+++ b/apps/website/content/docs/ag-ui/api/inject-agent.mdx
@@ -0,0 +1,121 @@
+# injectAgent()
+
+`injectAgent()` retrieves the AG-UI agent from Angular's dependency injection container. Call it in an Angular injection context — typically as a component field initializer. The returned object exposes Angular Signals for reactive UI state and async methods for user actions.
+
+Configuration is supplied globally via [provideAgent()](/docs/ag-ui/api/provide-agent) — `injectAgent()` itself takes no arguments.
+
+```ts
+import { injectAgent } from '@threadplane/ag-ui';
+
+readonly chat = injectAgent();
+
+await this.chat.submit({ message: 'Hello' });
+```
+
+Pair it with `provideAgent()` at bootstrap to configure the agent endpoint:
+
+```ts
+import { bootstrapApplication } from '@angular/platform-browser';
+import { provideAgent } from '@threadplane/ag-ui';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideAgent({ url: 'http://localhost:8000/my-agent' }),
+ ],
+});
+```
+
+## Runtime-neutral surface
+
+These fields are stable across runtime adapters and are what chat components consume.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `messages()` | `Message[]` | Chat messages with `role`, `content`, optional `toolCallIds`, citations, and reasoning. |
+| `status()` | `'idle' \| 'running' \| 'error'` | UI lifecycle status. |
+| `isLoading()` | `boolean` | Convenience signal for active streaming. |
+| `error()` | `unknown` | Latest runtime error, when present. |
+| `toolCalls()` | `ToolCall[]` | Tool calls projected into the chat contract. |
+| `state()` | `Record` | Latest agent state projected as a plain object. |
+| `interrupt()` | `AgentInterrupt \| undefined` | Current interrupt, when the backend pauses for human input. |
+| `submit(input, opts?)` | `Promise` | Submit a user message or resume payload. |
+| `stop()` | `Promise` | Abort the active run. |
+| `regenerate(index)` | `Promise` | Remove the assistant message at `index` and rerun from the preceding user message. |
+
+## AG-UI-specific surface
+
+The AG-UI adapter extends the neutral `Agent` contract with one additional signal:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `customEvents()` | `CustomStreamEvent[]` | Custom events emitted by the backend during a run. Accumulates per run; resets on each new `submit()`. |
+
+`injectAgent()` is typed as the neutral `Agent` interface; to access `customEvents` you must cast to `AgUiAgent`:
+
+```ts
+import { injectAgent, type AgUiAgent } from '@threadplane/ag-ui';
+
+const chat = injectAgent() as AgUiAgent;
+chat.customEvents(); // Signal
+```
+
+The chat a2ui bridge reads `customEvents` to light up live generative-UI streaming when your backend emits `a2ui-partial` events. See the [Custom Events guide](/docs/ag-ui/guides/custom-events) for backend wiring details.
+
+## Submit and resume
+
+Use the runtime-neutral submit shape for normal chat input:
+
+```ts
+await chat.submit({ message: 'Summarize this document' });
+```
+
+Resume an interrupt by passing a `resume` payload:
+
+```ts
+await chat.submit({ resume: { approved: true } });
+```
+
+## Regenerate semantics
+
+`regenerate(assistantMessageIndex)` has replace semantics: it keeps the user message before the selected assistant message, removes the selected assistant message and all later messages, syncs the rollback to the AG-UI source, then reruns with no new user message appended.
+
+```ts
+await chat.regenerate(3);
+```
+
+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.
+
+
+ `injectAgent()` 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.
+
+
+## What's Next
+
+
+
+ Configure the endpoint URL, headers, and telemetry for the agent provider.
+
+
+ Wire `customEvents` to live generative-UI streaming from any AG-UI backend.
+
+
+ Test components that call `injectAgent()` with the in-process FakeAgent.
+
+
+
+{/* Auto-rendered from api-docs.json — see page component */}
diff --git a/apps/website/content/docs/ag-ui/api/provide-agent.mdx b/apps/website/content/docs/ag-ui/api/provide-agent.mdx
new file mode 100644
index 000000000..9f1c30c18
--- /dev/null
+++ b/apps/website/content/docs/ag-ui/api/provide-agent.mdx
@@ -0,0 +1,83 @@
+# provideAgent()
+
+`provideAgent()` registers the singleton AG-UI agent configuration for every [injectAgent()](/docs/ag-ui/api/inject-agent) call in an Angular application. Call it once in `bootstrapApplication` or an `ApplicationConfig` to wire up the endpoint URL, optional identifiers, custom headers, and telemetry.
+
+`injectAgent()` itself takes no arguments — all configuration flows through `provideAgent()`.
+
+```ts
+import { bootstrapApplication } from '@angular/platform-browser';
+import { provideAgent } from '@threadplane/ag-ui';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideAgent({
+ url: 'http://localhost:8000/my-agent',
+ }),
+ ],
+});
+```
+
+## Configuration options
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `url` | `string` | HTTP endpoint for the AG-UI backend agent. Required. |
+| `agentId` | `string` | Optional agent identifier forwarded to the backend. |
+| `threadId` | `string` | Optional thread identifier for session continuity. |
+| `headers` | `Record` | Optional custom HTTP headers included on every request. |
+| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Optional app-owned telemetry sink. No telemetry is emitted unless this is provided. |
+
+## Static vs factory config
+
+Pass a plain `AgentConfig` object when the URL is known up front. Pass a `() => AgentConfig` factory when the config depends on runtime DI state — the factory runs inside an Angular injection context, so it may call `inject()` to read services or environment tokens.
+
+```ts
+// Factory form — reads an environment token at runtime
+provideAgent(() => {
+ const env = inject(APP_ENV);
+ return {
+ url: env.agentUrl,
+ headers: { Authorization: `Bearer ${env.apiKey}` },
+ };
+});
+```
+
+## Singleton model
+
+A single `provideAgent({...})` call configures the entire application. Every `injectAgent()` call resolves to the same configured agent.
+
+```ts
+provideAgent({ url: 'https://api.example.com/agent' });
+
+// Elsewhere, inside an injection context:
+const chat = injectAgent();
+```
+
+## What's Next
+
+
+
+ The primitive you call inside components after registering a provider.
+
+
+ End-to-end setup connecting a real AG-UI backend in minutes.
+
+
+ Swap the live backend for an in-process test double during development.
+
+
+
+{/* Auto-rendered from api-docs.json — see page component */}
diff --git a/apps/website/content/docs/ag-ui/api/to-agent.mdx b/apps/website/content/docs/ag-ui/api/to-agent.mdx
new file mode 100644
index 000000000..651c4f8fd
--- /dev/null
+++ b/apps/website/content/docs/ag-ui/api/to-agent.mdx
@@ -0,0 +1,80 @@
+# toAgent()
+
+`toAgent()` is the lower-level adapter function that wraps a raw AG-UI `AbstractAgent` into the runtime-neutral `Agent` contract used by `@threadplane/chat` components.
+
+```ts
+toAgent(source: AbstractAgent, options?: ToAgentOptions): AgUiAgent
+```
+
+Most applications should use [`provideAgent()`](/docs/ag-ui/api/provide-agent) instead — it constructs the `HttpAgent` source and calls `toAgent()` internally. Reach for `toAgent()` directly when you instantiate or customize the `AbstractAgent` yourself, for example to integrate a non-HTTP AG-UI transport.
+
+```ts
+import { HttpAgent } from '@ag-ui/client';
+import { toAgent } from '@threadplane/ag-ui';
+
+const source = new HttpAgent({ url: 'http://localhost:8000/my-agent' });
+const agent = toAgent(source, { telemetry: myTelemetrySink });
+```
+
+## ToAgentOptions
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Optional app-owned telemetry sink. No telemetry is emitted unless this is provided. |
+
+## AgUiAgent
+
+`toAgent()` returns an `AgUiAgent`, which extends the neutral `Agent` contract with one additional signal:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `customEvents()` | `Signal` | Custom events accumulated during a run. Resets at the start of each new run. |
+
+The standard `Agent` signals (`messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`, `interrupt`) and actions (`submit`, `stop`, `regenerate`) are all present.
+
+## CustomStreamEvent
+
+`CustomStreamEvent` is the element type of `AgUiAgent.customEvents`:
+
+```ts
+interface CustomStreamEvent {
+ /** Event name set by the backend (e.g. 'a2ui-partial', 'state_update'). */
+ name: string;
+ /** Arbitrary payload from the backend (JSON-string values are parsed). */
+ data: unknown;
+}
+```
+
+Custom events are surfaced from AG-UI `CUSTOM` protocol events whose `name` is not `on_interrupt`. The `on_interrupt` event is routed to the `interrupt` signal instead.
+
+## Lifecycle note
+
+The returned `AgUiAgent` does not manage its own lifetime. When using DI via `provideAgent()`, the provider's destroy hook handles cleanup. When calling `toAgent()` directly, treat the returned agent's lifecycle as tied to the `AbstractAgent` instance you constructed.
+
+## What's Next
+
+
+
+ The DI-friendly wrapper around `toAgent()` for most Angular apps.
+
+
+ How the AG-UI adapter fits into the broader `@threadplane/chat` design.
+
+
+ Which AG-UI protocol events map to which signals and actions.
+
+
+
+{/* Auto-rendered from api-docs.json — see page component */}
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 f376b29e7..a567ccfb6 100644
--- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx
+++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx
@@ -8,6 +8,10 @@
AG-UI is the open agent-to-UI protocol from the CopilotKit ecosystem. It standardizes how agent runtimes stream events (messages, tool calls, state updates) to a frontend. Used by **CrewAI**, **Mastra**, **Microsoft Agent Framework**, **AG2**, **Pydantic AI**, **AWS Strands**, and the **CopilotKit runtime**. One adapter unlocks all of them.
+
+The [AG-UI demo](https://ag-ui.threadplane.ai) runs this exact chat surface against an AG-UI backend — streaming, tool calls, and generative UI included. Compare it side by side with the [LangGraph demo](https://demo.threadplane.ai).
+
+
## How it fits
```text
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 be1059135..72dd3c823 100644
--- a/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx
+++ b/apps/website/content/docs/ag-ui/getting-started/quickstart.mdx
@@ -6,6 +6,10 @@ Let's bind `` from `@threadplane/chat` to an AG-UI backend in 5 minutes.
Angular 20+ project with Node.js 22+. If you need setup help, see the [Installation](/docs/ag-ui/getting-started/installation) guide.
+
+Want to see the finished result before you build? Open the live [AG-UI demo](https://ag-ui.threadplane.ai).
+
+
diff --git a/apps/website/content/docs/ag-ui/guides/citations.mdx b/apps/website/content/docs/ag-ui/guides/citations.mdx
index cf20972a6..108788e5e 100644
--- a/apps/website/content/docs/ag-ui/guides/citations.mdx
+++ b/apps/website/content/docs/ag-ui/guides/citations.mdx
@@ -117,7 +117,7 @@ const nextMessages = bridgeCitationsState(
);
```
-Most apps should not need this directly. The built-in AG-UI reducer already calls it for state snapshots and deltas.
+Most apps should not need this directly. The built-in AG-UI reducer already calls it for state snapshots and deltas. The `bridgeCitationsState(thread, messages)` helper merges thread-level citation state onto the message list; import it from `@threadplane/ag-ui` when you assemble messages yourself outside the standard adapter pipeline.
## Gotchas
diff --git a/apps/website/content/docs/ag-ui/guides/custom-events.mdx b/apps/website/content/docs/ag-ui/guides/custom-events.mdx
new file mode 100644
index 000000000..1fe6be66b
--- /dev/null
+++ b/apps/website/content/docs/ag-ui/guides/custom-events.mdx
@@ -0,0 +1,138 @@
+# Custom Events
+
+AG-UI `CUSTOM` events let a backend node push arbitrary data to the Angular client while a run is in progress. The adapter accumulates these events into the `customEvents` signal on the `AgUiAgent` returned by `injectAgent()`.
+
+```ts
+interface CustomStreamEvent {
+ name: string;
+ data: unknown;
+}
+```
+
+`customEvents` is a `Signal`. The list is reset to `[]` when `RUN_STARTED` arrives, so it only ever contains events from the current run.
+
+
+The special `CUSTOM` event with `name: "on_interrupt"` is handled separately — it populates `agent.interrupt` and does **not** appear in `customEvents`. See the [Interrupts guide](/docs/ag-ui/guides/interrupts).
+
+
+## Where Custom Events Come From
+
+A LangGraph node emits a custom event by writing to the stream writer with `stream_mode='custom'`:
+
+```python
+from langchain_core.runnables import RunnableConfig
+from langgraph.config import get_stream_writer
+
+def analysis_node(state: State, config: RunnableConfig) -> State:
+ writer = get_stream_writer()
+
+ # Emit a partial result as the node runs
+ writer({"name": "analysis_progress", "data": {"step": "scoring", "pct": 42}})
+
+ # ... do more work ...
+
+ writer({"name": "analysis_progress", "data": {"step": "scoring", "pct": 100}})
+ return state
+```
+
+The `ag-ui-langgraph` package surfaces this as an AG-UI `CUSTOM` event on the wire:
+
+```json
+{
+ "type": "CUSTOM",
+ "name": "analysis_progress",
+ "value": { "step": "scoring", "pct": 42 }
+}
+```
+
+The adapter JSON-parses `value` when it arrives as a string, so consumers always receive the structured object. The event is appended to `customEvents` as `{ name: "analysis_progress", data: { step: "scoring", pct: 42 } }`.
+
+## Reading Custom Events in Angular
+
+`injectAgent()` returns the neutral `Agent` type; cast it to `AgUiAgent` (exported from `@threadplane/ag-ui`) to reach the `customEvents` signal.
+
+### Reactive effect
+
+Use an `effect` to react every time new events arrive:
+
+```typescript
+import { Component, ChangeDetectionStrategy, effect, signal } from '@angular/core';
+import { ChatComponent } from '@threadplane/chat';
+import { injectAgent, type AgUiAgent } from '@threadplane/ag-ui';
+
+@Component({
+ standalone: true,
+ imports: [ChatComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ @if (progress() !== null) {
+
+ }
+ `,
+})
+export class AnalysisComponent {
+ protected readonly agent = injectAgent() as AgUiAgent;
+ protected readonly progress = signal(null);
+
+ constructor() {
+ effect(() => {
+ const events = this.agent.customEvents();
+ const last = [...events]
+ .reverse()
+ .find((e) => e.name === 'analysis_progress');
+ this.progress.set(
+ last ? (last.data as { pct: number }).pct : null,
+ );
+ });
+ }
+}
+```
+
+### Computed signal
+
+When you only need to derive a value, `computed` is more concise:
+
+```typescript
+import { Component, ChangeDetectionStrategy, computed } from '@angular/core';
+import { ChatComponent } from '@threadplane/chat';
+import { injectAgent, type AgUiAgent } from '@threadplane/ag-ui';
+import type { CustomStreamEvent } from '@threadplane/ag-ui';
+
+@Component({
+ standalone: true,
+ imports: [ChatComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+ `,
+})
+export class AnalysisComponent {
+ protected readonly agent = injectAgent() as AgUiAgent;
+
+ protected readonly progressEvents = computed(() =>
+ this.agent.customEvents().filter(
+ (e): e is CustomStreamEvent => e.name === 'analysis_progress',
+ ),
+ );
+}
+```
+
+Both patterns are zoneless-safe: Angular's signal graph tracks the `customEvents()` read and re-evaluates the derived value automatically.
+
+
+`customEvents` is the mechanism the chat composition uses for progressive a2ui surface updates — partial argument events accumulate here during a tool call and drive live rendering before the call completes. If you are building a custom a2ui integration over AG-UI, read `agent.customEvents()` the same way.
+
+
+## Relation to Interrupts
+
+`CUSTOM` events named `on_interrupt` follow a separate path: the adapter routes them to `agent.interrupt` (a `Signal`) and they never enter `customEvents`. This keeps the two signals purpose-distinct — `interrupt` drives human-in-the-loop approval flows, while `customEvents` carries all other backend-pushed data.
+
+See the [Interrupts guide](/docs/ag-ui/guides/interrupts) for the full interrupt lifecycle including `` and `submit({ resume })`.
+
+## See Also
+
+- [Architecture](/docs/ag-ui/concepts/architecture) — how the adapter reduces protocol events into Angular signals
+- [Event Mapping](/docs/ag-ui/reference/event-mapping) — full table of AG-UI event types and the agent fields they populate
+- [injectAgent()](/docs/ag-ui/api/inject-agent) — the injection function that returns `AgUiAgent`
diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx
index 015d3e808..daa0f5cf2 100644
--- a/apps/website/src/app/page.tsx
+++ b/apps/website/src/app/page.tsx
@@ -3,13 +3,15 @@ import { EcosystemStrip } from '../components/landing/EcosystemStrip';
import { Differentiator } from '../components/landing/Differentiator';
import { FeatureBlock } from '../components/landing/FeatureBlock';
import { BrowserFrame } from '../components/ui/BrowserFrame';
-import { LiveDemoFrame } from '../components/landing/LiveDemoFrame';
+import { DemoShowcase } from '../components/landing/DemoShowcase';
import { PilotBlock } from '../components/landing/PilotBlock';
import { WhitePaperBlock } from '../components/landing/WhitePaperBlock';
import { Promises } from '../components/landing/Promises';
import { HomeFAQ } from '../components/landing/HomeFAQ';
import { FinalCTA } from '../components/landing/FinalCTA';
import { RecentArticles } from '../components/landing/RecentArticles';
+import { Section } from '../components/ui/Section';
+import { Container } from '../components/ui/Container';
import { tokens } from '@threadplane/design-tokens';
import { createPageMetadata, LONG_SUBHEAD, PRIMARY_TAGLINE } from '../lib/site-metadata';
@@ -27,6 +29,13 @@ export default async function HomePage() {
+ {/* Interactive demo showcase */}
+
+
+
+
+
+
{/* Stream */}
}
+ visual={
+
+
+
+ }
/>
diff --git a/apps/website/src/components/docs/DocsSidebar.tsx b/apps/website/src/components/docs/DocsSidebar.tsx
index 9adf50440..9f593f721 100644
--- a/apps/website/src/components/docs/DocsSidebar.tsx
+++ b/apps/website/src/components/docs/DocsSidebar.tsx
@@ -217,6 +217,17 @@ export function DocsSidebar({ activeLibrary, activeSection, activeSlug }: Props)
+ {libConfig?.demoUrl && (
+
+ The identical Threadplane chat surface, running live against a LangGraph backend and an AG-UI backend. Switch tabs to compare — the front end never changes.
+