diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index d4d20142a..558aa674b 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -63,3 +63,13 @@ test('/llms.txt returns plain text', async ({ page }) => { const response = await page.goto('/llms.txt'); expect(response?.headers()['content-type']).toContain('text/plain'); }); + +test('marketing pages do not link to retired whitepaper PDFs', async ({ page }) => { + for (const route of ['/', '/angular', '/render', '/chat', '/pilot-to-prod', '/solutions']) { + await page.goto(route); + const retiredLinks = await page.locator('a[href$=".pdf"]').evaluateAll(links => + links.map(link => link.getAttribute('href')).filter(Boolean), + ); + expect(retiredLinks, `${route} has retired PDF links`).toEqual([]); + } +}); diff --git a/apps/website/emails/angular-download.ts b/apps/website/emails/angular-download.ts index 161d3c11a..16b3a6aa6 100644 --- a/apps/website/emails/angular-download.ts +++ b/apps/website/emails/angular-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/angular.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs/agent/api/agent'; export function angularDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Enterprise Guide to Agent Streaming

-

${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers six chapters: the last-mile problem, the agent() API, thread persistence, interrupt flows, full LangGraph feature coverage, and deterministic testing.

+

${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current @ngaf/langgraph API. We will send the updated version when it is ready. In the meantime, start with the current agent() reference.

- Download the Guide + Read the agent() Docs
`, }); diff --git a/apps/website/emails/chat-download.ts b/apps/website/emails/chat-download.ts index f4e497062..d347276ef 100644 --- a/apps/website/emails/chat-download.ts +++ b/apps/website/emails/chat-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/chat.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs/chat/components/chat'; export function chatDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Enterprise Guide to Agent Chat Interfaces

-

${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers five chapters: the sprint tax, batteries-included components, theming and design system integration, generative UI in chat, and debug tooling.

+

${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current @ngaf/chat API. We will send the updated version when it is ready. In the meantime, the current chat component docs are live.

- Download the Guide + Read the Chat Docs
`, }); diff --git a/apps/website/emails/drip-whitepaper-followup.ts b/apps/website/emails/drip-whitepaper-followup.ts index da4b05e57..1da5b0a9f 100644 --- a/apps/website/emails/drip-whitepaper-followup.ts +++ b/apps/website/emails/drip-whitepaper-followup.ts @@ -9,7 +9,7 @@ export function dripWhitepaperFollowupHtml(day: number): { subject: string; html

Whitepaper Follow-up

Did you get a chance to read Chapter 3?

Chapter 3 covers tool-call rendering — how to surface agent actions as real UI instead of raw JSON. It's the chapter most teams bookmark first.

- Read the Guide → + Read the Current Docs → `, showUnsubscribe: true, }), diff --git a/apps/website/emails/render-download.ts b/apps/website/emails/render-download.ts index e87c75c31..eb4a789c2 100644 --- a/apps/website/emails/render-download.ts +++ b/apps/website/emails/render-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/render.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs/render/getting-started/introduction'; export function renderDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Enterprise Guide to Generative UI

-

${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers five chapters: the coupling problem, declarative UI specs with Vercel's json-render standard, the component registry, streaming JSON patches, and state management.

+

${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current @ngaf/render API. We will send the updated version when it is ready. In the meantime, the current render docs are live.

- Download the Guide + Read the Render Docs
`, }); diff --git a/apps/website/emails/whitepaper-download.ts b/apps/website/emails/whitepaper-download.ts index 5a1978fe9..6ddd7a127 100644 --- a/apps/website/emails/whitepaper-download.ts +++ b/apps/website/emails/whitepaper-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepaper.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs'; export function whitepaperDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Angular Agent Readiness Guide

-

