diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index d4d20142a..558aa674b 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -63,3 +63,13 @@ test('/llms.txt returns plain text', async ({ page }) => { const response = await page.goto('/llms.txt'); expect(response?.headers()['content-type']).toContain('text/plain'); }); + +test('marketing pages do not link to retired whitepaper PDFs', async ({ page }) => { + for (const route of ['/', '/angular', '/render', '/chat', '/pilot-to-prod', '/solutions']) { + await page.goto(route); + const retiredLinks = await page.locator('a[href$=".pdf"]').evaluateAll(links => + links.map(link => link.getAttribute('href')).filter(Boolean), + ); + expect(retiredLinks, `${route} has retired PDF links`).toEqual([]); + } +}); diff --git a/apps/website/emails/angular-download.ts b/apps/website/emails/angular-download.ts index 161d3c11a..16b3a6aa6 100644 --- a/apps/website/emails/angular-download.ts +++ b/apps/website/emails/angular-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/angular.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs/agent/api/agent'; export function angularDownloadHtml(name?: string): string { return wrapEmail({ body: `
Your Enterprise Guide to Agent Streaming
-${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers six chapters: the last-mile problem, the agent() API, thread persistence, interrupt flows, full LangGraph feature coverage, and deterministic testing.
+${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current @ngaf/langgraph API. We will send the updated version when it is ready. In the meantime, start with the current agent() reference.
`, }); diff --git a/apps/website/emails/chat-download.ts b/apps/website/emails/chat-download.ts index f4e497062..d347276ef 100644 --- a/apps/website/emails/chat-download.ts +++ b/apps/website/emails/chat-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/chat.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs/chat/components/chat'; export function chatDownloadHtml(name?: string): string { return wrapEmail({ body: `Your Enterprise Guide to Agent Chat Interfaces
-${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers five chapters: the sprint tax, batteries-included components, theming and design system integration, generative UI in chat, and debug tooling.
+${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current @ngaf/chat API. We will send the updated version when it is ready. In the meantime, the current chat component docs are live.
`, }); diff --git a/apps/website/emails/drip-whitepaper-followup.ts b/apps/website/emails/drip-whitepaper-followup.ts index da4b05e57..1da5b0a9f 100644 --- a/apps/website/emails/drip-whitepaper-followup.ts +++ b/apps/website/emails/drip-whitepaper-followup.ts @@ -9,7 +9,7 @@ export function dripWhitepaperFollowupHtml(day: number): { subject: string; htmlWhitepaper Follow-up
Did you get a chance to read Chapter 3?
Chapter 3 covers tool-call rendering — how to surface agent actions as real UI instead of raw JSON. It's the chapter most teams bookmark first.
- Read the Guide → + Read the Current Docs → `, showUnsubscribe: true, }), diff --git a/apps/website/emails/render-download.ts b/apps/website/emails/render-download.ts index e87c75c31..eb4a789c2 100644 --- a/apps/website/emails/render-download.ts +++ b/apps/website/emails/render-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepapers/render.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs/render/getting-started/introduction'; export function renderDownloadHtml(name?: string): string { return wrapEmail({ body: `Your Enterprise Guide to Generative UI
-${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers five chapters: the coupling problem, declarative UI specs with Vercel's json-render standard, the component registry, streaming JSON patches, and state management.
+${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current @ngaf/render API. We will send the updated version when it is ready. In the meantime, the current render docs are live.
`, }); diff --git a/apps/website/emails/whitepaper-download.ts b/apps/website/emails/whitepaper-download.ts index 5a1978fe9..6ddd7a127 100644 --- a/apps/website/emails/whitepaper-download.ts +++ b/apps/website/emails/whitepaper-download.ts @@ -1,14 +1,14 @@ import { wrapEmail, esc } from './email-wrapper'; -const DOWNLOAD_URL = 'https://cacheplane.ai/whitepaper.pdf'; +const DOCS_URL = 'https://cacheplane.ai/docs'; export function whitepaperDownloadHtml(name?: string): string { return wrapEmail({ body: `Your Angular Agent Readiness Guide
-${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers six production-readiness dimensions: streaming state, thread persistence, tool-call rendering, human approval flows, generative UI, and deterministic testing.
+${name ? `Hi ${esc(name)}, t` : 'T'}he guide is being refreshed to match the current Angular Agent Framework API. We will send the updated version when it is ready. In the meantime, the docs cover the current agent(), chat, render, and AG-UI surfaces.
`, }); diff --git a/apps/website/public/whitepaper-preview.html b/apps/website/public/whitepaper-preview.html deleted file mode 100644 index e5cfc533c..000000000 --- a/apps/website/public/whitepaper-preview.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - -The Angular Agent Readiness Guide
-When you move from prototype to production, the requirements change fundamentally. What worked in a demo — direct API calls, synchronous state, manual zone management — falls apart at scale.
-Agent provides a signals-native approach that eliminates the boilerplate:
-@Component({...})
-export class ChatComponent {
- chat = agent<{ messages: BaseMessage[] }>({
- assistantId: 'chat_agent',
- });
-}
-
-Demos work with ephemeral state. Production agents need conversation history that survives page refreshes, tab switches, and navigation — wired to LangGraph's MemorySaver backend.
-provideAgent({
- apiUrl: 'http://localhost:2024',
- threadId: signal(localStorage.getItem('threadId')),
- onThreadId: (id) => localStorage.setItem('threadId', id),
-})
-
-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.
-
-
-
-Production agents that take consequential actions must pause for human approval before proceeding. This requires a tight loop between LangGraph's interrupt() primitive and Angular UI.
-// In your component
-approved = computed(() => this.chat.interrupt()?.approved ?? false);
-onApprove() {
- this.chat.submit(null, { command: { resume: true } });
-}
-
-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.
-defineAngularRegistry({
- 'data-table': DataTableComponent,
- 'booking-form': BookingFormComponent,
- 'chart-widget': ChartWidgetComponent,
-});
-
-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.
-const ref = createMockAgentRef();
-ref.messages.set([{ role: 'assistant', content: 'Hello' }]);
-ref.isStreaming.set(false);
-expect(fixture.nativeElement.querySelector('.message').textContent)
- .toBe('Hello');
-
-Ship LangGraph agents in Angular — without building the plumbing
-Your LangGraph backend is solid. The agent graph handles tool calls correctly, memory persists across turns, and streaming responses flow cleanly through your FastAPI or Node middleware. You fire up the demo, watch tokens stream into the terminal, and everything looks exactly right.
-Then you wire it to Angular.
-This is where teams lose weeks.
----
-The first symptom is subtle: the UI feels sluggish, change detection runs are exploding in DevTools, and memory climbs steadily during long agent responses. The instinct is to reach for `runOutsideAngular`. That helps — but it doesn't solve the underlying problem.
-`EventSource`, the browser primitive that powers SSE, is patched by zone.js at application bootstrap. Every `message` event it dispatches enters the Angular zone automatically. With a REST response, that's one event, one change detection cycle. With a streaming agent response, you're receiving dozens to hundreds of discrete `message` events per second — each one triggering a zone task, each zone task potentially scheduling a change detection pass. You haven't configured anything wrong. The architecture of zone.js and the architecture of SSE are simply incompatible at streaming throughput.
-Wrapping your `EventSource` subscription in `runOutsideAngular` moves the event handling out of the zone, but now you own the re-entry problem. Every token update that touches the DOM needs to be explicitly brought back into the zone at precisely the right moment. Do it too eagerly and you're back to thrashing. Do it too conservatively and your UI lags perceptibly behind the stream.
----
-Angular's change detection model is synchronous and pull-based. The framework walks the component tree, reads current state, and updates the DOM. This works exceptionally well for request-response patterns where state settles before the template renders.
-LLM token streams are fundamentally asynchronous and push-based. A 500-token response arrives as 500 separate events across several seconds. Each event mutates your accumulator state. If you're using `ChangeDetectionStrategy.OnPush` — which you should be — none of those mutations trigger re-renders automatically unless you're managing `markForCheck` yourself on every event. If you're not on `OnPush`, you're paying the full change detection cost on every token.
-Neither option is acceptable at scale without deliberate infrastructure around it.
----
-The Signals API is the right direction, but it introduces its own friction with streaming. A `WritableSignal` updated on every SSE `message` event will notify all reactive consumers on every token. For a long agent response with tool call interleaving, you're creating a high-frequency reactive pulse that propagates through the entire signal graph. Without explicit batching or debounce strategies, Signals and SSE share the same fundamental problem as zone-based change detection — they just fail differently.
----
-Teams reach for `fromEvent(source, 'message')` and expect the familiar RxJS composability to handle everything. It handles a lot — but the operator chains you built for REST responses don't account for multi-event agent protocols. LangGraph streams `metadata`, `updates`, `messages/complete`, and error events as distinct message types within a single SSE connection. Mapping that to a clean observable that maintains agent state, accumulates tokens, handles tool call boundaries, distinguishes stream errors from agent errors, and supports reconnection is non-trivial. Every team is building this from scratch.
----
-The production gap is expensive and consistent. Teams build zone-patch wrappers, token accumulator services, custom reconnection logic, and stream-type discriminators — then rebuild them on the next project. The demo worked because it ran outside a production Angular application with real component trees, real OnPush hierarchies, and real error conditions.
-The backend is not the problem. The missing layer is a production-grade streaming adapter built specifically for how Angular actually works — not how it works in isolation, but under load, with Signals, with OnPush, and with the full complexity of a multi-turn agent protocol on the wire.
-That layer is what this guide builds.
The core primitive in `@ngaf/langgraph` is `agent()` — a factory function that returns a structured ref containing typed signals wired directly to a LangGraph agent stream. You call it once, bind the signals in your template, and the component reacts to every streamed token without a subscription, a zone trigger, or an accumulation buffer in sight.
-`agent()` returns an `AgentRef` object with four signals:
-interface AgentRef {
- messages: Signal;
- isStreaming: Signal;
- error: Signal;
- interrupt: Signal;
-}
-
-Each signal is updated incrementally as chunks arrive over the stream. `messages()` reflects the full conversation state at every emission — already assembled, already typed. `isStreaming()` flips to `true` the moment a run begins and back to `false` on completion or error. `error()` and `interrupt()` surface exceptional states that would otherwise require you to wire up parallel error channels manually.
-Before `agent()` can do anything, the agent endpoint and stream transport need to be registered. That happens in `provideAgent()`, which you add to your application providers:
-bootstrapApplication(AppComponent, {
- providers: [
- provideAgent({
- url: 'https://your-langgraph-deployment/runs/stream',
- transport: 'sse',
- }),
- ],
-});
-
-`provideAgent()` configures a single shared transport layer — SSE by default, with WebSocket support available via the `transport` option. It also accepts authentication headers and retry policies at this level, keeping that configuration out of individual components entirely.
-Angular's `OnPush` change detection skips components unless an input reference changes or an event originates from within the component tree. The traditional workaround for async data — `async` pipe or manual `markForCheck()` calls — adds noise and often breaks in subtle ways under zoneless configurations.
-Signals sidestep this entirely. When a signal value changes, Angular's reactive graph marks only the affected views as dirty. `agent()` produces standard Angular signals, which means `OnPush` components react to streamed tokens automatically, without zone involvement, without `markForCheck()`, and without the `async` pipe acting as a syntactic intermediary.
-Because `messages()`, `isStreaming()`, `error()`, and `interrupt()` are signals, you call them directly in the template:
-@Component({
- selector: 'app-chat',
- changeDetection: ChangeDetectionStrategy.OnPush,
- template: `
- @for (msg of agent.messages(); track msg.id) {
-
- }
- @if (agent.isStreaming()) {
-
- }
- `,
-})
-export class ChatComponent {
- readonly agent = inject(AgentService).ref;
-}
-
-No `async` pipe. No `subscribe()`. No lifecycle hooks managing teardown. The template reads signal values and Angular handles the rest.
-The hand-rolled equivalent requires roughly 60–80 lines: an `HttpClient` call configured for streaming, an `EventSource` or `fetch` with `ReadableStream`, manual chunk parsing, a `BehaviorSubject` to accumulate tokens, a `takeUntilDestroyed` teardown, and explicit `markForCheck()` calls on every emission. You also write the error and interrupt handling yourself.
-With `agent()`, that entire surface area collapses to three lines of meaningful code:
-const ref = agent();
-ref.send({ role: 'user', content: input });
-// bind ref.messages() in template
-
-The complexity didn't disappear — it moved into the library, where it belongs. What remains in your component is only the code that describes your feature.
Production agent applications are not stateless. Users close tabs, return the next morning, and expect the conversation to resume exactly where it stopped. Building that experience requires more than a reliable backend—it requires a disciplined contract between LangGraph's checkpoint system and your Angular frontend.
-When LangGraph processes a run under a `MemorySaver` configuration, it persists the conversation graph state to a checkpoint store keyed by a `thread_id`. That identifier is the anchor for the entire session. On the Angular side, the agent library exposes this value through a reactive `threadId` signal that updates as soon as the backend confirms checkpoint creation. Every subsequent request that includes this `thread_id` in its config resumes from the last saved state—no reconstruction, no replaying events.
-Treat the `threadId` signal as your source of truth. Do not generate identifiers client-side and hope for synchronization. Let the backend issue the ID, observe it, and persist it.
-The `onThreadId` callback fires exactly once per new thread, immediately after the backend returns a checkpoint-backed ID. This is your window to write to `localStorage`:
-provideAgent({
- streamUrl: '/api/agent/stream',
- onThreadId: (id) => {
- const threads = JSON.parse(localStorage.getItem('agent_threads') ?? '[]');
- threads.unshift({ id, created: Date.now(), label: 'New conversation' });
- localStorage.setItem('agent_threads', JSON.stringify(threads));
- localStorage.setItem('agent_active_thread', id);
- },
- initialThreadId: localStorage.getItem('agent_active_thread') ?? undefined,
-})
-
-The `initialThreadId` input tells the agent to hydrate from an existing checkpoint on mount rather than starting a new thread. When provided, the agent skips the initialization handshake and immediately enables streaming against the existing state. If the ID is `undefined`, a fresh thread begins and `onThreadId` fires again.
-A thread switcher is straightforward in signal-based Angular. Read the thread list from `localStorage` into a component signal, render it, and call `setThreadId()` when the user selects an entry. Clearing history means removing the entry locally and optionally calling a backend deletion endpoint—though MemorySaver's in-memory default does not expose one out of the box.
-Wire up a "New conversation" button that clears `agent_active_thread` from storage and reinitializes the agent component. Since `initialThreadId` is an input, destroying and recreating the component is the cleanest reset path.
-`MemorySaver` holds checkpoints in process memory, making it suitable for development and single-instance deployments. Each `thread_id` maps to a serialized graph snapshot. The frontend lifecycle mirrors this directly: create a thread ID on first run, persist it, restore it on return, discard it on explicit reset. In production, swap `MemorySaver` for a persistent store—Postgres via `@langchain/langgraph-checkpoint-postgres` is the standard choice—without changing any frontend code.
-Before shipping thread persistence, verify the following:
-Agents that interact with external systems — firing off emails, mutating database records, executing privileged queries — cannot operate as fire-and-forget processes in an enterprise context. Execution must be pauseable, inspectable, and resumable with explicit human intent. LangGraph's `interrupt()` primitive and `@ngaf/langgraph`'s reactive surface for it give you exactly that, without polling loops or bespoke WebSocket plumbing.
-When a LangGraph node calls `interrupt(payload)`, graph execution halts at that checkpoint. The payload is an arbitrary object you define — typically the action description, affected resource identifiers, and any data the reviewer needs to make a decision. The graph persists this state to its checkpointer and waits. Nothing proceeds until a resume command arrives with the correct thread ID and checkpoint reference.
-The resume payload follows a typed contract: `{ action: "approve" | "edit" | "cancel", data?: unknown }`. On `approve`, the graph continues with the original node inputs. On `edit`, your modified `data` is injected, replacing what the node would have used. On `cancel`, LangGraph short-circuits the remaining path and routes to whatever terminal state you've configured for rejected flows.
-`@ngaf/langgraph` exposes this checkpoint state directly on the agent reference. The `interrupt` property is a computed signal that starts as `null` and transitions to an `InterruptState` object the moment the backend checkpoint is written:
-const agent = injectAgentRef({ threadId });
-const interrupt = agent.interrupt; // Signal
-const handleApprove = () =>
- agent.resume({ action: Command.RESUME, data: { decision: "approve" } });
-const handleEdit = (corrected: unknown) =>
- agent.resume({ action: Command.RESUME, data: { decision: "edit", payload: corrected } });
-const handleCancel = () =>
- agent.resume({ action: Command.RESUME, data: { decision: "cancel" } });
-
-No subscriptions. No manual HTTP calls to a resume endpoint. The signal reactivity handles template updates automatically — when `interrupt()` fires on the backend, your component re-renders with the interrupt context populated.
-`
-
-For teams with custom design systems, the panel exposes slot-based overrides without requiring you to reimplement the state machine behind it.
-Navigation mid-interrupt: The graph checkpoint persists server-side regardless of client state. On return to the thread, the interrupt signal will re-populate from the checkpoint. Guard your route exit with a confirmation dialog if the interrupt represents a time-sensitive action.
-Session expiry: If the user's session expires while a checkpoint is pending, re-authentication should restore the thread context. Ensure your auth interceptor reattaches credentials before the agent ref re-initializes — otherwise the resume call will 401 against an unauthenticated thread.
-Cancel with partial state: Cancel does not roll back side effects that occurred before `interrupt()` was called. If a node partially wrote records before pausing, your cancel handler needs to trigger compensating logic — either through a dedicated LangGraph cancel node or a separate cleanup endpoint. Treat `interrupt()` as a pre-execution gate, not a transaction boundary.
-Interrupt flows are where the difference between a prototype and a production agent becomes visible. Get the pause-resume contract right, and the rest of the approval UX is straightforward to build on top of it.
Basic chat streaming is a solved problem. The real engineering challenge begins when your graph does something more interesting—calls a tool, delegates to a subagent, or needs to rewind to a prior checkpoint. Most Angular LLM libraries handle the simple case and then quietly stop working. This chapter documents how `@ngaf/langgraph` exposes the full LangGraph feature surface through a coherent signal-based API.
----
-When a LangGraph node invokes a tool, the graph emits a structured sequence of events: the tool call intent, intermediate streaming deltas, and the final tool result. Rather than requiring you to parse raw SSE frames and reconstruct this sequence manually, `@ngaf/langgraph` surfaces tool invocation lifecycle through the agent ref directly.
-The `toolCalls` signal on the agent ref updates reactively as each tool call progresses. You get the tool name, input arguments as they stream in, execution status, and the final output—all typed, all reactive. Your component never needs to touch the underlying event stream to render a "Searching the web…" indicator or display structured tool output inline.
-This matters because tool call parsing is subtle. Argument deltas arrive as partial JSON strings. Parallel tool calls interleave. A naive implementation collapses these into noise. The library handles reconstruction and ordering internally.
----
-LangGraph supports nested graphs, and real production agents use them. Events emitted inside a subgraph are namespaced by graph path, which means consuming them correctly requires understanding the event hierarchy.
-`@ngaf/langgraph` flattens subgraph events into the parent stream while preserving their origin metadata. If you care about which subgraph produced a message—for routing UI panels, for logging, or for debugging—you have access to the full event path. If you don't care, the default signal behavior aggregates everything cleanly without requiring you to configure traversal logic.
----
-LangGraph's checkpoint system allows you to rewind graph state to any prior node execution and re-run from that point. This is not a niche feature—it's the foundation of any agent UI that allows users to correct mistakes or explore alternative paths.
-The agent ref exposes a `rewindTo(checkpointId)` method. Calling it terminates the current stream, restores the specified checkpoint, and begins re-streaming from that node. Signals update exactly as they would on an initial run. Your component code doesn't need to distinguish between a first run and a rewound run—the reactive contract is identical.
----
-DeepAgent introduces orchestrator-subagent topology at the stream level. An orchestrator dispatches work to specialized agents and aggregates their outputs. From the stream consumer's perspective, this produces interleaved events from multiple graph instances.
-`@ngaf/langgraph` maps each active agent to its own scoped signal context. The orchestrator's agent ref exposes a `subagents` signal—a reactive map of active agent IDs to their respective state signals. You can bind a list of subagent components to this map and let Angular's `@for` loop handle the rest. Agent addition, completion, and failure all propagate as signal updates without manual subscription management.
----
-LangGraph nodes can emit arbitrary structured events outside the message channel. These are the primitives for generative UI, analytics instrumentation, and intermediate state reporting.
-The `onCustomEvent` hook accepts a typed handler that fires whenever a matching event arrives in the stream. You define the event schema; the library routes and deserializes the payload. The handler can write directly to a component signal, dispatch to a store, or trigger a side effect. Custom events are not mixed into the message stream—they arrive as a clean parallel channel.
----
-The failure mode to avoid is a two-tier implementation: the library handles the simple case, and your team writes custom SSE parsing for everything else. That split is where bugs live and where onboarding breaks down. An integration layer is only valuable if it holds across the full feature surface. When it doesn't, engineers route around it, and you've paid the abstraction cost without capturing the benefit.
Agent UIs introduce testing challenges that standard Angular component tests don't prepare you for. Streaming responses are asynchronous and stateful. LLM outputs are non-deterministic by definition. Tool calls can arrive mid-stream, get interrupted, or fail silently. If your component tests make real network calls to an LLM backend, you've already lost — slow CI pipelines, flaky outcomes, and zero ability to reproduce the specific edge case that broke production on a Friday afternoon.
-The solution is full control over the data layer. Every signal value, every SSE event, every state transition should be scripted before the test runs.
----
-`MockStreamTransport` replaces your HTTP or WebSocket transport layer with a deterministic event queue. You script a sequence of SSE-style events — token chunks, tool call payloads, interrupt signals, error frames — and the transport emits them synchronously or on a controlled microtask schedule. No server required.
-const transport = new MockStreamTransport([
- { type: 'token', value: 'The capital' },
- { type: 'token', value: ' of France is Paris.' },
- { type: 'done' }
-]);
-
-Events can be queued to emit immediately or after a configurable tick, giving you precise control over partial-stream states. You can script an interrupted stream by placing an `interrupt` event mid-sequence, or trigger an error frame at any position to verify your component's error boundary behavior.
----
-For components that consume stream state through Angular Signals, `createMockStreamResourceRef()` gives you direct write access to the signals your component reads. Instead of driving state through the transport layer, you set signal values directly and assert on the resulting DOM.
-const ref = createMockStreamResourceRef({
- status: signal('streaming'),
- tokens: signal('Partial respon'),
- toolCalls: signal([]),
- error: signal(null)
-});
-
-This is the right primitive for isolated component tests. You're not testing the streaming pipeline — you're testing that your component renders correctly given a specific signal state. Keep the concerns separated.
----
-Every meaningful agent state needs a dedicated test. Set the signal values, run `fixture.detectChanges()`, and assert on the DOM.
-Thread switching tests should use `createMockStreamResourceRef()` to swap the active thread reference and confirm the component clears its previous state correctly before rendering the new thread's history.
----
-TestBed.configureTestingModule({
- imports: [AgentChatComponent],
- providers: [
- { provide: STREAM_TRANSPORT, useValue: new MockStreamTransport([]) },
- { provide: AgentStreamService, useClass: MockAgentStreamService }
- ]
-});
-fixture = TestBed.createComponent(AgentChatComponent);
-ref = createMockStreamResourceRef({ status: signal('idle'), tokens: signal('') });
-fixture.componentRef.setInput('streamRef', ref);
-fixture.detectChanges();
-
----
-Agent component tests should run completely offline and complete in under 100ms each. If a test exceeds that threshold, it's making real I/O, using real timers, or waiting on something it shouldn't know about. Treat a slow test as a design signal: your component has a seam that isn't properly abstracted. Fix the abstraction, not the timeout.
Production agent chat UI in days, not sprints
-Every Angular team building an agent application eventually arrives at the same moment: the backend is ready. The LLM is wired up, the tools are registered, the streaming endpoint is live. Then someone opens a new component file and types `chat.component.ts`, and the next six weeks disappear.
-This is the sprint tax—the recurring, predictable cost of building chat UI infrastructure that every team pays independently, from scratch, as though it hasn't been solved before.
-A production-ready agent chat interface is not a text box and a message list. The actual surface area looks like this:
-Teams that spend six weeks building this are not slow. They are absorbing a structural tax that has nothing to do with the differentiated value they are trying to ship. The chat chrome is table stakes. The agent graph, the tool integrations, the domain-specific interrupt logic, the retrieval pipeline—that is the product. The message list is not.
-The hidden costs compound the timeline. Accessibility for streaming content is genuinely hard: ARIA live regions can flood screen readers if implemented naively, and managing focus across dynamically inserted content requires deliberate engineering. Streaming display has edge cases that do not appear in demos—what happens when a chunk arrives after the user has already submitted the next message? What does the UI do with a malformed SSE frame? Tool call state machines look simple until you model cancellation, retry, and partial results simultaneously. These problems are not insurmountable, but they take time that no product roadmap budgets honestly.
-"Good enough for demo" is a message bubble that renders a full response after a fake typing delay. Production is a component that handles backpressure from a real streaming endpoint, recovers from network interruption without corrupting message state, displays tool execution in real time, and remains navigable by keyboard without visual regression. The gap between those two things is where most teams discover the actual scope of the problem—usually during QA in week five.
-Senior Angular engineers who spend three sprints on chat infrastructure are not spending those sprints on agent integration. They are not building the domain-specific tool interfaces, the custom interrupt flows, or the application state management that ties agent output to the rest of the product. Those are the things that will differentiate the application. The scroll behavior will not.
-`@ngaf/chat` exists to eliminate the sprint tax. Ship the chat UI on day one—message rendering, streaming, tool call cards, interrupt panels, accessibility, mobile layout, all of it—and spend the sprints on what actually matters: the integration work that is specific to your agent, your domain, and your users.
-The backend is ready. The chat UI should be too.
`@ngaf/chat` ships two distinct component tiers. The prebuilt tier gives you a production-ready chat interface with a single element. The headless tier gives you behavior and state without a single line of CSS opinion. Your team picks the tier that matches how much control you actually need — not how much you think you might need someday.
-Most teams start with prebuilt and never leave. That is the correct call.
----
-Headless components own the hard parts: state management, message streaming, keyboard interactions, accessibility semantics, and agent lifecycle integration. They render nothing styled.
-The four primitives are:
----
-`chat-prebuilt` is a single component that composes all four headless primitives internally, applies a production-quality default theme, and handles responsive layout. Zero configuration is required beyond connecting an agent reference.
-import { AgentService } from '@ngaf/langgraph';
-import { ChatPrebuiltComponent } from '@ngaf/chat';
-@Component({
- selector: 'app-support',
- imports: [ChatPrebuiltComponent],
- template: `
-
- `
-})
-export class SupportComponent {
- constructor(public agentService: AgentService) {}
-}
-
-That is a fully functional streaming chat interface. Message history, input handling, tool call display, and interrupt controls are all included.
----
-Both tiers consume the `AgentRef` produced by `@ngaf/langgraph`. The `AgentRef` is a stable reference that exposes an `AIMessage[]` signal, a send method, and a cancellation interface. Components bind to this ref and react to signal emissions via Angular's standard reactivity primitives — no custom change detection strategies required, no zone workarounds.
-When the agent emits a new token mid-stream, the `AIMessage[]` signal updates, and `chat-messages` incrementally patches the DOM. The mechanism is straightforward signal subscription. There is no proprietary diffing layer to reason about.
----
-The tiers are not mutually exclusive. A common pattern: use `chat-prebuilt` for the standard chat surface, then replace specific sections with headless primitives where your requirements diverge. If your design requires a custom tool call renderer — perhaps displaying structured data in a proprietary grid — you drop `chat-tool-calls` into that slot directly and style it yourself while leaving the rest of `chat-prebuilt` intact.
----
-Start with `chat-prebuilt`. Migrate a section to headless when a specific component's default rendering is structurally incompatible with your design system — not when you want to tweak padding or colors, which CSS custom properties handle without touching the component tier at all.
-Headless is not a reward for complexity. It is a tool for genuine structural divergence.
A chat interface that looks like it came from a SaaS starter kit is a trust problem. Users notice when a modal, sidebar, or embedded panel breaks the visual contract established by the rest of your product. For enterprise teams, that inconsistency signals a lack of ownership — and in agent-facing tools, ownership matters. This chapter covers how to bring `@ngaf/chat` fully into your design system without forking component source or writing brittle CSS overrides.
----
-`@ngaf/chat` exposes all visual decisions as CSS custom properties scoped under the `--cp-` namespace. These are not internal implementation details — they are the public API for visual configuration. Properties are declared on the host element and cascade normally through Shadow DOM boundaries where applicable, giving you a single override surface regardless of where components are mounted in the Angular component tree.
-The token surface covers six categories: color (surface, text, border, accent, semantic), typography (family, size scale, weight, line height), spacing (component padding, message gap, input height), shape (border radius at three scale points), motion (transition duration and easing), and elevation (box shadow values).
----
-If your design system already emits CSS custom properties — through Style Dictionary, Theo, or a Figma Tokens pipeline — mapping to `@ngaf/chat` is a one-to-one alias operation. Define the mappings in a single stylesheet or Angular style encapsulation block. Avoid hardcoding raw values into `--cp-` properties directly; alias through your own tokens so that upstream design system changes propagate automatically.
----
-Font family, size scale, and line height are independently configurable. `--cp-font-family` accepts any valid CSS font stack. Size tokens (`--cp-text-sm`, `--cp-text-base`, `--cp-text-lg`) map to the message body, metadata labels, and heading contexts respectively. If your type scale uses `rem` values derived from a root size override, `@ngaf/chat` respects document root sizing — no unit conversion required.
----
-Surface tokens (`--cp-surface-base`, `--cp-surface-raised`, `--cp-surface-input`) control background layering. Text tokens handle primary content, secondary metadata, and disabled states. `--cp-accent` drives interactive elements — send buttons, link text, focus rings. Semantic tokens (`--cp-color-error`, `--cp-color-warning`, `--cp-color-success`) control inline status indicators on message delivery states and tool call results. Map these directly to your semantic palette.
----
-Because the entire visual surface is token-driven, dark mode requires no component changes. Apply alternate token values under a `[data-theme="dark"]` attribute selector or a `prefers-color-scheme` media query. The component renders whatever the cascade resolves. If your Angular application manages theme switching through a class or attribute on `
` or ``, the chat components respond automatically. ----
-cp-chat-window {
- --cp-font-family: 'Inter', sans-serif;
- --cp-surface-base: var(--ds-background-primary);
- --cp-surface-raised: var(--ds-background-elevated);
- --cp-surface-input: var(--ds-background-secondary);
- --cp-text-primary: var(--ds-text-default);
- --cp-text-secondary: var(--ds-text-subtle);
- --cp-accent: var(--ds-color-brand-500);
- --cp-border-radius-md: var(--ds-radius-md);
- --cp-color-error: var(--ds-color-critical);
- --cp-transition-duration: 150ms;
-}
-
-Ten properties. Full brand alignment. No component source required.
----
-CSS custom properties cannot retheme structural decisions: message bubble layout, avatar placement geometry, the composition of the input toolbar, or the ordering of action elements. If your design system requires layout-level divergence — for example, a top-mounted input bar or a two-column transcript layout — the token API will not reach that far. That is the correct boundary for dropping to the headless tier, where `@ngaf/chat/headless` exposes behavior and state without any rendered output, and your team owns the template entirely. Use tokens for brand alignment; use headless for structural ownership.
Text responses have a ceiling. When a financial agent needs to surface a sortable data table or a scheduling agent needs to render an interactive booking form, prose falls short. Generative UI closes that gap by allowing agents to emit structured UI specifications directly into the message stream — specifications that the chat interface resolves into live Angular components.
-From the chat renderer's perspective, a generative UI message is just another message type. The message stream carries a `type` discriminator — `text`, `tool_call`, `ui_spec`, and so on. When `@ngaf/chat` encounters a `ui_spec` message, it routes it to the UI renderer instead of the text pipeline. The result is that a data table, approval card, or booking form appears inline between conversational turns, indistinguishable in position from any other message bubble, but rendered as a fully interactive Angular component.
-The json-render specification is a JSON-based declarative format for describing UI trees. An agent emits a payload describing component type, props, and children. The chat layer deserializes that payload and resolves each component node against a registry. Because the spec is declarative and serializable, it travels cleanly over SSE or WebSocket streams and requires no client-side code changes when an agent starts emitting new component types — only a registry entry.
-A2UI extends the base idea with agent-specific primitives that json-render doesn't cover. It introduces first-class concepts for approval flows (a structured action requires explicit user confirmation before the agent proceeds), structured data cards with typed field schemas, and multi-step task UI. Where json-render is a general rendering protocol, A2UI is opinionated about agent interaction patterns. `@ngaf/chat` supports both specs out of the box, detecting the discriminator on the incoming message and routing accordingly.
-The rendering layer is handled by `@ngaf/render`, which owns component resolution, prop binding, and the registry itself. `@ngaf/chat` delegates all spec rendering to `@ngaf/render` — the chat library doesn't need to know how components are resolved, only that the renderer accepts a spec and returns a mounted component. This separation keeps the chat layer focused on message orchestration and keeps the rendering logic reusable outside of chat contexts.
-Custom components are registered once and become available to any agent that knows the component identifier. The registry maps string keys to Angular component classes. Any component the agent might emit — a custom chart, a domain-specific form, a confirmation widget — needs a corresponding entry.
-import { provideChatRenderer } from '@ngaf/chat';
-import { provideRenderRegistry } from '@ngaf/render';
-import { DataTableComponent } from './components/data-table.component';
-import { BookingFormComponent } from './components/booking-form.component';
-import { ApprovalCardComponent } from './components/approval-card.component';
-export const appConfig: ApplicationConfig = {
- providers: [
- provideChatRenderer(),
- provideRenderRegistry({
- 'data-table': DataTableComponent,
- 'booking-form': BookingFormComponent,
- 'approval-card': ApprovalCardComponent,
- }),
- ],
-};
-
-Any component the agent emits by key is resolved here. Unknown keys fall back to a configurable fallback component rather than throwing.
-Agents don't emit complete UI specs in a single chunk. They stream JSON patches — RFC 6902 operations that incrementally build the spec as the model generates it. `@ngaf/chat` applies each patch to the in-progress spec and triggers Angular's change detection cycle. The effect is a component that populates live: table rows appear as the agent reasons through the data, form fields materialize in sequence. This isn't a loading spinner followed by a full render — it's the UI itself arriving progressively, which is both faster to first meaningful paint and more transparent about what the agent is doing.
Debugging a running agent is fundamentally different from debugging a REST call. There is no single request to inspect in the network tab. The state lives across a stream of discrete events, tool invocations happen inside the graph before responses surface, and interrupt flows depend on timing between the UI and the server-side checkpoint. Standard browser tooling was not built for this. `chat-debug` was.
-`chat-debug` is a developer overlay built into `@ngaf/chat`. When active, it renders a resizable panel alongside your chat interface that gives you a live view of four distinct concerns.
-Raw message state surfaces the full `AIMessage[]` array as it exists in the agent signal at any given moment — not a serialized snapshot, but the live signal value. You can watch content chunks arrive and accumulate during streaming without manually logging signal reads.
-The streaming event log shows every server-sent event in the order it was received, including event type, timestamp, and raw payload. This is where you catch malformed chunk sequences, unexpected `end` events, and latency gaps between tool execution and the next assistant turn.
-The tool call state machine tracks each tool invocation through its full lifecycle: `pending`, `executing`, `complete`, or `error`. For each call you see the tool name, the serialized input payload, the output value, and wall-clock execution time. When a tool is slow or returning unexpected output, this panel removes the guesswork.
-Interrupt state shows the full interrupt payload at the moment the graph pauses, including the checkpoint ID and any structured data the graph passed to the UI. After the user responds, the panel records what was submitted. If your interrupt flow has a bug, the before-and-after diff is right there.
-Drop `ChatDebugComponent` into any host component that already uses the `@ngaf/chat` primitives. No providers, no configuration file. In dev mode it activates automatically.
-import { ChatDebugComponent } from '@ngaf/chat/debug';
-@Component({
- imports: [ChatDebugComponent],
- template: `
-
-
- `
-})
-export class AppShell {}
-
-`chat-debug` reads from the same injected `AgentChatService` instance your chat interface uses. There is no secondary data pipeline and no performance overhead on the message handling path itself.
-Clicking any message in the raw state view expands it into a structured inspector showing message type, role, content blocks, and metadata fields. Streamed messages show their final assembled content alongside a chunk count. Tool call messages link directly to the corresponding entry in the tool call state machine view.
-The tool call inspector is the most useful surface for backend integration bugs. Input and output payloads are formatted as syntax-highlighted JSON. Execution timing lets you correlate slow tool responses with latency you see in the event log.
-Agent signals registered through `@ngaf/chat` appear as named signals in the Angular DevTools component tree. You can inspect `messageStream`, `interruptState`, and `toolCallRegistry` directly in the DevTools panel without instrumentation code. The signal graph reflects live state, so you can pause, inspect, and resume without affecting the stream.
-`chat-debug` uses a build-time token that resolves to a no-op component in production. The overlay never renders, and tree-shaking removes the debug module entirely from the production bundle. No conditional guards in application code are required. The separation is enforced at the library level.
-When a stream misbehaves in development, `chat-debug` gives you the full picture in one place. That is its only job, and it does it without adding complexity to your application architecture.
Agents that render UI — without coupling to your frontend
-If you've integrated a generative agent into an Angular application, you've probably written something like this:
-
-
-
-
-
-
-
-This is clean, readable, and completely reasonable for a prototype. It is also quietly catastrophic for a production system at scale.
-The problem isn't the switch statement. The problem is what the switch statement represents: a hardcoded, implicit contract between two systems that were never designed to evolve together.
----
-When a component switches on `agentOutput.type`, you've created a dependency that runs in both directions. The frontend depends on the agent emitting specific, known type strings. The agent—or more precisely, the engineers tuning it—starts depending on the frontend to remain stable. If you add a `'timeline'` component to your library, that string has to appear in the agent's output schema before the component is useful. If the agent starts emitting `'comparison-table'` to reflect a new capability, the frontend silently falls through to the fallback until a developer manually adds the case.
-Neither system can move independently. You've coupled iteration velocity in two entirely separate codebases through an undocumented string literal.
----
-In practice, this coupling has a human cost. Every time an AI or backend team adds an agent capability, extends an output type, or restructures a response payload, a frontend engineer has to be involved. They need to add a case, wire a component, test the binding, and ship a deploy. What should be a model-layer change becomes a cross-team sprint ticket.
-At a single-agent, single-frontend scale, this is annoying. At enterprise scale—multiple agents serving multiple products across multiple frontend teams—it becomes an organizational blocker. Agent iteration speed, which is inherently fast and experimental, gets throttled to frontend release cadence. Teams start avoiding capability changes not because they're technically hard, but because the coordination overhead is too expensive.
----
-Imagine three agents: a customer support agent, an internal analytics agent, and a sales assistant. Each is consumed by two separate Angular applications maintained by different teams. Each application has its own `ngSwitch` tree, its own component mappings, its own interpretation of what `type: 'chart'` means in terms of props and layout. When the analytics agent adds a new output format, the change has to propagate to six switch statements across two codebases owned by teams with different priorities and deployment schedules.
-The system has no center of gravity. It has six points of failure.
----
-The architectural shift is conceptually simple: agents should not emit data that the frontend decides how to render. Agents should emit UI specifications—declarative descriptions of intent that a rendering layer interprets. The agent says "render a comparison table with these columns and this data." The frontend honors that intent using whatever component best fits its design system.
-This separates concerns cleanly. Agents own what to show. Frontends own how to show it.
----
-This architecture only works if the specification format is shared, stable, and not owned by any single team. Proprietary formats recreate the coupling problem at a higher layer—now your agents are locked to your renderer, and your renderer is locked to your schema definitions.
-What this problem space needs is an open, versioned UI specification standard: something analogous to what OpenAPI did for REST contracts. A format that agent frameworks can target, that Angular renderers can implement, and that multiple teams can build tooling around without negotiating the schema on every iteration.
-That standard doesn't fully exist yet. The rest of this guide is about building toward it.
When a language model generates UI, you need a stable contract between what the model emits and what your frontend renders. Without one, you're parsing freeform HTML strings, maintaining brittle prompt templates, and coupling your rendering logic to whichever model happens to be in production this week. The json-render specification solves this by defining UI as structured, framework-agnostic JSON. The agent emits a document. The frontend interprets it. Neither side cares how the other is built.
-A json-render document is a tree of nodes. Each node carries three fields: `type`, `props`, and `children`.
-{
- "type": "DataTable",
- "props": {
- "columns": ["Region", "Revenue", "YoY Growth"],
- "caption": "Q3 Performance Summary",
- "striped": true
- },
- "children": [
- {
- "type": "TableRow",
- "props": { "highlight": true },
- "children": [
- { "type": "TableCell", "props": { "value": "EMEA" } },
- { "type": "TableCell", "props": { "value": "$4.2M" } },
- { "type": "TableCell", "props": { "value": "+18%" } }
- ]
- }
- ]
-}
-
-`type` maps to a registered component. `props` are typed inputs. `children` are recursively resolved. The renderer never sees template syntax — just a node graph it already knows how to walk.
-Framework-agnostic specs create portability. An agent prompt that targets json-render works whether the frontend is Angular, React, or Vue. Swap the renderer; keep the prompt. This matters at the enterprise level where you may have multiple teams consuming the same agent infrastructure across different stacks.
-LLM prompt stability is the less obvious benefit. When your output schema is formally specified, you can provide the spec as part of the system prompt — as a JSON Schema or TypeScript interface — and get consistent, validatable output. Models respond well to schemas with explicit field descriptions and constrained `type` enumerations. Deviation rates drop significantly when the model has a schema to conform to rather than an example to imitate.
-Community tooling compounds over time. Validators, mock generators, visual debuggers, and testing utilities built against the spec work across every implementation.
-json-render handles dynamic behavior declaratively. Conditionals use a `$if` directive on the node:
-{ "type": "AlertBanner", "props": { "message": "Quota exceeded" }, "$if": "metrics.quotaUsed > 0.9" }
-
-Iteration uses `$for` with a binding expression:
-{ "type": "UserCard", "$for": "user in users", "props": { "name": "{{user.name}}", "role": "{{user.role}}" } }
-
-Computed properties use `{{expression}}` interpolation within prop values. Expressions are evaluated against a runtime context object supplied by the host application. The spec deliberately keeps the expression language minimal — it is not a scripting environment.
-Google's A2UI spec extends json-render for agent-native patterns. It introduces `intent` nodes, which describe what the agent wants to accomplish rather than prescribing a specific component. A2UI also formalizes `action` bindings — structured event handlers that dispatch typed payloads back to the agent rather than calling arbitrary JavaScript. For teams building bidirectional agent interfaces, A2UI's interaction model is worth adopting alongside the base spec.
-`@ngaf/render` implements json-render for Angular via the `
@Component({
- template: ` `
-})
-export class AgentShellComponent {
- registry = { DataTable, TableRow, TableCell, AlertBanner, UserCard };
- agentOutput = signal(null);
-}
-
-The directive resolves types against the registry, instantiates components dynamically, binds props, and projects children. Streaming is handled through JSON Patch — as the agent streams tokens, partial patches are applied to the document signal incrementally. Components update as their subtree resolves. You get progressive rendering without polling.
-Include the json-render JSON Schema in your system prompt. Constrain the `type` field to your registered component names. Instruct the model to emit only valid nodes and avoid HTML interpolation. Few-shot examples accelerate conformance, but schema enforcement does more work. Validate output at the boundary with a schema validator before passing documents to `
The spec is the API. Treat it like one.
The component registry is the load-bearing joint between a json-render document and your Angular application. A render spec can name any component it wants; the registry decides what that name actually means at runtime. Get this layer right and you have a stable, evolvable surface for generative UI. Get it wrong and you have a fragile string-matching problem scattered across your codebase.
-`defineAngularRegistry()` accepts a plain object where each key is the component name a spec will reference, and each value is the corresponding Angular component class. Nothing more.
-import { defineAngularRegistry } from '@ngaf/render';
-import { MetricCardComponent } from './metric-card.component';
-import { DataTableComponent } from './data-table.component';
-import { AlertBannerComponent } from './alert-banner.component';
-import { ChartComponent } from './chart.component';
-export const appRegistry = defineAngularRegistry({
- MetricCard: MetricCardComponent,
- DataTable: DataTableComponent,
- AlertBanner: AlertBannerComponent,
- Chart: ChartComponent,
-});
-
-The registry object is typed: keys are strings, values must be Angular component classes. If a component class doesn't match what the type system expects, you'll know at compile time rather than at render time.
-Register the registry in your application's dependency injection tree using `provideRenderRegistry()`. This belongs in `app.config.ts` alongside your other application-level providers:
-providers: [
- provideRenderRegistry(appRegistry),
-]
-
-From this point on, the render infrastructure can resolve component names anywhere in the component tree without you wiring anything else manually.
-The `
If the component tree is nested, `
Once a component is resolved, `
Type coercion is not performed. If your `@Input()` expects a number and the spec delivers a string, that's your contract to enforce, either through input transforms (available in Angular 17+) or a validation layer upstream of the renderer.
-When `
Specs generated today may reference component names that your registry won't recognize in six months — or vice versa. Version your registry explicitly by maintaining additive-only additions to registered component names and deprecating rather than removing entries. If a component's contract changes significantly, register it under a new name and keep the old binding pointing to a compatibility wrapper. This mirrors how you'd version a REST API: old consumers keep working while new specs can adopt the updated surface.
-The registry is your schema. Treat it with the same discipline you'd apply to any other public interface.
Waiting for a complete UI specification before rendering anything is the wrong default. If your agent is generating a 50-row data table, your users should see the first rows within milliseconds of the agent starting output — not after a three-second round trip to receive, parse, and hydrate the full JSON payload. Streaming is what separates a generative UI from a slow API call with extra steps.
-The naive approach is to re-emit the entire UI spec on each token or checkpoint. This breaks down fast. A moderately complex spec — tables, nested cards, conditional sections — can run to tens of kilobytes. Replacing the full document on every update means the renderer diffs the entire tree on every tick, Angular's change detection fires against a new object reference each time, and you're pushing O(spec size) work through the pipe regardless of how small the actual change was. At scale, this becomes a throughput problem that no amount of `OnPush` optimization will fully compensate for.
-RFC 6902 defines JSON Patch as a sequence of operations — `add`, `replace`, `remove`, `move`, `copy`, `test` — applied to a target JSON document using JSON Pointer paths. Rather than transmitting the document, the agent emits only what changed:
-[
- { "op": "add", "path": "/rows/49", "value": { "id": 50, "name": "..." } },
- { "op": "replace", "path": "/status", "value": "complete" }
-]
-
-This is the correct abstraction for generative UI streaming. The agent's output token stream maps cleanly to patch operations: a new row is an `add`, a corrected value is a `replace`, a retracted field is a `remove`. The frontend accumulates these deltas against a baseline spec document rather than swapping documents wholesale.
-Before a complete patch operation has arrived, you need somewhere to be. `@ngaf/render` handles this through two mechanisms working in parallel.
-First, partial-JSON parsing allows the renderer to extract and apply structurally valid operations from an incomplete stream. If three operations are fully formed and the fourth is mid-token, the first three are applied immediately. The incomplete operation is buffered until its closing delimiter arrives.
-Second, skeleton states are declared in the baseline spec. Before any patches arrive, `@ngaf/render` renders placeholder elements — shimmer rows, empty card outlines, header shells — based on the spec's `skeleton` annotations. As patches land, real content replaces placeholders in place. Users see a structured, stable layout from the first render frame.
-@Component({ changeDetection: ChangeDetectionStrategy.OnPush })
-export class ReportViewComponent {
- private render = inject(CacheplaneRenderService);
-spec = signal(BASELINE_SPEC);
-ngOnInit() {
- this.render
- .streamPatches('/api/agent/report')
- .pipe(takeUntilDestroyed())
- .subscribe(patch => {
- this.spec.update(current => applyPatch(current, patch).newDocument);
- });
- }
-}
-
-`streamPatches` returns an `Observable
Patch-based updates are O(change), not O(spec size). A 50-row table receiving its fifty-first row processes exactly one `add` operation. The rest of the spec is untouched in memory and the renderer skips it entirely. This holds regardless of how large the accumulated spec grows — the cost of each update is proportional to the diff, not the document. For enterprise UIs where specs grow complex over a session, this is not a micro-optimization; it's the architecture that keeps rendering predictable under load.
Static specs describe structure. They fall short the moment your UI needs to reflect derived state — a total calculated from line items, a label that changes based on a flag, a list of cards rendered from an array. For generative UI to be production-ready, the spec itself must support dynamic behavior. `@ngaf/render` addresses this through `signalStateStore()`, computed properties, and repeat loops — mechanisms that let an agent define reactive UI logic declaratively, without pushing that responsibility into custom component code.
-`signalStateStore()` creates a Signal-backed state container that both the rendering engine and your Angular components can read from and write to. When an agent generates a spec, it can seed this store with initial values. Components rendered from that spec subscribe to the store automatically — no `@Input()` wiring, no manual change detection.
-const store = signalStateStore({
- quantity: 3,
- unitPrice: 49.99,
- discountRate: 0.1,
- label: 'computed:subtotal * (1 - discountRate)',
- subtotal: 'computed:quantity * unitPrice',
- items: [
- { id: 1, name: 'Widget A' },
- { id: 2, name: 'Widget B' },
- ],
-});
-
-The store holds both raw values and computed expressions as first-class fields. The rendering engine resolves computed fields at render time and keeps them reactive — when `quantity` updates, anything that depends on `subtotal` or `label` updates automatically through Angular's Signal graph.
-Computed expressions are string-prefixed with `computed:` and evaluated against the current store context. The engine parses these at spec load time and registers them as Angular `computed()` signals derived from their dependencies.
-This means an agent can express UI logic like conditional labels, formatted totals, or visibility rules entirely within the spec payload. No custom pipe. No component method. The expression evaluates deterministically given the same store state, which matters both for correctness and for testing.
-The expression language is intentionally constrained — arithmetic, string interpolation, ternary operators, and property access. Complex branching belongs in components, not in spec strings.
-When a spec field is an array, the `repeat` directive instructs the renderer to iterate and instantiate a child component template for each element:
-{
- "type": "repeat",
- "source": "items",
- "template": { "type": "card", "title": "{{item.name}}" }
-}
-
-The renderer binds each iteration's context to the current element, making `item` available as a local reference within the template. This covers the majority of list-rendering patterns without requiring a custom Angular component to manage the loop.
-The boundary is load-bearing. Computed expressions in the spec are appropriate for derivations that the agent needs to express — formatting, simple math, conditional values driven by store state. They are not appropriate for side effects, async operations, service calls, or branching logic with more than two outcomes.
-If a computed expression starts to read like a function body, that logic belongs in a component. The spec should describe *what* the UI reflects, not *how* it gets there.
-Because computed expressions evaluate against a plain store object, they are fully testable without rendering:
-const store = signalStateStore({ quantity: 4, unitPrice: 25 });
-expect(resolveComputed('quantity * unitPrice', store)).toBe(100);
-
-`resolveComputed()` is a pure utility export — no DOM, no `TestBed`, no component fixture. This makes computed logic fast to validate in unit tests and straightforward to include in CI pipelines that verify agent-generated specs before they reach the renderer.
-Deterministic evaluation is a feature, not a side effect. It means you can test agent output like any other pure function.
$1')
- .replace(/^### (.+)$/gm, '${config.subtitle}
-Part of the Cacheplane Angular Agent Framework.
- trackWhitepaperDownloadClick('angular', { - surface: 'library_landing', - source_section: 'angular-whitepaper-gate', - library: 'agent', - cta_id: 'angular_whitepaper_download', - })} + - ↓ Download PDF + Read Current Docs @@ -130,7 +124,7 @@ export function AngularWhitePaperGate() { fontSize: '0.62rem', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, color: tokens.colors.textMuted, marginBottom: 16, }}> - Optional — Get notified of updates + Get notified when the guide is refreshed {formState === 'done' ? (