From a35fedb9b4f9eb4ac867d54678256caada628be1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 16 May 2026 10:32:16 -0700 Subject: [PATCH 1/3] docs: polish browser QA findings --- .../content/docs/render/a2ui/overview.mdx | 90 ++++++++++++++++++- .../docs/[library]/[section]/[slug]/page.tsx | 21 +++-- apps/website/src/lib/docs.spec.ts | 14 ++- apps/website/src/lib/docs.ts | 23 ++++- 4 files changed, 136 insertions(+), 12 deletions(-) diff --git a/apps/website/content/docs/render/a2ui/overview.mdx b/apps/website/content/docs/render/a2ui/overview.mdx index 069a0b88a..accdfca8b 100644 --- a/apps/website/content/docs/render/a2ui/overview.mdx +++ b/apps/website/content/docs/render/a2ui/overview.mdx @@ -38,13 +38,95 @@ That behavior is deliberate. Agent streams are partial. The parser should not cr ## Minimal Protocol Stream -A surface usually needs three messages: components, data, and a root. This is the wire format accepted by the parser and surface store. +A surface usually needs three messages: components, data, and a root. On the wire, each envelope is one newline-delimited JSON object after the sentinel: ```text ---a2ui_JSON--- -{"surfaceUpdate":{"surfaceId":"contact","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","name","submit"]},"gap":12}}},{"id":"title","component":{"Text":{"text":{"literalString":"Contact us"},"usageHint":"h2"}}},{"id":"name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"}}}},{"id":"submit_label","component":{"Text":{"text":{"literalString":"Send"}}}},{"id":"submit","component":{"Button":{"child":"submit_label","primary":true,"action":{"name":"formSubmit","context":[{"key":"name","value":{"path":"/name"}}]}}}}]}} -{"dataModelUpdate":{"surfaceId":"contact","contents":[{"key":"name","valueString":""}]}} -{"beginRendering":{"surfaceId":"contact","root":"root"}} +{"surfaceUpdate":{...}} +{"dataModelUpdate":{...}} +{"beginRendering":{...}} +``` + +The same `surfaceUpdate` envelope, expanded for readability, looks like this: + +```json +{ + "surfaceUpdate": { + "surfaceId": "contact", + "components": [ + { + "id": "root", + "component": { + "Column": { + "children": { "explicitList": ["title", "name", "submit"] }, + "gap": 12 + } + } + }, + { + "id": "title", + "component": { + "Text": { + "text": { "literalString": "Contact us" }, + "usageHint": "h2" + } + } + }, + { + "id": "name", + "component": { + "TextField": { + "label": { "literalString": "Name" }, + "text": { "path": "/name" } + } + } + }, + { + "id": "submit_label", + "component": { + "Text": { + "text": { "literalString": "Send" } + } + } + }, + { + "id": "submit", + "component": { + "Button": { + "child": "submit_label", + "primary": true, + "action": { + "name": "formSubmit", + "context": [ + { "key": "name", "value": { "path": "/name" } } + ] + } + } + } + } + ] + } +} +``` + +The data and root envelopes are smaller: + +```json +{ + "dataModelUpdate": { + "surfaceId": "contact", + "contents": [{ "key": "name", "valueString": "" }] + } +} +``` + +```json +{ + "beginRendering": { + "surfaceId": "contact", + "root": "root" + } +} ``` Components use keyed union definitions: 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 8d244a413..960903cf5 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { tokens } from '@ngaf/design-tokens'; import { DocsSidebar } from '../../../../../components/docs/DocsSidebar'; @@ -5,7 +6,7 @@ import { MdxRenderer } from '../../../../../components/docs/MdxRenderer'; import { DocsSearch } from '../../../../../components/docs/DocsSearch'; import { DocsBreadcrumb } from '../../../../../components/docs/DocsBreadcrumb'; import { DocsPrevNext } from '../../../../../components/docs/DocsPrevNext'; -import { getDocBySlug, getAllDocSlugs } from '../../../../../lib/docs'; +import { getDocBySlug, getAllDocSlugs, getDocMetadata } from '../../../../../lib/docs'; import { ApiDocRenderer, type ApiDocEntry } from '../../../../../components/docs/ApiDocRenderer'; import { DocsTOC } from '../../../../../components/docs/DocsTOC'; import { extractHeadings } from '../../../../../lib/extract-headings'; @@ -33,15 +34,23 @@ const API_NAME_MAP: Record> = { }, }; +interface DocsRouteProps { + params: Promise<{ library: string; section: string; slug: string }>; +} + export function generateStaticParams() { return getAllDocSlugs().map(({ library, section, slug }) => ({ library, section, slug })); } -export default async function DocsPage({ - params, -}: { - params: Promise<{ library: string; section: string; slug: string }>; -}) { +export async function generateMetadata({ params }: DocsRouteProps): Promise { + const { library, section, slug } = await params; + return getDocMetadata(library, section, slug) ?? { + title: 'Docs - Angular Agent Framework', + description: 'Angular Agent Framework documentation', + }; +} + +export default async function DocsPage({ params }: DocsRouteProps) { const { library, section, slug } = await params; const libConfig = getLibraryConfig(library); diff --git a/apps/website/src/lib/docs.spec.ts b/apps/website/src/lib/docs.spec.ts index 25d5782d9..abb9811cb 100644 --- a/apps/website/src/lib/docs.spec.ts +++ b/apps/website/src/lib/docs.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getAllDocSlugs, getDocBySlug } from './docs'; +import { getAllDocSlugs, getDocBySlug, getDocMetadata } from './docs'; import { allDocsPages } from './docs-config'; describe('website docs bindings', () => { @@ -28,6 +28,14 @@ describe('website docs bindings', () => { expect(doc?.title).toBe('Introduction'); }); + it('resolves page metadata for configured docs', () => { + expect(getDocMetadata('ag-ui', 'reference', 'event-mapping')).toEqual({ + title: 'Event Mapping - AG-UI Docs - Angular Agent Framework', + description: + 'Adapter for any AG-UI-compatible backend (CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, AWS Strands, CopilotKit runtime)', + }); + }); + it('returns null for non-existent doc', () => { expect(getDocBySlug('agent', 'guides', 'nonexistent')).toBeNull(); }); @@ -35,4 +43,8 @@ describe('website docs bindings', () => { it('returns null for non-existent library', () => { expect(getDocBySlug('nonexistent', 'getting-started', 'introduction')).toBeNull(); }); + + it('returns null metadata for non-existent docs', () => { + expect(getDocMetadata('agent', 'guides', 'nonexistent')).toBeNull(); + }); }); diff --git a/apps/website/src/lib/docs.ts b/apps/website/src/lib/docs.ts index b6a70bdaa..266b3b23f 100644 --- a/apps/website/src/lib/docs.ts +++ b/apps/website/src/lib/docs.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { docsConfig, type DocsPage, getLibraryPages } from './docs-config'; +import { docsConfig, type DocsPage, getLibraryConfig, getLibraryPages } from './docs-config'; const resolveContentDir = (library: string): string => { const workspacePath = path.join(process.cwd(), 'apps', 'website', 'content', 'docs', library); @@ -14,6 +14,11 @@ export interface ResolvedDoc { title: string; } +export interface ResolvedDocMetadata { + title: string; + description: string; +} + export function getDocBySlug(library: string, section: string, slug: string): ResolvedDoc | null { const pages = getLibraryPages(library); const page = pages.find((p) => p.section === section && p.slug === slug); @@ -32,6 +37,22 @@ export function getDocBySlug(library: string, section: string, slug: string): Re }; } +export function getDocMetadata( + library: string, + section: string, + slug: string +): ResolvedDocMetadata | null { + const doc = getDocBySlug(library, section, slug); + if (!doc) return null; + + const lib = getLibraryConfig(library); + const libraryTitle = lib?.title ?? 'Docs'; + return { + title: `${doc.title} - ${libraryTitle} Docs - Angular Agent Framework`, + description: lib?.description ?? 'Angular Agent Framework documentation', + }; +} + export function getAllDocSlugs(): { library: string; section: string; slug: string }[] { return docsConfig.flatMap((lib) => lib.sections.flatMap((s) => From d34785eadf57b6df6892fc8172d9ca12f0412c0e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 16 May 2026 10:35:19 -0700 Subject: [PATCH 2/3] docs: refresh generated api docs --- .../content/docs/ag-ui/api/api-docs.json | 28 ++++++- .../content/docs/agent/api/api-docs.json | 6 ++ .../content/docs/chat/api/api-docs.json | 84 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/apps/website/content/docs/ag-ui/api/api-docs.json b/apps/website/content/docs/ag-ui/api/api-docs.json index 114894dba..2050e56d7 100644 --- a/apps/website/content/docs/ag-ui/api/api-docs.json +++ b/apps/website/content/docs/ag-ui/api/api-docs.json @@ -381,6 +381,12 @@ "description": "", "optional": true }, + { + "name": "telemetry", + "type": "false | AgentRuntimeTelemetrySink", + "description": "Optional app-owned telemetry sink. No telemetry is emitted unless this is provided.", + "optional": true + }, { "name": "threadId", "type": "string", @@ -422,6 +428,20 @@ ], "examples": [] }, + { + "name": "ToAgentOptions", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "telemetry", + "type": "false | AgentRuntimeTelemetrySink", + "description": "Optional app-owned telemetry sink. No telemetry is emitted unless this is provided.", + "optional": true + } + ], + "examples": [] + }, { "name": "bridgeCitationsState", "kind": "function", @@ -501,13 +521,19 @@ "name": "toAgent", "kind": "function", "description": "Wraps an AG-UI AbstractAgent into the runtime-neutral Agent contract.\n\nThe adapter subscribes to source.subscribe({ onEvent }) and reduces every\nevent into the produced Agent's signals. submit() optimistically appends the\nuser message to both our signals and the source agent's internal message\nlist, then calls source.runAgent(). stop() calls source.abortRun().\n\nSubscription cleanup: the returned Agent does NOT manage its own lifetime.\nCallers using DI should rely on the provider's destroy hook; direct callers\nof toAgent() should treat the returned object's lifecycle as tied to the\nagent instance they constructed. The subscriber registered via\nsource.subscribe() will fire for the lifetime of source.", - "signature": "toAgent(source: AbstractAgent<>): Agent", + "signature": "toAgent(source: AbstractAgent<>, options: ToAgentOptions): Agent", "params": [ { "name": "source", "type": "AbstractAgent<>", "description": "", "optional": false + }, + { + "name": "options", + "type": "ToAgentOptions", + "description": "", + "optional": false } ], "returns": { diff --git a/apps/website/content/docs/agent/api/api-docs.json b/apps/website/content/docs/agent/api/api-docs.json index d84a6f3fe..98b71793f 100644 --- a/apps/website/content/docs/agent/api/api-docs.json +++ b/apps/website/content/docs/agent/api/api-docs.json @@ -684,6 +684,12 @@ "description": "Tool names that indicate a subagent invocation.", "optional": true }, + { + "name": "telemetry", + "type": "false | AgentRuntimeTelemetrySink", + "description": "Optional app-owned telemetry sink. No telemetry is emitted unless this is provided.", + "optional": true + }, { "name": "threadId", "type": "string | Signal | null", diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 6caf6fc1b..2dc3925dc 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -5791,6 +5791,76 @@ ], "examples": [] }, + { + "name": "AgentRuntimeTelemetryPayload", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "event", + "type": "AgentRuntimeTelemetryEvent", + "description": "", + "optional": false + }, + { + "name": "properties", + "type": "AgentRuntimeTelemetryProperties", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "AgentRuntimeTelemetryProperties", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "durationMs", + "type": "number", + "description": "", + "optional": true + }, + { + "name": "errorClass", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "model", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "provider", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "requestType", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "surface", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "transport", + "type": "string", + "description": "", + "optional": false + } + ], + "examples": [] + }, { "name": "AgentStateUpdateEvent", "kind": "interface", @@ -6864,6 +6934,20 @@ "signature": "AgentStateUpdateEvent | AgentCustomEvent", "examples": [] }, + { + "name": "AgentRuntimeTelemetryEvent", + "kind": "type", + "description": "", + "signature": "\"ngaf:runtime_instance_created\" | \"ngaf:runtime_request_created\" | \"ngaf:stream_started\" | \"ngaf:stream_ended\" | \"ngaf:stream_errored\"", + "examples": [] + }, + { + "name": "AgentRuntimeTelemetrySink", + "kind": "type", + "description": "", + "signature": "object", + "examples": [] + }, { "name": "AgentStatus", "kind": "type", From 1969509f8f8b3a118f5bd7b1019c3512b396a7f9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 16 May 2026 10:38:26 -0700 Subject: [PATCH 3/3] docs: refresh render api docs --- apps/website/content/docs/render/api/api-docs.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/website/content/docs/render/api/api-docs.json b/apps/website/content/docs/render/api/api-docs.json index 9c0773165..faee0368f 100644 --- a/apps/website/content/docs/render/api/api-docs.json +++ b/apps/website/content/docs/render/api/api-docs.json @@ -33,6 +33,18 @@ "description": "", "optional": false }, + { + "name": "filteredRepeatInputs", + "type": "Signal[]>", + "description": "`repeatInputs` filtered per-item to declared component inputs.", + "optional": false + }, + { + "name": "filteredResolvedInputs", + "type": "Signal>", + "description": "`resolvedInputs` filtered down to keys the target component actually\ndeclares — silences NG0303 dev-mode warnings from framework-only\ninputs (bindings/emit/loading/childKeys/spec) passed to simple view\ncomponents that don't declare them.", + "optional": false + }, { "name": "mountClass", "type": "Signal",