diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 558aa674b..322ee3b0d 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -64,12 +64,29 @@ test('/llms.txt returns plain text', async ({ page }) => { 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']) { +test('marketing pages link to downloadable whitepaper PDFs', async ({ page }) => { + const expectedDownloads: Record = { + '/': '/whitepaper.pdf', + '/angular': '/whitepapers/angular.pdf', + '/render': '/whitepapers/render.pdf', + '/chat': '/whitepapers/chat.pdf', + }; + + for (const [route, href] of Object.entries(expectedDownloads)) { 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([]); + await expect(page.locator(`a[href="${href}"]`).first(), `${route} links ${href}`).toBeVisible(); + } +}); + +test('whitepaper PDFs are served as static downloads', async ({ request }) => { + for (const href of [ + '/whitepaper.pdf', + '/whitepapers/angular.pdf', + '/whitepapers/render.pdf', + '/whitepapers/chat.pdf', + ]) { + const response = await request.get(href); + expect(response.ok(), `${href} responds successfully`).toBe(true); + expect(response.headers()['content-type'], `${href} content type`).toContain('application/pdf'); } }); diff --git a/apps/website/emails/angular-download.ts b/apps/website/emails/angular-download.ts index 16b3a6aa6..161d3c11a 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 DOCS_URL = 'https://cacheplane.ai/docs/agent/api/agent'; +const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/angular.pdf'; export function angularDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Enterprise Guide to Agent Streaming

-

${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.

+

${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.

- Read the agent() Docs + Download the Guide
`, }); diff --git a/apps/website/emails/chat-download.ts b/apps/website/emails/chat-download.ts index d347276ef..f4e497062 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 DOCS_URL = 'https://cacheplane.ai/docs/chat/components/chat'; +const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/chat.pdf'; export function chatDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Enterprise Guide to Agent Chat Interfaces

-

${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.

+

${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.

- Read the Chat Docs + Download the Guide
`, }); diff --git a/apps/website/emails/drip-whitepaper-followup.ts b/apps/website/emails/drip-whitepaper-followup.ts index 1da5b0a9f..8a6ec7472 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 Current Docs → + Read the Guide → `, showUnsubscribe: true, }), @@ -58,7 +58,7 @@ export function dripWhitepaperFollowupHtml(day: number): { subject: string; html body: `

Let's Connect

Ready to ship your agent? Let's talk.

-

If your team is evaluating how to take an Angular + LangGraph agent to production, I'd love to hear what you're building. Reply to this email or schedule a conversation — no pitch, just a technical discussion about your use case.

+

If your team is evaluating how to take an Angular + LangGraph agent to production, I'd love to hear what you're building. Reply to this email or schedule a conversation — no pitch, just a technical discussion about your use case.

`, showUnsubscribe: true, }), diff --git a/apps/website/emails/render-download.ts b/apps/website/emails/render-download.ts index eb4a789c2..e87c75c31 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 DOCS_URL = 'https://cacheplane.ai/docs/render/getting-started/introduction'; +const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/render.pdf'; export function renderDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Enterprise Guide to Generative UI

-

${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.

+

${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.

- Read the Render Docs + Download the Guide
`, }); diff --git a/apps/website/emails/whitepaper-download.ts b/apps/website/emails/whitepaper-download.ts index 6ddd7a127..5a1978fe9 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 DOCS_URL = 'https://cacheplane.ai/docs'; +const DOWNLOAD_URL = 'https://cacheplane.ai/whitepaper.pdf'; export function whitepaperDownloadHtml(name?: string): string { return wrapEmail({ body: `

Your Angular Agent Readiness Guide

-

${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.

+

${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.

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

From
Prototype
to
Production

+

The Angular Agent Readiness Guide

+
cacheplane.ai · 2026
+
+ + +
+

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

+

# Streaming State Management

+

Server-sent events don't play nicely with Angular's change detection. Zone.js patches `EventSource`, but the resulting microtask scheduling creates timing issues—tokens arrive faster than digest cycles complete, leading to dropped renders or, worse, accumulated state that suddenly flushes in a visual stutter. Teams typically respond by wrapping streams in `NgZone.run()`, manually calling `detectChanges()`, or building elaborate buffer-and-flush mechanisms. All of these approaches share a common failure mode: they work in development and break under production load.

+

The root issue isn't Zone.js itself—it's the impedance mismatch between push-based streaming and Angular's pull-based change detection model. When your LangGraph agent streams 50 tokens per second, you need state primitives that coalesce updates intelligently while remaining reactive enough to drive smooth UI. Custom solutions invariably choose wrong: either they batch too aggressively (laggy typing effect) or too little (CPU saturation from excess renders).

+

Signals as the Synchronization Primitive

+

The `agent()` function returns a signals-native interface that sidesteps these problems entirely. Rather than exposing raw event streams that require manual subscription management, it provides computed signals that update atomically as tokens arrive:

+
@Component({
+  selector: 'app-chat',
+  template: `
+    
+    
+  `,
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ChatComponent {
+  private readonly threadId = signal(undefined);
+  
+  readonly chat = agent({
+    assistantId: 'support_agent',
+    threadId: this.threadId,
+    onThreadId: id => this.threadId.set(id)
+  });
+}
+
+

The `messages()` signal returns `Message[]`—a runtime-neutral representation that updates as the stream progresses. Internally, the framework handles token accumulation, message boundary detection, and state reconciliation. Your component simply reads the signal; Angular's signal-based reactivity handles the rest.

+

The `isLoading()` signal deserves specific attention. It returns `true` from the moment you call `submit()` until the stream completes or errors. This eliminates the polling patterns teams often implement—checking message array lengths, tracking "last update" timestamps, or maintaining parallel loading flags that drift out of sync with actual stream state.

+

OnPush Compatibility

+

Signals and `OnPush` change detection are natural partners, but the pairing requires attention. When `messages()` updates, Angular marks the component dirty through signal dependencies, not through Zone.js event interception. This means your streaming UI actually predates OnPush—it *requires* it for correct behavior under load.

+

The production checklist question—"Are your message signals OnPush-compatible?"—is really asking whether your component tree properly propagates signal reads. If a parent component reads `messages()` and passes the array to a child via `@Input()`, the child must also use `OnPush` or it won't re-render when the array reference changes. The fix is straightforward: either pass the signal itself (`[messages]="chat.messages"`) or ensure `OnPush` propagates down your component tree.

+

Streaming state management in Angular isn't inherently difficult. It becomes difficult when you fight the framework's reactivity model instead of leveraging it. Signals provide the coalescing, the timing, and the change detection integration. Your job is to read them.

+
+
+
Chapter 2
+

Thread Persistence

+

# Thread Persistence

+

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

+

Why Stateless Agent UIs Fail in Production

+

Every agent demo you've seen starts fresh on page load. That's fine for a conference talk. In production, users expect continuity. They start a conversation, close their laptop, resume tomorrow, and pick up where they left off. Without thread persistence, you're forcing users to re-explain context every session. Worse, you're wasting LLM tokens reconstructing state the backend already has.

+

LangGraph's MemorySaver stores complete conversation history server-side, keyed by thread ID. Your frontend's job is simple: remember which thread the user was talking to and reconnect on mount.

+

The threadId Signal and onThreadId Callback

+

The `agent()` function accepts a reactive `threadId` signal and an `onThreadId` callback. When `threadId` is undefined, the backend creates a new thread and fires `onThreadId` with the generated ID. Your callback persists it. On subsequent loads, you initialize the signal from storage, and the agent resumes the existing conversation.

+

This pattern keeps thread lifecycle management declarative. You don't manually coordinate thread creation with message sending. The agent handles it.

+

Persisting to localStorage

+

The implementation is straightforward:

+
@Component({
+  selector: 'app-chat',
+  template: `
+    
+      
+      
+    
+  `
+})
+export class ChatComponent {
+  private readonly threadId = signal(
+    localStorage.getItem('chat_thread') ?? undefined
+  );
+

readonly chat = agent({ + assistantId: 'support_agent', + threadId: this.threadId, + onThreadId: id => { + this.threadId.set(id); + localStorage.setItem('chat_thread', id); + } + }); +} +

+

On first visit, `threadId` is undefined. The backend creates a thread, `onThreadId` fires, and you persist. On refresh, you read from localStorage, pass the existing ID, and the agent loads history from MemorySaver.

+

Thread List UI and Conversation Switching

+

Production apps typically need multiple conversations. A sidebar shows thread history; clicking switches context. The pattern extends naturally:

+
readonly threads = signal(
+  JSON.parse(localStorage.getItem('thread_list') ?? '[]')
+);
+

readonly activeThreadId = signal(this.threads()[0]);

+

readonly chat = agent({ + assistantId: 'support_agent', + threadId: this.activeThreadId, + onThreadId: id => { + this.activeThreadId.set(id); + this.threads.update(list => [id, ...list]); + localStorage.setItem('thread_list', JSON.stringify(this.threads())); + } +});

+

newConversation() { + this.activeThreadId.set(undefined); +}

+

switchThread(id: string) { + this.activeThreadId.set(id); +} +

+

When `activeThreadId` changes, the agent reconnects to that thread and `messages()` reflects the restored history. No manual fetching. The reactive binding handles it.

+

For production, you'll likely move thread metadata to an API—titles, timestamps, archival status. The pattern remains identical: reactive signal in, persistence callback out.

+

Production Checklist

+

Before shipping, verify thread persistence end-to-end:

+
  • Does your agent UI resume threads correctly after a browser refresh? Open a conversation, send messages, refresh the page. History should load automatically without user action.
  • +
  • Does creating a new conversation properly clear state and generate a fresh thread ID?
  • +
  • Does switching between existing threads load the correct history?
  • +
  • Are thread IDs persisted before the user can navigate away?
  • +
+Thread persistence is table stakes for production agent UIs. The framework gives you the primitives. Wire them correctly, and users get the continuity they expect.
+
+
+
Chapter 3
+

Tool-Call Rendering

+

# Tool-Call Rendering

+

LangGraph agents don't just generate text—they invoke tools mid-stream, and your UI needs to reflect that execution state in real time. This means showing steps as they appear, displaying final results, and collapsing completed calls into browsable history. Getting this wrong creates a UI that feels broken during the most interesting parts of agent behavior.

+

The Raw Stream Problem

+

Tool call events arrive as discrete chunks in the SSE stream. A single tool invocation might produce five or six events: an initial call with arguments, multiple intermediate steps as the tool executes, and a final result. The raw payload includes nested metadata, partial JSON for arguments that stream incrementally, and status fields that change meaning depending on tool type.

+

Hand-parsing these events is fragile. You end up maintaining state machines to track which call is active, handling out-of-order delivery, and writing defensive code for malformed chunks. Testing becomes painful because you need to simulate realistic streaming sequences. Every edge case—interrupted calls, parallel tool execution, retry logic—adds branching complexity.

+

The framework solves this by exposing `toolCalls()` as a normalized signal on the agent surface. Each tool call object includes its current status, accumulated steps, and final result. The stream parsing happens once, correctly, inside the transport layer.

+

Headless and Prebuilt Options

+

`@ngaf/chat` provides two components for tool call rendering. `` is the headless primitive—it manages the structural rendering of multiple concurrent calls but leaves visual presentation to you. `` is the prebuilt option that handles common patterns: status indicators, step lists, collapsible sections, and error states.

+

For most production apps, start with the prebuilt card and customize from there:

+

+  
+    @for (call of calls; track call.id) {
+      
+      
+    }
+  
+
+
+

The card component reads status from each tool call object and adjusts its presentation accordingly. Running calls show a live step feed. Completed calls collapse to a summary with expandable history. Failed calls surface error details without disrupting the message flow.

+

Progressive Disclosure

+

Real-time tool execution benefits from progressive disclosure. Users want to see that something is happening—steps appearing as the tool runs—but they don't want permanent visual clutter once the call completes.

+

The `expandedByDefault` binding above handles this: calls expand while running, then collapse automatically on completion. Users can still click to expand history, but the default state keeps the conversation readable.

+

This pattern matters more than it seems. Agents that invoke multiple tools per response can generate substantial step output. Without automatic collapsing, the chat becomes a wall of tool metadata instead of a conversation.

+

Production Checklist

+

Before shipping, verify this behavior:

+

Do your tool call cards handle partial step state during streaming?

+

Steps arrive incrementally. A step might appear with an initial status, then update moments later with results. Your rendering logic should handle these transitions without flicker or layout shift. Test with slow network simulation to catch timing-dependent bugs that don't surface on localhost.

+
+
+
Chapter 4
+

Human Approval Flows

+

# Human Approval Flows (Interrupts)

+

Production agents that modify external state—sending emails, initiating payments, deploying infrastructure—require human oversight before execution. LangGraph provides the `interrupt()` primitive for this purpose: a mechanism that pauses graph execution at designated checkpoints and waits for explicit human authorization before proceeding.

+

The LangGraph Interrupt Pattern

+

When a LangGraph node calls `interrupt()`, execution halts and the graph emits an interrupt event containing the pending action's metadata. The graph remains suspended until it receives a `Command.RESUME` with one of three directives: proceed with the original action, proceed with modified parameters, or abort entirely. This checkpoint-based approach ensures that no consequential action executes without explicit human consent.

+

The challenge lies in surfacing this interrupt state to users and capturing their response without introducing fragile infrastructure. Polling-based solutions waste resources and introduce latency. Custom WebSocket implementations require maintaining connection state, handling reconnections, and synchronizing interrupt lifecycle across multiple browser tabs. Both approaches scatter interrupt logic across services, components, and connection handlers.

+

The `interrupt()` Signal

+

The `agent()` function exposes interrupt state through a dedicated signal:

+
readonly chat = agent({
+  assistantId: 'deployment_agent',
+  threadId: this.threadId,
+  onThreadId: id => this.threadId.set(id)
+});
+

// chat.interrupt() returns AgentInterrupt | undefined +

+

When `interrupt()` returns a defined value, the agent is paused and awaiting human input. The `AgentInterrupt` object contains the action metadata emitted by the graph—typically a description of the pending operation and any parameters the user might modify. When `interrupt()` returns `undefined`, no approval is pending.

+

This signal-based approach eliminates the need for manual subscription management or imperative state tracking. Angular's reactivity system propagates interrupt state changes automatically, and the signal remains consistent across component re-renders.

+

UI Components for Interrupt Handling

+

The `@ngaf/chat` package provides two components for rendering interrupt flows. `` offers a prebuilt approval interface with sensible defaults. For custom designs, the headless `` component exposes the interrupt state and action handlers without imposing markup or styling.

+

Both components support three user actions that map directly to resume commands:

+
  • Approve: Resume execution with the original parameters
  • +
  • Edit: Resume execution with user-modified parameters
  • +
  • Cancel: Abort the pending action entirely
  • +
+
@Component({
+  template: `
+    @if (chat.interrupt(); as interrupt) {
+      
+    }
+  `
+})
+export class DeploymentChatComponent {
+  readonly chat = agent({
+    assistantId: 'deployment_agent',
+    threadId: this.threadId,
+    onThreadId: id => this.threadId.set(id)
+  });
+}
+
+

The edit flow passes modified parameters through the `$event` payload, allowing users to adjust action details before approval. The cancel flow terminates the pending action and allows the conversation to continue without executing the interrupted operation.

+

Production Considerations

+

Interrupt flows introduce a class of edge cases that prototype implementations often ignore. Users close browser tabs. Sessions expire. Network connections drop mid-approval.

+

Production checklist item: *Can your agent UI recover gracefully if a user cancels an interrupt?*

+

Cancellation should not leave the agent in an undefined state. The graph must handle abort commands cleanly, the UI must reflect the cancellation immediately, and subsequent user messages should resume normal conversation flow. Test this path explicitly—it executes more frequently in production than most teams anticipate.

+
+
+
Chapter 5
+

Generative UI

+

# Generative UI

+

Text responses hit a ceiling. When your data analysis agent returns a markdown table, users copy-paste into spreadsheets. When your booking agent describes available slots, users re-enter the same information into a form. The gap between agent output and user action creates friction that compounds across every interaction.

+

Production agents close this gap by emitting structured UI specifications alongside their responses. The agent doesn't return "Here are your results in a table" — it returns a render spec that becomes a live, interactive table component.

+

The Custom Event Pattern

+

LangGraph agents emit structured data through custom events during stream execution. Your agent code decides when to emit UI specifications:

+
# Agent-side: emit a render spec as a custom event
+await writer.write({
+    "type": "data_table",
+    "columns": ["date", "amount", "category"],
+    "rows": rows_so_far
+})
+
+

On the Angular side, `@ngaf/langgraph` surfaces these through the agent's event stream. The `@ngaf/render` package consumes these specs and resolves them to Angular components at runtime.

+

Registry-Based Resolution

+

The registry pattern decouples agent output from component implementation. Your agent emits a type identifier. Your frontend maps that identifier to a component. Neither side knows implementation details of the other.

+
import { defineAngularRegistry } from '@ngaf/render';
+import { DataTableComponent } from './components/data-table.component';
+import { ReservationFormComponent } from './components/reservation-form.component';
+import { ChartComponent } from './components/chart.component';
+

export const uiRegistry = defineAngularRegistry({ + data_table: DataTableComponent, + reservation_form: ReservationFormComponent, + chart: ChartComponent, + // Add components without touching agent code +}); +

+

Components receive the spec's data through a standardized input contract. Your `DataTableComponent` receives `columns` and `rows` — it doesn't know or care that a Python agent emitted them.

+

Template usage is direct:

+

+
+

Or configure the registry at the provider level with `provideRender({ registry: uiRegistry })` and omit it from individual templates.

+

Progressive Updates Through JSON Patch

+

Static specs work for complete data. Streaming scenarios require progressive updates. When your agent processes a large dataset, users shouldn't wait for completion before seeing results.

+

`@ngaf/render` supports JSON Patch streaming for incremental UI updates. The agent emits patches as data arrives:

+
# Initial spec
+await writer.write({"type": "data_table", "columns": [...], "rows": []})
+

# Patches as rows arrive +for row in process_rows(): + await writer.write({"op": "add", "path": "/rows/-", "value": row}) +

+

The frontend applies patches to the live spec. Rows appear as they're processed. Charts animate as data points arrive. Users see progress, not loading spinners.

+

The Decoupling Advantage

+

Tight coupling between agent and frontend creates deployment dependencies. Changing a table column requires coordinated releases. Adding a new visualization blocks on frontend implementation.

+

The registry pattern inverts this. Agents emit specs against a stable contract. Frontend teams add components independently. You can ship a new `heatmap` type in your registry without redeploying agents — they'll use it when ready.

+

This also enables A/B testing component implementations, graceful degradation for unknown types, and environment-specific registries (richer components in desktop, simplified in mobile).

+

---

+

Production checkpoint: Can your agent emit UI components without tight coupling to the frontend codebase? If adding a new visualization requires changes to both agent and frontend in lockstep, the integration is too brittle for production iteration speed.

+
+
+
Chapter 6
+

Deterministic Testing

+

# Deterministic Testing

+

Agent UIs are notoriously difficult to test. Every call to a live LLM introduces variability—different token sequences, timing variations, occasional model updates that subtly change output format. The result is flaky tests, slow CI pipelines, and an inability to reproduce the exact edge case a user reported. Teams ship agent features with low confidence because their test suites can't verify behavior deterministically.

+

Why Live LLM Testing Fails

+

Testing against real LLM APIs introduces three fundamental problems. First, response content varies between runs. The same prompt might yield slightly different phrasing, breaking snapshot tests or exact-match assertions. Second, latency compounds. A single agent interaction might take 2-5 seconds; a test suite with 50 agent tests becomes a 4-minute bottleneck. Third, you can't manufacture edge cases on demand. How do you test interrupt handling if the model decides not to request human input? How do you verify your tool call UI when the model skips the tool entirely?

+

Deterministic testing requires control over the event stream itself.

+

MockAgentTransport: Scripted Event Sequences

+

`MockAgentTransport` replaces the network layer entirely. You provide a scripted sequence of events, and the transport emits them on demand. No server, no network, no variability.

+

This approach lets you test streaming behavior by controlling exactly when each token arrives. You can simulate interrupts at precise moments, inject tool calls with specific payloads, and verify error handling by emitting failure events. Your tests become reproducible scenarios rather than probabilistic hopes.

+

mockLangGraphAgent(): Writable Signal Control

+

For component-level testing, `mockLangGraphAgent()` provides an even more direct approach. It returns an agent surface where every signal is writable—you set the state, and your component reacts.

+
describe('ChatComponent', () => {
+  it('displays interrupt panel when interrupt is pending', () => {
+    const agent = mockLangGraphAgent();
+    const fixture = TestBed.createComponent(ChatComponent);
+    fixture.componentRef.setInput('agent', agent);
+    
+    agent.interrupt.set({
+      value: { question: 'Confirm deletion?' },
+      options: ['confirm', 'cancel']
+    });
+    fixture.detectChanges();
+    
+    expect(fixture.nativeElement.querySelector('chat-interrupt-panel')).toBeTruthy();
+    expect(fixture.nativeElement.textContent).toContain('Confirm deletion?');
+  });
+});
+
+

This pattern isolates component behavior from streaming mechanics. You're testing how your UI responds to state—not whether the transport correctly parses SSE frames.

+

Testing in Isolation

+

Each agent capability becomes independently testable. For streaming, set `isLoading` to true and progressively update `messages` to verify your typing indicators and incremental rendering. For tool calls, populate `toolCalls` with specific payloads and assert your `ChatToolCallCardComponent` renders the expected UI. For generative UI via render-spec, test your registered components against static specs without involving the agent layer at all.

+

Interrupts deserve particular attention. Set `interrupt` to various payloads and verify your `ChatInterruptPanelComponent` handles each type—multiple choice, free text, confirmation dialogs. Call the resume function and assert `interrupt` clears correctly.

+

Production Checklist

+

Before shipping agent features, verify this: Do your agent component tests run offline and complete in under 100ms each?

+

If not, you're either hitting real APIs or your test setup carries unnecessary overhead. Deterministic agent testing should feel like testing any other Angular component—fast, reliable, and completely under your control.

+
+ + + diff --git a/apps/website/public/whitepaper.pdf b/apps/website/public/whitepaper.pdf new file mode 100644 index 000000000..75ccfd758 Binary files /dev/null and b/apps/website/public/whitepaper.pdf differ diff --git a/apps/website/public/whitepapers/angular-preview.html b/apps/website/public/whitepapers/angular-preview.html new file mode 100644 index 000000000..96ddc9354 --- /dev/null +++ b/apps/website/public/whitepapers/angular-preview.html @@ -0,0 +1,389 @@ + + + + + + + + + + + +
+
@ngaf/langgraph · Enterprise Guide
+

The
Enterprise
Guide
to
Agent
Streaming
in
Angular

+

Ship LangGraph agents in Angular — without building the plumbing

+
cacheplane.ai · 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

+

# The Last-Mile Problem

+

You've built the backend. The LangGraph agent handles multi-step reasoning, calls tools, maintains conversation memory, and streams responses token by token. You've tested it with curl, watched it work in LangGraph Studio, maybe even built a quick React prototype. The agent architecture is solid.

+

Then you integrate it with your Angular application.

+

Zone Pollution Is Architectural, Not Configurable

+

The first symptom appears quickly: performance degradation during streaming. Every SSE event triggers zone.js change detection. A typical LLM response generates hundreds of token events over several seconds. Each event runs through `Zone.wrap()`, schedules a microtask, and triggers a full change detection cycle. Your application becomes unresponsive while the agent is responding.

+

The instinctive fix—running the EventSource outside the zone—creates new problems. Updates don't reach templates. Manual `ChangeDetectorRef.detectChanges()` calls scatter through your codebase. You're now maintaining zone-aware and zone-unaware code paths for the same data flow.

+

This isn't a configuration problem you can solve with `NgZone.runOutsideAngular()`. It's a fundamental mismatch between SSE's event model and Angular's zone-based change detection architecture.

+

Synchronous Templates, Asynchronous Tokens

+

Angular's template binding model expects synchronous state reads. Signals improved this, but the core assumption remains: when a template renders, it reads current values and completes. LLM token streams don't work this way. Tokens arrive continuously, accumulate into partial content, and may include control events (tool calls, interrupts) interleaved with text.

+

The naive implementation—updating a signal on every token—violates Angular's expectation of stable reads during change detection. You get ExpressionChangedAfterItHasBeenChecked errors, visual flickering, or worse: dropped tokens during rapid updates.

+

Batching tokens into animation frames helps but introduces its own complexity. You're now managing accumulation buffers, flush timing, and ensuring final state consistency when streams complete or error.

+

Push vs. Pull: The Reactivity Mismatch

+

RxJS Observables are push-based. Angular signals are pull-based. LLM streams are push-based with ordering guarantees. Bridging these models requires careful coordination.

+

Your REST-era patterns don't transfer. An HTTP response completes atomically; you handle loading, success, or error states. A streaming agent response is loading *and* partially successful *and* potentially errored, simultaneously. Tool calls arrive mid-stream. Human interrupts pause processing indefinitely. Partial content is valid content.

+

The standard `toSignal()` approach gives you the latest emission but loses the accumulated message history. Building that accumulation logic—correctly handling message append vs. replace semantics, tool call lifecycle states, and interrupt coordination—requires understanding LangGraph's event protocol, not just Angular's reactivity model.

+

The Real Cost

+

Teams solve these problems. They build zone-patch utilities, token accumulator services, retry-with-backoff wrappers, and error boundary components. They write tests for partial stream failure, reconnection logic, and concurrent stream management.

+

Then the next project starts, and they build it again. Or they copy the code, discover edge cases the original didn't handle, and fork into divergent implementations.

+

The gap between a working demo and production-safe Angular integration is measured in weeks of engineering time, repeated across every team building agent-powered features. The backend streaming problem was solved. The frontend streaming problem keeps getting re-solved.

+
+
+
Chapter 2
+

The agent() API

+

# The agent() API

+

The `agent()` function is the primary interface for streaming LangGraph agents into Angular components. It returns a `LangGraphAgent` instance containing reactive signals that update automatically as the agent stream progresses. No subscriptions. No cleanup. No zone gymnastics.

+

Signal Architecture

+

Calling `agent()` returns an object with typed signals covering the full agent lifecycle:

+
  • `messages()` — The accumulated message history as `Message[]`, updated with each stream chunk
  • +
  • `isLoading()` — Boolean signal indicating active stream processing
  • +
  • `error()` — The current error state, or `undefined` when healthy
  • +
  • `interrupt()` — `AgentInterrupt | undefined`, populated when the agent yields control for human input
  • +
  • `status()` — Granular connection state: `'idle'` | `'connecting'` | `'streaming'` | `'interrupted'` | `'error'`
  • +
  • `toolCalls()` — Active tool invocations extracted from the message stream
  • +
  • `state()` — The current agent state object from the LangGraph thread
  • +
+For cases requiring access to the raw LangGraph protocol, additional signals like `langGraphMessages()` expose the unprocessed message format. +

Provider Configuration

+

Before `agent()` can connect, configure the transport layer with `provideAgent()`:

+
provideAgent({
+  transport: new FetchStreamTransport()
+})
+
+

This registers the stream transport globally. Individual `agent()` calls then specify their endpoint:

+
readonly chat = agent({
+  assistantId: 'support_agent',
+  apiUrl: 'https://api.example.com/langgraph',
+  threadId: this.threadId,
+  onThreadId: id => this.threadId.set(id)
+});
+
+

The `assistantId` identifies the deployed agent. The `apiUrl` points to your LangGraph API endpoint. Thread management is handled through the `threadId` input and `onThreadId` callback.

+

Why Signals Work with OnPush

+

Angular's `OnPush` change detection strategy only triggers updates when input references change or when signals read in the template emit new values. Because `agent()` returns signals—not observables requiring `async` pipes—the framework detects changes automatically when stream chunks arrive.

+

No `markForCheck()`. No `ChangeDetectorRef` injection. The signals integrate with Angular's reactivity system at the primitive level.

+

Template Binding

+

Bind agent state directly in templates without ceremony:

+
@Component({
+  template: `
+    @if (chat.isLoading()) {
+      
+    }
+    @for (message of chat.messages(); track message.id) {
+      
+    }
+    @if (chat.error(); as error) {
+      
+    }
+  `,
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ChatComponent {
+  readonly chat = agent({ assistantId: 'chat_agent' });
+}
+
+

Ten lines. The stream connects, messages accumulate, loading state toggles, errors surface—all reactive, all type-safe.

+

The Alternative

+

Without `agent()`, the equivalent implementation requires manual stream handling:

+
// Manual approach: ~60 lines of subscription management,
+// token accumulation, error handling, cleanup logic,
+// and change detection triggers
+
+

With `agent()`:

+
readonly chat = agent({ assistantId: 'chat_agent' });
+
+

The signal-native design eliminates the subscription lifecycle entirely. When the component destroys, the signals become inert. No `takeUntilDestroyed()`. No `ngOnDestroy`. The framework handles it.

+
+
+
Chapter 3
+

Thread Persistence & Memory

+

# Thread Persistence & Memory

+

Production agent applications are stateful. Users expect to close a browser tab, return hours later, and resume exactly where they left off. This requires tight coordination between LangGraph's checkpoint system and your Angular frontend's thread management.

+

The Thread Lifecycle

+

LangGraph's `MemorySaver` backend persists conversation state against a `threadId`. Every message, tool call, and state mutation is checkpointed. The frontend's job is simple: track which `threadId` the user is working with and ensure it survives page reloads.

+

The `agent()` surface exposes this through two mechanisms. First, `threadId` accepts a signal containing the current thread identifier—pass `undefined` to create a new conversation. Second, `onThreadId` fires when LangGraph assigns an ID to a newly created thread.

+
@Component({
+  template: ``,
+  providers: [provideAgent()]
+})
+export class ChatPage {
+  private readonly storage = inject(ThreadStorageService);
+  
+  readonly threadId = signal(
+    this.storage.getActiveThreadId()
+  );
+  
+  readonly chat = agent({
+    assistantId: 'support_agent',
+    threadId: this.threadId,
+    onThreadId: id => {
+      this.storage.setActiveThreadId(id);
+      this.storage.addToThreadList(id);
+      this.threadId.set(id);
+    }
+  });
+}
+
+

When `threadId` is `undefined`, the first `submit()` call triggers thread creation on the backend. LangGraph responds with the assigned ID, which flows through `onThreadId`. You persist it, update your signal, and subsequent messages automatically route to the correct checkpoint.

+

Restoring State on Mount

+

When a user returns with an existing `threadId`, LangGraph's checkpoint system handles restoration automatically. The backend loads the conversation history from `MemorySaver`, and the frontend receives the full message stream during the initial connection handshake.

+

This means your `messages()` signal populates with historical content without additional API calls. The `langGraphCheckpoint()` signal exposes metadata about the restored state—useful for debugging or displaying "last active" timestamps.

+

Building a Thread List

+

Most applications need more than single-thread persistence. Users expect to manage multiple conversations:

+
@Component({
+  template: `
+    
+    
+  `
+})
+export class MultiThreadChat {
+  readonly threadIds = signal(this.storage.getAllThreadIds());
+  readonly threadId = signal(this.storage.getActiveThreadId());
+  
+  switchThread(id: string) {
+    this.threadId.set(id);
+    this.storage.setActiveThreadId(id);
+  }
+  
+  newThread() {
+    this.threadId.set(undefined);
+  }
+}
+
+

Switching threads is a signal update. The `agent()` reactive system handles reconnection, state restoration, and UI synchronization.

+

Production Considerations

+

Server-side thread expiration creates a failure mode your UI must handle. `MemorySaver` configurations often include TTLs—threads expire after periods of inactivity. When a user selects a stale `threadId`, the backend returns an error rather than conversation history.

+

Watch the `error()` signal for thread-not-found conditions. Your recovery logic should remove the invalid ID from local storage, notify the user, and optionally create a fresh thread automatically.

+

Production checklist:

+
  • Does your thread list handle deleted or expired server-side threads gracefully?
  • +
  • Are you cleaning up localStorage when threads fail to load?
  • +
  • Do you display meaningful state when restoration is in progress versus complete?
  • +
  • Have you considered thread metadata (titles, timestamps) beyond raw IDs?
  • +
+The `MemorySaver` backend and Angular's signal-based reactivity create a clean separation of concerns. The backend owns durability; the frontend owns navigation. Keep that boundary crisp.
+
+
+
Chapter 4
+

Interrupt & Approval Flows

+

# Interrupt & Approval Flows

+

Agents that modify external systems—sending emails, executing database writes, triggering deployments—require human oversight. Autonomous execution without checkpoints creates liability, compliance violations, and irreversible mistakes. LangGraph's `interrupt()` primitive solves this at the graph level, pausing execution mid-stream until a human provides explicit authorization. `@ngaf/langgraph` surfaces this as a reactive signal, making approval workflows native to Angular's change detection without polling, websockets, or custom resume endpoints.

+

How LangGraph Interrupt Works

+

When a LangGraph node calls `interrupt()`, the graph halts execution and persists its current state to the configured checkpointer. The interrupt payload—containing context about the pending action—is sent to the client as part of the stream. Execution remains suspended until the client sends a resume command with one of three directives: approve the action as-is, provide edited parameters, or cancel entirely.

+

The resume payload structure is straightforward:

+
{ action: 'approve' }                    // Proceed with original parameters
+{ action: 'edit', args: { ... } }        // Proceed with modified parameters  
+{ action: 'cancel', reason?: string }    // Abort the pending action
+
+

LangGraph's `Command.RESUME` handles the routing. The graph receives the payload and either continues execution, re-executes with new arguments, or terminates gracefully.

+

The interrupt() Signal

+

The agent surface exposes `interrupt()` as a signal that transitions from `undefined` to an `AgentInterrupt` object when the graph pauses:

+
interface AgentInterrupt {
+  id: string;
+  type: string;
+  payload: unknown;
+  timestamp: number;
+}
+
+

This signal integrates directly with Angular's reactivity model. Components re-render automatically when an interrupt arrives—no subscription management, no manual change detection triggers. When the user responds and execution resumes, the signal returns to `undefined`.

+

Prebuilt Approval UI

+

`@ngaf/chat` provides ``, a ready-to-use approval interface:

+
@Component({
+  selector: 'app-agent',
+  template: `
+    
+    
+  `
+})
+export class AgentComponent {
+  readonly chat = agent({ assistantId: 'ops_agent' });
+}
+
+

The panel renders contextual information from the interrupt payload, displays approve/cancel actions, and handles the resume flow. For custom requirements, bind the signal directly:

+
@Component({
+  template: `
+    @if (chat.interrupt(); as int) {
+      
+

{{ int.payload.description }}

+ + +
+ } + ` +}) +export class CustomApprovalComponent { + readonly chat = agent({ assistantId: 'ops_agent' }); +} +
+

Calling `interrupt()` with a resume payload sends the command and clears the signal.

+

Edge Cases

+

Navigation during interrupt: LangGraph persists interrupt state server-side. If the user navigates away, the interrupt remains active. Re-initializing the agent with the same `threadId` restores the pending interrupt automatically.

+

Session expiration: Checkpointed state survives session boundaries. The interrupt signal repopulates on reconnection, though your application should handle re-authentication before allowing resume actions.

+

Cancel with partial state: Cancellation doesn't roll back prior node executions. If three nodes completed before the interrupt, those effects persist. Design graphs with compensation logic for actions that require atomicity, or structure interrupts to occur before side effects rather than after.

+

Multiple pending interrupts: LangGraph supports sequential interrupts within a single run. The `interrupt()` signal reflects the current pending interrupt; each resume advances to the next pause point or completion.

+

Human-in-the-loop isn't optional for production agents. `@ngaf/langgraph` makes it reactive, type-safe, and compatible with Angular's rendering model—approval flows become UI state, not infrastructure problems.

+
+
+
Chapter 5
+

Full LangGraph Feature Coverage

+

# Full LangGraph Feature Coverage

+

Most Angular LLM integrations handle the basics: send a message, stream tokens, render a response. The moment you need tool calls, subgraphs, or multi-agent coordination, you're writing raw SSE parsers and manually reconciling state. @ngaf/langgraph exists specifically to avoid that cliff—every LangGraph feature surfaces through the same reactive signals your components already consume.

+

Tool Call Streaming

+

LangGraph emits tool invocations as structured events mid-stream. Rather than parsing `tool_call` chunks yourself, the agent ref exposes them directly:

+
readonly chat = agent({
+  assistantId: 'research_agent',
+  apiUrl: 'https://api.smith.langchain.com'
+});
+

// In your template +@for (call of chat.toolCalls(); track call.id) { + +} +

+

The `toolCalls()` signal updates as invocations arrive, complete as the agent processes results, and clear when the turn ends. No manual event filtering. Tool call arguments stream incrementally—useful for showing users what data the agent is requesting before results return.

+

Subgraph Support

+

Nested graphs emit events with their own namespaces. @ngaf/langgraph flattens these into the primary stream while preserving hierarchy through the `subagents()` signal:

+
// Parent agent spawns child graphs for specialized tasks
+const activeSubagents = this.chat.subagents();
+// Returns SubagentInfo[] with id, name, status for each active subgraph
+
+

Child graph messages, tool calls, and state updates flow through the same signals. When a subgraph completes, its final state merges into the parent. Your components don't need conditional logic for nested versus top-level events—they render identically.

+

Time Travel

+

LangGraph checkpoints graph state at each node. Rewinding means re-streaming from a prior checkpoint:

+
// Rewind to a previous state and continue from there
+this.chat.interrupt({ 
+  checkpoint_id: 'abc123',
+  action: 'rewind' 
+});
+
+

The `langGraphCheckpoints()` signal exposes available restore points. After rewinding, `messages()` reflects the restored state and streaming continues from that node. This enables "undo" flows, A/B comparison of agent paths, and debugging without replaying the entire conversation.

+

DeepAgent Multi-Agent Coordination

+

DeepAgent orchestrates multiple specialized agents through a supervisor pattern. At the stream level, this means interleaved events from distinct agents with coordination metadata. The agent ref normalizes this:

+
// Each agent's output tagged with origin
+const messages = this.chat.messages();
+// Message.metadata.agent identifies the source agent
+

// Coordination state available through +const graphState = this.chat.langGraphState(); +// Includes active_agent, delegation_history, shared_context +

+

Your UI can render agent-specific styling, show delegation chains, or visualize the coordination graph—all from signals that update as events arrive.

+

The onCustomEvent Hook

+

Agents emit structured events beyond messages: progress indicators, analytics payloads, generative UI specs. The `onCustomEvent` callback captures these without polluting the message stream:

+
readonly chat = agent({
+  assistantId: 'ui_agent',
+  onCustomEvent: (event) => {
+    if (event.type === 'render_component') {
+      this.dynamicUI.set(event.payload);
+    }
+  }
+});
+
+

This separates concerns cleanly: messages render in the chat, custom events drive application-specific behavior.

+

Why Full Coverage Matters

+

The pattern we've seen repeatedly: teams adopt a library for basic chat, then bypass it entirely when requirements expand. They end up maintaining parallel implementations—the library for simple flows, raw SSE handling for everything else. That's two mental models, two bug surfaces, two upgrade paths.

+

Full feature coverage eliminates that bifurcation. Tool calls, subgraphs, time travel, and multi-agent coordination all flow through the same `agent()` call. When LangGraph adds capabilities, they surface through existing signals rather than requiring new integration code. Your components stay declarative. Your state stays reactive. The complexity lives in the library, not your application.

+
+
+
Chapter 6
+

Deterministic Testing

+

# Deterministic Testing

+

Agent UIs are notoriously difficult to test. Real LLM calls introduce latency measured in seconds, non-deterministic outputs, rate limits, and network dependencies that make CI pipelines slow and flaky. A test that passes locally might fail in CI because the model returned a slightly different response, or the API throttled your request, or the stream took longer than your timeout.

+

The solution is deterministic mocking at the transport layer. NGAF provides two complementary approaches: `MockAgentTransport` for scripting realistic SSE event sequences, and `mockLangGraphAgent()` for direct signal manipulation when you need fine-grained control.

+

MockAgentTransport: Scripted Event Sequences

+

`MockAgentTransport` replaces `FetchStreamTransport` in tests, emitting a predetermined sequence of SSE events without any network calls. You script exactly what the agent receives—message chunks, tool calls, interrupts, errors—and the transport replays them synchronously or with configurable delays.

+
import { TestBed } from '@angular/core/testing';
+import { MockAgentTransport, provideAgent } from '@ngaf/langgraph';
+

describe('ChatComponent', () => { + let transport: MockAgentTransport;

+

beforeEach(() => { + transport = new MockAgentTransport(); + TestBed.configureTestingModule({ + imports: [ChatComponent], + providers: [provideAgent({ transport })] + }); + }); +}); +

+

This setup runs entirely offline. No HTTP interceptors, no mock servers, no environment configuration. The transport is synchronous by default, meaning your test assertions execute immediately after triggering a submit.

+

Testing Agent States

+

Every agent state your UI handles needs a corresponding test. `MockAgentTransport` lets you script each scenario explicitly:

+

Streaming in progress: Emit partial message events without a completion event. Assert that `isLoading()` returns true and the message list shows the partial content.

+

Stream complete: Emit the full event sequence including the completion marker. Assert that `isLoading()` returns false and messages contain the final content.

+

Interrupt pending: Script an interrupt event mid-stream. Assert that `interrupt()` returns the interrupt payload and your interrupt panel renders the expected options.

+

Error state: Emit an error event. Assert that `error()` contains the error details and your error UI appears.

+

Direct Signal Control with mockLangGraphAgent

+

When testing component rendering in isolation—without exercising the transport layer—use `mockLangGraphAgent()` to create an agent instance with directly controllable signals:

+
const mockAgent = mockLangGraphAgent({
+  messages: signal([{ role: 'assistant', content: 'Test response' }]),
+  status: signal('complete'),
+  toolCalls: signal([{ id: 'tc1', name: 'search', args: { query: 'test' } }])
+});
+
+

Pass this mock to components that accept an agent input. You control exactly what signals return, making it trivial to test tool call rendering, generative UI output, and edge cases like empty states or malformed data.

+

Testing Thread Switching

+

Thread switching tests verify that your component correctly handles `threadId` changes and `onThreadId` callbacks. Script a sequence where the transport emits a new thread ID, then assert that your `onThreadId` handler persisted the value and subsequent messages associate with the correct thread.

+

The Benchmark

+

Agent component tests should run offline and complete in under 100ms each. This isn't aspirational—it's achievable when you eliminate network calls and async delays. A test suite with 50 agent UI tests should finish in under 5 seconds, run identically on developer machines and CI, and produce the same results every time.

+

If your agent tests take longer or exhibit flakiness, you're either hitting real infrastructure or introducing unnecessary async delays in your mocks. Fix the mocking strategy, not the timeout thresholds.

+
+ + + diff --git a/apps/website/public/whitepapers/angular.pdf b/apps/website/public/whitepapers/angular.pdf new file mode 100644 index 000000000..0f8da9ae0 Binary files /dev/null and b/apps/website/public/whitepapers/angular.pdf differ diff --git a/apps/website/public/whitepapers/chat-preview.html b/apps/website/public/whitepapers/chat-preview.html new file mode 100644 index 000000000..3dcfa93f5 --- /dev/null +++ b/apps/website/public/whitepapers/chat-preview.html @@ -0,0 +1,271 @@ + + + + + + + + + + + +
+
@ngaf/chat · Enterprise Guide
+

The
Enterprise
Guide
to
Agent
Chat
Interfaces
in
Angular

+

Production agent chat UI in days, not sprints

+
cacheplane.ai · 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

+

# The Sprint Tax

+

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.

+

This isn't a skill gap. It's a structural inefficiency baked into how we approach agent interfaces.

+

The Inventory

+

Here's what every production chat UI actually needs:

+

Message rendering: Not just text. Markdown with code highlighting, tables, lists, inline formatting. Messages that stream in token-by-token. Messages that arrive complete. Messages from the user, from the agent, from tools. Each with different visual treatment.

+

Streaming display: Tokens arriving at variable rates. Buffering strategies that balance responsiveness against flicker. Cursor indicators. Graceful handling when the stream errors mid-message. Reconnection logic when it drops.

+

Tool call cards: Tools that are pending, executing, complete, or failed. Tools that return structured data you need to render contextually. Tools that run for 30 seconds and need progress indication. Tools that spawn sub-agents with their own message streams.

+

Interrupt panels: Human-in-the-loop flows where the agent needs input before proceeding. Multiple choice, free text, confirmation dialogs. State that persists if the user navigates away. Resume logic that picks up where it left off.

+

Auto-scroll: Sounds simple. Scroll to bottom as new content arrives. Except: don't scroll if the user scrolled up to read history. Do scroll if they scroll back down. Handle window resize. Handle virtual scrolling for conversations with thousands of messages. Handle images that load asynchronously and shift layout.

+

Accessibility: Screen reader announcements for new messages. Focus management that doesn't trap users. Keyboard navigation through the conversation. ARIA live regions that don't spam. High contrast support. Reduced motion support.

+

Mobile layout: Viewport that shifts when the keyboard appears. Touch targets that meet minimum sizes. Gesture handling that doesn't conflict with browser gestures.

+

The Hidden Costs

+

Accessibility is harder than it looks. Your first implementation will fail an audit. Your second implementation will annoy screen reader users. Your third implementation will work, but you'll have spent two weeks on it.

+

Streaming token display has edge cases. What happens when you receive a partial UTF-8 sequence? What about markdown that's valid mid-stream but invalid once complete? What about code blocks that open but haven't closed yet?

+

Tool call state machines are complex. A tool can be requested, approved, executing, streaming results, complete, or failed. It can timeout. It can be cancelled. The user can interrupt. The agent can retry. Each state transition needs UI treatment.

+

"Good Enough for Demo" vs. Production

+

Demo chat UI: Messages appear. Input works. It scrolls.

+

Production chat UI: Messages stream smoothly. Tool calls show real-time progress. Errors recover gracefully. History loads incrementally. Mobile works. Accessibility audits pass. Performance stays stable at 10,000 messages.

+

The gap between these isn't a weekend. It's eight weeks of senior engineer time, plus ongoing maintenance.

+

The Opportunity Cost

+

Here's the actual problem: you're paying senior Angular engineers to solve problems that have already been solved. Problems that have nothing to do with what makes your agent application valuable.

+

While your team is debugging auto-scroll edge cases, your agent backend is ready and waiting. While they're implementing tool call state machines, your differentiating features aren't getting built. While they're fixing accessibility audit failures, your competitors are shipping.

+

The @ngaf/chat Thesis

+

`@ngaf/chat` ships the complete inventory: ``, ``, ``, ``, ``. Production-grade. Accessible. Mobile-ready. Streaming-optimized.

+

The thesis is simple: ship the chat UI on day one. Spend the sprints on what differentiates your product—the agent logic, the tool integrations, the domain-specific features that your competitors can't copy.

+

The sprint tax is optional. Stop paying it.

+
+
+
Chapter 2
+

Batteries-Included Components

+

# Batteries-Included Components

+

@ngaf/chat ships two component tiers: headless primitives that encapsulate behavior without styling opinions, and prebuilt compositions that deliver production-ready interfaces with minimal configuration. The separation lets teams adopt complete solutions immediately while preserving escape hatches for custom requirements.

+

The Headless Tier

+

Headless components own behavior and state management but emit no styled markup. They expose content projection slots and structural directives for complete template control.

+

`` manages scroll position, virtualization hints, and message grouping logic. It consumes `Message[]` from the agent surface and handles the complexity of streaming message updates—partial content, role transitions, and optimistic UI states. Your templates define how each message renders.

+

`` handles submit semantics, keyboard shortcuts, disabled states during streaming, and multiline expansion. It exposes form control bindings without prescribing input styling.

+

`` and `` manage tool invocation display, including pending states, execution results, and error handling. `` surfaces human-in-the-loop decision points when the agent requires input to proceed.

+

These primitives compose freely. Use them when your design system mandates specific markup structures or when accessibility requirements demand particular ARIA patterns.

+

The Prebuilt Composition Tier

+

The `` component assembles headless primitives with production styling and sensible defaults. It accepts an agent instance and renders a complete interface:

+
@Component({
+  selector: 'app-support',
+  template: ``,
+})
+export class SupportComponent {
+  supportAgent = agent({
+    transport: new FetchStreamTransport(),
+    assistantId: 'support-assistant',
+    apiUrl: '/api/langgraph',
+  });
+}
+
+

Six lines deliver a functional chat interface with message history, streaming indicators, input handling, and tool call display. The component handles loading states, error presentation, and responsive layout without additional configuration.

+

Companion components—`ChatMessageListComponent`, `ChatInputComponent`, `ChatToolCallsComponent`, `ChatInterruptPanelComponent`, `ChatDebugComponent`—can be used independently when you need prebuilt styling for specific sections while customizing others.

+

Composing Tiers

+

The practical pattern: use prebuilt components for standard sections, drop to headless for custom requirements. A typical enterprise implementation might use the prebuilt message list and input while providing a custom tool call renderer that integrates with internal component libraries.

+

The `` component accepts content projection for this purpose. Override specific slots while retaining default behavior elsewhere. When projection proves insufficient, decompose to individual prebuilt components, then to headless primitives as customization needs escalate.

+

The Agent Contract

+

Both tiers consume the runtime-neutral `Agent` contract returned by `agent()` from @ngaf/langgraph. This contract exposes signals: `messages()` returns `Message[]` representing the conversation, `status()` indicates connection state, `isLoading()` reflects pending operations, `toolCalls()` surfaces invocations, and `state()` provides LangGraph checkpoint data.

+

Components bind directly to these signals. When the agent streams a response, `messages()` updates reactively. Components re-render affected sections without manual change detection. The `Message` type from @ngaf/chat provides the runtime-neutral representation—role, content, metadata—that components consume regardless of the underlying LangGraph message format.

+

Choosing Your Tier

+

Start with ``. It covers the common case and establishes baseline functionality in minutes. Drop to prebuilt companions when you need to rearrange layout or inject custom sections between standard elements. Move to headless primitives when your design system requires specific DOM structures or when you're building novel interaction patterns.

+

Migration between tiers is mechanical: prebuilt components are compositions of headless primitives with styling applied. Extracting customization points means identifying which primitive to expose and which styling to preserve. The agent contract remains constant across tiers—your backend integration stays unchanged regardless of which component tier renders the interface.

+
+
+
Chapter 3
+

Theming & Design System Integration

+

# Theming & Design System Integration

+

A chat interface that looks like a demo is a liability in production. Users notice when components don't match the rest of the application—inconsistent border radius, wrong font stack, off-brand colors. These details erode trust in the product and in the AI features you're shipping.

+

@ngaf/chat exposes its visual design decisions through CSS custom properties, giving you control over appearance without touching component internals or maintaining forks.

+

The CSS Custom Property API

+

Every visual decision in @ngaf/chat maps to a custom property. The components read these values at runtime, so overriding them in your stylesheet changes the rendered output immediately.

+

The naming convention follows a predictable pattern: `--chat-{component}-{property}`. Component-level tokens reference global tokens, which reference your design system tokens. This layering lets you override at whatever granularity makes sense—change one button's border radius or change every border radius in the chat interface with a single line.

+

Design Token Mapping

+

If your team already maintains design tokens, integration is direct assignment. Map your existing tokens to the chat component tokens in a single stylesheet:

+
:root {
+  /* Typography */
+  --chat-font-family: var(--ds-font-family-body);
+  --chat-font-size-base: var(--ds-font-size-md);
+  --chat-line-height: var(--ds-line-height-normal);
+

/* Colors */ + --chat-surface-primary: var(--ds-color-surface-elevated); + --chat-surface-secondary: var(--ds-color-surface-sunken); + --chat-text-primary: var(--ds-color-text-primary); + --chat-text-secondary: var(--ds-color-text-muted); + --chat-accent: var(--ds-color-brand-primary);

+

/* Shape */ + --chat-border-radius: var(--ds-radius-md); + --chat-spacing-unit: var(--ds-spacing-base);

+

/* State colors */ + --chat-color-error: var(--ds-color-feedback-error); + --chat-color-success: var(--ds-color-feedback-success); +} +

+

This mapping becomes your single source of truth. When design updates the brand's border radius, the chat components update automatically because they reference your tokens, not hardcoded values.

+

Typography Integration

+

Chat interfaces are text-heavy. Typography consistency matters more here than in most UI contexts.

+

@ngaf/chat exposes tokens for font family, size scale (base, small, large), line height, and font weight. Message content, timestamps, tool call labels, and input placeholders all reference these tokens. Set them once, and the entire typographic hierarchy follows your design system.

+

Color System Integration

+

Surface colors control backgrounds—the chat container, message bubbles, input field. Text colors handle primary content and secondary metadata. Accent colors drive interactive elements and visual emphasis. Semantic state colors handle error states, success confirmations, and loading indicators.

+

The token structure assumes you have these categories in your design system. If you don't, use literal values.

+

Dark Mode Support

+

The token system supports dark mode without component changes. Override the custom properties inside a dark mode selector:

+
[data-theme="dark"] {
+  --chat-surface-primary: var(--ds-color-surface-elevated-dark);
+  --chat-text-primary: var(--ds-color-text-primary-dark);
+}
+
+

Components don't contain theme-switching logic. They read current token values. Your application controls when those values change.

+

Limitations of Token-Based Theming

+

CSS custom properties control visual properties—colors, spacing, typography, borders. They don't control structure.

+

If you need different DOM layout, custom animations, or component composition that diverges from the default, tokens won't help. This is where the headless pattern applies: use `agent()` directly with your own components, keeping the streaming infrastructure while owning the entire render layer.

+

Tokens handle 80% of enterprise theming needs. The headless tier handles the rest.

+
+
+
Chapter 4
+

Generative UI in Chat

+

# Generative UI in Chat

+

Text is a bottleneck. When a financial agent needs to present quarterly results, streaming prose about revenue figures wastes cognitive load. When a scheduling agent confirms a booking, a wall of text obscures the actionable details. The most capable agent interfaces solve this by rendering structured UI directly in the message stream—tables, forms, approval cards—alongside conversational text.

+

Structured UI in the Message Stream

+

@ngaf/chat treats generative UI as a first-class message type. When an agent emits a UI specification instead of text, the chat renders it inline using @ngaf/render. From the user's perspective, the conversation flows naturally: the agent explains context in prose, then presents a rendered component for interaction.

+

This works because the Agent contract exposes messages as a heterogeneous stream. Text messages render as text. UI messages resolve to Angular components through a registry. The chat component handles both transparently.

+

The json-render Spec

+

The json-render specification defines a minimal contract for declarative UI. An agent emits a JSON object with a `type` field identifying the component and a `props` field containing its inputs:

+
{"type": "data-table", "props": {"columns": ["Quarter", "Revenue"], "rows": [...]}}
+
+

The renderer resolves `data-table` to an Angular component, binds `props` to its inputs, and inserts it into the DOM. No custom parsing, no message-type switches in templates. The specification stays minimal intentionally—agents describe *what* to render, not *how*.

+

A2UI: Agent-Specific Patterns

+

Google's A2UI specification extends json-render with patterns specific to agent interactions: approval workflows, structured actions, and rich data displays. Where json-render handles arbitrary components, A2UI codifies the common cases—confirmation dialogs, multi-step forms, action buttons with pending states.

+

@ngaf/render supports both specifications. You can mix A2UI's structured patterns with custom json-render components in the same message stream.

+

Registry Integration

+

Component resolution flows through a registry. You define which component handles each type, then provide it to the chat context:

+
import { defineAngularRegistry, provideRender } from '@ngaf/render';
+import { DataTableComponent } from './data-table.component';
+import { BookingFormComponent } from './booking-form.component';
+import { ApprovalCardComponent } from './approval-card.component';
+

const registry = defineAngularRegistry({ + 'data-table': DataTableComponent, + 'booking-form': BookingFormComponent, + 'approval-card': ApprovalCardComponent, +});

+

// In your providers array +provideRender({ registry }) +

+

The chat component picks up the registry through dependency injection. When a message contains a render spec, it resolves and instantiates the component automatically.

+

Streaming Patches

+

Agents rarely emit complete UI specifications in one shot. A data table streams rows as they arrive. A form populates fields progressively. @ngaf/render handles this through JSON Patch streaming—the agent emits an initial skeleton, then streams RFC 6902 patches that mutate the specification incrementally.

+

In the chat context, this creates the live update effect users expect. A table appears with headers, then rows populate one by one. A summary card fills in as the agent processes. The render layer applies patches reactively; bound components update without re-instantiation.

+

This matters for perceived latency. Users see structure immediately, then watch it fill in—progress feels continuous rather than blocked on complete responses.

+
+
+
Chapter 5
+

Debug Tooling

+

# Debug Tooling

+

Debugging agent chat is hard. The message stream is opaque, tool call state transitions happen in milliseconds, and interrupt flows have timing edge cases that only surface under load. You can't step through a streaming conversation the way you step through synchronous code.

+

`` 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. One component, zero configuration.

+

What chat-debug Shows

+

The debug panel exposes four primary views:

+

Message State. The current `Message[]` array, rendered as expandable JSON. You see every message the agent has processed—user inputs, assistant responses, tool results—with full metadata intact.

+

Streaming Event Log. A chronological log of every event received from the transport layer. This includes partial tokens during streaming, state updates, tool call initiations, and completion signals. The log timestamps each event to the millisecond.

+

Tool Call State Machine. Each tool call passes through distinct states: pending, executing, completed, or failed. The debug panel visualizes this state machine for every tool call in the current conversation, showing transitions as they happen.

+

Interrupt Payload. When the agent requests human intervention, the full interrupt payload appears in the panel—the interrupt type, the data the agent is requesting, and any context it provided. After user action, you see both the original payload and the response.

+

Adding chat-debug

+

Drop it into any chat interface:

+
import { ChatDebugComponent } from '@ngaf/chat';
+

@Component({ + selector: 'app-support-chat', + imports: [ChatComponent, ChatDebugComponent], + template: ` + + + ` +}) +export class SupportChatComponent { + agent = agent({ + transport: new FetchStreamTransport(), + assistantId: 'support-agent', + apiUrl: '/api/langgraph' + }); +} +

+

No configuration required. The panel renders in the bottom-right corner with a toggle to expand and collapse.

+

Inspecting Individual Messages

+

Click any message in the Message State view to expand it. You'll see:

+
  • Message type: user, assistant, tool, or system
  • +
  • Content: the full text or structured content
  • +
  • Metadata: timestamps, message IDs, and any custom properties your agent attaches
  • +
  • Token count: if your transport provides it, the token usage for that message
  • +
+For assistant messages during active streaming, content updates live as tokens arrive. +

Tool Call Debugging

+

The tool call view shows each invocation with:

+
  • Tool name: the function the agent called
  • +
  • Input payload: the exact JSON the agent passed to the tool
  • +
  • Output: the tool's response, once execution completes
  • +
  • Execution timing: start time, end time, and duration in milliseconds
  • +
+When a tool call fails, the panel displays the error message and stack trace. This is where you'll catch malformed inputs, timeout issues, and permission errors. +

Interrupt State Inspection

+

Interrupts are the hardest flow to debug without tooling. The agent pauses, waits for user input, then resumes—but what exactly is it waiting for?

+

The interrupt view shows the full payload the agent sent when requesting the interrupt. After the user responds, you see both sides: what was asked and what was provided. This catches mismatches between what your interrupt UI collects and what the agent expects.

+

Integration with Angular DevTools

+

Agent signals created by `agent()` appear in Angular DevTools like any other signal. In the component tree, you'll see `messages`, `status`, `isLoading`, `toolCalls`, and `state` as inspectable signals with their current values.

+

This means standard Angular debugging workflows apply. Set a breakpoint, inspect signal values, trace reactivity—the agent surface behaves like any other signal-based state.

+

Production Safety

+

`` uses Angular's `isDevMode()` check internally. In production builds, the component renders nothing—no DOM nodes, no event listeners, no performance overhead. Leave it in your templates; the build process handles the rest.

+

For teams that need debug capabilities in staging environments, pass `[forceEnable]="true"` to override the dev mode check. Use this sparingly and gate it behind feature flags.

+
+ + + diff --git a/apps/website/public/whitepapers/chat.pdf b/apps/website/public/whitepapers/chat.pdf new file mode 100644 index 000000000..fd83e4ec6 Binary files /dev/null and b/apps/website/public/whitepapers/chat.pdf differ diff --git a/apps/website/public/whitepapers/render-preview.html b/apps/website/public/whitepapers/render-preview.html new file mode 100644 index 000000000..6258a0c33 --- /dev/null +++ b/apps/website/public/whitepapers/render-preview.html @@ -0,0 +1,380 @@ + + + + + + + + + + + +
+
@ngaf/render · Enterprise Guide
+

The
Enterprise
Guide
to
Generative
UI
in
Angular

+

Agents that render UI — without coupling to your frontend

+
cacheplane.ai · 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

+

# The Coupling Problem

+

Every Angular developer who has integrated an LLM agent hits the same wall. The agent returns structured output—maybe a product recommendation, a data visualization, or a multi-step form. You need to render it. The obvious solution:

+
@Component({
+  template: `
+    @switch (output.type) {
+      @case ('product') {  }
+      @case ('chart') {  }
+      @case ('form') {  }
+    }
+  `
+})
+export class AgentResponseComponent {
+  @Input() output: AgentOutput;
+}
+
+

This works. Ship it. Demo goes well.

+

Then the agent team adds a new capability. They want to render comparison tables. Your switch statement doesn't handle it. The agent emits valid output, the frontend shows nothing. Now you're in a deploy queue behind three other teams, waiting to add `@case ('table')`.

+

You've created a bidirectional dependency that will strangle your velocity.

+

The Dependency Graph You Didn't Want

+

The coupling runs both directions. The agent's output schema is constrained by what the frontend can render. The frontend's component library is constrained by what the agent might emit. Change either side and you break the contract.

+

This creates a coordination tax on every feature:

+

1. Agent team proposes new output type +2. Frontend team reviews, estimates component work +3. Component gets built, tested, merged +4. Agent team waits for frontend deploy +5. Agent capability finally ships

+

In practice, steps 2-4 take weeks. The agent team starts working around limitations. They emit markdown instead of structured data because at least markdown renders. You lose type safety, styling control, and interaction capabilities.

+

Scale Multiplies the Problem

+

One agent, one frontend, one team—manageable. Now consider reality:

+
  • Three agents sharing common UI patterns
  • +
  • Two frontends (web app, internal dashboard)
  • +
  • Separate teams for agent development and frontend platform
  • +
+Your switch statements are now scattered across repositories. Each frontend has its own mapping logic. Agent teams don't know which frontends support which output types. Capability matrices live in Confluence pages that nobody updates. +

When someone asks "can the agent render an approval workflow?"—the answer requires archaeology across four codebases.

+

Inverting the Dependency

+

The fix is architectural: agents emit UI specifications, not domain data. The frontend interprets specs through a registry, not a switch statement.

+
// Agent emits spec
+{
+  "component": "data-table",
+  "props": { "columns": [...], "rows": [...] }
+}
+

// Frontend interprets via registry +const registry = defineAngularRegistry({ + 'data-table': DataTableComponent, + 'product-card': ProductCardComponent, + // ... +}); +

+

New capability? Agent emits it. If the registry has a mapping, it renders. If not, fallback behavior. No frontend deploy required for agent changes. No agent prompt changes required for component refactors.

+

The registry becomes the contract. Version it. Document it. Let teams evolve independently.

+

The Standard Problem

+

A registry per frontend doesn't solve coordination—it moves it. You still need agreement on what `"component": "data-table"` means, what props it accepts, how deeply specs can nest.

+

Without a shared specification, you'll build three proprietary formats. Your agents will need frontend-specific prompt branches. Your component libraries will drift.

+

This is why the approach demands an open spec—a grammar for describing UI that agents can target and any frontend can interpret. Not a component library. Not a design system. A protocol.

+

The next chapter introduces that protocol.

+
+
+
Chapter 2
+

Declarative UI Specs & the json-render Standard

+

# Declarative UI Specs & the json-render Standard

+

The Problem with Ad-Hoc UI Generation

+

When LLMs generate UI, the output format matters as much as the content. Without a formal specification, teams end up with brittle prompt engineering, framework-specific JSON schemas, and UI descriptions that break when models update or requirements change. The json-render specification solves this by defining a framework-agnostic standard for describing component trees as structured JSON.

+

Anatomy of a json-render Document

+

A json-render document describes a component tree using three primitives: component name, props, and children.

+
{
+  "component": "card",
+  "props": {
+    "title": "Q3 Revenue",
+    "variant": "elevated"
+  },
+  "children": [
+    {
+      "component": "metric",
+      "props": {
+        "value": 2847000,
+        "format": "currency",
+        "trend": "up"
+      }
+    },
+    {
+      "component": "sparkline",
+      "props": {
+        "data": [12, 19, 15, 25, 22, 30, 28]
+      }
+    }
+  ]
+}
+
+

The structure is intentionally minimal. Component names are strings—the rendering framework resolves them to actual implementations. Props are arbitrary JSON objects validated at render time. Children form a recursive tree of the same structure.

+

Why an Open Standard Matters

+

Framework Portability: A json-render document generated for Angular works identically in React, Vue, or any conforming renderer. Backend teams don't couple to frontend implementation choices. The same agent serves web, mobile, and embedded contexts.

+

LLM Prompt Stability: Models trained on consistent schemas produce more reliable output. When json-render becomes the canonical format across projects, prompt engineering compounds—patterns that work in one system transfer directly to others.

+

Community Tooling: Validators, visual editors, and testing utilities build against the spec rather than proprietary formats. Schema validation catches malformed output before it reaches the renderer.

+

Control Flow in the Spec

+

The specification handles dynamic rendering through reserved component types:

+
{
+  "component": "$if",
+  "props": { "condition": "{{ user.isAdmin }}" },
+  "children": [{ "component": "admin-panel", "props": {} }]
+}
+
+

Iteration uses `$for`:

+
{
+  "component": "$for",
+  "props": { "each": "{{ items }}", "as": "item" },
+  "children": [
+    {
+      "component": "list-item",
+      "props": { "label": "{{ item.name }}" }
+    }
+  ]
+}
+
+

Computed properties use template expressions. The renderer evaluates these against a provided context object, keeping the spec declarative while supporting dynamic data binding.

+

Google's A2UI Extension

+

Google's Agent-to-UI (A2UI) specification extends json-render with agent-specific patterns. It adds constructs for streaming updates, tool call visualization, and interrupt handling—concepts that don't exist in static UI rendering but are fundamental to agent interactions.

+

A2UI defines how partial UI updates arrive during generation, how tool invocations surface to users, and how human-in-the-loop checkpoints integrate with component trees. The `@ngaf/render` package implements both specifications.

+

The @ngaf/render Implementation

+

The `` directive consumes json-render documents and instantiates Angular components:

+
import { Component, signal } from '@angular/core';
+import { RenderSpecComponent, defineAngularRegistry } from '@ngaf/render';
+import { CardComponent, MetricComponent, SparklineComponent } from './components';
+

const registry = defineAngularRegistry({ + card: CardComponent, + metric: MetricComponent, + sparkline: SparklineComponent +});

+

@Component({ + selector: 'app-dashboard', + imports: [RenderSpecComponent], + template: `` +}) +export class DashboardComponent { + registry = registry; + uiSpec = signal(null); +} +

+

The registry maps component names to Angular components. Unknown components throw at render time—fail fast rather than silent degradation.

+

Prompting for Valid Output

+

LLMs generate spec-compliant JSON when prompts include the schema and examples:

+
Generate a json-render document for a user profile card.
+

Schema: { component: string, props: object, children?: array } +Available components: card, avatar, text, badge

+

Example output: +{"component": "card", "props": {"variant": "outlined"}, "children": [...]} +

+

Structured output modes (JSON mode, function calling) enforce syntactic validity. Schema validation catches semantic errors—referencing undefined components or invalid prop types.

+

The specification creates a stable contract. Agents emit it. Renderers consume it. Neither side knows how the other works.

+ +
+
Chapter 3
+

The Component Registry

+

# The Component Registry

+

A render-spec document references components by string name. The registry resolves those names to Angular component classes at render time. Without this mapping layer, the open standard would be theoretical—the registry makes it executable.

+

Defining the Registry

+

`defineAngularRegistry()` accepts a record of component names to Angular component classes:

+
import { defineAngularRegistry } from '@ngaf/render';
+import { CardComponent } from './card.component';
+import { ButtonComponent } from './button.component';
+import { DataTableComponent } from './data-table.component';
+import { AlertComponent } from './alert.component';
+

export const uiRegistry = defineAngularRegistry({ + 'Card': CardComponent, + 'Button': ButtonComponent, + 'DataTable': DataTableComponent, + 'Alert': AlertComponent +}); +

+

The keys are the exact strings that appear in your render-spec `component` fields. The values are the component classes themselves—not selectors, not factory functions. This directness means the registry is statically analyzable and tree-shakeable.

+

Providing the Registry

+

Two patterns, depending on your architecture.

+

Direct binding works for isolated cases where a single component owns the rendering context:

+
@Component({
+  template: ``
+})
+export class PreviewComponent {
+  registry = uiRegistry;
+  spec = input.required();
+}
+
+

Dependency injection suits applications where multiple components render specs against a shared registry:

+
// app.config.ts
+export const appConfig: ApplicationConfig = {
+  providers: [
+    provideRender({ registry: uiRegistry })
+  ]
+};
+
+

When both are present, the direct `[registry]` binding takes precedence. This lets you override the global registry for specific rendering contexts—useful for sandboxed previews or A/B testing component implementations.

+

Resolution and Input Mapping

+

`` walks the spec tree, resolving each `component` string against the registry. For each node, it instantiates the corresponding Angular component and maps the spec's `props` object to `@Input()` bindings.

+

The mapping is direct: a prop named `title` binds to an input named `title`. No transformation, no case conversion. If your component expects `@Input() headerText` but the spec sends `header_text`, the binding fails silently—Angular's standard behavior for unknown inputs.

+

This is intentional. The registry defines *which* components exist; your component contracts define *what* they accept. Keep those contracts stable or version them explicitly.

+

Unknown Components

+

When a spec references a component name not present in the registry, `` renders nothing for that node by default. No error thrown, no console warning—just a gap in the output.

+

For development, enable strict mode through `provideRender({ strict: true })`. This throws on unknown component names, surfacing mismatches immediately.

+

For production, consider a fallback component:

+
export const uiRegistry = defineAngularRegistry({
+  // ... your components
+  '__fallback__': UnknownComponentPlaceholder
+});
+
+

The `__fallback__` key is a convention, not a framework feature. Your error boundary strategy depends on your domain—some applications should fail visibly, others should degrade gracefully.

+

Registry Versioning

+

Specs persist. Registries evolve. The mismatch creates a versioning problem.

+

The cleanest solution: never remove component names from the registry. Deprecate by redirecting old names to new implementations:

+
export const uiRegistry = defineAngularRegistry({
+  'DataGrid': DataGridV2Component,      // current
+  'DataTable': DataGridV2Component,     // legacy alias
+});
+
+

For breaking changes in prop shape, version the component name itself (`CardV2`) or handle transformation inside the component. The registry stays stable; the component absorbs the complexity.

+
+
+
Chapter 4
+

Streaming JSON Patches

+

# Streaming JSON Patches

+

Generative UI collapses the moment you wait for complete responses. A data table with 50 rows, each containing nested product details, might produce 15KB of JSON. Sending the full document on every update—when a single cell changes—creates unnecessary latency and forces the UI to block until the entire payload arrives. Users stare at spinners while the agent has already produced usable content.

+

@ngaf/render solves this with JSON Patch (RFC 6902), streaming incremental operations that mutate the UI spec in place as the agent generates it.

+

The Problem with Full-Document Streaming

+

Consider an agent building a dashboard. The initial render spec might be 8KB. Adding a chart adds 2KB. Traditional approaches either:

+

1. Wait for the complete spec before rendering anything +2. Re-transmit the entire document on each change

+

Both approaches scale poorly. A spec that grows through 20 incremental updates would transmit 20 full copies—potentially hundreds of kilobytes for what amounts to a few patch operations.

+

JSON Patch RFC 6902

+

JSON Patch defines three core operations for mutating JSON documents:

+
  • add: Insert a value at a path (`/dashboard/widgets/3`)
  • +
  • replace: Swap a value at an existing path
  • +
  • remove: Delete a value at a path
  • +
+Each operation targets a specific JSON Pointer location. The patch `[{"op": "add", "path": "/rows/-", "value": {"id": 42, "name": "Widget"}}]` appends a single row without touching the rest of the document. +

Patch-Based Agent Output

+

Instead of emitting complete specs, the agent streams patch operations as it generates content:

+
{"op": "add", "path": "/rows/0", "value": {"id": 1, "status": "pending"}}
+{"op": "add", "path": "/rows/1", "value": {"id": 2, "status": "active"}}
+{"op": "replace", "path": "/rows/0/status", "value": "complete"}
+
+

Each line is independently parseable. The render layer applies patches immediately, updating only the affected portion of the component tree.

+

Partial-JSON Parsing and Skeleton States

+

Real streams don't arrive in neat lines. TCP chunks split mid-token. @ngaf/render handles incomplete JSON by maintaining parse state across chunks, rendering valid portions while buffering incomplete fragments.

+

Skeleton states emerge naturally from this model. The agent can emit structural placeholders first—empty arrays, loading indicators—then fill them progressively:

+
@Component({
+  template: ``,
+})
+export class DashboardComponent {
+  private store = signalStateStore({ rows: [] });
+  spec = this.store.state;
+  registry = defineAngularRegistry({ DataTable, SkeletonRow });
+

constructor() { + this.streamPatches().subscribe(patch => this.store.applyPatch(patch)); + } +} +

+

The `signalStateStore` from @ngaf/render manages immutable state updates. Each `applyPatch` call triggers fine-grained Angular signals, re-rendering only components bound to changed paths.

+

Performance Characteristics

+

Patch-based updates are O(change), not O(spec size). Appending one row to a 500-row table touches one array slot. The differ doesn't walk the entire spec; it applies the operation directly to the target path.

+

This matters at scale. A real-time monitoring dashboard receiving 10 updates per second would choke on full-document replacement. With patches, each update carries only the delta—typically under 200 bytes—and applies in microseconds.

+

The tradeoff is complexity at the agent layer. Your backend must track spec state and emit valid patches. But the rendering performance gains compound: faster time-to-first-paint, lower bandwidth, and UI that feels alive as the agent thinks.

+
+
+
Chapter 5
+

State Management & Computed Functions

+

# State Management & Computed Functions

+

Static UI specs hit a wall fast. The moment you need a total that updates when line items change, or a button that disables based on form state, you're beyond what static JSON can express. Production generative UI requires computed properties and collection rendering—capabilities that @ngaf/render delivers through `signalStateStore()` and spec-level computed functions.

+

Agent-Managed State with signalStateStore()

+

`signalStateStore()` creates a reactive state container that both agents and components can manipulate. The agent initializes state through the spec, components update it via user interaction, and computed properties derive new values automatically.

+
import { signalStateStore } from '@ngaf/render';
+

const store = signalStateStore({ + items: [ + { name: 'Widget', price: 25, quantity: 2 }, + { name: 'Gadget', price: 40, quantity: 1 } + ], + taxRate: 0.08 +});

+

const spec = { + type: 'invoice', + state: store, + subtotal: { $compute: 'items.reduce((sum, i) => sum + i.price * i.quantity, 0)' }, + tax: { $compute: 'subtotal * taxRate' }, + total: { $compute: 'subtotal + tax' }, + lineItems: { + $repeat: 'items', + type: 'line-item', + name: { $compute: '$item.name' }, + amount: { $compute: '$item.price * $item.quantity' } + } +}; +

+

The store exposes signals. When `items` changes—whether from agent streaming or user input—`subtotal`, `tax`, and `total` recompute. No imperative wiring required.

+

Computed Properties: Declarative Derived State

+

Computed properties use the `$compute` key to define expressions evaluated at render time. These expressions access the state store's current values and can reference other computed properties, enabling dependency chains.

+

The expression syntax is intentionally constrained JavaScript. It supports property access, arithmetic, array methods, and ternary operators—enough for UI logic, not enough to become a security liability. The renderer evaluates expressions in a sandboxed context with access only to the state store and iteration variables.

+

Repeat Loops for Collections

+

The `$repeat` directive iterates over arrays in state, rendering a component instance for each element. Within the repeated block, `$item` references the current element and `$index` provides the iteration index. This handles the common case of rendering lists, tables, and card grids without requiring the agent to enumerate every instance.

+

Drawing the Line: Spec Logic vs. Component Logic

+

Computed functions handle derived *data*. Components handle derived *behavior*. If you're calculating a display value—totals, formatted dates, conditional text—that belongs in the spec. If you're managing focus, coordinating animations, or handling complex validation workflows, that belongs in the component.

+

The heuristic: if an agent could reasonably want to change the logic, put it in the spec. If the logic is intrinsic to how the component works, keep it in the component.

+

Testing Computed Behavior

+

Test computed properties by manipulating the state store and asserting against the rendered output:

+
it('should recompute total when items change', () => {
+  const store = signalStateStore({ items: [{ price: 10, quantity: 1 }], taxRate: 0.1 });
+  const spec = { type: 'invoice', state: store, total: { $compute: 'items[0].price * items[0].quantity * (1 + taxRate)' } };
+  
+  const fixture = TestBed.createComponent(TestHost);
+  fixture.componentInstance.spec = spec;
+  fixture.detectChanges();
+  
+  expect(fixture.nativeElement.textContent).toContain('11'); // 10 * 1.1
+  
+  store.update(s => ({ ...s, items: [{ price: 20, quantity: 2 }] }));
+  fixture.detectChanges();
+  
+  expect(fixture.nativeElement.textContent).toContain('44'); // 40 * 1.1
+});
+
+

Drive the store, trigger change detection, assert the DOM. The reactive graph handles the rest.

+
+ + + diff --git a/apps/website/public/whitepapers/render.pdf b/apps/website/public/whitepapers/render.pdf new file mode 100644 index 000000000..0f0cacb29 Binary files /dev/null and b/apps/website/public/whitepapers/render.pdf differ diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts new file mode 100644 index 000000000..a77ae6b4d --- /dev/null +++ b/apps/website/scripts/generate-whitepaper.ts @@ -0,0 +1,710 @@ +import Anthropic from '@anthropic-ai/sdk'; +import fs from 'fs'; +import path from 'path'; +import puppeteer from 'puppeteer'; + +const loadEnvFile = (process as typeof process & { loadEnvFile?: (path?: string) => void }).loadEnvFile; +if (!process.env['ANTHROPIC_API_KEY'] && loadEnvFile && fs.existsSync('.env')) { + loadEnvFile('.env'); +} + +const client = new Anthropic(); +const MODEL = process.env['ANTHROPIC_MODEL'] ?? 'claude-opus-4-5'; + +const CURRENT_API_CONTEXT = `You are writing public technical whitepapers for Cacheplane Angular Agent Framework. + +Use only the current API surface: +- Package names are @ngaf/langgraph, @ngaf/render, @ngaf/chat, and @ngaf/ag-ui. +- @ngaf/langgraph exposes agent(), provideAgent(), LangGraphAgent, MockAgentTransport, FetchStreamTransport, and mockLangGraphAgent(). +- agent() returns a runtime-neutral chat surface with messages(), status(), isLoading(), error(), toolCalls(), state(), submit(), stop(), regenerate(), interrupt(), subagents(), and LangGraph-specific langGraph* signals. interrupt() is AgentInterrupt | undefined on the runtime-neutral surface. +- Configure LangGraph with assistantId and apiUrl. Do not use graphId or url as @ngaf/langgraph option names. +- @ngaf/chat consumes the runtime-neutral Agent contract and exports ChatComponent, ChatMessageListComponent, ChatInputComponent, ChatToolCallsComponent, ChatToolCallCardComponent, ChatInterruptPanelComponent, and ChatDebugComponent. Selectors are , , , , , , and . +- Chat messages use Message[] from @ngaf/chat for the runtime-neutral surface. Raw LangGraph messages, when needed, are exposed through langGraphMessages(). +- Angular examples should call agent() directly in a component field initializer, for example: readonly chat = agent({ assistantId: 'chat_agent', threadId: this.threadId, onThreadId: id => this.threadId.set(id) }). Do not inject LangGraphAgent as a service. Do not invent a wrapper service around LangGraphAgent. +- @ngaf/render exposes render-spec, defineAngularRegistry(), provideRender(), signalStateStore(), JSON Patch streaming, and A2UI support. Registry examples may pass [registry] directly to or configure provideRender({ registry }). + +Never mention legacy names: streamResource, provideStreamResource, AgentRef, MockStreamTransport, createMockStreamResourceRef, createMockAgentRef, injectAgentRef, isStreaming, @cacheplane/angular, @cacheplane/render, @cacheplane/chat, @cacheplane/langgraph, AgentService, or chat-prebuilt.`; + +const BANNED_TERMS = [ + 'streamResource', + 'provideStreamResource', + 'AgentRef', + 'MockStreamTransport', + 'createMockStreamResourceRef', + 'createMockAgentRef', + 'injectAgentRef', + 'isStreaming', + '@cacheplane/angular', + '@cacheplane/render', + '@cacheplane/chat', + '@cacheplane/langgraph', + 'AgentService', + 'chat-prebuilt', + 'AIMessage[]', + 'provideRenderRegistry', + 'evaluateComputed', + 'inject(LangGraphAgent)', + 'langGraph.agent', + 'import { ChatDebug }', + 'imports: [ChatDebug]', + 'InterruptState', + 'returns `null`', +]; + +// ── 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: '@ngaf/langgraph · 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 agent() exposes messages() as Signal +- How isLoading() lets developers drive loading UI without polling +- Code example: minimal agent() 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: provideAgent() plus agent({ threadId }) with thread persistence (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 agent() 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 MockAgentTransport approach: scripted event sequences, no server needed +- mockLangGraphAgent(): writable signals you control directly in tests +- How to test streaming, interrupts, tool calls, and generative UI in isolation +- Code example: mockLangGraphAgent() 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 LangGraphAgent with typed signals: messages(), isLoading(), error(), interrupt(), and langGraph* raw signals +- 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 undefined to an AgentInterrupt 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: +- MockAgentTransport: scripting a deterministic sequence of SSE events without a server +- mockLangGraphAgent(): 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 MockAgentTransport (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 directly to or via provideRender({ registry }) 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 behavior by driving signalStateStore() state and asserting the rendered output. Do not cite a separate testing helper API. + +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 composition tier: plus companion components for a full chat interface with minimal 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 runtime-neutral Agent contract from @ngaf/langgraph +- Message rendering: how Message[] from the agent signal maps to chat message display +- Code example: with an agent instance (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: Message[] 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.ai · ${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, + system: CURRENT_API_CONTEXT, + messages: [{ role: 'user', content: chapter.prompt }], + }); + const content = message.content[0]; + if (content.type !== 'text') throw new Error(`Unexpected content type: ${content.type}`); + const banned = BANNED_TERMS.filter(term => content.text.includes(term)); + if (banned.length) { + throw new Error(`Generated chapter "${chapter.title}" included stale API terms: ${banned.join(', ')}`); + } + 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 88ce9c302..276d2a6fb 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 RESPONSE_EMAILS: Record string> = { +const DOWNLOAD_EMAILS: Record string> = { overview: whitepaperDownloadHtml, angular: angularDownloadHtml, render: renderDownloadHtml, chat: chatDownloadHtml, }; -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', +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', }; export async function POST(req: NextRequest) { @@ -55,20 +55,20 @@ export async function POST(req: NextRequest) { console.error('Failed to write signup:', err); } - // Send confirmation + schedule drip + sync contacts (best-effort) + // Send download confirmation + schedule drip + sync contacts (best-effort) try { - const responseHtml = RESPONSE_EMAILS[paper](name || undefined); + const downloadHtml = DOWNLOAD_EMAILS[paper](name || undefined); await Promise.all([ sendEmail({ from: FROM, to: email, - subject: RESPONSE_SUBJECTS[paper], - html: responseHtml, + subject: DOWNLOAD_SUBJECTS[paper], + html: downloadHtml, }), scheduleWhitepaperDrip(email, paper), addToAudience(email, name || undefined), loopsUpsertContact({ email, firstName: name || undefined, source: `whitepaper-${paper}` }), - loopsSendEvent({ email, eventName: 'whitepaper_signup', properties: { paper } }), + loopsSendEvent({ email, eventName: 'whitepaper_downloaded', 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 554a34023..5681b4628 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 — guide update signup */} + {/* 6. Lead gen — whitepaper download */} {/* 7. Final CTA — "Ready to stop stalling?" */} diff --git a/apps/website/src/components/landing/ChatFeaturesSection.tsx b/apps/website/src/components/landing/ChatFeaturesSection.tsx index a53e208b2..0c54a67a2 100644 --- a/apps/website/src/components/landing/ChatFeaturesSection.tsx +++ b/apps/website/src/components/landing/ChatFeaturesSection.tsx @@ -182,7 +182,7 @@ const FEATURES: Record = { stream: { label: 'Streaming', color: '#6C8EFF', rgb: '108,142,255', badgeText: 'chat-messages', left: [{ tag: '', body: 'Token-by-token rendering with live cursor. Signals-native, fully OnPush.', color: '#6C8EFF', rgb: '108,142,255' }], - right: [{ tag: 'isStreaming()', body: 'Reactive signal — true while tokens arrive. Drive spinners and disable inputs without polling.', color: '#6C8EFF', rgb: '108,142,255' }], + right: [{ tag: 'isLoading()', body: 'Reactive signal — true while tokens arrive. Drive spinners and disable inputs without polling.', color: '#6C8EFF', rgb: '108,142,255' }], question: 'Walk me through LangGraph state.', run: runStream, }, genui: { diff --git a/apps/website/src/components/landing/PilotFooterCTA.tsx b/apps/website/src/components/landing/PilotFooterCTA.tsx index ec4776b8c..99eb0e9b0 100644 --- a/apps/website/src/components/landing/PilotFooterCTA.tsx +++ b/apps/website/src/components/landing/PilotFooterCTA.tsx @@ -117,7 +117,8 @@ export function PilotFooterCTA() { {/* Secondary CTA */} - Read the Docs + Download the Guide diff --git a/apps/website/src/components/landing/PilotHero.tsx b/apps/website/src/components/landing/PilotHero.tsx index 73d920b2d..6651aa262 100644 --- a/apps/website/src/components/landing/PilotHero.tsx +++ b/apps/website/src/components/landing/PilotHero.tsx @@ -156,7 +156,8 @@ export function PilotHero() { {/* Secondary CTA */} - Read the Docs + Download the Guide diff --git a/apps/website/src/components/landing/WhitePaperSection.tsx b/apps/website/src/components/landing/WhitePaperSection.tsx index 421200133..d4ce2590b 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 } from '../../lib/analytics/client'; +import { track, trackWhitepaperDownloadClick } from '../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -112,10 +112,16 @@ 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, }}> - ✓ Thanks! We'll send the refreshed guide when it is ready. + ✓ Check your inbox — the guide is on its way! trackWhitepaperDownloadClick('overview', { + surface: 'home', + source_section: 'whitepaper-section', + cta_id: 'whitepaper_section_direct_download', + })} style={{ display: 'inline-block', marginTop: 12, @@ -125,7 +131,7 @@ export function WhitePaperSection() { fontFamily: 'Inter, sans-serif', }} > - Read the current docs + or download directly ) : ( @@ -173,11 +179,17 @@ export function WhitePaperSection() { transition: 'background .2s, border-color .2s', }} > - {formState === 'submitting' ? 'Sending…' : 'Send me updates'} + {formState === 'submitting' ? 'Sending…' : 'Send me the guide'} trackWhitepaperDownloadClick('overview', { + surface: 'home', + source_section: 'whitepaper-section', + cta_id: 'whitepaper_section_direct_download', + })} style={{ display: 'inline-block', marginTop: 12, @@ -187,7 +199,7 @@ export function WhitePaperSection() { fontFamily: 'Inter, sans-serif', }} > - Read the current docs + or download directly )} @@ -214,8 +226,8 @@ export function WhitePaperSection() { fontStyle: 'italic', fontSize: '1rem', color: tokens.colors.textSecondary, lineHeight: 1.55, marginBottom: 28, }}> - 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. + The Angular Agent Readiness Guide. Six chapters. Six production-readiness dimensions. + What separates demos from shipped products.

    diff --git a/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx b/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx index bd11fbd72..b276522ef 100644 --- a/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx +++ b/apps/website/src/components/landing/angular/AngularCodeShowcase.tsx @@ -9,17 +9,23 @@ const chat = agent({ }); // Reactive signals — OnPush compatible -chat.messages(); // Signal -chat.isLoading(); // Signal -chat.interrupt(); // Signal`; +chat.messages(); // Signal +chat.isLoading(); // Signal +chat.interrupt(); // Signal`; -const SNIPPET_2 = `import { provideAgent } from '@ngaf/langgraph'; +const SNIPPET_2 = `import { agent, provideAgent, MockAgentTransport, FetchStreamTransport } from '@ngaf/langgraph'; provideAgent({ apiUrl: environment.langgraphUrl, transport: isTest ? new MockAgentTransport(fixtures) : new FetchStreamTransport(), +}); + +const chat = agent({ + assistantId: 'my-agent', + threadId: savedThreadId, + onThreadId: (id) => localStorage.setItem('threadId', id), });`; const SNIPPETS = [ diff --git a/apps/website/src/components/landing/angular/AngularFooterCTA.tsx b/apps/website/src/components/landing/angular/AngularFooterCTA.tsx index 0577a52a9..41ec29347 100644 --- a/apps/website/src/components/landing/angular/AngularFooterCTA.tsx +++ b/apps/website/src/components/landing/angular/AngularFooterCTA.tsx @@ -82,7 +82,8 @@ export function AngularFooterCTA() { - Get Guide Updates + Download the Guide diff --git a/apps/website/src/components/landing/angular/AngularHero.tsx b/apps/website/src/components/landing/angular/AngularHero.tsx index fcb85621f..5da6c049d 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() { - - Get Guide Updates + Download the Guide - 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. + Six chapters covering the last-mile gap, the agent() API, thread persistence, interrupts, time-travel, and deterministic testing with MockAgentTransport.

    Part of the Cacheplane Angular Agent Framework.

    - trackWhitepaperDownloadClick('angular', { + surface: 'library_landing', + source_section: 'angular-whitepaper-gate', + library: 'agent', + cta_id: 'angular_whitepaper_download', + })} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: tokens.colors.accent, color: '#fff', @@ -114,7 +120,7 @@ export function AngularWhitePaperGate() { fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', textDecoration: 'none', boxShadow: '0 4px 16px rgba(0,64,144,.28)', }}> - Read Current Docs + ↓ Download PDF @@ -124,7 +130,7 @@ export function AngularWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Get notified when the guide is refreshed + Optional — Get notified of updates

    {formState === 'done' ? (
    - ✓ Thanks! We'll reach out when the refreshed guide is ready. + ✓ Thanks! We'll reach out when the guide is updated.
    ) : (
    diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx index 482fa40a2..56ac3d524 100644 --- a/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx +++ b/apps/website/src/components/landing/chat-landing/ChatLandingFooterCTA.tsx @@ -82,7 +82,8 @@ export function ChatLandingFooterCTA() { - Get Guide Updates + Download the Guide diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingHero.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingHero.tsx index af2bba11a..a2b55828d 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() { - - Get Guide Updates + Download the Guide - 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. + 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.

    Part of the Cacheplane Angular Agent Framework.

    - trackWhitepaperDownloadClick('chat', { + surface: 'library_landing', + source_section: 'chat-whitepaper-gate', + library: 'chat', + cta_id: 'chat_whitepaper_download', + })} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: tokens.colors.chatPurple, color: '#fff', @@ -114,7 +120,7 @@ export function ChatLandingWhitePaperGate() { fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', textDecoration: 'none', boxShadow: '0 4px 16px rgba(90,0,200,.28)', }}> - Read Current Docs + ↓ Download PDF @@ -124,7 +130,7 @@ export function ChatLandingWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Get notified when the guide is refreshed + Optional — Get notified of updates

    {formState === 'done' ? (
    - ✓ Thanks! We'll reach out when the refreshed guide is ready. + ✓ Thanks! We'll reach out when the guide is updated.
    ) : ( diff --git a/apps/website/src/components/landing/render/RenderFooterCTA.tsx b/apps/website/src/components/landing/render/RenderFooterCTA.tsx index b229a9b77..949091453 100644 --- a/apps/website/src/components/landing/render/RenderFooterCTA.tsx +++ b/apps/website/src/components/landing/render/RenderFooterCTA.tsx @@ -82,7 +82,8 @@ export function RenderFooterCTA() { - Get Guide Updates + Download the Guide diff --git a/apps/website/src/components/landing/render/RenderHero.tsx b/apps/website/src/components/landing/render/RenderHero.tsx index b7cce3f51..248fe262b 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() { - - Get Guide Updates + Download the Guide - 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. + 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.

    Part of the Cacheplane Angular Agent Framework.

    - trackWhitepaperDownloadClick('render', { + surface: 'library_landing', + source_section: 'render-whitepaper-gate', + library: 'render', + cta_id: 'render_whitepaper_download', + })} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: tokens.colors.renderGreen, color: '#fff', @@ -114,7 +120,7 @@ export function RenderWhitePaperGate() { fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', textDecoration: 'none', boxShadow: '0 4px 16px rgba(26,122,64,.28)', }}> - Read Current Docs + ↓ Download PDF @@ -124,7 +130,7 @@ export function RenderWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Get notified when the guide is refreshed + Optional — Get notified of updates

    {formState === 'done' ? (
    - ✓ Thanks! We'll reach out when the refreshed guide is ready. + ✓ Thanks! We'll reach out when the guide is updated.
    ) : ( diff --git a/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx b/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx index 41f55c205..70d3dc0bf 100644 --- a/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx +++ b/apps/website/src/components/landing/solutions/SolutionFooterCTA.tsx @@ -101,7 +101,8 @@ export function SolutionFooterCTA({ color, headline, subtext }: SolutionFooterCT Start Your Pilot → - Read the Docs + Download the Guide diff --git a/apps/website/src/components/landing/solutions/SolutionHero.tsx b/apps/website/src/components/landing/solutions/SolutionHero.tsx index 59ba4fcc0..19317cd96 100644 --- a/apps/website/src/components/landing/solutions/SolutionHero.tsx +++ b/apps/website/src/components/landing/solutions/SolutionHero.tsx @@ -107,7 +107,8 @@ export function SolutionHero({ solution }: SolutionHeroProps) { Start a Pilot - Read the Docs + Download the Guide
    diff --git a/apps/website/src/components/shared/AnnouncementToast.tsx b/apps/website/src/components/shared/AnnouncementToast.tsx index fdc6e5821..c14712cb1 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 } from '../../lib/analytics/client'; +import { track, trackWhitepaperDownloadClick } 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, }}> - The Angular Agent Readiness Guide is being refreshed for the current framework API. + Six production-readiness dimensions for Angular agents. Get the guide.

    -

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

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

    )} diff --git a/apps/website/src/lib/analytics/client.ts b/apps/website/src/lib/analytics/client.ts index 8a8112e1d..ad403bef1 100644 --- a/apps/website/src/lib/analytics/client.ts +++ b/apps/website/src/lib/analytics/client.ts @@ -32,3 +32,10 @@ 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 ac7ca904f..a6099642c 100644 --- a/apps/website/src/lib/analytics/events.ts +++ b/apps/website/src/lib/analytics/events.ts @@ -1,6 +1,7 @@ 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 07fc0d818..87ababb3f 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: 'Read the docs or start a pilot. Your compliance team will thank you.', + ctaSubtext: 'Download the guide 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: 'Read the docs or start a pilot. Ship a conversational BI experience in weeks, not quarters.', + ctaSubtext: 'Download the guide 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: 'Read the docs or start a pilot. Resolve more tickets, escalate smarter, and keep your customers happy.', + ctaSubtext: 'Download the guide 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 ee0fb6445..a8f24acfb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "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-docs": "npm run generate-api-docs && npm run generate-narrative-docs", + "generate-whitepaper": "npx tsx apps/website/scripts/generate-whitepaper.ts" }, "private": true, "overrides": {