From 9d6ae42f60d16866fe8a9f3acd0f2c15a0a4036c Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 22 May 2026 09:29:18 -0700 Subject: [PATCH 1/4] feat(website): polish blog template + reusable AG-UI arch diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable infrastructure changes ahead of publishing the AG-UI tutorial post. - MdxRenderer is now layout-agnostic: docs and blog each own their outer chrome (sidebar padding lived in MdxRenderer and was fighting the blog page's article wrapper). Docs page absorbs the chrome it relied on; root element switched from
to
to avoid nested-article hydration mismatches. - Tabs body wrapper no longer paints its own border/background — the inner rehype-pretty-code
already owns its surface, and the wrapper was creating a visible "extending border" below short code blocks. - global.css: restore disc/decimal list markers + padding on .docs-prose ul/ol (Tailwind preflight had stripped them), drop the duplicate inset box-shadow ring on code-block
, and add responsive rules for the
  new arch diagram so it collapses to a single column under 720px.
- New AgUiArchDiagram component: three brand-styled boxes (Backend →
  @ngaf/ag-ui adapter → @ngaf/chat UI) with labeled SSE / Agent-contract
  arrows. Replaces hand-drawn ASCII art that drifted out of alignment
  due to rehype-pretty-code line padding + JetBrains Mono fallbacks for
  box-drawing glyphs.
- Blog post template (apps/website/src/app/blog/[slug]/page.tsx): proper
  paddingTop offset so the date no longer clips behind the fixed nav,
  brand  with category/date/reading-time, large Garamond H1
  matching the homepage hero, description as bodyLg subhead, tag chips
  driven by frontmatter.tags, and a right-column DocsTOC at ≥xl widths.
  Adds a UTC-anchored formatDate to avoid "2026-05-21" rendering as
  "May 20" west of UTC.
- .claude/launch.json: add ag-ui-streaming entry pointing at the AG-UI
  cockpit demo for an upcoming follow-up that will swap in a real hero
  screenshot once libs/render tsconfig is unblocked.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 .claude/launch.json                           |   7 +
 apps/website/src/app/blog/[slug]/page.tsx     | 120 ++++++++++----
 .../docs/[library]/[section]/[slug]/page.tsx  |  16 +-
 apps/website/src/app/global.css               |  25 ++-
 .../src/components/docs/AgUiArchDiagram.tsx   | 148 ++++++++++++++++++
 .../src/components/docs/MdxRenderer.tsx       |  40 ++---
 apps/website/src/components/docs/mdx/Tabs.tsx |   9 +-
 7 files changed, 300 insertions(+), 65 deletions(-)
 create mode 100644 apps/website/src/components/docs/AgUiArchDiagram.tsx

diff --git a/.claude/launch.json b/.claude/launch.json
index 9c61833b..479a9a84 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -20,6 +20,13 @@
       "runtimeArgs": ["-c", "export PATH=/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH && npx nx serve cockpit-langgraph-streaming-angular --port 4300"],
       "port": 4300
     },
+    {
+      "name": "ag-ui-streaming",
+      "runtimeExecutable": "/bin/bash",
+      "runtimeArgs": ["-c", "export PATH=/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH && npx nx serve cockpit-ag-ui-streaming-angular --port $PORT"],
+      "port": 4350,
+      "autoPort": true
+    },
     {
       "name": "examples-chat",
       "runtimeExecutable": "/bin/bash",
diff --git a/apps/website/src/app/blog/[slug]/page.tsx b/apps/website/src/app/blog/[slug]/page.tsx
index 0c2b9e5e..48b6c937 100644
--- a/apps/website/src/app/blog/[slug]/page.tsx
+++ b/apps/website/src/app/blog/[slug]/page.tsx
@@ -1,9 +1,14 @@
 import type { Metadata } from 'next';
 import { notFound } from 'next/navigation';
+import { tokens } from '@ngaf/design-tokens';
 import { MdxRenderer } from '../../../components/docs/MdxRenderer';
+import { DocsTOC } from '../../../components/docs/DocsTOC';
 import { AuthorByline } from '../../../components/blog/AuthorByline';
+import { TagChips } from '../../../components/blog/TagChips';
+import { Eyebrow } from '../../../components/ui/Eyebrow';
 import { getAllPosts, getPostBySlug } from '../../../lib/blog';
 import { getAuthor } from '../../../lib/blog-authors';
+import { extractHeadings } from '../../../lib/extract-headings';
 import { createPageMetadata } from '../../../lib/site-metadata';
 
 interface Params {
@@ -28,40 +33,97 @@ export async function generateMetadata({ params }: Params): Promise {
   });
 }
 
