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 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. 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. 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. 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. 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. 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. The Angular Agent Readiness Guide # 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). 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: 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. 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. # 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. 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 `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. The implementation is straightforward: 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. Production apps typically need multiple conversations. A sidebar shows thread history; clicking switches context. The pattern extends naturally: readonly activeThreadId = signal 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. Before shipping, verify thread persistence end-to-end: # 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. 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. `@ngaf/chat` provides two components for tool call rendering. ` For most production apps, start with the prebuilt card and customize from there: 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. 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. 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. # 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. 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 `agent()` function exposes interrupt state through a dedicated signal: // 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. The `@ngaf/chat` package provides two components for rendering interrupt flows. ` Both components support three user actions that map directly to resume commands: 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. 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. # 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. LangGraph agents emit structured data through custom events during stream execution. Your agent code decides when to emit UI specifications: 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. 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. 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. 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: # 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. 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. # 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. 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` 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. 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. 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. 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. 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. Ship LangGraph agents in Angular — without building the plumbing # 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. 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. 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. 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. 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. # 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. Calling `agent()` returns an object with typed signals covering the full agent lifecycle: Before `agent()` can connect, configure the transport layer with `provideAgent()`: This registers the stream transport globally. Individual `agent()` calls then specify their endpoint: 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. 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. Bind agent state directly in templates without ceremony: Ten lines. The stream connects, messages accumulate, loading state toggles, errors surface—all reactive, all type-safe. Without `agent()`, the equivalent implementation requires manual stream handling: With `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. # 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. 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. 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. 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. Most applications need more than single-thread persistence. Users expect to manage multiple conversations: Switching threads is a signal update. The `agent()` reactive system handles reconnection, state restoration, and UI synchronization. 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: # 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. 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: 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 agent surface exposes `interrupt()` as a signal that transitions from `undefined` to an `AgentInterrupt` object when the graph pauses: 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`. `@ngaf/chat` provides ` 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: {{ int.payload.description }} Calling `interrupt()` with a resume payload sends the command and clears the signal. 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. # 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. LangGraph emits tool invocations as structured events mid-stream. Rather than parsing `tool_call` chunks yourself, the agent ref exposes them directly: // 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. Nested graphs emit events with their own namespaces. @ngaf/langgraph flattens these into the primary stream while preserving hierarchy through the `subagents()` signal: 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. LangGraph checkpoints graph state at each node. Rewinding means re-streaming from a prior checkpoint: 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 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: // 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. Agents emit structured events beyond messages: progress indicators, analytics payloads, generative UI specs. The `onCustomEvent` callback captures these without polluting the message stream: This separates concerns cleanly: messages render in the chat, custom events drive application-specific behavior. 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. # 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` 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. 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. 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. When testing component rendering in isolation—without exercising the transport layer—use `mockLangGraphAgent()` to create an agent instance with directly controllable signals: 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. 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. 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. Production agent chat UI in days, not sprints # 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. 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. 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. 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. 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. `@ngaf/chat` ships the complete inventory: ` 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. # 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. Headless components own behavior and state management but emit no styled markup. They expose content projection slots and structural directives for complete template control. ` ` ` These primitives compose freely. Use them when your design system mandates specific markup structures or when accessibility requirements demand particular ARIA patterns. The ` 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. 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 ` 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. Start with ` 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. # 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. 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. If your team already maintains design tokens, integration is direct assignment. Map your existing tokens to the chat component tokens in a single stylesheet: /* 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. 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. 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. The token system supports dark mode without component changes. Override the custom properties inside a dark mode selector: Components don't contain theme-switching logic. They read current token values. Your application controls when those values change. 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. # 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. @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 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: 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*. 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. Component resolution flows through a registry. You define which component handles each type, then provide it to the chat context: 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. 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. # 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. ` 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. Drop it into any chat interface: @Component({
+ selector: 'app-support-chat',
+ imports: [ChatComponent, ChatDebugComponent],
+ template: `
+ No configuration required. The panel renders in the bottom-right corner with a toggle to expand and collapse. Click any message in the Message State view to expand it. You'll see: The tool call view shows each invocation with: 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. 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. ` 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. Agents that render UI — without coupling to your frontend # 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: 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 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. One agent, one frontend, one team—manageable. Now consider reality: When someone asks "can the agent render an approval workflow?"—the answer requires archaeology across four codebases. The fix is architectural: agents emit UI specifications, not domain data. The frontend interprets specs through a registry, not a switch statement. // 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. 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. # Declarative UI Specs & the json-render Standard 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. A json-render document describes a component tree using three primitives: component name, props, and children. 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. 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. The specification handles dynamic rendering through reserved component types: Iteration uses `$for`: 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 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 ` const registry = defineAngularRegistry({
+ card: CardComponent,
+ metric: MetricComponent,
+ sparkline: SparklineComponent
+}); @Component({
+ selector: 'app-dashboard',
+ imports: [RenderSpecComponent],
+ template: ` The registry maps component names to Angular components. Unknown components throw at render time—fail fast rather than silent degradation. LLMs generate spec-compliant JSON when prompts include the schema and examples: 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. # 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. `defineAngularRegistry()` accepts a record of component names to Angular component classes: 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. Two patterns, depending on your architecture. Direct binding works for isolated cases where a single component owns the rendering context: Dependency injection suits applications where multiple components render specs against a shared registry: 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. ` 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. When a spec references a component name not present in the registry, ` For development, enable strict mode through `provideRender({ strict: true })`. This throws on unknown component names, surfacing mismatches immediately. For production, consider a fallback component: 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. 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: 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. # 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. 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 defines three core operations for mutating JSON documents: Instead of emitting complete specs, the agent streams patch operations as it generates content: Each line is independently parseable. The render layer applies patches immediately, updating only the affected portion of the component tree. 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: 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. 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. # 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. `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. 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 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. 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. 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. Test computed properties by manipulating the state store and asserting against the rendered output: Drive the store, trigger change detection, assert the DOM. The reactive graph handles the rest.From
+
Prototype
to
ProductionContents
+
+ Streaming State Management
+ Signals as the Synchronization Primitive
+
+@Component({
+ selector: 'app-chat',
+ template: `
+ OnPush Compatibility
+Thread Persistence
+ Why Stateless Agent UIs Fail in Production
+The threadId Signal and onThreadId Callback
+Persisting to localStorage
+
+@Component({
+ selector: 'app-chat',
+ template: `
+ Thread List UI and Conversation Switching
+
+readonly threads = signalProduction Checklist
+
+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.Tool-Call Rendering
+ The Raw Stream Problem
+Headless and Prebuilt Options
+
+Progressive Disclosure
+Production Checklist
+Human Approval Flows
+ The LangGraph Interrupt Pattern
+The `interrupt()` Signal
+
+readonly chat = agent({
+ assistantId: 'deployment_agent',
+ threadId: this.threadId,
+ onThreadId: id => this.threadId.set(id)
+});
+UI Components for Interrupt Handling
+
+
+@Component({
+ template: `
+ @if (chat.interrupt(); as interrupt) {
+ Production Considerations
+Generative UI
+ The Custom Event Pattern
+
+# Agent-side: emit a render spec as a custom event
+await writer.write({
+ "type": "data_table",
+ "columns": ["date", "amount", "category"],
+ "rows": rows_so_far
+})
+Registry-Based Resolution
+
+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';
+
+Progressive Updates Through JSON Patch
+
+# Initial spec
+await writer.write({"type": "data_table", "columns": [...], "rows": []})
+The Decoupling Advantage
+Deterministic Testing
+ Why Live LLM Testing Fails
+MockAgentTransport: Scripted Event Sequences
+mockLangGraphAgent(): Writable Signal Control
+
+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?');
+ });
+});
+Testing in Isolation
+Production Checklist
+The
+
Enterprise
Guide
to
Agent
Streaming
in
AngularContents
+
+ The Last-Mile Problem
+ Zone Pollution Is Architectural, Not Configurable
+Synchronous Templates, Asynchronous Tokens
+Push vs. Pull: The Reactivity Mismatch
+The Real Cost
+The agent() API
+ Signal Architecture
+
+For cases requiring access to the raw LangGraph protocol, additional signals like `langGraphMessages()` expose the unprocessed message format.
+Provider Configuration
+
+provideAgent({
+ transport: new FetchStreamTransport()
+})
+
+readonly chat = agent({
+ assistantId: 'support_agent',
+ apiUrl: 'https://api.example.com/langgraph',
+ threadId: this.threadId,
+ onThreadId: id => this.threadId.set(id)
+});
+Why Signals Work with OnPush
+Template Binding
+
+@Component({
+ template: `
+ @if (chat.isLoading()) {
+ The Alternative
+
+// Manual approach: ~60 lines of subscription management,
+// token accumulation, error handling, cleanup logic,
+// and change detection triggers
+
+readonly chat = agent({ assistantId: 'chat_agent' });
+Thread Persistence & Memory
+ The Thread Lifecycle
+
+@Component({
+ template: `Restoring State on Mount
+Building a Thread List
+
+@Component({
+ template: `
+
+ Production Considerations
+
+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.Interrupt & Approval Flows
+ How LangGraph Interrupt Works
+
+{ action: 'approve' } // Proceed with original parameters
+{ action: 'edit', args: { ... } } // Proceed with modified parameters
+{ action: 'cancel', reason?: string } // Abort the pending action
+The interrupt() Signal
+
+interface AgentInterrupt {
+ id: string;
+ type: string;
+ payload: unknown;
+ timestamp: number;
+}
+Prebuilt Approval UI
+
+@Component({
+ selector: 'app-agent',
+ template: `
+
+@Component({
+ template: `
+ @if (chat.interrupt(); as int) {
+ Edge Cases
+Full LangGraph Feature Coverage
+ Tool Call Streaming
+
+readonly chat = agent({
+ assistantId: 'research_agent',
+ apiUrl: 'https://api.smith.langchain.com'
+});
+Subgraph Support
+
+// Parent agent spawns child graphs for specialized tasks
+const activeSubagents = this.chat.subagents();
+// Returns SubagentInfo[] with id, name, status for each active subgraph
+Time Travel
+
+// Rewind to a previous state and continue from there
+this.chat.interrupt({
+ checkpoint_id: 'abc123',
+ action: 'rewind'
+});
+DeepAgent Multi-Agent Coordination
+
+// Each agent's output tagged with origin
+const messages = this.chat.messages();
+// Message.metadata.agent identifies the source agent
+The onCustomEvent Hook
+
+readonly chat = agent({
+ assistantId: 'ui_agent',
+ onCustomEvent: (event) => {
+ if (event.type === 'render_component') {
+ this.dynamicUI.set(event.payload);
+ }
+ }
+});
+Why Full Coverage Matters
+Deterministic Testing
+ MockAgentTransport: Scripted Event Sequences
+
+import { TestBed } from '@angular/core/testing';
+import { MockAgentTransport, provideAgent } from '@ngaf/langgraph';
+Testing Agent States
+Direct Signal Control with mockLangGraphAgent
+
+const mockAgent = mockLangGraphAgent({
+ messages: signal([{ role: 'assistant', content: 'Test response' }]),
+ status: signal('complete'),
+ toolCalls: signal([{ id: 'tc1', name: 'search', args: { query: 'test' } }])
+});
+Testing Thread Switching
+The Benchmark
+The
+
Enterprise
Guide
to
Agent
Chat
Interfaces
in
AngularContents
+
+ The Sprint Tax
+ The Inventory
+The Hidden Costs
+"Good Enough for Demo" vs. Production
+The Opportunity Cost
+The @ngaf/chat Thesis
+Batteries-Included Components
+ The Headless Tier
+The Prebuilt Composition Tier
+
+@Component({
+ selector: 'app-support',
+ template: `Composing Tiers
+The Agent Contract
+Choosing Your Tier
+Theming & Design System Integration
+ The CSS Custom Property API
+Design Token Mapping
+
+: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);
+Typography Integration
+Color System Integration
+Dark Mode Support
+
+[data-theme="dark"] {
+ --chat-surface-primary: var(--ds-color-surface-elevated-dark);
+ --chat-text-primary: var(--ds-color-text-primary-dark);
+}
+Limitations of Token-Based Theming
+Generative UI in Chat
+ Structured UI in the Message Stream
+The json-render Spec
+
+{"type": "data-table", "props": {"columns": ["Quarter", "Revenue"], "rows": [...]}}
+A2UI: Agent-Specific Patterns
+Registry Integration
+
+import { defineAngularRegistry, provideRender } from '@ngaf/render';
+import { DataTableComponent } from './data-table.component';
+import { BookingFormComponent } from './booking-form.component';
+import { ApprovalCardComponent } from './approval-card.component';
+Streaming Patches
+Debug Tooling
+ What chat-debug Shows
+Adding chat-debug
+
+import { ChatDebugComponent } from '@ngaf/chat';
+Inspecting Individual Messages
+
+For assistant messages during active streaming, content updates live as tokens arrive.
+Tool Call Debugging
+
+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
+Integration with Angular DevTools
+Production Safety
+The
+
Enterprise
Guide
to
Generative
UI
in
AngularContents
+
+ The Coupling Problem
+
+@Component({
+ template: `
+ @switch (output.type) {
+ @case ('product') { The Dependency Graph You Didn't Want
+Scale Multiplies the Problem
+
+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.
+Inverting the Dependency
+
+// Agent emits spec
+{
+ "component": "data-table",
+ "props": { "columns": [...], "rows": [...] }
+}
+The Standard Problem
+Declarative UI Specs & the json-render Standard
+ The Problem with Ad-Hoc UI Generation
+Anatomy of a json-render Document
+
+{
+ "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]
+ }
+ }
+ ]
+}
+Why an Open Standard Matters
+Control Flow in the Spec
+
+{
+ "component": "$if",
+ "props": { "condition": "{{ user.isAdmin }}" },
+ "children": [{ "component": "admin-panel", "props": {} }]
+}
+
+{
+ "component": "$for",
+ "props": { "each": "{{ items }}", "as": "item" },
+ "children": [
+ {
+ "component": "list-item",
+ "props": { "label": "{{ item.name }}" }
+ }
+ ]
+}
+Google's A2UI Extension
+The @ngaf/render Implementation
+
+import { Component, signal } from '@angular/core';
+import { RenderSpecComponent, defineAngularRegistry } from '@ngaf/render';
+import { CardComponent, MetricComponent, SparklineComponent } from './components';
+Prompting for Valid Output
+
+Generate a json-render document for a user profile card.
+The Component Registry
+ Defining the Registry
+
+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';
+Providing the Registry
+
+@Component({
+ template: `
+// app.config.ts
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideRender({ registry: uiRegistry })
+ ]
+};
+Resolution and Input Mapping
+Unknown Components
+
+export const uiRegistry = defineAngularRegistry({
+ // ... your components
+ '__fallback__': UnknownComponentPlaceholder
+});
+Registry Versioning
+
+export const uiRegistry = defineAngularRegistry({
+ 'DataGrid': DataGridV2Component, // current
+ 'DataTable': DataGridV2Component, // legacy alias
+});
+Streaming JSON Patches
+ The Problem with Full-Document Streaming
+JSON Patch RFC 6902
+
+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
+
+{"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"}
+Partial-JSON Parsing and Skeleton States
+
+@Component({
+ template: `Performance Characteristics
+State Management & Computed Functions
+ Agent-Managed State with signalStateStore()
+
+import { signalStateStore } from '@ngaf/render';
+Computed Properties: Declarative Derived State
+Repeat Loops for Collections
+Drawing the Line: Spec Logic vs. Component Logic
+Testing Computed Behavior
+
+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
+});
+
')
+ .replace(/^### (.+)$/gm, '$1$1
')
+ .replace(/^## (.+)$/gm, '$1
')
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ .replace(/^- (.+)$/gm, '${match}
`)
+ .split('\n\n')
+ .map(block => {
+ if (block.startsWith('
${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', + })} 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' ? (