${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers six production-readiness dimensions: streaming state, thread persistence, tool-call rendering, human approval flows, generative UI, and deterministic testing.

+

${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current Angular Agent Framework API. We will send the updated version when it is ready. In the meantime, the docs cover the current agent(), chat, render, and AG-UI surfaces.

- Download the Guide + Read the Current Docs
`, }); diff --git a/apps/website/public/whitepaper-preview.html b/apps/website/public/whitepaper-preview.html deleted file mode 100644 index e5cfc533c..000000000 --- a/apps/website/public/whitepaper-preview.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - -
-
Agent · Production Readiness Guide
-

From Prototype
to Production

-

The Angular Agent Readiness Guide

-
cacheplane.io · 2026
-
⚠ PREVIEW — placeholder content. Set ANTHROPIC_API_KEY and run npm run generate-whitepaper for real content.
-
- - -
-

Contents

- -
- 01 - Streaming State Management -
-
- 02 - Thread Persistence -
-
- 03 - Tool-Call Rendering -
-
- 04 - Human Approval Flows -
-
- 05 - Generative UI -
-
- 06 - Deterministic Testing -
-
- - - -
-
Chapter 1
-

Streaming State Management

-

Overview

-

When you move from prototype to production, the requirements change fundamentally. What worked in a demo — direct API calls, synchronous state, manual zone management — falls apart at scale.

-

The Signals-Native Approach

-

Agent provides a signals-native approach that eliminates the boilerplate:

-
@Component({...})
-export class ChatComponent {
-  chat = agent<{ messages: BaseMessage[] }>({
-    assistantId: 'chat_agent',
-  });
-}
-
-

Production Checklist

-
-
-
-
Chapter 2
-

Thread Persistence

-

Overview

-

Demos work with ephemeral state. Production agents need conversation history that survives page refreshes, tab switches, and navigation — wired to LangGraph's MemorySaver backend.

-

The threadId Pattern

-
provideAgent({
-  apiUrl: 'http://localhost:2024',
-  threadId: signal(localStorage.getItem('threadId')),
-  onThreadId: (id) => localStorage.setItem('threadId', id),
-})
-
-

Production Checklist

-
-
-
-
Chapter 3
-

Tool-Call Rendering

-

Overview

-

LangGraph agents invoke tools mid-stream. The UI needs to show tool execution state in real time — steps appearing as the tool runs, a final result, and collapsible history.

-

Progressive Disclosure

-

-
-
-

Production Checklist

-
-
-
-
Chapter 4
-

Human Approval Flows

-

Overview

-

Production agents that take consequential actions must pause for human approval before proceeding. This requires a tight loop between LangGraph's interrupt() primitive and Angular UI.

-

The Interrupt Pattern

-
// In your component
-approved = computed(() => this.chat.interrupt()?.approved ?? false);
-onApprove() {
-  this.chat.submit(null, { command: { resume: true } });
-}
-
-

Production Checklist

-
-
-
-
Chapter 5
-

Generative UI

-

Overview

-

The most advanced production agents emit structured UI specs — not just text. A data analysis agent might render a live table. A booking agent might render a reservation form.

-

The Registry Pattern

-
defineAngularRegistry({
-  'data-table': DataTableComponent,
-  'booking-form': BookingFormComponent,
-  'chart-widget': ChartWidgetComponent,
-});
-
-

Production Checklist

-
-
-
-
Chapter 6
-

Deterministic Testing

-

Overview

-

Agent UIs are notoriously hard to test because they depend on live LLM responses. Flaky tests, slow CI, and inability to reproduce edge cases are the main reasons agent UIs ship with low confidence.

-

The MockAgentTransport Approach

-
const ref = createMockAgentRef();
-ref.messages.set([{ role: 'assistant', content: 'Hello' }]);
-ref.isStreaming.set(false);
-expect(fixture.nativeElement.querySelector('.message').textContent)
-  .toBe('Hello');
-
-

Production Checklist

-
-
- - - \ No newline at end of file diff --git a/apps/website/public/whitepaper.pdf b/apps/website/public/whitepaper.pdf deleted file mode 100644 index 69bdf349b..000000000 Binary files a/apps/website/public/whitepaper.pdf and /dev/null differ diff --git a/apps/website/public/whitepapers/angular-preview.html b/apps/website/public/whitepapers/angular-preview.html deleted file mode 100644 index c7410b9f5..000000000 --- a/apps/website/public/whitepapers/angular-preview.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - - - - - -
-
@ngaf/langgraph · Enterprise Guide
-

The
Enterprise
Guide
to
Agent
Streaming
in
Angular

-

Ship LangGraph agents in Angular — without building the plumbing

-
cacheplane.io · 2026
-
- - -
-

Contents

- -
- 01 - The Last-Mile Problem -
-
- 02 - The agent() API -
-
- 03 - Thread Persistence & Memory -
-
- 04 - Interrupt & Approval Flows -
-
- 05 - Full LangGraph Feature Coverage -
-
- 06 - Deterministic Testing -
-
- - - -
-
Chapter 1
-

The Last-Mile Problem

-

Chapter 3: The Last-Mile Problem

-

Your LangGraph backend is solid. The agent graph handles tool calls correctly, memory persists across turns, and streaming responses flow cleanly through your FastAPI or Node middleware. You fire up the demo, watch tokens stream into the terminal, and everything looks exactly right.

-

Then you wire it to Angular.

-

This is where teams lose weeks.

-

---

-

Zone Pollution Is Structural, Not Configurational

-

The first symptom is subtle: the UI feels sluggish, change detection runs are exploding in DevTools, and memory climbs steadily during long agent responses. The instinct is to reach for `runOutsideAngular`. That helps — but it doesn't solve the underlying problem.

-

`EventSource`, the browser primitive that powers SSE, is patched by zone.js at application bootstrap. Every `message` event it dispatches enters the Angular zone automatically. With a REST response, that's one event, one change detection cycle. With a streaming agent response, you're receiving dozens to hundreds of discrete `message` events per second — each one triggering a zone task, each zone task potentially scheduling a change detection pass. You haven't configured anything wrong. The architecture of zone.js and the architecture of SSE are simply incompatible at streaming throughput.

-

Wrapping your `EventSource` subscription in `runOutsideAngular` moves the event handling out of the zone, but now you own the re-entry problem. Every token update that touches the DOM needs to be explicitly brought back into the zone at precisely the right moment. Do it too eagerly and you're back to thrashing. Do it too conservatively and your UI lags perceptibly behind the stream.

-

---

-

The Change Detection Timing Mismatch

-

Angular's change detection model is synchronous and pull-based. The framework walks the component tree, reads current state, and updates the DOM. This works exceptionally well for request-response patterns where state settles before the template renders.

-

LLM token streams are fundamentally asynchronous and push-based. A 500-token response arrives as 500 separate events across several seconds. Each event mutates your accumulator state. If you're using `ChangeDetectionStrategy.OnPush` — which you should be — none of those mutations trigger re-renders automatically unless you're managing `markForCheck` yourself on every event. If you're not on `OnPush`, you're paying the full change detection cost on every token.

-

Neither option is acceptable at scale without deliberate infrastructure around it.

-

---

-

Signals Don't Automatically Fix This

-

The Signals API is the right direction, but it introduces its own friction with streaming. A `WritableSignal` updated on every SSE `message` event will notify all reactive consumers on every token. For a long agent response with tool call interleaving, you're creating a high-frequency reactive pulse that propagates through the entire signal graph. Without explicit batching or debounce strategies, Signals and SSE share the same fundamental problem as zone-based change detection — they just fail differently.

-

---

-

Why Your RxJS Patterns Don't Transfer

-

Teams reach for `fromEvent(source, 'message')` and expect the familiar RxJS composability to handle everything. It handles a lot — but the operator chains you built for REST responses don't account for multi-event agent protocols. LangGraph streams `metadata`, `updates`, `messages/complete`, and error events as distinct message types within a single SSE connection. Mapping that to a clean observable that maintains agent state, accumulates tokens, handles tool call boundaries, distinguishes stream errors from agent errors, and supports reconnection is non-trivial. Every team is building this from scratch.

-

---

-

The Real Cost

-

The production gap is expensive and consistent. Teams build zone-patch wrappers, token accumulator services, custom reconnection logic, and stream-type discriminators — then rebuild them on the next project. The demo worked because it ran outside a production Angular application with real component trees, real OnPush hierarchies, and real error conditions.

-

The backend is not the problem. The missing layer is a production-grade streaming adapter built specifically for how Angular actually works — not how it works in isolation, but under load, with Signals, with OnPush, and with the full complexity of a multi-turn agent protocol on the wire.

-

That layer is what this guide builds.

-
-
-
Chapter 2
-

The agent() API

-

Chapter 3: The agent() API

-

The core primitive in `@ngaf/langgraph` is `agent()` — a factory function that returns a structured ref containing typed signals wired directly to a LangGraph agent stream. You call it once, bind the signals in your template, and the component reacts to every streamed token without a subscription, a zone trigger, or an accumulation buffer in sight.

-

What agent() Returns

-

`agent()` returns an `AgentRef` object with four signals:

-
interface AgentRef {
-  messages:    Signal;
-  isStreaming:  Signal;
-  error:        Signal;
-  interrupt:    Signal;
-}
-
-

Each signal is updated incrementally as chunks arrive over the stream. `messages()` reflects the full conversation state at every emission — already assembled, already typed. `isStreaming()` flips to `true` the moment a run begins and back to `false` on completion or error. `error()` and `interrupt()` surface exceptional states that would otherwise require you to wire up parallel error channels manually.

-

Configuring the Transport with provideAgent()

-

Before `agent()` can do anything, the agent endpoint and stream transport need to be registered. That happens in `provideAgent()`, which you add to your application providers:

-
bootstrapApplication(AppComponent, {
-  providers: [
-    provideAgent({
-      url: 'https://your-langgraph-deployment/runs/stream',
-      transport: 'sse',
-    }),
-  ],
-});
-
-

`provideAgent()` configures a single shared transport layer — SSE by default, with WebSocket support available via the `transport` option. It also accepts authentication headers and retry policies at this level, keeping that configuration out of individual components entirely.

-

Why Signal-Native Design Works with OnPush

-

Angular's `OnPush` change detection skips components unless an input reference changes or an event originates from within the component tree. The traditional workaround for async data — `async` pipe or manual `markForCheck()` calls — adds noise and often breaks in subtle ways under zoneless configurations.

-

Signals sidestep this entirely. When a signal value changes, Angular's reactive graph marks only the affected views as dirty. `agent()` produces standard Angular signals, which means `OnPush` components react to streamed tokens automatically, without zone involvement, without `markForCheck()`, and without the `async` pipe acting as a syntactic intermediary.

-

Template Binding Without async Pipe

-

Because `messages()`, `isStreaming()`, `error()`, and `interrupt()` are signals, you call them directly in the template:

-
@Component({
-  selector: 'app-chat',
-  changeDetection: ChangeDetectionStrategy.OnPush,
-  template: `
-    @for (msg of agent.messages(); track msg.id) {
-      
-    }
-    @if (agent.isStreaming()) {
-      
-    }
-  `,
-})
-export class ChatComponent {
-  readonly agent = inject(AgentService).ref;
-}
-
-

No `async` pipe. No `subscribe()`. No lifecycle hooks managing teardown. The template reads signal values and Angular handles the rest.

-

The Contrast

-

The hand-rolled equivalent requires roughly 60–80 lines: an `HttpClient` call configured for streaming, an `EventSource` or `fetch` with `ReadableStream`, manual chunk parsing, a `BehaviorSubject` to accumulate tokens, a `takeUntilDestroyed` teardown, and explicit `markForCheck()` calls on every emission. You also write the error and interrupt handling yourself.

-

With `agent()`, that entire surface area collapses to three lines of meaningful code:

-
const ref = agent();
-ref.send({ role: 'user', content: input });
-// bind ref.messages() in template
-
-

The complexity didn't disappear — it moved into the library, where it belongs. What remains in your component is only the code that describes your feature.

-
-
-
Chapter 3
-

Thread Persistence & Memory

-

Chapter 6: Thread Persistence & Memory

-

Production agent applications are not stateless. Users close tabs, return the next morning, and expect the conversation to resume exactly where it stopped. Building that experience requires more than a reliable backend—it requires a disciplined contract between LangGraph's checkpoint system and your Angular frontend.

-

How threadId Flows from LangGraph to Angular

-

When LangGraph processes a run under a `MemorySaver` configuration, it persists the conversation graph state to a checkpoint store keyed by a `thread_id`. That identifier is the anchor for the entire session. On the Angular side, the agent library exposes this value through a reactive `threadId` signal that updates as soon as the backend confirms checkpoint creation. Every subsequent request that includes this `thread_id` in its config resumes from the last saved state—no reconstruction, no replaying events.

-

Treat the `threadId` signal as your source of truth. Do not generate identifiers client-side and hope for synchronization. Let the backend issue the ID, observe it, and persist it.

-

Persisting threadId on Creation

-

The `onThreadId` callback fires exactly once per new thread, immediately after the backend returns a checkpoint-backed ID. This is your window to write to `localStorage`:

-
provideAgent({
-  streamUrl: '/api/agent/stream',
-  onThreadId: (id) => {
-    const threads = JSON.parse(localStorage.getItem('agent_threads') ?? '[]');
-    threads.unshift({ id, created: Date.now(), label: 'New conversation' });
-    localStorage.setItem('agent_threads', JSON.stringify(threads));
-    localStorage.setItem('agent_active_thread', id);
-  },
-  initialThreadId: localStorage.getItem('agent_active_thread') ?? undefined,
-})
-
-

The `initialThreadId` input tells the agent to hydrate from an existing checkpoint on mount rather than starting a new thread. When provided, the agent skips the initialization handshake and immediately enables streaming against the existing state. If the ID is `undefined`, a fresh thread begins and `onThreadId` fires again.

-

Building a Thread List UI

-

A thread switcher is straightforward in signal-based Angular. Read the thread list from `localStorage` into a component signal, render it, and call `setThreadId()` when the user selects an entry. Clearing history means removing the entry locally and optionally calling a backend deletion endpoint—though MemorySaver's in-memory default does not expose one out of the box.

-

Wire up a "New conversation" button that clears `agent_active_thread` from storage and reinitializes the agent component. Since `initialThreadId` is an input, destroying and recreating the component is the cleanest reset path.

-

How MemorySaver Maps to the Frontend Lifecycle

-

`MemorySaver` holds checkpoints in process memory, making it suitable for development and single-instance deployments. Each `thread_id` maps to a serialized graph snapshot. The frontend lifecycle mirrors this directly: create a thread ID on first run, persist it, restore it on return, discard it on explicit reset. In production, swap `MemorySaver` for a persistent store—Postgres via `@langchain/langgraph-checkpoint-postgres` is the standard choice—without changing any frontend code.

-

Production Checklist

-

Before shipping thread persistence, verify the following:

- -Thread persistence is one of those features that feels optional until a user loses an hour of work. Build it correctly from the start.
-
-
-
Chapter 4
-

Interrupt & Approval Flows

-

Chapter 7: Interrupt & Approval Flows

-

Agents that interact with external systems — firing off emails, mutating database records, executing privileged queries — cannot operate as fire-and-forget processes in an enterprise context. Execution must be pauseable, inspectable, and resumable with explicit human intent. LangGraph's `interrupt()` primitive and `@ngaf/langgraph`'s reactive surface for it give you exactly that, without polling loops or bespoke WebSocket plumbing.

-

How LangGraph interrupt() Works

-

When a LangGraph node calls `interrupt(payload)`, graph execution halts at that checkpoint. The payload is an arbitrary object you define — typically the action description, affected resource identifiers, and any data the reviewer needs to make a decision. The graph persists this state to its checkpointer and waits. Nothing proceeds until a resume command arrives with the correct thread ID and checkpoint reference.

-

The resume payload follows a typed contract: `{ action: "approve" | "edit" | "cancel", data?: unknown }`. On `approve`, the graph continues with the original node inputs. On `edit`, your modified `data` is injected, replacing what the node would have used. On `cancel`, LangGraph short-circuits the remaining path and routes to whatever terminal state you've configured for rejected flows.

-

The Interrupt Signal in Angular

-

`@ngaf/langgraph` exposes this checkpoint state directly on the agent reference. The `interrupt` property is a computed signal that starts as `null` and transitions to an `InterruptState` object the moment the backend checkpoint is written:

-
const agent = injectAgentRef({ threadId });
-

const interrupt = agent.interrupt; // Signal

-

const handleApprove = () => - agent.resume({ action: Command.RESUME, data: { decision: "approve" } });

-

const handleEdit = (corrected: unknown) => - agent.resume({ action: Command.RESUME, data: { decision: "edit", payload: corrected } });

-

const handleCancel = () => - agent.resume({ action: Command.RESUME, data: { decision: "cancel" } }); -

-

No subscriptions. No manual HTTP calls to a resume endpoint. The signal reactivity handles template updates automatically — when `interrupt()` fires on the backend, your component re-renders with the interrupt context populated.

-

The Prebuilt Interrupt Panel

-

`` accepts the interrupt signal and your handlers, rendering a structured approval UI with the action description, resource context, and three action buttons. You bind it, you wire the handlers, and you're done. No template scaffolding, no building approval modals from scratch:

-

-
-

For teams with custom design systems, the panel exposes slot-based overrides without requiring you to reimplement the state machine behind it.

-

Edge Cases Worth Handling

-

Navigation mid-interrupt: The graph checkpoint persists server-side regardless of client state. On return to the thread, the interrupt signal will re-populate from the checkpoint. Guard your route exit with a confirmation dialog if the interrupt represents a time-sensitive action.

-

Session expiry: If the user's session expires while a checkpoint is pending, re-authentication should restore the thread context. Ensure your auth interceptor reattaches credentials before the agent ref re-initializes — otherwise the resume call will 401 against an unauthenticated thread.

-

Cancel with partial state: Cancel does not roll back side effects that occurred before `interrupt()` was called. If a node partially wrote records before pausing, your cancel handler needs to trigger compensating logic — either through a dedicated LangGraph cancel node or a separate cleanup endpoint. Treat `interrupt()` as a pre-execution gate, not a transaction boundary.

-

Interrupt flows are where the difference between a prototype and a production agent becomes visible. Get the pause-resume contract right, and the rest of the approval UX is straightforward to build on top of it.

-
-
-
Chapter 5
-

Full LangGraph Feature Coverage

-

Chapter 4: Full LangGraph Feature Coverage

-

Basic chat streaming is a solved problem. The real engineering challenge begins when your graph does something more interesting—calls a tool, delegates to a subagent, or needs to rewind to a prior checkpoint. Most Angular LLM libraries handle the simple case and then quietly stop working. This chapter documents how `@ngaf/langgraph` exposes the full LangGraph feature surface through a coherent signal-based API.

-

---

-

Tool Call Streaming

-

When a LangGraph node invokes a tool, the graph emits a structured sequence of events: the tool call intent, intermediate streaming deltas, and the final tool result. Rather than requiring you to parse raw SSE frames and reconstruct this sequence manually, `@ngaf/langgraph` surfaces tool invocation lifecycle through the agent ref directly.

-

The `toolCalls` signal on the agent ref updates reactively as each tool call progresses. You get the tool name, input arguments as they stream in, execution status, and the final output—all typed, all reactive. Your component never needs to touch the underlying event stream to render a "Searching the web…" indicator or display structured tool output inline.

-

This matters because tool call parsing is subtle. Argument deltas arrive as partial JSON strings. Parallel tool calls interleave. A naive implementation collapses these into noise. The library handles reconstruction and ordering internally.

-

---

-

Subgraph Event Propagation

-

LangGraph supports nested graphs, and real production agents use them. Events emitted inside a subgraph are namespaced by graph path, which means consuming them correctly requires understanding the event hierarchy.

-

`@ngaf/langgraph` flattens subgraph events into the parent stream while preserving their origin metadata. If you care about which subgraph produced a message—for routing UI panels, for logging, or for debugging—you have access to the full event path. If you don't care, the default signal behavior aggregates everything cleanly without requiring you to configure traversal logic.

-

---

-

Time Travel

-

LangGraph's checkpoint system allows you to rewind graph state to any prior node execution and re-run from that point. This is not a niche feature—it's the foundation of any agent UI that allows users to correct mistakes or explore alternative paths.

-

The agent ref exposes a `rewindTo(checkpointId)` method. Calling it terminates the current stream, restores the specified checkpoint, and begins re-streaming from that node. Signals update exactly as they would on an initial run. Your component code doesn't need to distinguish between a first run and a rewound run—the reactive contract is identical.

-

---

-

DeepAgent Multi-Agent Coordination

-

DeepAgent introduces orchestrator-subagent topology at the stream level. An orchestrator dispatches work to specialized agents and aggregates their outputs. From the stream consumer's perspective, this produces interleaved events from multiple graph instances.

-

`@ngaf/langgraph` maps each active agent to its own scoped signal context. The orchestrator's agent ref exposes a `subagents` signal—a reactive map of active agent IDs to their respective state signals. You can bind a list of subagent components to this map and let Angular's `@for` loop handle the rest. Agent addition, completion, and failure all propagate as signal updates without manual subscription management.

-

---

-

The `onCustomEvent` Hook

-

LangGraph nodes can emit arbitrary structured events outside the message channel. These are the primitives for generative UI, analytics instrumentation, and intermediate state reporting.

-

The `onCustomEvent` hook accepts a typed handler that fires whenever a matching event arrives in the stream. You define the event schema; the library routes and deserializes the payload. The handler can write directly to a component signal, dispatch to a store, or trigger a side effect. Custom events are not mixed into the message stream—they arrive as a clean parallel channel.

-

---

-

Why Full Coverage Is Non-Negotiable

-

The failure mode to avoid is a two-tier implementation: the library handles the simple case, and your team writes custom SSE parsing for everything else. That split is where bugs live and where onboarding breaks down. An integration layer is only valuable if it holds across the full feature surface. When it doesn't, engineers route around it, and you've paid the abstraction cost without capturing the benefit.

-
-
-
Chapter 6
-

Deterministic Testing

-

Chapter 7: Deterministic Testing

-

Agent UIs introduce testing challenges that standard Angular component tests don't prepare you for. Streaming responses are asynchronous and stateful. LLM outputs are non-deterministic by definition. Tool calls can arrive mid-stream, get interrupted, or fail silently. If your component tests make real network calls to an LLM backend, you've already lost — slow CI pipelines, flaky outcomes, and zero ability to reproduce the specific edge case that broke production on a Friday afternoon.

-

The solution is full control over the data layer. Every signal value, every SSE event, every state transition should be scripted before the test runs.

-

---

-

MockStreamTransport

-

`MockStreamTransport` replaces your HTTP or WebSocket transport layer with a deterministic event queue. You script a sequence of SSE-style events — token chunks, tool call payloads, interrupt signals, error frames — and the transport emits them synchronously or on a controlled microtask schedule. No server required.

-
const transport = new MockStreamTransport([
-  { type: 'token', value: 'The capital' },
-  { type: 'token', value: ' of France is Paris.' },
-  { type: 'done' }
-]);
-
-

Events can be queued to emit immediately or after a configurable tick, giving you precise control over partial-stream states. You can script an interrupted stream by placing an `interrupt` event mid-sequence, or trigger an error frame at any position to verify your component's error boundary behavior.

-

---

-

createMockStreamResourceRef()

-

For components that consume stream state through Angular Signals, `createMockStreamResourceRef()` gives you direct write access to the signals your component reads. Instead of driving state through the transport layer, you set signal values directly and assert on the resulting DOM.

-
const ref = createMockStreamResourceRef({
-  status: signal('streaming'),
-  tokens: signal('Partial respon'),
-  toolCalls: signal([]),
-  error: signal(null)
-});
-
-

This is the right primitive for isolated component tests. You're not testing the streaming pipeline — you're testing that your component renders correctly given a specific signal state. Keep the concerns separated.

-

---

-

Testing Each Agent State

-

Every meaningful agent state needs a dedicated test. Set the signal values, run `fixture.detectChanges()`, and assert on the DOM.

- -Tool call rendering follows the same pattern. Populate `toolCalls` with a scripted payload and assert that the correct tool UI component is instantiated with the right inputs. Generative UI output — dynamic component injection based on tool response schema — should be tested by scripting the full tool result payload and verifying that `NgComponentOutlet` or your equivalent mechanism mounts the expected component. -

Thread switching tests should use `createMockStreamResourceRef()` to swap the active thread reference and confirm the component clears its previous state correctly before rendering the new thread's history.

-

---

-

TestBed Setup

-
TestBed.configureTestingModule({
-  imports: [AgentChatComponent],
-  providers: [
-    { provide: STREAM_TRANSPORT, useValue: new MockStreamTransport([]) },
-    { provide: AgentStreamService, useClass: MockAgentStreamService }
-  ]
-});
-fixture = TestBed.createComponent(AgentChatComponent);
-ref = createMockStreamResourceRef({ status: signal('idle'), tokens: signal('') });
-fixture.componentRef.setInput('streamRef', ref);
-fixture.detectChanges();
-
-

---

-

The Benchmark

-

Agent component tests should run completely offline and complete in under 100ms each. If a test exceeds that threshold, it's making real I/O, using real timers, or waiting on something it shouldn't know about. Treat a slow test as a design signal: your component has a seam that isn't properly abstracted. Fix the abstraction, not the timeout.

-
- - - \ No newline at end of file diff --git a/apps/website/public/whitepapers/angular.pdf b/apps/website/public/whitepapers/angular.pdf deleted file mode 100644 index 195135a30..000000000 Binary files a/apps/website/public/whitepapers/angular.pdf and /dev/null differ diff --git a/apps/website/public/whitepapers/chat-preview.html b/apps/website/public/whitepapers/chat-preview.html deleted file mode 100644 index 7c63a83ed..000000000 --- a/apps/website/public/whitepapers/chat-preview.html +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - -
-
@ngaf/chat · Enterprise Guide
-

The
Enterprise
Guide
to
Agent
Chat
Interfaces
in
Angular

-

Production agent chat UI in days, not sprints

-
cacheplane.io · 2026
-
- - -
-

Contents

- -
- 01 - The Sprint Tax -
-
- 02 - Batteries-Included Components -
-
- 03 - Theming & Design System Integration -
-
- 04 - Generative UI in Chat -
-
- 05 - Debug Tooling -
-
- - - -
-
Chapter 1
-

The Sprint Tax

-

Chapter 3: The Sprint Tax

-

Every Angular team building an agent application eventually arrives at the same moment: the backend is ready. The LLM is wired up, the tools are registered, the streaming endpoint is live. Then someone opens a new component file and types `chat.component.ts`, and the next six weeks disappear.

-

This is the sprint tax—the recurring, predictable cost of building chat UI infrastructure that every team pays independently, from scratch, as though it hasn't been solved before.

-

The Inventory

-

A production-ready agent chat interface is not a text box and a message list. The actual surface area looks like this:

- -Each item on this list is a contained engineering problem. Together, they constitute four to six weeks of sprint capacity for a senior engineer—before any iteration begins. -

Structural Inefficiency, Not a Skill Gap

-

Teams that spend six weeks building this are not slow. They are absorbing a structural tax that has nothing to do with the differentiated value they are trying to ship. The chat chrome is table stakes. The agent graph, the tool integrations, the domain-specific interrupt logic, the retrieval pipeline—that is the product. The message list is not.

-

The hidden costs compound the timeline. Accessibility for streaming content is genuinely hard: ARIA live regions can flood screen readers if implemented naively, and managing focus across dynamically inserted content requires deliberate engineering. Streaming display has edge cases that do not appear in demos—what happens when a chunk arrives after the user has already submitted the next message? What does the UI do with a malformed SSE frame? Tool call state machines look simple until you model cancellation, retry, and partial results simultaneously. These problems are not insurmountable, but they take time that no product roadmap budgets honestly.

-

Demo Versus Production

-

"Good enough for demo" is a message bubble that renders a full response after a fake typing delay. Production is a component that handles backpressure from a real streaming endpoint, recovers from network interruption without corrupting message state, displays tool execution in real time, and remains navigable by keyboard without visual regression. The gap between those two things is where most teams discover the actual scope of the problem—usually during QA in week five.

-

The Opportunity Cost

-

Senior Angular engineers who spend three sprints on chat infrastructure are not spending those sprints on agent integration. They are not building the domain-specific tool interfaces, the custom interrupt flows, or the application state management that ties agent output to the rest of the product. Those are the things that will differentiate the application. The scroll behavior will not.

-

The Thesis

-

`@ngaf/chat` exists to eliminate the sprint tax. Ship the chat UI on day one—message rendering, streaming, tool call cards, interrupt panels, accessibility, mobile layout, all of it—and spend the sprints on what actually matters: the integration work that is specific to your agent, your domain, and your users.

-

The backend is ready. The chat UI should be too.

-
-
-
Chapter 2
-

Batteries-Included Components

-

Chapter 4: Batteries-Included Components

-

Two Tiers, One Decision

-

`@ngaf/chat` ships two distinct component tiers. The prebuilt tier gives you a production-ready chat interface with a single element. The headless tier gives you behavior and state without a single line of CSS opinion. Your team picks the tier that matches how much control you actually need — not how much you think you might need someday.

-

Most teams start with prebuilt and never leave. That is the correct call.

-

---

-

The Headless Tier

-

Headless components own the hard parts: state management, message streaming, keyboard interactions, accessibility semantics, and agent lifecycle integration. They render nothing styled.

-

The four primitives are:

- -Each primitive accepts an `agentRef` input and wires itself to the shared agent context. You bring the markup and styles. The component brings everything else. This is the right choice when your design system has strict structural requirements or when you are embedding agent interactions inside an existing, highly composed UI. -

---

-

The Prebuilt Tier

-

`chat-prebuilt` is a single component that composes all four headless primitives internally, applies a production-quality default theme, and handles responsive layout. Zero configuration is required beyond connecting an agent reference.

-
import { AgentService } from '@ngaf/langgraph';
-import { ChatPrebuiltComponent } from '@ngaf/chat';
-

@Component({ - selector: 'app-support', - imports: [ChatPrebuiltComponent], - template: ` - - ` -}) -export class SupportComponent { - constructor(public agentService: AgentService) {} -} -

-

That is a fully functional streaming chat interface. Message history, input handling, tool call display, and interrupt controls are all included.

-

---

-

How the Component Model Connects

-

Both tiers consume the `AgentRef` produced by `@ngaf/langgraph`. The `AgentRef` is a stable reference that exposes an `AIMessage[]` signal, a send method, and a cancellation interface. Components bind to this ref and react to signal emissions via Angular's standard reactivity primitives — no custom change detection strategies required, no zone workarounds.

-

When the agent emits a new token mid-stream, the `AIMessage[]` signal updates, and `chat-messages` incrementally patches the DOM. The mechanism is straightforward signal subscription. There is no proprietary diffing layer to reason about.

-

---

-

Composing the Two Tiers

-

The tiers are not mutually exclusive. A common pattern: use `chat-prebuilt` for the standard chat surface, then replace specific sections with headless primitives where your requirements diverge. If your design requires a custom tool call renderer — perhaps displaying structured data in a proprietary grid — you drop `chat-tool-calls` into that slot directly and style it yourself while leaving the rest of `chat-prebuilt` intact.

-

---

-

When to Migrate

-

Start with `chat-prebuilt`. Migrate a section to headless when a specific component's default rendering is structurally incompatible with your design system — not when you want to tweak padding or colors, which CSS custom properties handle without touching the component tier at all.

-

Headless is not a reward for complexity. It is a tool for genuine structural divergence.

-
-
-
Chapter 3
-

Theming & Design System Integration

-

Chapter 6: Theming & Design System Integration

-

A chat interface that looks like it came from a SaaS starter kit is a trust problem. Users notice when a modal, sidebar, or embedded panel breaks the visual contract established by the rest of your product. For enterprise teams, that inconsistency signals a lack of ownership — and in agent-facing tools, ownership matters. This chapter covers how to bring `@ngaf/chat` fully into your design system without forking component source or writing brittle CSS overrides.

-

---

-

The CSS Custom Property API

-

`@ngaf/chat` exposes all visual decisions as CSS custom properties scoped under the `--cp-` namespace. These are not internal implementation details — they are the public API for visual configuration. Properties are declared on the host element and cascade normally through Shadow DOM boundaries where applicable, giving you a single override surface regardless of where components are mounted in the Angular component tree.

-

The token surface covers six categories: color (surface, text, border, accent, semantic), typography (family, size scale, weight, line height), spacing (component padding, message gap, input height), shape (border radius at three scale points), motion (transition duration and easing), and elevation (box shadow values).

-

---

-

Design Token Mapping

-

If your design system already emits CSS custom properties — through Style Dictionary, Theo, or a Figma Tokens pipeline — mapping to `@ngaf/chat` is a one-to-one alias operation. Define the mappings in a single stylesheet or Angular style encapsulation block. Avoid hardcoding raw values into `--cp-` properties directly; alias through your own tokens so that upstream design system changes propagate automatically.

-

---

-

Typography Integration

-

Font family, size scale, and line height are independently configurable. `--cp-font-family` accepts any valid CSS font stack. Size tokens (`--cp-text-sm`, `--cp-text-base`, `--cp-text-lg`) map to the message body, metadata labels, and heading contexts respectively. If your type scale uses `rem` values derived from a root size override, `@ngaf/chat` respects document root sizing — no unit conversion required.

-

---

-

Color System Integration

-

Surface tokens (`--cp-surface-base`, `--cp-surface-raised`, `--cp-surface-input`) control background layering. Text tokens handle primary content, secondary metadata, and disabled states. `--cp-accent` drives interactive elements — send buttons, link text, focus rings. Semantic tokens (`--cp-color-error`, `--cp-color-warning`, `--cp-color-success`) control inline status indicators on message delivery states and tool call results. Map these directly to your semantic palette.

-

---

-

Dark Mode

-

Because the entire visual surface is token-driven, dark mode requires no component changes. Apply alternate token values under a `[data-theme="dark"]` attribute selector or a `prefers-color-scheme` media query. The component renders whatever the cascade resolves. If your Angular application manages theme switching through a class or attribute on `` or ``, the chat components respond automatically.

-

---

-

Brand Override Example

-
cp-chat-window {
-  --cp-font-family: 'Inter', sans-serif;
-  --cp-surface-base: var(--ds-background-primary);
-  --cp-surface-raised: var(--ds-background-elevated);
-  --cp-surface-input: var(--ds-background-secondary);
-  --cp-text-primary: var(--ds-text-default);
-  --cp-text-secondary: var(--ds-text-subtle);
-  --cp-accent: var(--ds-color-brand-500);
-  --cp-border-radius-md: var(--ds-radius-md);
-  --cp-color-error: var(--ds-color-critical);
-  --cp-transition-duration: 150ms;
-}
-
-

Ten properties. Full brand alignment. No component source required.

-

---

-

Token Limits and the Headless Tier

-

CSS custom properties cannot retheme structural decisions: message bubble layout, avatar placement geometry, the composition of the input toolbar, or the ordering of action elements. If your design system requires layout-level divergence — for example, a top-mounted input bar or a two-column transcript layout — the token API will not reach that far. That is the correct boundary for dropping to the headless tier, where `@ngaf/chat/headless` exposes behavior and state without any rendered output, and your team owns the template entirely. Use tokens for brand alignment; use headless for structural ownership.

-
-
-
Chapter 4
-

Generative UI in Chat

-

Chapter 7: Generative UI in Chat

-

Text responses have a ceiling. When a financial agent needs to surface a sortable data table or a scheduling agent needs to render an interactive booking form, prose falls short. Generative UI closes that gap by allowing agents to emit structured UI specifications directly into the message stream — specifications that the chat interface resolves into live Angular components.

-

How Generative UI Messages Work in the Stream

-

From the chat renderer's perspective, a generative UI message is just another message type. The message stream carries a `type` discriminator — `text`, `tool_call`, `ui_spec`, and so on. When `@ngaf/chat` encounters a `ui_spec` message, it routes it to the UI renderer instead of the text pipeline. The result is that a data table, approval card, or booking form appears inline between conversational turns, indistinguishable in position from any other message bubble, but rendered as a fully interactive Angular component.

-

The json-render Spec

-

The json-render specification is a JSON-based declarative format for describing UI trees. An agent emits a payload describing component type, props, and children. The chat layer deserializes that payload and resolves each component node against a registry. Because the spec is declarative and serializable, it travels cleanly over SSE or WebSocket streams and requires no client-side code changes when an agent starts emitting new component types — only a registry entry.

-

Google's A2UI Spec

-

A2UI extends the base idea with agent-specific primitives that json-render doesn't cover. It introduces first-class concepts for approval flows (a structured action requires explicit user confirmation before the agent proceeds), structured data cards with typed field schemas, and multi-step task UI. Where json-render is a general rendering protocol, A2UI is opinionated about agent interaction patterns. `@ngaf/chat` supports both specs out of the box, detecting the discriminator on the incoming message and routing accordingly.

-

Integration with @ngaf/render

-

The rendering layer is handled by `@ngaf/render`, which owns component resolution, prop binding, and the registry itself. `@ngaf/chat` delegates all spec rendering to `@ngaf/render` — the chat library doesn't need to know how components are resolved, only that the renderer accepts a spec and returns a mounted component. This separation keeps the chat layer focused on message orchestration and keeps the rendering logic reusable outside of chat contexts.

-

The Registry Pattern in Chat

-

Custom components are registered once and become available to any agent that knows the component identifier. The registry maps string keys to Angular component classes. Any component the agent might emit — a custom chart, a domain-specific form, a confirmation widget — needs a corresponding entry.

-
import { provideChatRenderer } from '@ngaf/chat';
-import { provideRenderRegistry } from '@ngaf/render';
-import { DataTableComponent } from './components/data-table.component';
-import { BookingFormComponent } from './components/booking-form.component';
-import { ApprovalCardComponent } from './components/approval-card.component';
-

export const appConfig: ApplicationConfig = { - providers: [ - provideChatRenderer(), - provideRenderRegistry({ - 'data-table': DataTableComponent, - 'booking-form': BookingFormComponent, - 'approval-card': ApprovalCardComponent, - }), - ], -}; -

-

Any component the agent emits by key is resolved here. Unknown keys fall back to a configurable fallback component rather than throwing.

-

Progressive Rendering via Streaming JSON Patches

-

Agents don't emit complete UI specs in a single chunk. They stream JSON patches — RFC 6902 operations that incrementally build the spec as the model generates it. `@ngaf/chat` applies each patch to the in-progress spec and triggers Angular's change detection cycle. The effect is a component that populates live: table rows appear as the agent reasons through the data, form fields materialize in sequence. This isn't a loading spinner followed by a full render — it's the UI itself arriving progressively, which is both faster to first meaningful paint and more transparent about what the agent is doing.

-
-
-
Chapter 5
-

Debug Tooling

-

Chapter 7: Debug Tooling

-

Debugging a running agent is fundamentally different from debugging a REST call. There is no single request to inspect in the network tab. The state lives across a stream of discrete events, tool invocations happen inside the graph before responses surface, and interrupt flows depend on timing between the UI and the server-side checkpoint. Standard browser tooling was not built for this. `chat-debug` was.

-

What chat-debug Exposes

-

`chat-debug` is a developer overlay built into `@ngaf/chat`. When active, it renders a resizable panel alongside your chat interface that gives you a live view of four distinct concerns.

-

Raw message state surfaces the full `AIMessage[]` array as it exists in the agent signal at any given moment — not a serialized snapshot, but the live signal value. You can watch content chunks arrive and accumulate during streaming without manually logging signal reads.

-

The streaming event log shows every server-sent event in the order it was received, including event type, timestamp, and raw payload. This is where you catch malformed chunk sequences, unexpected `end` events, and latency gaps between tool execution and the next assistant turn.

-

The tool call state machine tracks each tool invocation through its full lifecycle: `pending`, `executing`, `complete`, or `error`. For each call you see the tool name, the serialized input payload, the output value, and wall-clock execution time. When a tool is slow or returning unexpected output, this panel removes the guesswork.

-

Interrupt state shows the full interrupt payload at the moment the graph pauses, including the checkpoint ID and any structured data the graph passed to the UI. After the user responds, the panel records what was submitted. If your interrupt flow has a bug, the before-and-after diff is right there.

-

Adding chat-debug

-

Drop `ChatDebugComponent` into any host component that already uses the `@ngaf/chat` primitives. No providers, no configuration file. In dev mode it activates automatically.

-
import { ChatDebugComponent } from '@ngaf/chat/debug';
-

@Component({ - imports: [ChatDebugComponent], - template: ` - - - ` -}) -export class AppShell {} -

-

`chat-debug` reads from the same injected `AgentChatService` instance your chat interface uses. There is no secondary data pipeline and no performance overhead on the message handling path itself.

-

Inspecting Messages and Tool Calls

-

Clicking any message in the raw state view expands it into a structured inspector showing message type, role, content blocks, and metadata fields. Streamed messages show their final assembled content alongside a chunk count. Tool call messages link directly to the corresponding entry in the tool call state machine view.

-

The tool call inspector is the most useful surface for backend integration bugs. Input and output payloads are formatted as syntax-highlighted JSON. Execution timing lets you correlate slow tool responses with latency you see in the event log.

-

Angular DevTools Integration

-

Agent signals registered through `@ngaf/chat` appear as named signals in the Angular DevTools component tree. You can inspect `messageStream`, `interruptState`, and `toolCallRegistry` directly in the DevTools panel without instrumentation code. The signal graph reflects live state, so you can pause, inspect, and resume without affecting the stream.

-

Production Safety

-

`chat-debug` uses a build-time token that resolves to a no-op component in production. The overlay never renders, and tree-shaking removes the debug module entirely from the production bundle. No conditional guards in application code are required. The separation is enforced at the library level.

-

When a stream misbehaves in development, `chat-debug` gives you the full picture in one place. That is its only job, and it does it without adding complexity to your application architecture.

-
- - - \ No newline at end of file diff --git a/apps/website/public/whitepapers/chat.pdf b/apps/website/public/whitepapers/chat.pdf deleted file mode 100644 index eb893a7e1..000000000 Binary files a/apps/website/public/whitepapers/chat.pdf and /dev/null differ diff --git a/apps/website/public/whitepapers/render-preview.html b/apps/website/public/whitepapers/render-preview.html deleted file mode 100644 index 3e47c7464..000000000 --- a/apps/website/public/whitepapers/render-preview.html +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - - - - -
-
@ngaf/render · Enterprise Guide
-

The
Enterprise
Guide
to
Generative
UI
in
Angular

-

Agents that render UI — without coupling to your frontend

-
cacheplane.io · 2026
-
- - -
-

Contents

- -
- 01 - The Coupling Problem -
-
- 02 - Declarative UI Specs & the json-render Standard -
-
- 03 - The Component Registry -
-
- 04 - Streaming JSON Patches -
-
- 05 - State Management & Computed Functions -
-
- - - -
-
Chapter 1
-

The Coupling Problem

-

Chapter 3: The Coupling Problem

-

The Antipattern You're Already Writing

-

If you've integrated a generative agent into an Angular application, you've probably written something like this:

-

-  
-  
-  
-  
-
-
-

This is clean, readable, and completely reasonable for a prototype. It is also quietly catastrophic for a production system at scale.

-

The problem isn't the switch statement. The problem is what the switch statement represents: a hardcoded, implicit contract between two systems that were never designed to evolve together.

-

---

-

Bidirectional Dependency

-

When a component switches on `agentOutput.type`, you've created a dependency that runs in both directions. The frontend depends on the agent emitting specific, known type strings. The agent—or more precisely, the engineers tuning it—starts depending on the frontend to remain stable. If you add a `'timeline'` component to your library, that string has to appear in the agent's output schema before the component is useful. If the agent starts emitting `'comparison-table'` to reflect a new capability, the frontend silently falls through to the fallback until a developer manually adds the case.

-

Neither system can move independently. You've coupled iteration velocity in two entirely separate codebases through an undocumented string literal.

-

---

-

Frontend Engineers Become the Bottleneck

-

In practice, this coupling has a human cost. Every time an AI or backend team adds an agent capability, extends an output type, or restructures a response payload, a frontend engineer has to be involved. They need to add a case, wire a component, test the binding, and ship a deploy. What should be a model-layer change becomes a cross-team sprint ticket.

-

At a single-agent, single-frontend scale, this is annoying. At enterprise scale—multiple agents serving multiple products across multiple frontend teams—it becomes an organizational blocker. Agent iteration speed, which is inherently fast and experimental, gets throttled to frontend release cadence. Teams start avoiding capability changes not because they're technically hard, but because the coordination overhead is too expensive.

-

---

-

Where It Breaks at Scale

-

Imagine three agents: a customer support agent, an internal analytics agent, and a sales assistant. Each is consumed by two separate Angular applications maintained by different teams. Each application has its own `ngSwitch` tree, its own component mappings, its own interpretation of what `type: 'chart'` means in terms of props and layout. When the analytics agent adds a new output format, the change has to propagate to six switch statements across two codebases owned by teams with different priorities and deployment schedules.

-

The system has no center of gravity. It has six points of failure.

-

---

-

The Right Mental Model: Agents Emit UI Specs

-

The architectural shift is conceptually simple: agents should not emit data that the frontend decides how to render. Agents should emit UI specifications—declarative descriptions of intent that a rendering layer interprets. The agent says "render a comparison table with these columns and this data." The frontend honors that intent using whatever component best fits its design system.

-

This separates concerns cleanly. Agents own what to show. Frontends own how to show it.

-

---

-

The Case for an Open Spec

-

This architecture only works if the specification format is shared, stable, and not owned by any single team. Proprietary formats recreate the coupling problem at a higher layer—now your agents are locked to your renderer, and your renderer is locked to your schema definitions.

-

What this problem space needs is an open, versioned UI specification standard: something analogous to what OpenAPI did for REST contracts. A format that agent frameworks can target, that Angular renderers can implement, and that multiple teams can build tooling around without negotiating the schema on every iteration.

-

That standard doesn't fully exist yet. The rest of this guide is about building toward it.

-
-
-
Chapter 2
-

Declarative UI Specs & the json-render Standard

-

Chapter 4: Declarative UI Specs & the json-render Standard

-

The Contract Between Agent and Interface

-

When a language model generates UI, you need a stable contract between what the model emits and what your frontend renders. Without one, you're parsing freeform HTML strings, maintaining brittle prompt templates, and coupling your rendering logic to whichever model happens to be in production this week. The json-render specification solves this by defining UI as structured, framework-agnostic JSON. The agent emits a document. The frontend interprets it. Neither side cares how the other is built.

-

Anatomy of a json-render Document

-

A json-render document is a tree of nodes. Each node carries three fields: `type`, `props`, and `children`.

-
{
-  "type": "DataTable",
-  "props": {
-    "columns": ["Region", "Revenue", "YoY Growth"],
-    "caption": "Q3 Performance Summary",
-    "striped": true
-  },
-  "children": [
-    {
-      "type": "TableRow",
-      "props": { "highlight": true },
-      "children": [
-        { "type": "TableCell", "props": { "value": "EMEA" } },
-        { "type": "TableCell", "props": { "value": "$4.2M" } },
-        { "type": "TableCell", "props": { "value": "+18%" } }
-      ]
-    }
-  ]
-}
-
-

`type` maps to a registered component. `props` are typed inputs. `children` are recursively resolved. The renderer never sees template syntax — just a node graph it already knows how to walk.

-

Why an Open Standard Matters

-

Framework-agnostic specs create portability. An agent prompt that targets json-render works whether the frontend is Angular, React, or Vue. Swap the renderer; keep the prompt. This matters at the enterprise level where you may have multiple teams consuming the same agent infrastructure across different stacks.

-

LLM prompt stability is the less obvious benefit. When your output schema is formally specified, you can provide the spec as part of the system prompt — as a JSON Schema or TypeScript interface — and get consistent, validatable output. Models respond well to schemas with explicit field descriptions and constrained `type` enumerations. Deviation rates drop significantly when the model has a schema to conform to rather than an example to imitate.

-

Community tooling compounds over time. Validators, mock generators, visual debuggers, and testing utilities built against the spec work across every implementation.

-

Conditional Rendering, Iteration, and Computed Properties

-

json-render handles dynamic behavior declaratively. Conditionals use a `$if` directive on the node:

-
{ "type": "AlertBanner", "props": { "message": "Quota exceeded" }, "$if": "metrics.quotaUsed > 0.9" }
-
-

Iteration uses `$for` with a binding expression:

-
{ "type": "UserCard", "$for": "user in users", "props": { "name": "{{user.name}}", "role": "{{user.role}}" } }
-
-

Computed properties use `{{expression}}` interpolation within prop values. Expressions are evaluated against a runtime context object supplied by the host application. The spec deliberately keeps the expression language minimal — it is not a scripting environment.

-

Google's A2UI Extension

-

Google's A2UI spec extends json-render for agent-native patterns. It introduces `intent` nodes, which describe what the agent wants to accomplish rather than prescribing a specific component. A2UI also formalizes `action` bindings — structured event handlers that dispatch typed payloads back to the agent rather than calling arbitrary JavaScript. For teams building bidirectional agent interfaces, A2UI's interaction model is worth adopting alongside the base spec.

-

@ngaf/render in Angular

-

`@ngaf/render` implements json-render for Angular via the `` directive. Pass it a document and a component registry:

-
@Component({
-  template: ``
-})
-export class AgentShellComponent {
-  registry = { DataTable, TableRow, TableCell, AlertBanner, UserCard };
-  agentOutput = signal(null);
-}
-
-

The directive resolves types against the registry, instantiates components dynamically, binds props, and projects children. Streaming is handled through JSON Patch — as the agent streams tokens, partial patches are applied to the document signal incrementally. Components update as their subtree resolves. You get progressive rendering without polling.

-

Prompting LLMs for Spec-Compliant Output

-

Include the json-render JSON Schema in your system prompt. Constrain the `type` field to your registered component names. Instruct the model to emit only valid nodes and avoid HTML interpolation. Few-shot examples accelerate conformance, but schema enforcement does more work. Validate output at the boundary with a schema validator before passing documents to `` — invalid nodes should be caught there, not inside your component tree.

-

The spec is the API. Treat it like one.

-
-
-
Chapter 3
-

The Component Registry

-

Chapter 4: The Component Registry

-

The component registry is the load-bearing joint between a json-render document and your Angular application. A render spec can name any component it wants; the registry decides what that name actually means at runtime. Get this layer right and you have a stable, evolvable surface for generative UI. Get it wrong and you have a fragile string-matching problem scattered across your codebase.

-

Mapping Names to Components

-

`defineAngularRegistry()` accepts a plain object where each key is the component name a spec will reference, and each value is the corresponding Angular component class. Nothing more.

-
import { defineAngularRegistry } from '@ngaf/render';
-import { MetricCardComponent } from './metric-card.component';
-import { DataTableComponent } from './data-table.component';
-import { AlertBannerComponent } from './alert-banner.component';
-import { ChartComponent } from './chart.component';
-

export const appRegistry = defineAngularRegistry({ - MetricCard: MetricCardComponent, - DataTable: DataTableComponent, - AlertBanner: AlertBannerComponent, - Chart: ChartComponent, -}); -

-

The registry object is typed: keys are strings, values must be Angular component classes. If a component class doesn't match what the type system expects, you'll know at compile time rather than at render time.

-

Providing the Registry

-

Register the registry in your application's dependency injection tree using `provideRenderRegistry()`. This belongs in `app.config.ts` alongside your other application-level providers:

-
providers: [
-  provideRenderRegistry(appRegistry),
-]
-
-

From this point on, the render infrastructure can resolve component names anywhere in the component tree without you wiring anything else manually.

-

How `` Resolves Components

-

The `` directive is where resolution actually happens. When it receives a spec node, it reads the `component` field, looks up that string in the injected registry, and dynamically instantiates the matching Angular component into its view container. This is a straightforward `ViewContainerRef.createComponent()` call under the hood — no magic, no reflection, just Angular's native dynamic component API.

-

If the component tree is nested, `` recurses, resolving each child node the same way. The registry lookup happens at render time, not at compile time, which is what makes generative output possible.

-

Input Mapping

-

Once a component is resolved, `` maps the spec's `props` object onto Angular `@Input()` bindings. Each key in `props` that matches a declared `@Input()` on the component class gets assigned directly. Keys that don't match a declared input are silently ignored — the renderer does not throw on extra props, because spec generators frequently include more context than any single component needs.

-

Type coercion is not performed. If your `@Input()` expects a number and the spec delivers a string, that's your contract to enforce, either through input transforms (available in Angular 17+) or a validation layer upstream of the renderer.

-

Handling Unknown Components

-

When `` encounters a component name not present in the registry, the default behavior is to render nothing and emit a warning to the console. For production applications, that's usually insufficient. The registry accepts a `fallback` component that receives the original spec node as an input, letting you render a visible placeholder, log to your observability pipeline, or surface a graceful error state to the user. Treat the fallback as an error boundary: scope it, style it, and make failures observable.

-

Registry Versioning

-

Specs generated today may reference component names that your registry won't recognize in six months — or vice versa. Version your registry explicitly by maintaining additive-only additions to registered component names and deprecating rather than removing entries. If a component's contract changes significantly, register it under a new name and keep the old binding pointing to a compatibility wrapper. This mirrors how you'd version a REST API: old consumers keep working while new specs can adopt the updated surface.

-

The registry is your schema. Treat it with the same discipline you'd apply to any other public interface.

-
-
-
Chapter 4
-

Streaming JSON Patches

-

Chapter 7: Streaming JSON Patches

-

Waiting for a complete UI specification before rendering anything is the wrong default. If your agent is generating a 50-row data table, your users should see the first rows within milliseconds of the agent starting output — not after a three-second round trip to receive, parse, and hydrate the full JSON payload. Streaming is what separates a generative UI from a slow API call with extra steps.

-

Why Full-Document Streaming Falls Apart

-

The naive approach is to re-emit the entire UI spec on each token or checkpoint. This breaks down fast. A moderately complex spec — tables, nested cards, conditional sections — can run to tens of kilobytes. Replacing the full document on every update means the renderer diffs the entire tree on every tick, Angular's change detection fires against a new object reference each time, and you're pushing O(spec size) work through the pipe regardless of how small the actual change was. At scale, this becomes a throughput problem that no amount of `OnPush` optimization will fully compensate for.

-

JSON Patch: The Right Primitive

-

RFC 6902 defines JSON Patch as a sequence of operations — `add`, `replace`, `remove`, `move`, `copy`, `test` — applied to a target JSON document using JSON Pointer paths. Rather than transmitting the document, the agent emits only what changed:

-
[
-  { "op": "add", "path": "/rows/49", "value": { "id": 50, "name": "..." } },
-  { "op": "replace", "path": "/status", "value": "complete" }
-]
-
-

This is the correct abstraction for generative UI streaming. The agent's output token stream maps cleanly to patch operations: a new row is an `add`, a corrected value is a `replace`, a retracted field is a `remove`. The frontend accumulates these deltas against a baseline spec document rather than swapping documents wholesale.

-

Partial-JSON Parsing and Skeleton States

-

Before a complete patch operation has arrived, you need somewhere to be. `@ngaf/render` handles this through two mechanisms working in parallel.

-

First, partial-JSON parsing allows the renderer to extract and apply structurally valid operations from an incomplete stream. If three operations are fully formed and the fourth is mid-token, the first three are applied immediately. The incomplete operation is buffered until its closing delimiter arrives.

-

Second, skeleton states are declared in the baseline spec. Before any patches arrive, `@ngaf/render` renders placeholder elements — shimmer rows, empty card outlines, header shells — based on the spec's `skeleton` annotations. As patches land, real content replaces placeholders in place. Users see a structured, stable layout from the first render frame.

-

Consuming Patch Events in Angular

-
@Component({ changeDetection: ChangeDetectionStrategy.OnPush })
-export class ReportViewComponent {
-  private render = inject(CacheplaneRenderService);
-

spec = signal(BASELINE_SPEC);

-

ngOnInit() { - this.render - .streamPatches('/api/agent/report') - .pipe(takeUntilDestroyed()) - .subscribe(patch => { - this.spec.update(current => applyPatch(current, patch).newDocument); - }); - } -} -

-

`streamPatches` returns an `Observable` backed by an `EventSource`. Each emission is a discrete, valid patch batch. `applyPatch` from `fast-json-patch` is synchronous and returns the mutated document. Because `spec` is a signal, Angular schedules a targeted re-render without a full zone traversal.

-

Performance Characteristics

-

Patch-based updates are O(change), not O(spec size). A 50-row table receiving its fifty-first row processes exactly one `add` operation. The rest of the spec is untouched in memory and the renderer skips it entirely. This holds regardless of how large the accumulated spec grows — the cost of each update is proportional to the diff, not the document. For enterprise UIs where specs grow complex over a session, this is not a micro-optimization; it's the architecture that keeps rendering predictable under load.

-
-
-
Chapter 5
-

State Management & Computed Functions

-

Chapter 4: State Management & Computed Functions

-

Static specs describe structure. They fall short the moment your UI needs to reflect derived state — a total calculated from line items, a label that changes based on a flag, a list of cards rendered from an array. For generative UI to be production-ready, the spec itself must support dynamic behavior. `@ngaf/render` addresses this through `signalStateStore()`, computed properties, and repeat loops — mechanisms that let an agent define reactive UI logic declaratively, without pushing that responsibility into custom component code.

-

signalStateStore(): Agent-Managed Reactive State

-

`signalStateStore()` creates a Signal-backed state container that both the rendering engine and your Angular components can read from and write to. When an agent generates a spec, it can seed this store with initial values. Components rendered from that spec subscribe to the store automatically — no `@Input()` wiring, no manual change detection.

-
const store = signalStateStore({
-  quantity: 3,
-  unitPrice: 49.99,
-  discountRate: 0.1,
-  label: 'computed:subtotal * (1 - discountRate)',
-  subtotal: 'computed:quantity * unitPrice',
-  items: [
-    { id: 1, name: 'Widget A' },
-    { id: 2, name: 'Widget B' },
-  ],
-});
-
-

The store holds both raw values and computed expressions as first-class fields. The rendering engine resolves computed fields at render time and keeps them reactive — when `quantity` updates, anything that depends on `subtotal` or `label` updates automatically through Angular's Signal graph.

-

Computed Properties: Derived State in the Spec

-

Computed expressions are string-prefixed with `computed:` and evaluated against the current store context. The engine parses these at spec load time and registers them as Angular `computed()` signals derived from their dependencies.

-

This means an agent can express UI logic like conditional labels, formatted totals, or visibility rules entirely within the spec payload. No custom pipe. No component method. The expression evaluates deterministically given the same store state, which matters both for correctness and for testing.

-

The expression language is intentionally constrained — arithmetic, string interpolation, ternary operators, and property access. Complex branching belongs in components, not in spec strings.

-

Repeat Loops: Rendering Collections

-

When a spec field is an array, the `repeat` directive instructs the renderer to iterate and instantiate a child component template for each element:

-
{
-  "type": "repeat",
-  "source": "items",
-  "template": { "type": "card", "title": "{{item.name}}" }
-}
-
-

The renderer binds each iteration's context to the current element, making `item` available as a local reference within the template. This covers the majority of list-rendering patterns without requiring a custom Angular component to manage the loop.

-

Drawing the Line Between Spec Logic and Component Logic

-

The boundary is load-bearing. Computed expressions in the spec are appropriate for derivations that the agent needs to express — formatting, simple math, conditional values driven by store state. They are not appropriate for side effects, async operations, service calls, or branching logic with more than two outcomes.

-

If a computed expression starts to read like a function body, that logic belongs in a component. The spec should describe *what* the UI reflects, not *how* it gets there.

-

Testing Computed Functions

-

Because computed expressions evaluate against a plain store object, they are fully testable without rendering:

-
const store = signalStateStore({ quantity: 4, unitPrice: 25 });
-expect(resolveComputed('quantity * unitPrice', store)).toBe(100);
-
-

`resolveComputed()` is a pure utility export — no DOM, no `TestBed`, no component fixture. This makes computed logic fast to validate in unit tests and straightforward to include in CI pipelines that verify agent-generated specs before they reach the renderer.

-

Deterministic evaluation is a feature, not a side effect. It means you can test agent output like any other pure function.

-
- - - \ No newline at end of file diff --git a/apps/website/public/whitepapers/render.pdf b/apps/website/public/whitepapers/render.pdf deleted file mode 100644 index 77736bd83..000000000 Binary files a/apps/website/public/whitepapers/render.pdf and /dev/null differ diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts deleted file mode 100644 index b13089359..000000000 --- a/apps/website/scripts/generate-whitepaper.ts +++ /dev/null @@ -1,660 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk'; -import fs from 'fs'; -import path from 'path'; -import puppeteer from 'puppeteer'; - -const client = new Anthropic(); -const MODEL = process.env['ANTHROPIC_MODEL'] ?? 'claude-opus-4-5'; - -// ── Config type ────────────────────────────────────────────────────────── -interface WhitepaperConfig { - id: string; - title: string; - subtitle: string; - eyebrow: string; - coverGradient: string; - outputPdf: string; - outputHtml: string; - chapters: Array<{ id: string; title: string; prompt: string }>; -} - -// ── Whitepaper configs ─────────────────────────────────────────────────── -const WHITEPAPERS: Record = { - overview: { - id: 'overview', - title: 'From Prototype to Production', - subtitle: 'The Angular Agent Readiness Guide', - eyebrow: 'StreamResource · Production Readiness Guide', - coverGradient: 'linear-gradient(135deg,#fef0f3 0%,#f4f0ff 45%,#eaf3ff 70%,#e6f4ff 100%)', - outputPdf: 'apps/website/public/whitepaper.pdf', - outputHtml: 'apps/website/public/whitepaper-preview.html', - chapters: [ - { - id: 'streaming-state', - title: 'Streaming State Management', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". - -Chapter topic: Streaming State Management - -Context: Angular teams building LangGraph-powered agents must wire SSE event streams into reactive UI. Without the right primitives, they end up with custom zone-patching, manual subscription management, and brittle token accumulation logic that breaks under load. - -Cover: -- Why streaming state is hard in Angular (zone.js, change detection, timing) -- The signals-native approach: how streamResource() exposes messages() as Signal -- How isStreaming() lets developers drive loading UI without polling -- Code example: minimal streamResource() setup (TypeScript snippet, 8-12 lines) -- Production checklist item: "Are your message signals OnPush-compatible?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'thread-persistence', - title: 'Thread Persistence', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". - -Chapter topic: Thread Persistence - -Context: Demos work with ephemeral state. Production agents need conversation history that survives page refreshes, tab switches, and navigation — wired to LangGraph's MemorySaver backend. - -Cover: -- Why stateless agent UIs fail in production -- The threadId signal and onThreadId callback pattern -- How to persist threadId to localStorage and restore on mount -- Thread list UI and switching between conversations -- Code example: provideStreamResource() with threadId (8-12 lines) -- Production checklist item: "Does your agent UI resume threads correctly after a browser refresh?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'tool-call-rendering', - title: 'Tool-Call Rendering', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". - -Chapter topic: Tool-Call Rendering - -Context: LangGraph agents invoke tools mid-stream. The UI needs to show tool execution state in real time — steps appearing as the tool runs, a final result, and collapsible history — without parsing raw SSE events by hand. - -Cover: -- What tool call events look like in the raw stream -- Why hand-parsing is fragile and hard to test -- The headless primitive and prebuilt option -- Progressive disclosure: showing steps live, collapsing on completion -- Code example: binding (8-12 lines of Angular template) -- Production checklist item: "Do your tool call cards handle partial step state during streaming?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'human-approval-flows', - title: 'Human Approval Flows', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". - -Chapter topic: Human Approval Flows (Interrupts) - -Context: Production agents that take consequential actions — sending emails, deploying services, modifying data — must pause for human approval before proceeding. This requires a tight loop between LangGraph's interrupt() primitive and Angular UI. - -Cover: -- The LangGraph interrupt() and Command.RESUME pattern -- Why polling and custom websocket approaches are brittle -- The interrupt() signal in streamResource() and how it maps to approval state -- headless and prebuilt -- The three approval actions: approve, edit, cancel — and how each maps to a resume command -- Code example: interrupt signal binding (8-12 lines) -- Production checklist item: "Can your agent UI recover gracefully if a user cancels an interrupt?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'generative-ui', - title: 'Generative UI', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". - -Chapter topic: Generative UI - -Context: The most advanced production agents emit structured UI specs — not just text. A data analysis agent might render a live table. A booking agent might render a reservation form. Without a framework for this, teams either hardcode component logic into the agent or skip the feature entirely. - -Cover: -- The onCustomEvent pattern in LangGraph: how agents emit structured data -- The @ngaf/render approach: json-render specs, defineAngularRegistry(), -- How JSON patch streaming enables progressive UI updates (rows appearing as data arrives) -- The registry pattern: decoupling agent from component implementation -- Code example: defineAngularRegistry() registration (8-12 lines) -- Production checklist item: "Can your agent emit UI components without tight coupling to the frontend codebase?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'deterministic-testing', - title: 'Deterministic Testing', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "From Prototype to Production: The Angular Agent Readiness Guide". - -Chapter topic: Deterministic Testing - -Context: Agent UIs are notoriously hard to test because they depend on live LLM responses. Flaky tests, slow CI, and inability to reproduce edge cases are the main reasons agent UIs ship with low confidence. - -Cover: -- Why testing agent components against real LLM APIs is impractical -- The MockStreamTransport approach: scripted event sequences, no server needed -- createMockStreamResourceRef(): writable signals you control directly in tests -- How to test streaming, interrupts, tool calls, and generative UI in isolation -- Code example: createMockStreamResourceRef() test pattern (10-14 lines) -- Production checklist item: "Do your agent component tests run offline and complete in under 100ms each?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - ], - }, - - angular: { - id: 'angular', - title: 'The Enterprise Guide to Agent Streaming in Angular', - subtitle: 'Ship LangGraph agents in Angular — without building the plumbing', - eyebrow: '@ngaf/langgraph · Enterprise Guide', - coverGradient: 'linear-gradient(135deg, #eaf3ff 0%, #e6f4ff 45%, #f4f0ff 70%, #fef0f3 100%)', - outputPdf: 'apps/website/public/whitepapers/angular.pdf', - outputHtml: 'apps/website/public/whitepapers/angular-preview.html', - chapters: [ - { - id: 'last-mile-problem', - title: 'The Last-Mile Problem', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Streaming in Angular". - -Chapter topic: The Last-Mile Problem - -Context: Teams have built powerful LangGraph backends with sophisticated agent graphs, tool calling, and memory. Then they hit Angular. SSE streams don't integrate cleanly with zone.js. Signals and change detection are at odds with streaming event sequences. The backend works — the frontend gap is real and expensive. - -Cover: -- Why SSE + Angular zones is a zone pollution problem, not a configuration problem -- How token streaming conflicts with Angular's synchronous change detection model -- The signal reactivity mismatch: LLM streams are push-based, Angular templates expect pull -- Why existing RxJS patterns from REST don't translate to streaming agent responses -- The cost: teams building custom zone-patch wrappers, token accumulators, and error retry logic from scratch on every project -- The gap between "it works in the demo" and "it's production-safe in Angular" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'agent-api', - title: 'The agent() API', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Streaming in Angular". - -Chapter topic: The agent() API - -Context: @ngaf/langgraph exposes a signal-native API for streaming LangGraph agents into Angular components. The core primitive is agent() — a function that returns reactive signals wired directly to the agent stream, with no manual subscription management, no zone-patching, and no token accumulation logic. - -Cover: -- How agent() returns a structured ref with typed signals: messages(), isStreaming(), error(), interrupt() -- The provideAgent() provider and how it configures the agent endpoint and stream transport -- Why the signal-native design works with OnPush change detection out of the box -- How to bind agent state directly in Angular templates without async pipe or manual subscriptions -- Code example: minimal agent() setup with template binding (10-14 lines) -- The contrast: what the equivalent hand-rolled code looks like vs. agent() in 3 lines - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'thread-persistence-memory', - title: 'Thread Persistence & Memory', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Streaming in Angular". - -Chapter topic: Thread Persistence & Memory - -Context: Production agent applications are stateful across sessions. Users expect to return to a conversation where they left off. This requires coordinating LangGraph's MemorySaver backend with a frontend that can persist, restore, and switch between threads. - -Cover: -- The threadId signal: how it flows from LangGraph's checkpoint system to the Angular frontend -- Persisting threadId to localStorage on creation via the onThreadId callback -- Restoring thread state on component mount: initialThreadId input and what it triggers -- Building a thread list UI: listing stored thread IDs, switching active thread, clearing history -- How LangGraph MemorySaver maps to the frontend thread lifecycle -- Code example: provideAgent() with thread persistence pattern (8-12 lines) -- Production checklist: "Does your thread list handle deleted or expired server-side threads gracefully?" - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'interrupt-approval-flows', - title: 'Interrupt & Approval Flows', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Streaming in Angular". - -Chapter topic: Interrupt & Approval Flows - -Context: Agents that take real-world actions — sending emails, executing queries, modifying records — must pause for human confirmation. LangGraph's interrupt() primitive enables this on the backend. @ngaf/langgraph surfaces it as a reactive signal, eliminating the need for polling, websockets, or custom resume endpoints. - -Cover: -- How LangGraph interrupt() pauses graph execution and what the resume payload looks like -- The interrupt() signal in the agent ref: how it transitions from null to an InterruptState object -- Command.RESUME and how the three actions (approve, edit, cancel) each map to different resume payloads -- The prebuilt: approval UI out of the box, no template work needed -- Handling the edge cases: user navigates away mid-interrupt, session expires, cancel with partial state -- Code example: interrupt signal binding with approve/cancel handlers (8-12 lines) - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'full-langgraph-coverage', - title: 'Full LangGraph Feature Coverage', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Streaming in Angular". - -Chapter topic: Full LangGraph Feature Coverage - -Context: Most Angular LLM integrations support basic chat. @ngaf/langgraph is designed for the full LangGraph feature surface: tool calls, subgraphs, time travel, and DeepAgent multi-agent coordination. Teams shouldn't have to drop down to raw SSE parsing to access advanced graph features. - -Cover: -- Tool call streaming: how tool invocation events surface through the agent ref without manual parsing -- Subgraph support: how nested graph events bubble through the stream and into Angular signals -- Time travel: rewinding graph state to a prior checkpoint and re-streaming from that point -- DeepAgent: what multi-agent coordination looks like at the stream level and how it maps to Angular signals -- The onCustomEvent hook: consuming structured non-message agent output (for generative UI, analytics, etc.) -- Why full coverage matters: avoiding the pattern where teams bypass the library for advanced features - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'deterministic-testing-angular', - title: 'Deterministic Testing', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Streaming in Angular". - -Chapter topic: Deterministic Testing - -Context: Angular component tests for agent UIs are only useful if they run fast, offline, and deterministically. Real LLM calls in tests mean slow CI, flaky outcomes, and inability to reproduce specific edge cases like interrupted streams or tool call errors. - -Cover: -- MockStreamTransport: scripting a deterministic sequence of SSE events without a server -- createMockStreamResourceRef(): directly controlling signal values in Angular component tests -- How to test each agent state: streaming in progress, stream complete, interrupt pending, error state -- Testing tool call rendering, generative UI output, and thread switching in isolation -- TestBed setup with MockStreamTransport (8-12 lines) -- The benchmark: agent component tests should run offline and complete in under 100ms each - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - ], - }, - - render: { - id: 'render', - title: 'The Enterprise Guide to Generative UI in Angular', - subtitle: 'Agents that render UI — without coupling to your frontend', - eyebrow: '@ngaf/render · Enterprise Guide', - coverGradient: 'linear-gradient(135deg, #e8f5e9 0%, #eaf3ff 45%, #f4f0ff 70%, #fef0f3 100%)', - outputPdf: 'apps/website/public/whitepapers/render.pdf', - outputHtml: 'apps/website/public/whitepapers/render-preview.html', - chapters: [ - { - id: 'coupling-problem', - title: 'The Coupling Problem', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Generative UI in Angular". - -Chapter topic: The Coupling Problem - -Context: When agents emit structured output that needs to become UI, the naive approach is to hardcode the mapping in the frontend: inspect the agent output, switch on a type field, render a component. This works for demos. In production, it means every agent capability change requires a frontend deploy, component library changes leak into agent prompts, and iteration speed collapses. - -Cover: -- The antipattern: ngSwitch on agent output type hardcoded in every consuming component -- Why this creates a bidirectional dependency between agent logic and frontend implementation -- The real cost: frontend engineers become blockers on every agent capability change -- How this pattern breaks at scale — multiple agents, multiple frontends, multiple teams -- What a decoupled architecture looks like: agents emit UI specs, frontends interpret them -- The open standard opportunity: why this needs a shared spec, not a proprietary format - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'json-render-standard', - title: 'Declarative UI Specs & the json-render Standard', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Generative UI in Angular". - -Chapter topic: Declarative UI Specs & the json-render Standard - -Context: Vercel's json-render spec defines a framework-agnostic standard for describing UI as structured JSON. An agent emits a json-render document. A frontend interprets it. Neither side knows how the other is implemented. @ngaf/render implements this standard for Angular, with streaming JSON patch support on top. - -Cover: -- What a json-render document looks like: component name, props, children (concrete example) -- Why an open standard matters: portability across frameworks, LLM prompt stability, community tooling -- How the spec handles conditional rendering, iteration, and computed properties -- Google's A2UI spec and how it extends json-render for agent-specific patterns -- The @ngaf/render implementation: directive consumes a json-render document -- How LLMs generate valid json-render output: prompt patterns that produce spec-compliant JSON - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'component-registry', - title: 'The Component Registry', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Generative UI in Angular". - -Chapter topic: The Component Registry - -Context: A json-render document references components by name. The registry is what maps those names to actual Angular components. defineAngularRegistry() is the @ngaf/render API for declaring this mapping — it's the seam between the open standard and your specific component library. - -Cover: -- How defineAngularRegistry() maps string component names to Angular component classes -- Providing the registry via provideRenderRegistry() for dependency injection -- The directive: how it resolves component names at render time -- Input mapping: how json-render props become Angular @Input() bindings -- Handling unknown component names: fallback rendering and error boundaries -- Code example: defineAngularRegistry() with 3-4 registered components (10-14 lines) -- Registry versioning: how to handle spec evolution without breaking existing renders - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'streaming-json-patches', - title: 'Streaming JSON Patches', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Generative UI in Angular". - -Chapter topic: Streaming JSON Patches - -Context: Generative UI is most powerful when it streams. A data table with 50 rows should appear progressively — rows rendering as the agent produces them, not after a 3-second wait for the full JSON payload. @ngaf/render uses JSON Patch (RFC 6902) to apply incremental updates to the UI spec as it streams, enabling skeleton states and progressive rendering. - -Cover: -- Why streaming full JSON documents on each update is impractical for large UI specs -- JSON Patch RFC 6902: add, replace, remove operations on a JSON document -- How the agent emits patch operations instead of full spec replacements -- Partial-JSON parsing: rendering valid portions of an incomplete JSON stream -- Skeleton states: how to show placeholder UI while the spec is still arriving -- Code example: consuming streaming patch events in a @ngaf/render component (8-12 lines) -- Performance: why patch-based updates are O(change) not O(spec size) - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'state-management-computed', - title: 'State Management & Computed Functions', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Generative UI in Angular". - -Chapter topic: State Management & Computed Functions - -Context: Static UI specs only go so far. Production generative UI needs computed properties — values derived from other spec fields — and repeat loops for rendering collections. @ngaf/render's signalStateStore() and computed function support bring dynamic behavior into the spec without requiring custom component logic. - -Cover: -- signalStateStore(): agent-managed state that components can read and update -- Computed properties in the json-render spec: expressions evaluated at render time -- Repeat loops: iterating over spec arrays to render collections of components -- How computed functions let agents define derived UI state declaratively -- The boundary between spec-level logic and component-level logic — where to draw the line -- Code example: signalStateStore() with computed properties in a spec (8-12 lines) -- Testing computed functions: deterministic evaluation without rendering - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - ], - }, - - chat: { - id: 'chat', - title: 'The Enterprise Guide to Agent Chat Interfaces in Angular', - subtitle: 'Production agent chat UI in days, not sprints', - eyebrow: '@ngaf/chat · Enterprise Guide', - coverGradient: 'linear-gradient(135deg, #f3e8ff 0%, #f4f0ff 45%, #eaf3ff 70%, #e6f4ff 100%)', - outputPdf: 'apps/website/public/whitepapers/chat.pdf', - outputHtml: 'apps/website/public/whitepapers/chat-preview.html', - chapters: [ - { - id: 'sprint-tax', - title: 'The Sprint Tax', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Chat Interfaces in Angular". - -Chapter topic: The Sprint Tax - -Context: Every team building an Angular agent application eventually builds the same chat UI from scratch: message list, input box, streaming token display, auto-scroll, loading states, error handling. It takes 4-6 weeks. Then they iterate on it for another 4-6 weeks. Meanwhile, the agent backend is ready and waiting. - -Cover: -- The inventory of things every chat UI needs: message rendering, streaming display, tool call cards, interrupt panels, auto-scroll, accessibility, mobile layout -- Why building this from scratch on every project is a structural inefficiency, not a skill gap -- The hidden costs: accessibility is harder than it looks, streaming token display has edge cases, tool call state machines are complex -- What "good enough for demo" looks like vs. what production chat UI actually requires -- The opportunity cost: senior Angular engineers spending sprints on chat chrome instead of agent integration -- The @ngaf/chat thesis: ship the chat UI on day one, spend the sprints on what differentiates your product - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'batteries-included-components', - title: 'Batteries-Included Components', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Chat Interfaces in Angular". - -Chapter topic: Batteries-Included Components - -Context: @ngaf/chat ships two tiers of components: headless primitives that own behavior and state with no styling opinions, and prebuilt components that are production-ready out of the box. Teams choose the tier that matches their customization needs. - -Cover: -- The headless tier: chat-messages, chat-input, chat-tool-calls, chat-interrupt — behavior without styling -- The prebuilt tier: chat-prebuilt — a full chat interface in one component, zero configuration -- How the two tiers compose: using prebuilt for 90% of UI, dropping to headless for custom sections -- The component model: how @ngaf/chat connects to the agent ref from @ngaf/langgraph -- Message rendering: how AIMessage[] from the agent signal maps to chat message display -- Code example: with an agent ref (6-10 lines) -- When to use headless vs. prebuilt and how to migrate between them - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'theming-design-system', - title: 'Theming & Design System Integration', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Chat Interfaces in Angular". - -Chapter topic: Theming & Design System Integration - -Context: Chat UI that looks like a generic chatbot is a product liability. Enterprise teams need chat components that match their design system — typography, color palette, border radius, spacing. @ngaf/chat uses CSS custom properties and design tokens to make this integration straightforward without requiring component source access. - -Cover: -- The CSS custom property API: how @ngaf/chat exposes design decisions as variables -- Design token mapping: aligning chat component tokens with your existing design system tokens -- Typography integration: font family, size scale, and line height control -- Color system integration: surface colors, text colors, accent colors, and semantic state colors -- Dark mode: how the token system supports light/dark switching without component changes -- Code example: CSS custom property overrides for a custom brand (10-15 lines of CSS) -- What cannot be themed via tokens — and when to drop to the headless tier instead - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'generative-ui-chat', - title: 'Generative UI in Chat', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Chat Interfaces in Angular". - -Chapter topic: Generative UI in Chat - -Context: The most capable agent chat interfaces don't just display text — they render agent-generated UI directly in the message stream. A financial agent renders a live data table. A scheduling agent renders a booking form. @ngaf/chat supports both the json-render spec and Google's A2UI spec out of the box, with streaming patch support. - -Cover: -- How generative UI messages appear in the chat message stream alongside text messages -- The json-render spec: how agents emit structured UI specs that chat renders automatically -- Google's A2UI spec: what it adds for agent-specific UI patterns (actions, approvals, structured data) -- How @ngaf/chat integrates with @ngaf/render for component resolution -- The registry pattern in a chat context: registering custom components that agents can emit -- Code example: enabling generative UI in chat with a component registry (8-12 lines) -- Progressive rendering: how streaming JSON patches create the live UI update effect in chat - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - { - id: 'debug-tooling', - title: 'Debug Tooling', - prompt: `Write a 400-600 word chapter for an engineering white paper titled "The Enterprise Guide to Agent Chat Interfaces in Angular". - -Chapter topic: Debug Tooling - -Context: Debugging agent chat is hard. The message stream is opaque, tool call state transitions are fast, and interrupt flows have timing edge cases. chat-debug is @ngaf/chat's built-in debug panel — a developer overlay that surfaces agent state, raw message events, tool call history, and interrupt state in real time. - -Cover: -- What chat-debug shows: raw AIMessage[] state, streaming event log, tool call state machine, interrupt payload -- How to add chat-debug to any chat interface: one component, zero configuration in dev mode -- Inspecting individual messages: expanding message content, viewing message type and metadata -- Tool call debugging: seeing tool name, input payload, output, and execution timing -- Interrupt state inspection: viewing the full interrupt payload before and after user action -- Integration with Angular DevTools: how agent signals appear in the component tree -- Production safety: how chat-debug strips itself from production builds - -Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engineers.`, - }, - ], - }, -}; - -// ── Markdown to HTML converter ─────────────────────────────────────────── -function mdToHTML(md: string): string { - return md - .replace(/```[\w]*\n([\s\S]*?)```/g, '
$1
') - .replace(/^### (.+)$/gm, '

$1

') - .replace(/^## (.+)$/gm, '

$1

') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/^- (.+)$/gm, '
  • $1
  • ') - .replace(/(
  • [^\n]+<\/li>\n?)+/g, match => `
      ${match}
    `) - .split('\n\n') - .map(block => { - if (block.startsWith('${trimmed}

    ` : ''; - }) - .join('\n'); -} - -// ── HTML builder ───────────────────────────────────────────────────────── -function buildHTML( - chapters: Array<{ title: string; content: string }>, - config: WhitepaperConfig, -): string { - const tocHTML = chapters.map((ch, i) => ` -
    - ${String(i + 1).padStart(2, '0')} - ${ch.title} -
    `).join(''); - - const chaptersHTML = chapters.map((ch, i) => ` -
    -
    Chapter ${i + 1}
    -

    ${ch.title}

    -
    ${mdToHTML(ch.content)}
    -
    `).join(''); - - return ` - - - - - - - - - - -
    -
    ${config.eyebrow}
    -

    ${config.title.replace(/ /g, '
    ')}

    -

    ${config.subtitle}

    -
    cacheplane.io · ${new Date().getFullYear()}
    -
    - - -
    -

    Contents

    - ${tocHTML} -
    - - -${chaptersHTML} - - -`; -} - -// ── PDF renderer ───────────────────────────────────────────────────────── -async function renderPDF(html: string, outputPath: string): Promise { - console.log(' Launching browser for PDF render...'); - const browser = await puppeteer.launch({ headless: true }); - const page = await browser.newPage(); - await page.setContent(html, { waitUntil: 'networkidle0' }); - await page.pdf({ - path: outputPath, - format: 'A4', - printBackground: true, - margin: { top: '0', right: '0', bottom: '0', left: '0' }, - }); - await browser.close(); -} - -// ── Chapter generator ──────────────────────────────────────────────────── -async function generateChapter( - chapter: WhitepaperConfig['chapters'][0], -): Promise { - console.log(` Generating: ${chapter.title}...`); - const message = await client.messages.create({ - model: MODEL, - max_tokens: 1500, - messages: [{ role: 'user', content: chapter.prompt }], - }); - const content = message.content[0]; - if (content.type !== 'text') throw new Error(`Unexpected content type: ${content.type}`); - return content.text; -} - -// ── Single whitepaper runner ───────────────────────────────────────────── -async function generateWhitepaper(config: WhitepaperConfig): Promise { - console.log(`\n── ${config.id} ─────────────────────────────────────`); - console.log(`Title: ${config.title}`); - console.log(`Output: ${config.outputPdf}\n`); - - const generatedChapters: Array<{ title: string; content: string }> = []; - - for (const chapter of config.chapters) { - const content = await generateChapter(chapter); - generatedChapters.push({ title: chapter.title, content }); - } - - console.log('\nBuilding HTML document...'); - const html = buildHTML(generatedChapters, config); - fs.mkdirSync(path.dirname(config.outputHtml), { recursive: true }); - fs.writeFileSync(config.outputHtml, html, 'utf8'); - console.log(` HTML preview: ${config.outputHtml}`); - - console.log('Rendering PDF...'); - fs.mkdirSync(path.dirname(config.outputPdf), { recursive: true }); - await renderPDF(html, config.outputPdf); - - const stat = fs.statSync(config.outputPdf); - console.log(` PDF saved to ${config.outputPdf} (${Math.round(stat.size / 1024)}KB)`); -} - -// ── Main ───────────────────────────────────────────────────────────────── -async function main() { - console.log('CachePlane White Paper Generator\n'); - console.log(`Model: ${MODEL}`); - - const paperArg = process.argv.find(a => a.startsWith('--paper='))?.split('=')[1] - ?? (process.argv.includes('--paper') ? process.argv[process.argv.indexOf('--paper') + 1] : undefined); - - if (paperArg) { - const config = WHITEPAPERS[paperArg]; - if (!config) { - console.error(`Unknown whitepaper: "${paperArg}". Available: ${Object.keys(WHITEPAPERS).join(', ')}`); - process.exit(1); - } - await generateWhitepaper(config); - } else { - console.log(`Generating all whitepapers: ${Object.keys(WHITEPAPERS).join(', ')}\n`); - for (const config of Object.values(WHITEPAPERS)) { - await generateWhitepaper(config); - } - } - - console.log('\nDone.'); -} - -main().catch(err => { - console.error('Generation failed:', err); - process.exit(1); -}); diff --git a/apps/website/src/app/api/whitepaper-signup/route.ts b/apps/website/src/app/api/whitepaper-signup/route.ts index 276d2a6fb..88ce9c302 100644 --- a/apps/website/src/app/api/whitepaper-signup/route.ts +++ b/apps/website/src/app/api/whitepaper-signup/route.ts @@ -15,18 +15,18 @@ const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson const VALID_PAPERS: PaperId[] = ['overview', 'angular', 'render', 'chat']; -const DOWNLOAD_EMAILS: Record string> = { +const RESPONSE_EMAILS: Record string> = { overview: whitepaperDownloadHtml, angular: angularDownloadHtml, render: renderDownloadHtml, chat: chatDownloadHtml, }; -const DOWNLOAD_SUBJECTS: Record = { - overview: 'Your Angular Agent Readiness Guide', - angular: 'Your Enterprise Guide to Agent Streaming', - render: 'Your Enterprise Guide to Generative UI', - chat: 'Your Enterprise Guide to Agent Chat Interfaces', +const RESPONSE_SUBJECTS: Record = { + overview: 'You are on the Angular Agent Readiness Guide update list', + angular: 'You are on the Agent Streaming Guide update list', + render: 'You are on the Generative UI Guide update list', + chat: 'You are on the Agent Chat Guide update list', }; export async function POST(req: NextRequest) { @@ -55,20 +55,20 @@ export async function POST(req: NextRequest) { console.error('Failed to write signup:', err); } - // Send download confirmation + schedule drip + sync contacts (best-effort) + // Send confirmation + schedule drip + sync contacts (best-effort) try { - const downloadHtml = DOWNLOAD_EMAILS[paper](name || undefined); + const responseHtml = RESPONSE_EMAILS[paper](name || undefined); await Promise.all([ sendEmail({ from: FROM, to: email, - subject: DOWNLOAD_SUBJECTS[paper], - html: downloadHtml, + subject: RESPONSE_SUBJECTS[paper], + html: responseHtml, }), scheduleWhitepaperDrip(email, paper), addToAudience(email, name || undefined), loopsUpsertContact({ email, firstName: name || undefined, source: `whitepaper-${paper}` }), - loopsSendEvent({ email, eventName: 'whitepaper_downloaded', properties: { paper } }), + loopsSendEvent({ email, eventName: 'whitepaper_signup', properties: { paper } }), ]); } catch (err) { console.error('[whitepaper-signup] email pipeline failed:', err); diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index 5681b4628..554a34023 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -26,7 +26,7 @@ export default async function HomePage() { {/* 5. Product — the three-library stack */} - {/* 6. Lead gen — whitepaper download */} + {/* 6. Lead gen — guide update signup */} {/* 7. Final CTA — "Ready to stop stalling?" */} diff --git a/apps/website/src/components/landing/LibrariesSection.tsx b/apps/website/src/components/landing/LibrariesSection.tsx index 1e6c966e7..b4f77e083 100644 --- a/apps/website/src/components/landing/LibrariesSection.tsx +++ b/apps/website/src/components/landing/LibrariesSection.tsx @@ -11,7 +11,7 @@ const LIBRARIES = [ color: tokens.colors.accent, rgb: '0,64,144', oneLiner: 'Signal-native streaming for LangGraph agents', - chips: ['agent()', 'provideAgent()', 'interrupt()', 'MockStreamTransport'], + chips: ['agent()', 'provideAgent()', 'interrupt()', 'MockAgentTransport'], href: '/angular', ctaLabel: 'Explore Angular', }, diff --git a/apps/website/src/components/landing/PilotFooterCTA.tsx b/apps/website/src/components/landing/PilotFooterCTA.tsx index 99eb0e9b0..ec4776b8c 100644 --- a/apps/website/src/components/landing/PilotFooterCTA.tsx +++ b/apps/website/src/components/landing/PilotFooterCTA.tsx @@ -117,8 +117,7 @@ export function PilotFooterCTA() { {/* Secondary CTA */} - Download the Guide + Read the Docs diff --git a/apps/website/src/components/landing/PilotHero.tsx b/apps/website/src/components/landing/PilotHero.tsx index 6651aa262..73d920b2d 100644 --- a/apps/website/src/components/landing/PilotHero.tsx +++ b/apps/website/src/components/landing/PilotHero.tsx @@ -156,8 +156,7 @@ export function PilotHero() { {/* Secondary CTA */} - Download the Guide + Read the Docs diff --git a/apps/website/src/components/landing/WhitePaperSection.tsx b/apps/website/src/components/landing/WhitePaperSection.tsx index 9e293bb4a..421200133 100644 --- a/apps/website/src/components/landing/WhitePaperSection.tsx +++ b/apps/website/src/components/landing/WhitePaperSection.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { tokens } from '../../../lib/design-tokens'; import { analyticsEvents } from '../../lib/analytics/events'; -import { track, trackWhitepaperDownloadClick } from '../../lib/analytics/client'; +import { track } from '../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -112,16 +112,10 @@ export function WhitePaperSection() { background: 'rgba(26,122,64,.07)', border: '1px solid rgba(26,122,64,.2)', fontSize: '0.88rem', color: '#1a7a40', lineHeight: 1.55, }}> - ✓ Check your inbox — the guide is on its way! + ✓ Thanks! We'll send the refreshed guide when it is ready. trackWhitepaperDownloadClick('overview', { - surface: 'home', - source_section: 'whitepaper-section', - cta_id: 'whitepaper_section_direct_download', - })} + href="/docs" style={{ display: 'inline-block', marginTop: 12, @@ -131,7 +125,7 @@ export function WhitePaperSection() { fontFamily: 'Inter, sans-serif', }} > - or download directly + Read the current docs ) : ( @@ -179,17 +173,11 @@ export function WhitePaperSection() { transition: 'background .2s, border-color .2s', }} > - {formState === 'submitting' ? 'Sending…' : 'Send me the guide'} + {formState === 'submitting' ? 'Sending…' : 'Send me updates'} trackWhitepaperDownloadClick('overview', { - surface: 'home', - source_section: 'whitepaper-section', - cta_id: 'whitepaper_section_direct_download', - })} + href="/docs" style={{ display: 'inline-block', marginTop: 12, @@ -199,7 +187,7 @@ export function WhitePaperSection() { fontFamily: 'Inter, sans-serif', }} > - or download directly + Read the current docs )} @@ -226,8 +214,8 @@ export function WhitePaperSection() { fontStyle: 'italic', fontSize: '1rem', color: tokens.colors.textSecondary, lineHeight: 1.55, marginBottom: 28, }}> - The Angular Agent Readiness Guide. Six chapters. Six production-readiness dimensions. - What separates demos from shipped products. + The Angular Agent Readiness Guide is being refreshed to match the current framework. + Join the update list, or use the docs for the live API surface.

    diff --git a/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx b/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx index 81bcf0dad..bd11fbd72 100644 --- a/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx +++ b/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx @@ -4,24 +4,21 @@ import { HighlightedCode } from '../HighlightedCode'; const SNIPPET_1 = `import { agent } from '@ngaf/langgraph'; const chat = agent({ - graphId: 'my-agent', - url: 'https://my-langgraph.cloud/api', + assistantId: 'my-agent', + apiUrl: 'https://my-langgraph.cloud', }); // Reactive signals — OnPush compatible -chat.messages(); // Signal -chat.isStreaming(); // Signal -chat.interrupt(); // Signal`; +chat.messages(); // Signal +chat.isLoading(); // Signal +chat.interrupt(); // Signal`; const SNIPPET_2 = `import { provideAgent } from '@ngaf/langgraph'; provideAgent({ - graphId: 'my-agent', - url: environment.langgraphUrl, - threadId: savedThreadId, - onThreadId: (id) => localStorage.setItem('threadId', id), + apiUrl: environment.langgraphUrl, transport: isTest - ? new MockStreamTransport(fixtures) + ? new MockAgentTransport(fixtures) : new FetchStreamTransport(), });`; diff --git a/apps/website/src/components/landing/angular/AngularFooterCTA.tsx b/apps/website/src/components/landing/angular/AngularFooterCTA.tsx index 41ec29347..0577a52a9 100644 --- a/apps/website/src/components/landing/angular/AngularFooterCTA.tsx +++ b/apps/website/src/components/landing/angular/AngularFooterCTA.tsx @@ -82,8 +82,7 @@ export function AngularFooterCTA() { - Download the Guide + Get Guide Updates diff --git a/apps/website/src/components/landing/angular/AngularHero.tsx b/apps/website/src/components/landing/angular/AngularHero.tsx index 5da6c049d..fcb85621f 100644 --- a/apps/website/src/components/landing/angular/AngularHero.tsx +++ b/apps/website/src/components/landing/angular/AngularHero.tsx @@ -57,14 +57,14 @@ export function AngularHero() { - - Download the Guide + Get Guide Updates - Six chapters covering the last-mile gap, the agent() API, thread persistence, interrupts, time-travel, and deterministic testing with MockAgentTransport. + The guide is being refreshed to match the current @ngaf/langgraph API. Join the update list, or use the live docs for the current agent(), thread persistence, interrupts, time-travel, and testing surfaces.

    Part of the Cacheplane Angular Agent Framework.

    - trackWhitepaperDownloadClick('angular', { - surface: 'library_landing', - source_section: 'angular-whitepaper-gate', - library: 'agent', - cta_id: 'angular_whitepaper_download', - })} + - ↓ Download PDF + Read Current Docs @@ -130,7 +124,7 @@ export function AngularWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Optional — Get notified of updates + Get notified when the guide is refreshed

    {formState === 'done' ? (
    - ✓ Thanks! We'll reach out when the guide is updated. + ✓ Thanks! We'll reach out when the refreshed guide is ready.
    ) : (
    diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx index 56ac3d524..482fa40a2 100644 --- a/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx +++ b/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx @@ -82,8 +82,7 @@ export function ChatLandingFooterCTA() { - Download the Guide + Get Guide Updates diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingHero.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingHero.tsx index a2b55828d..af2bba11a 100644 --- a/apps/website/src/components/landing/chat-landing/ChatLandingHero.tsx +++ b/apps/website/src/components/landing/chat-landing/ChatLandingHero.tsx @@ -57,14 +57,14 @@ export function ChatLandingHero() { - - Download the Guide + Get Guide Updates - Five chapters covering the sprint tax, batteries-included components, theming and design system integration, generative UI with Vercel json-render, Google A2UI support, and debug tooling. + The guide is being refreshed to match the current @ngaf/chat API. Join the update list, or use the live docs for the current chat component, runtime-neutral Agent contract, generative UI, and debug tooling.

    Part of the Cacheplane Angular Agent Framework.

    - trackWhitepaperDownloadClick('chat', { - surface: 'library_landing', - source_section: 'chat-whitepaper-gate', - library: 'chat', - cta_id: 'chat_whitepaper_download', - })} + - ↓ Download PDF + Read Current Docs @@ -130,7 +124,7 @@ export function ChatLandingWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Optional — Get notified of updates + Get notified when the guide is refreshed

    {formState === 'done' ? (
    - ✓ Thanks! We'll reach out when the guide is updated. + ✓ Thanks! We'll reach out when the refreshed guide is ready.
    ) : ( diff --git a/apps/website/src/components/landing/render/RenderFooterCTA.tsx b/apps/website/src/components/landing/render/RenderFooterCTA.tsx index 949091453..b229a9b77 100644 --- a/apps/website/src/components/landing/render/RenderFooterCTA.tsx +++ b/apps/website/src/components/landing/render/RenderFooterCTA.tsx @@ -82,8 +82,7 @@ export function RenderFooterCTA() { - Download the Guide + Get Guide Updates diff --git a/apps/website/src/components/landing/render/RenderHero.tsx b/apps/website/src/components/landing/render/RenderHero.tsx index 248fe262b..b7cce3f51 100644 --- a/apps/website/src/components/landing/render/RenderHero.tsx +++ b/apps/website/src/components/landing/render/RenderHero.tsx @@ -57,14 +57,14 @@ export function RenderHero() { - - Download the Guide + Get Guide Updates - Five chapters covering the coupling problem, declarative UI specs with Vercel's json-render standard and Google's A2UI protocol, the component registry, streaming JSON patches, and signal-native state management. + The guide is being refreshed to match the current @ngaf/render API. Join the update list, or use the live docs for the current json-render, A2UI, registry, patch streaming, and state management surfaces.

    Part of the Cacheplane Angular Agent Framework.

    - trackWhitepaperDownloadClick('render', { - surface: 'library_landing', - source_section: 'render-whitepaper-gate', - library: 'render', - cta_id: 'render_whitepaper_download', - })} + - ↓ Download PDF + Read Current Docs @@ -130,7 +124,7 @@ export function RenderWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Optional — Get notified of updates + Get notified when the guide is refreshed

    {formState === 'done' ? (
    - ✓ Thanks! We'll reach out when the guide is updated. + ✓ Thanks! We'll reach out when the refreshed guide is ready.
    ) : ( diff --git a/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx b/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx index 70d3dc0bf..41f55c205 100644 --- a/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx +++ b/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx @@ -101,8 +101,7 @@ export function SolutionFooterCTA({ color, headline, subtext }: SolutionFooterCT Start Your Pilot → - Download the Guide + Read the Docs diff --git a/apps/website/src/components/landing/solutions/SolutionHero.tsx b/apps/website/src/components/landing/solutions/SolutionHero.tsx index 19317cd96..59ba4fcc0 100644 --- a/apps/website/src/components/landing/solutions/SolutionHero.tsx +++ b/apps/website/src/components/landing/solutions/SolutionHero.tsx @@ -107,8 +107,7 @@ export function SolutionHero({ solution }: SolutionHeroProps) { Start a Pilot - Download the Guide + Read the Docs
    diff --git a/apps/website/src/components/shared/AnnouncementToast.tsx b/apps/website/src/components/shared/AnnouncementToast.tsx index c14712cb1..fdc6e5821 100644 --- a/apps/website/src/components/shared/AnnouncementToast.tsx +++ b/apps/website/src/components/shared/AnnouncementToast.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { tokens } from '@ngaf/design-tokens'; import { analyticsEvents } from '../../lib/analytics/events'; -import { track, trackWhitepaperDownloadClick } from '../../lib/analytics/client'; +import { track } from '../../lib/analytics/client'; /** * Bump this date to re-show the toast for all users. @@ -163,7 +163,7 @@ export function AnnouncementToast() { lineHeight: 1.5, marginBottom: 16, }}> - Six production-readiness dimensions for Angular agents. Get the guide. + The Angular Agent Readiness Guide is being refreshed for the current framework API.

    - { - trackWhitepaperDownloadClick('overview', { - surface: 'toast', - source_section: 'announcement-toast', - cta_id: 'toast_direct_download', - }); - dismiss(); - }} - style={{ - display: 'inline-block', - marginTop: 10, - fontSize: '0.7rem', - color: tokens.colors.textMuted, - textDecoration: 'underline', - fontFamily: 'Inter, sans-serif', - }} - > - or download directly - +

    + The guide is being refreshed. We will send the updated version when it is ready. +

    )} @@ -289,7 +276,7 @@ export function AnnouncementToast() { color: '#1a7a40', lineHeight: 1.5, }}> - ✓ Check your inbox — the guide is on its way! + ✓ Thanks! We'll send the refreshed guide when it is ready.

    )} diff --git a/apps/website/src/lib/analytics/client.ts b/apps/website/src/lib/analytics/client.ts index ad403bef1..8a8112e1d 100644 --- a/apps/website/src/lib/analytics/client.ts +++ b/apps/website/src/lib/analytics/client.ts @@ -32,10 +32,3 @@ export function trackExternalLinkClick(destinationUrl: string, properties: Analy ...properties, }); } - -export function trackWhitepaperDownloadClick(paper: AnalyticsProperties['paper'], properties: AnalyticsProperties) { - track(analyticsEvents.marketingWhitepaperDownloadClick, { - paper, - ...properties, - }); -} diff --git a/apps/website/src/lib/analytics/events.ts b/apps/website/src/lib/analytics/events.ts index a6099642c..ac7ca904f 100644 --- a/apps/website/src/lib/analytics/events.ts +++ b/apps/website/src/lib/analytics/events.ts @@ -1,7 +1,6 @@ export const analyticsEvents = { marketingCtaClick: 'marketing:cta_click', marketingExternalLinkClick: 'marketing:external_link_click', - marketingWhitepaperDownloadClick: 'marketing:whitepaper_download_click', marketingWhitepaperSignupSubmit: 'marketing:whitepaper_signup_submit', marketingWhitepaperSignupSuccess: 'marketing:whitepaper_signup_success', marketingWhitepaperSignupFail: 'marketing:whitepaper_signup_fail', diff --git a/apps/website/src/lib/solutions-data.ts b/apps/website/src/lib/solutions-data.ts index 87ababb3f..07fc0d818 100644 --- a/apps/website/src/lib/solutions-data.ts +++ b/apps/website/src/lib/solutions-data.ts @@ -77,7 +77,7 @@ export const SOLUTIONS: SolutionConfig[] = [ { metric: '<5 min', label: 'Time to reproduce any agent decision for auditors' }, ], ctaHeadline: 'Ship compliant AI agents — without the compliance tax', - ctaSubtext: 'Download the guide or start a pilot. Your compliance team will thank you.', + ctaSubtext: 'Read the docs or start a pilot. Your compliance team will thank you.', metaTitle: 'Compliance & Audit — Angular Agent Framework Solutions', metaDescription: 'Ship AI agents with human-in-the-loop approvals, audit trails, and deterministic testing. Built for SOX, HIPAA, and GDPR.', }, @@ -126,7 +126,7 @@ export const SOLUTIONS: SolutionConfig[] = [ { metric: '<2s', label: 'First visual streamed — no waiting for full query completion' }, ], ctaHeadline: 'Turn your data into conversations', - ctaSubtext: 'Download the guide or start a pilot. Ship a conversational BI experience in weeks, not quarters.', + ctaSubtext: 'Read the docs or start a pilot. Ship a conversational BI experience in weeks, not quarters.', metaTitle: 'Analytics & BI — Angular Agent Framework Solutions', metaDescription: 'Build conversational BI with natural language queries, real-time streaming charts, and generative UI — all in Angular.', }, @@ -175,7 +175,7 @@ export const SOLUTIONS: SolutionConfig[] = [ { metric: '3x', label: 'Faster resolution for escalated tickets with AI-prepared context' }, ], ctaHeadline: 'Support agents that make your team better', - ctaSubtext: 'Download the guide or start a pilot. Resolve more tickets, escalate smarter, and keep your customers happy.', + ctaSubtext: 'Read the docs or start a pilot. Resolve more tickets, escalate smarter, and keep your customers happy.', metaTitle: 'Customer Support — Angular Agent Framework Solutions', metaDescription: 'Build AI support agents with human escalation, full context handoff, and production-ready chat UI in Angular.', }, diff --git a/package.json b/package.json index b335d11ef..ee0fb6445 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "generate-agent-context": "npx tsx --tsconfig apps/website/tsconfig.json apps/website/scripts/generate-agent-context.ts", "generate-api-docs": "npx tsx apps/website/scripts/generate-api-docs.ts", "generate-narrative-docs": "npx tsx apps/website/scripts/generate-narrative-docs.ts", - "generate-docs": "npm run generate-api-docs && npm run generate-narrative-docs", - "generate-whitepaper": "npx tsx apps/website/scripts/generate-whitepaper.ts" + "generate-docs": "npm run generate-api-docs && npm run generate-narrative-docs" }, "private": true, "overrides": { @@ -117,4 +116,4 @@ } } } -} \ No newline at end of file +}