+function formatDate(iso: string): string {
+  // Parse YYYY-MM-DD as a date in UTC, then format using UTC parts to avoid
+  // timezone shifts (e.g. "2026-05-21" rendering as "May 20" west of UTC).
+  const d = new Date(`${iso}T00:00:00Z`);
+  return d.toLocaleDateString('en-US', {
+    month: 'long',
+    day: 'numeric',
+    year: 'numeric',
+    timeZone: 'UTC',
+  });
+}
+
+function readingTimeMin(markdown: string): number {
+  const words = markdown
+    .replace(/```[\s\S]*?```/g, '') // strip code fences (not real reading)
+    .replace(/[#*_`>\-]/g, ' ')
+    .split(/\s+/)
+    .filter(Boolean).length;
+  return Math.max(1, Math.round(words / 220));
+}
+
 export default async function BlogPostPage({ params }: Params) {
   const { slug } = await params;
   const post = getPostBySlug(slug);
   if (!post || post.frontmatter.draft) notFound();
   const author = getAuthor(post.frontmatter.author);
+  const minutes = readingTimeMin(post.content);
+  const primaryTag = post.frontmatter.tags?.[0]
+    ? post.frontmatter.tags[0].toUpperCase()
+    : 'POST';
+  const headings = extractHeadings(post.content);
+
   return (
-    
-
- -

- {post.frontmatter.title} -

- -
- -
+
+
+
+
+ + {primaryTag} · {formatDate(post.frontmatter.date)} · {minutes} min read + +

+ {post.frontmatter.title} +

+

+ {post.frontmatter.description} +

+
+ +
+ {post.frontmatter.tags && post.frontmatter.tags.length > 0 ? ( + + ) : null} +
+ +
+ +
+
); } diff --git a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx index 5a0a9e5f..739a386d 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -74,13 +74,15 @@ export default async function DocsPage({ params }: DocsRouteProps) {
- +
+ +
{section === 'api' && (() => { const entries = loadApiDocs(library); const nameMap = API_NAME_MAP[library] ?? {}; diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css index a712b3bc..112e18fc 100644 --- a/apps/website/src/app/global.css +++ b/apps/website/src/app/global.css @@ -53,7 +53,7 @@ html { padding: 1.25rem 1.5rem; border-radius: 0.75rem; border: 1px solid rgba(0, 0, 0, 0.1); - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); overflow-x: auto; font-size: 0.8rem; line-height: 1.7; @@ -102,8 +102,11 @@ html { .docs-prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 1rem; font-family: var(--font-garamond); } .docs-prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; font-family: var(--font-garamond); } .docs-prose p { line-height: 1.75; margin-bottom: 1.25rem; } -.docs-prose ul, .docs-prose ol { margin-bottom: 1.25rem; } +.docs-prose ul, .docs-prose ol { margin-bottom: 1.25rem; padding-left: 1.5rem; } +.docs-prose ul { list-style: disc; } +.docs-prose ol { list-style: decimal; } .docs-prose li { margin-bottom: 0.25rem; } +.docs-prose li::marker { color: var(--color-text-muted, #555770); } .docs-table-scroll { max-width: 100%; overflow-x: auto; margin: 1.5rem 0; } .docs-prose table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin: 0; } @@ -139,6 +142,24 @@ html { border-radius: var(--radius-sm); } +/* AG-UI architecture diagram */ +.ag-ui-arch-grid { + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr; + align-items: stretch; + gap: 0; +} +@media (max-width: 720px) { + .ag-ui-arch-grid { + grid-template-columns: 1fr; + } + .ag-ui-arch-arrow { + transform: rotate(90deg); + padding: 12px 0; + margin: 4px auto; + } +} + /* Docs — readable column max-width */ .docs-prose { max-width: 70ch; diff --git a/apps/website/src/components/docs/AgUiArchDiagram.tsx b/apps/website/src/components/docs/AgUiArchDiagram.tsx new file mode 100644 index 00000000..7148f194 --- /dev/null +++ b/apps/website/src/components/docs/AgUiArchDiagram.tsx @@ -0,0 +1,148 @@ +import { tokens } from '@ngaf/design-tokens'; + +interface BoxProps { + eyebrow: string; + title: string; + meta: string; + tone?: 'neutral' | 'accent'; +} + +function Box({ eyebrow, title, meta, tone = 'neutral' }: BoxProps) { + const isAccent = tone === 'accent'; + return ( +
+ + {eyebrow} + + + {title} + + + {meta} + +
+ ); +} + +function ArrowLabel({ label, sub }: { label: string; sub: string }) { + return ( +
+ + {label} + + + + + + {sub} + +
+ ); +} + +export function AgUiArchDiagram() { + return ( +
+
+ + + + + +
+
+ Backend speaks AG-UI over SSE → adapter exposes a signal-shaped Agent contract → chat UI renders. +
+
+ ); +} diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index df306bb2..33fafa03 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -8,6 +8,7 @@ import { CodeGroup } from './mdx/CodeGroup'; import { Pre } from './mdx/CodeBlock'; import { FeatureChips } from './mdx/FeatureChips'; import { ArchFlowDiagram } from './ArchFlowDiagram'; +import { AgUiArchDiagram } from './AgUiArchDiagram'; import { type LibraryId } from '../../lib/docs-config'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; @@ -23,6 +24,7 @@ const mdxComponents = { CardGroup, CodeGroup, ArchFlowDiagram, + AgUiArchDiagram, FeatureChips, pre: Pre, table: ({ children, ...rest }: React.HTMLAttributes) => ( @@ -59,26 +61,24 @@ interface MdxRendererProps { export function MdxRenderer({ source, library, section, slug, title }: MdxRendererProps) { return ( -
-
- -
+
+
); } diff --git a/apps/website/src/components/docs/mdx/Tabs.tsx b/apps/website/src/components/docs/mdx/Tabs.tsx index 684f51a6..699f2add 100644 --- a/apps/website/src/components/docs/mdx/Tabs.tsx +++ b/apps/website/src/components/docs/mdx/Tabs.tsx @@ -56,13 +56,8 @@ export function Tabs({ items, children }: { items?: string[]; children: React.Re ))}
- {/* Tab body */} -
+ {/* Tab body — no wrapper border/background; the inner code block owns its surface */} +
{tabs[active]}
From 6c63812a766f3b58953846bc018b3578fd670b44 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 22 May 2026 09:30:31 -0700 Subject: [PATCH 2/4] docs(blog): "Build Fullstack Agentic Angular Apps Using AG-UI" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New long-form tutorial post (~2,700 words) targeted at Angular devs new to the agent stack, covering the AG-UI protocol, the @ngaf/ag-ui adapter, and a worked example using @ngaf/chat. - Personal proof-of-source in the lede (Head of Ecosystem and Partnerships at CopilotKit; AG-UI protocol contributor) so readers know the recommendations come from inside the work. - Featured-post frontmatter (featured: true) so it surfaces on /blog. - Hero screenshot deferred — libs/render tsconfig is blocking the AG-UI cockpit demo build; tracked as a follow-up that will swap in a real streaming-mid-frame shot once unblocked. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...stack-agentic-angular-apps-using-ag-ui.mdx | 327 ++++++++++++++++++ .../hero.png | Bin 0 -> 72694 bytes 2 files changed, 327 insertions(+) create mode 100644 apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx create mode 100644 apps/website/public/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui/hero.png diff --git a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx new file mode 100644 index 00000000..f3ba33a3 --- /dev/null +++ b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx @@ -0,0 +1,327 @@ +--- +title: "Build Fullstack Agentic Angular Apps Using AG-UI" +description: "A practical, signal-native walkthrough for wiring any AG-UI backend (LangGraph, CrewAI, Mastra, Pydantic AI, Microsoft Agent Framework) to a production Angular chat UI." +date: 2026-05-21 +tags: [tutorial, ag-ui, angular, agents, langgraph] +author: brian +featured: true +--- + +Learn how to build a fullstack agentic Angular app using the AG-UI protocol, from the backend event stream all the way to a signal-driven chat surface in your component. + +A quick note on where I'm coming from. I was Head of Ecosystem and Partnerships at CopilotKit, where AG-UI was born, and I'm a contributor to the protocol. I've watched it evolve inside CopilotKit into the open, framework-agnostic protocol it is today. So when I say "this is the right shape," I'm saying it from the inside. + +I'll also say this plainly: I love AG-UI. Markus and the CopilotKit team did an *excellent* job designing and building it. The protocol is small, the event model is honest, and the schema is the kind of thing you can hold in your head over a coffee. That's rare, and it matters more than people give it credit for. + +I've been shipping agentic features in Angular for the last 3 years (well, really since gpt-3.5 dropped), and one piece that matters more than people give it credit for is the *protocol* between the agent and the UI. The model and the framework get the attention, but having an open, shared wire format is what makes the work portable. AG-UI is the first one that really feels right for that, and the event model maps onto Angular signals like it was designed for them. (It wasn't. That's just a happy accident.) + +Let's wire one up. + +## Goals + +- Understand what AG-UI is and why it matters for Angular devs. +- Wire an AG-UI-compatible backend to an Angular 20+ app using `@ngaf/ag-ui` and `@ngaf/chat`. +- See how 17 protocol events become a handful of signals you can read from a template. +- Handle the parts that turn a demo into a product: tool calls, interrupts, threads, fallbacks. +- Have fun! + +## What is AG-UI? + +AG-UI is the **Agent-User Interaction Protocol**. It's an open, event-based protocol for streaming an agent's state to a user-facing app over plain HTTP. + +The official one-liner from the docs: + +> An open, lightweight, event-based protocol that standardizes how AI agents connect to user-facing applications. + +That's the whole pitch. It standardizes the wire so the agent doesn't care what frontend you use, and the frontend doesn't care what backend you wrote. + +It was introduced by [CopilotKit](https://www.copilotkit.ai/blog/introducing-ag-ui-the-protocol-where-agents-meet-users) in 2025. Markus and the team open-sourced it after years of integrating LangGraph and CrewAI agents into frontend apps and realizing the wire format was the durable piece. It's now the third member of an emerging triad of agent protocols: + +- **MCP**: agent ↔ tools. +- **A2A**: agent ↔ other agents. +- **AG-UI**: agent ↔ user. + +A real production agent typically speaks all three. The interesting one for us is AG-UI, because it's the one your *users* see. + +### Why Angular devs should care + +For the last two years, every interesting agentic UI library has been React-first. CopilotKit, assistant-ui, Vercel AI SDK UI. All React. If you were building an Angular app, your options were: + +1. Reach for `EventSource`, hand-roll the SSE parsing, and reinvent message lists, status, tool cards, and interrupts for the fifth time. +2. Iframe a React app into your Angular app. (Please don't.) +3. Wait. + +AG-UI changes the math. The protocol is framework-agnostic, the official `@ag-ui/client` SDK is plain TypeScript with RxJS, and the event model is a sequence of small writes to a growing list. Which is *exactly* what Angular signals are. + +For me, the moment it clicked was reading the event schema. Seventeen events. That's it. You can hold the whole protocol in your head. + +## The fullstack picture + +Before any code, let's draw the seams. + + + +Three boxes. Two seams. + +**The backend.** Whatever you want, as long as it can emit AG-UI events. LangGraph, CrewAI, Mastra, Microsoft Agent Framework, Pydantic AI, AG2, AWS Strands, Agno. They all have first-party or partnership adapters. If your backend isn't on that list, you write a small middleware that yields AG-UI events. The [middleware guide](https://docs.ag-ui.com/quickstart/middleware) walks through it. + +**The wire.** Server-Sent Events. Plain HTTP, no WebSocket gymnastics, no custom binary framing. Your firewall, load balancer, and reverse proxy already know what to do with it. + +**The Angular side.** This is what ThreadPlane provides. `@ngaf/ag-ui` is the adapter. It consumes the AG-UI event stream and exposes a runtime-neutral `Agent` contract built from signals. `@ngaf/chat` is the UI. It reads from that contract and renders. The two are decoupled on purpose. We'll get to why. + +## Let's wire it up + +We'll start with a fresh Angular 20+ app. I'll assume you already have one, or you can spin one up with `ng new`. + +### Install the packages + + + + +```bash +npm install @ngaf/ag-ui @ngaf/chat marked +``` + + + + +```bash +pnpm add @ngaf/ag-ui @ngaf/chat marked +``` + + + + +```bash +yarn add @ngaf/ag-ui @ngaf/chat marked +``` + + + + +`marked` is the markdown renderer the chat uses for assistant messages. It's a peer dep so you can swap it. + +### Provide the agent + +```ts +// app.config.ts +import { ApplicationConfig } from '@angular/core'; +import { provideAgUiAgent } from '@ngaf/ag-ui'; +import { provideChat } from '@ngaf/chat'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAgUiAgent({ url: 'http://localhost:8000/agent' }), + provideChat({ assistantName: 'Astra' }), + ], +}; +``` + +That's the whole bootstrap. `provideAgUiAgent` is the AG-UI transport. It wraps the official `@ag-ui/client` `HttpAgent` and exposes the signal-shaped contract via DI. `provideChat` is the chat UI's configuration. + +Notice they're independent. `@ngaf/chat` doesn't know it's talking to an AG-UI backend. It just reads from the `Agent` contract. We'll lean on that boundary later. + +### Render the chat + +```ts +// chat-page.component.ts +import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { AG_UI_AGENT } from '@ngaf/ag-ui'; +import { ChatComponent } from '@ngaf/chat'; + +@Component({ + selector: 'app-chat-page', + standalone: true, + imports: [ChatComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ +
+ `, +}) +export class ChatPageComponent { + protected readonly agent = inject(AG_UI_AGENT); +} +``` + +That's it. Three files, maybe twenty lines of code, and you have a working streaming chat backed by any AG-UI-compatible agent. + +
+ The AG-UI streaming demo running in the browser. A user message reads 'What is Angular Agent Framework?' and the assistant has streamed back a multi-sentence response with regenerate, copy, and feedback controls underneath. +
The AG-UI streaming demo running on @ngaf/chat with the @ngaf/ag-ui adapter — FakeAgent provides the events, but the same code drives real LangGraph/CrewAI/Mastra backends.
+
+ +No `EventSource`. No reducer. No manual subscribe-and-render plumbing. No store. + +Spin up your agent backend, point `url` at it, and the chat just works. + +## How AG-UI events become signals + +This is the part I find genuinely *freakin' cool*, so indulge me for a moment. + +The AG-UI protocol has seventeen event types, grouped into five families: + +- **Lifecycle**: `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR`, `STEP_STARTED`, `STEP_FINISHED`. +- **Text messages**: `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END`, `TEXT_MESSAGE_CHUNK`. +- **Tool calls**: `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END`, `TOOL_CALL_RESULT`, `TOOL_CALL_CHUNK`. +- **State sync**: `STATE_SNAPSHOT`, `STATE_DELTA`, `MESSAGES_SNAPSHOT`. +- **Reasoning**: for thinking-style models like o1 and Claude with extended thinking. + +Having worked on the protocol from the inside, I'll tell you the families are doing real work. They're not arbitrary. Lifecycle is for "is something happening?" Text messages are the streaming triad you already know from chat UIs. Tool calls are deliberately incremental so you can render the *intent* before the arguments are fully formed. State sync uses RFC 6902 JSON Patch so the wire stays small even when the agent's state is large. Every event earns its keep. + +ThreadPlane's `@ngaf/ag-ui` runs each event through a small reducer that updates a handful of signals on the `Agent` contract: + +- `messages()`: `Message[]`, the chat history. `TEXT_MESSAGE_CONTENT` appends a delta to the in-flight assistant message. +- `status()`: `'idle' | 'running' | 'error' | 'paused'`. Driven by the `RUN_*` events. +- `toolCalls()`: `ToolCall[]`. `TOOL_CALL_START` appends, `TOOL_CALL_ARGS` streams JSON into the args, `TOOL_CALL_END` marks complete, `TOOL_CALL_RESULT` populates the result. +- `interrupt()`: the current human-in-the-loop pause, if any. +- `error()`: populated by `RUN_ERROR`. + +Your template reads these directly: + +```html +

Status: {{ agent.status() }}

+ +@for (message of agent.messages(); track message.id) { + +} + +@if (agent.interrupt(); as pause) { + +} +``` + +No `async` pipe gymnastics. No `OnDestroy` to clean up a subscription. No manual change-detection plumbing. + +The reason this works is that streaming, at the data-shape level, is just a sequence of small writes to a growing list. And signals are a sequence of small writes to a growing list. The mental model and the runtime model are the same shape. + +In my opinion, that's the *real* reason Angular is a good fit for agentic UIs. Not the templates. Not standalone components. The shape of the state. + +## Tool calls and interrupts + +Two surfaces that separate a "chat with a model" from a "real agent" are tool calls and interrupts. Let's look at both. + +### Tool calls + +When the agent decides to call a tool, you get a stream of events: + +```text +TOOL_CALL_START { id: "1", name: "search_repo" } +TOOL_CALL_ARGS { id: "1", delta: "{\"quer" } +TOOL_CALL_ARGS { id: "1", delta: "y\": \"a" } +TOOL_CALL_ARGS { id: "1", delta: "uth flow\"}" } +TOOL_CALL_END { id: "1" } +TOOL_CALL_RESULT { id: "1", result: { files: [...] } } +``` + +The `` component renders this as a tool-call card by default. A small block in the message list showing the tool name, the args (formatted, even mid-stream), a spinner while it's running, and the result when it returns. + +You almost never want the default forever. You want *your* card for your most important tools: branded, with a click-through to the data, an approve/reject button, whatever your product needs. + +```html + + + + + +``` + +The slot pattern is intentional. The chat ships defaults so you can demo in a day, and it gets out of your way the moment you have a real design system to hit. + +### Interrupts + +An interrupt is the agent saying *"I need a human before I do this thing."* + +You see them in production agents constantly: approve this database write, confirm this email send, choose between these three branches, fill in this missing field. AG-UI surfaces interrupts as state on the agent, and `@ngaf/chat` renders them inline in the message list with whatever resume affordance you give it. + +```html +@if (agent.interrupt(); as pause) { +
+

{{ pause.message }}

+ + +
+} +``` + +The agent stays paused until you call `resume()`. On the backend, your graph picks up exactly where it left off. From a user's perspective, the chat feels *collaborative* instead of *autonomous-and-scary*, which is the entire point of human-in-the-loop in the first place. + +## Threads, persistence, and the things demos skip + +A single-thread chat is a demo. A real product remembers conversations across sessions and across devices. + +AG-UI itself is stateless on the wire. Every run carries a `threadId`, and the backend is responsible for persistence. Most adapters expose thread CRUD as a small API: + +- list threads +- create a thread +- switch threads +- delete a thread + +The Angular-side pattern is to bind the `threadId` to a signal (usually from your router or a sidebar selection) and the chat re-reads the new thread automatically. No manual unsubscribe. No race conditions. + +```ts +protected readonly threadId = signal(null); + +constructor() { + effect(() => { + const id = this.threadId(); + if (id) this.agent.switchThread(id); + }); +} +``` + +What you *do* with that capability is a product decision, not a framework one. Scope threads per project, per task, per user session. There's no right answer. From my experience, the wrong answer is to ignore the question entirely and ship a chat where every refresh starts from zero. + +If you want a starting point, `@ngaf/chat` exposes a `` primitive that handles the layout without locking you into a persistence model. + +## Swap the backend without changing the UI + +This is the part that pays off the protocol bet. + +Say you started with a Python LangGraph backend, shipped to production, and a quarter later you decide you want to migrate one graph to Mastra because the team writing it lives in TypeScript. With AG-UI, that's a backend change. Your Angular code does not move. + +```ts +// before +provideAgUiAgent({ url: '/agents/langgraph' }), + +// after +provideAgUiAgent({ url: '/agents/mastra' }), +``` + +That's the diff. + +The same applies in reverse. If you're already on a LangGraph stack and want a slightly tighter coupling than AG-UI gives you, `@ngaf/langgraph` is a direct adapter for LangGraph's native streaming API. It speaks the same `Agent` contract to `@ngaf/chat`, so your UI doesn't change either way. Pick whichever fits your team. + +For me, the durable bet is AG-UI. I'm biased (I helped grow the ecosystem around it at CopilotKit, and I'm a contributor to the spec), but the list of backends that already speak it covers most of what you'd reach for in 2026, and the protocol is small enough that I trust it not to drift into a multi-page spec that breaks every six months. The community around it is healthy, the maintainers are responsive, and the design choices have aged well. + +## A note on the rest of the iceberg + +The scaffold above is *short* on purpose. The framework intentionally hides the parts you don't want to think about on day one. + +The parts you'll want on day thirty: + +- **Errors and retries.** Per-message retry, transport-level backoff for transient SSE drops, graceful degradation when streaming is unavailable. Some corporate proxies buffer SSE. You'll find out the hard way if you don't plan for it. +- **Generative UI.** When the agent wants to render a richer surface than a tool-call card, `@ngaf/render` lets the backend stream a UI spec and the frontend resolves it against a registry of your approved Angular components. No arbitrary code, no `eval`, no design-system bypass. The agent picks from a menu you control. +- **Observability.** `@ngaf/telemetry` ships a PostHog-shaped sink that is *off by default*. Turn it on per-environment, point it at your own analytics, never ship app content to a vendor you didn't pick. +- **Testing.** Because the contract is signals all the way down, the testing story is "write a signal, the chat re-renders." `@ngaf/ag-ui` ships a `FakeAgent` you can hand-feed events to in a unit test. No SSE harness, no fixture loader, no test-only DI dance. + +Each of those is its own post. The point here is just that the protocol-to-signal-to-template chain is the *spine*, and everything else hangs off it. + +## Conclusion + +AG-UI is the protocol Angular devs have been quietly waiting for, whether they knew it or not. It standardizes the wire between the agent and the UI, it's small enough to hold in your head, and the event model maps onto Angular signals so cleanly that the runtime almost writes itself. Markus and the CopilotKit crew deserve real credit for that. *Thank you* for designing something Angular could meet on its own terms. + +With ThreadPlane (`@ngaf/ag-ui` and `@ngaf/chat` on npm), the wiring is three lines: a provider, an inject, and a ``. The interesting work (your tool cards, your interrupt flows, your generative UI surfaces, your design system) is where you actually want to spend the day. + +If you've been building agents in Angular by hand-rolling SSE parsing and gluing together React components in iframes, *stop*. The whole stack is here, it's open source (the adapters are MIT; `@ngaf/chat` is source-available with a free non-commercial tier), and the protocol underneath has real momentum from the community building it. + +I'd love to hear what you ship. If you wire this up and the docs come up short, [open an issue](https://github.com/threadplane/angular-agent-framework/issues) or come find us on Discord. And if you're building this inside an enterprise Angular app (design system, multi-tenant, regulated), [talk to us](/contact?source=blog_ag_ui_pillar&track=enterprise). We've helped a few teams through that gauntlet already. + +Now go ship something. 💚 diff --git a/apps/website/public/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui/hero.png b/apps/website/public/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..1b6df7c7a3ebf5fe8fbe47b7e3a8176b0626bec7 GIT binary patch literal 72694 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 Date: Fri, 22 May 2026 09:55:23 -0700 Subject: [PATCH 3/4] fix(website): drop unnecessary escape in readingTimeMin regex ESLint no-useless-escape flagged \- inside a character class. Moving the hyphen to the end of the class drops the escape without changing the matched set. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/website/src/app/blog/[slug]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/src/app/blog/[slug]/page.tsx b/apps/website/src/app/blog/[slug]/page.tsx index 48b6c937..44165d91 100644 --- a/apps/website/src/app/blog/[slug]/page.tsx +++ b/apps/website/src/app/blog/[slug]/page.tsx @@ -48,7 +48,7 @@ function formatDate(iso: string): string { function readingTimeMin(markdown: string): number { const words = markdown .replace(/```[\s\S]*?```/g, '') // strip code fences (not real reading) - .replace(/[#*_`>\-]/g, ' ') + .replace(/[#*_`>-]/g, ' ') .split(/\s+/) .filter(Boolean).length; return Math.max(1, Math.round(words / 220)); From 8b7ece41d103a5e0d0f38ccbea0534e144eb26d8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 22 May 2026 10:14:01 -0700 Subject: [PATCH 4/4] fix(website): restore
wrapper on docs slug pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MdxRenderer refactor (root
to avoid nested-article hydration mismatch on blog) inadvertently dropped the
tag from docs pages too, breaking the docs e2e selectors (page.locator('article'), page.locator('article h2')). Restore the
on the docs page wrapper instead of inside MdxRenderer. The blog page keeps its own outer
; both surfaces now have exactly one
, satisfying semantic HTML and the existing e2e contracts without re-introducing the hydration warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx index 739a386d..80292566 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -74,7 +74,7 @@ export default async function DocsPage({ params }: DocsRouteProps) {
-
+
-
+
{section === 'api' && (() => { const entries = loadApiDocs(library); const nameMap = API_NAME_MAP[library] ?? {};