From a7f51a7f06a40043749f7c76e937deceb940dc79 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 18:15:45 +0000 Subject: [PATCH] feat(events): define semantic route descriptors Admit only the seven G10 event families, type their canonical/native props, and add Agent.Context as the immediate guidance vocabulary. --- .../semantic-event-route-descriptors.md | 7 +++ packages/agent-bundle/src/api.ts | 20 ++++++- .../agent-bundle/src/build/entry-shell.ts | 1 + packages/agent-bundle/src/index.ts | 20 ++++++- packages/agent-bundle/src/routes/contract.ts | 49 +++++++++++++++-- packages/agent-bundle/src/routes/graph.ts | 38 +++++++++++-- packages/agent-bundle/src/routes/index.ts | 22 +++++++- packages/agent-bundle/src/routes/public.ts | 55 +++++++++++++++++++ packages/agent-bundle/src/routes/typegen.ts | 8 ++- packages/agent-bundle/src/routes/types.ts | 3 + .../agent-bundle/tests/route-graph.test.ts | 48 ++++++++++++++-- packages/rsc-runtime/src/agent-document.ts | 9 +++ packages/rsc-runtime/src/decode-document.ts | 3 + packages/rsc-runtime/src/elements.ts | 3 + packages/rsc-runtime/src/index.ts | 1 + .../rsc-runtime/tests/agent-document.test.ts | 2 + packages/rsc-runtime/tests/dispatcher.test.ts | 2 + 17 files changed, 274 insertions(+), 17 deletions(-) create mode 100644 .changeset/semantic-event-route-descriptors.md diff --git a/.changeset/semantic-event-route-descriptors.md b/.changeset/semantic-event-route-descriptors.md new file mode 100644 index 000000000..731346df9 --- /dev/null +++ b/.changeset/semantic-event-route-descriptors.md @@ -0,0 +1,7 @@ +--- +"agent-bundle": minor +"@agent-bundle/runtime": minor +--- + +Add the seven v1 semantic event-route descriptors and the `Agent.Context` +document vocabulary used for immediate host guidance. diff --git a/packages/agent-bundle/src/api.ts b/packages/agent-bundle/src/api.ts index 3f556fe88..22fd5db4d 100644 --- a/packages/agent-bundle/src/api.ts +++ b/packages/agent-bundle/src/api.ts @@ -12,7 +12,25 @@ import { emptyCompiledRouteGraph } from './routes/graph.ts'; import { inspectRouteGraph, type RouteGraphInspection } from './routes/inspect.ts'; import { mcpServerStateDirectory, runMcpForeground } from './services/mcp-run.ts'; export { compileRouteGraph, emptyCompiledRouteGraph, isEmptyRouteGraph } from './routes/graph.ts'; -export type { AppRouteConfig, PromptConfig, ResourceConfig, RouteSchema, RouteSchemaOutput, ToolConfig, ToolRouteProps } from './routes/public.ts'; +export { canonicalAgentEvents } from './routes/public.ts'; +export type { + AgentEventCanonicalIdentity, + AgentEventDelivery, + AgentEventFallbackMode, + AgentEventNativePayload, + AgentEventProvenance, + AgentEventRouteConfig, + AgentEventRouteProps, + AgentEventRuntimeMode, + AppRouteConfig, + CanonicalAgentEvent, + PromptConfig, + ResourceConfig, + RouteSchema, + RouteSchemaOutput, + ToolConfig, + ToolRouteProps, +} from './routes/public.ts'; export { inspectRouteGraph } from './routes/inspect.ts'; export type { RouteGraphInspection } from './routes/inspect.ts'; export { emptyRouteConfig } from './routes/types.ts'; diff --git a/packages/agent-bundle/src/build/entry-shell.ts b/packages/agent-bundle/src/build/entry-shell.ts index 55f76e5f8..fe943e1e2 100644 --- a/packages/agent-bundle/src/build/entry-shell.ts +++ b/packages/agent-bundle/src/build/entry-shell.ts @@ -275,6 +275,7 @@ export const generatedRouteMcpEntrySource = (options: GeneratedRouteMcpEntryOpti 'const appendNode = (node, content) => {', ' switch (node.kind) {', " case 'result': for (const child of node.children) appendNode(child, content); break;", + " case 'context':", " case 'markdown':", " case 'text': content.push({ text: node.text, type: 'text' }); break;", " case 'json': content.push({ text: JSON.stringify(node.value), type: 'text' }); break;", diff --git a/packages/agent-bundle/src/index.ts b/packages/agent-bundle/src/index.ts index 69f0b9fc3..294565297 100644 --- a/packages/agent-bundle/src/index.ts +++ b/packages/agent-bundle/src/index.ts @@ -4,7 +4,25 @@ import type { PortableConfigExtension } from './adapters/portable.ts'; import type { AgentBundleConfig as CoreAgentBundleConfig } from './core/types.ts'; export { defineConfig, pathTokens, pluginRootEnvAnchor } from './core/types.ts'; -export type { AppRouteConfig, PromptConfig, ResourceConfig, RouteSchema, RouteSchemaOutput, ToolConfig, ToolRouteProps } from './routes/public.ts'; +export { canonicalAgentEvents } from './routes/public.ts'; +export type { + AgentEventCanonicalIdentity, + AgentEventDelivery, + AgentEventFallbackMode, + AgentEventNativePayload, + AgentEventProvenance, + AgentEventRouteConfig, + AgentEventRouteProps, + AgentEventRuntimeMode, + AppRouteConfig, + CanonicalAgentEvent, + PromptConfig, + ResourceConfig, + RouteSchema, + RouteSchemaOutput, + ToolConfig, + ToolRouteProps, +} from './routes/public.ts'; export { compareEvals, runEvals, startDevServer } from './api.ts'; export { createCodexEvalHarness, diff --git a/packages/agent-bundle/src/routes/contract.ts b/packages/agent-bundle/src/routes/contract.ts index 2c818f02c..3d19e5ba2 100644 --- a/packages/agent-bundle/src/routes/contract.ts +++ b/packages/agent-bundle/src/routes/contract.ts @@ -28,12 +28,16 @@ const diagnostic = ( recovery: string, ): Diagnostic => ({ code, message, recovery, severity: 'error', sourcePath }); -/** Validates G8's one executable route contract without evaluating the module. */ -export const validateRouteModuleContract = ( +interface RouteModuleShape { + readonly asyncDefault: boolean; + readonly named: ReadonlySet; + readonly splitExport: boolean; +} + +const inspectRouteModule = ( moduleText: string, relativePath: string, - sourcePath: string, -): readonly Diagnostic[] => { +): RouteModuleShape => { const sourceFile = ts.createSourceFile(relativePath, moduleText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); const named = new Set(); let asyncDefault = false; @@ -71,6 +75,16 @@ export const validateRouteModuleContract = ( } } + return { asyncDefault, named, splitExport }; +}; + +/** Validates G8's one executable MCP route contract without evaluating the module. */ +export const validateRouteModuleContract = ( + moduleText: string, + relativePath: string, + sourcePath: string, +): readonly Diagnostic[] => { + const { asyncDefault, named, splitExport } = inspectRouteModule(moduleText, relativePath); const missing = ['inputSchema', 'resultSchema'].filter((name) => !named.has(name)); const diagnostics: Diagnostic[] = []; if (missing.length > 0 || !asyncDefault) { @@ -95,3 +109,30 @@ export const validateRouteModuleContract = ( } return Object.freeze(diagnostics); }; + +/** Validates an event route's single async component contract without requiring MCP schemas. */ +export const validateEventRouteModuleContract = ( + moduleText: string, + relativePath: string, + sourcePath: string, +): readonly Diagnostic[] => { + const { asyncDefault, splitExport } = inspectRouteModule(moduleText, relativePath); + const diagnostics: Diagnostic[] = []; + if (!asyncDefault) { + diagnostics.push(diagnostic( + 'AB4810', + `Event route module ${relativePath} does not satisfy the public route contract: default export is not an async function component.`, + sourcePath, + 'Export one async default Server Component receiving { canonical, native, signal }.', + )); + } + if (splitExport) { + diagnostics.push(diagnostic( + 'AB4811', + `Event route module ${relativePath} exports execute or render; routed modules use one async default Server Component instead of an execute/render split.`, + sourcePath, + 'Move execution into the async default component and render Agent.* elements from that component.', + )); + } + return Object.freeze(diagnostics); +}; diff --git a/packages/agent-bundle/src/routes/graph.ts b/packages/agent-bundle/src/routes/graph.ts index db5c85d59..4fcbe6a2c 100644 --- a/packages/agent-bundle/src/routes/graph.ts +++ b/packages/agent-bundle/src/routes/graph.ts @@ -6,12 +6,13 @@ import fastGlob from 'fast-glob'; import { isProjectPathIgnored, readProjectIgnoreRules, toPosixPath } from '../config/ignore.ts'; import { extractRouteConfig } from './config-extract.ts'; -import { validateRouteModuleContract } from './contract.ts'; +import { validateEventRouteModuleContract, validateRouteModuleContract } from './contract.ts'; import type { Diagnostic } from '../core/diagnostics.ts'; import { digest } from '../core/digest.ts'; import { deepFreeze } from '../core/freeze.ts'; import { isRecord } from '../core/strict-json.ts'; import type { AgentBundleConfig } from '../core/types.ts'; +import { canonicalAgentEvents, type CanonicalAgentEvent } from './public.ts'; import { emptyRouteConfig, type CompiledAgentRoute, @@ -35,6 +36,7 @@ type ProjectIgnoreRules = Awaited>; const routeGlobs = [ 'src/mcp/*/{tools,resources,prompts,apps}/*.{ts,tsx}', 'src/events/*/*.{ts,tsx}', + 'src/events/stop.{ts,tsx}', 'src/providers/*.{ts,tsx}', 'src/cli/**/*.{ts,tsx}', // Scripts also discover .jsx: the stage-1 script gate judges rendered @@ -105,6 +107,7 @@ interface DiscoveredProviderModule { } interface DiscoveredRouteModule { + readonly event?: CanonicalAgentEvent; readonly id: string; /** The path-derived name segments; each must satisfy the safe-identity rule. */ readonly identitySegments: readonly string[]; @@ -138,10 +141,11 @@ const classifyModule = (source: string, relativePath: string): DiscoveredModule }; } if (collection === 'events') { - const family = segments[2]!; + const event = segments.length === 3 ? stem : `${segments[2]!}/${stem}`; return { - id: `event:${family}/${stem}`, - identitySegments: [family, stem], + event: event as CanonicalAgentEvent, + id: `event:${event}`, + identitySegments: event.split('/'), kind: 'event-route', relativePath, source, @@ -302,6 +306,7 @@ const compiledRoute = ( config: Readonly>, ): CompiledAgentRoute => ({ config, + ...(module.event === undefined ? {} : { event: module.event }), id: module.id, kind: module.kind, provenance: { kind: 'conventional', relativePath: module.relativePath }, @@ -331,6 +336,7 @@ const extractedModuleConfig = async ( const routeIdentity = (route: CompiledAgentRoute): Readonly> => ({ config: route.config, + ...(route.event === undefined ? {} : { event: route.event }), id: route.id, kind: route.kind, relativePath: route.provenance.relativePath, @@ -392,6 +398,19 @@ export const compileRouteGraph = async ( const relativePath = toPosixPath(relative(projectRoot, source)); if (isPrivateRoutePath(relativePath) || isProjectPathIgnored(rules, projectRoot, source)) continue; const module = classifyModule(source, relativePath); + if ( + module.surface === 'route' && + module.kind === 'event-route' && + !canonicalAgentEvents.includes(module.event!) + ) { + diagnostics.push(routeError( + 'AB4813', + `Event route ${relativePath} declares ${JSON.stringify(module.event)}, which is outside the #97 v1 event vocabulary.`, + `Use one of: ${canonicalAgentEvents.join(', ')}.`, + source, + )); + continue; + } const unsafeSegment = module.identitySegments.find((segment) => !safeIdentitySegment.test(segment)); if (unsafeSegment !== undefined) { diagnostics.push(routeError( @@ -432,6 +451,17 @@ export const compileRouteGraph = async ( continue; } const route = compiledRoute(module, await extractedModuleConfig(module, diagnostics)); + if (route.kind === 'event-route') { + try { + diagnostics.push(...validateEventRouteModuleContract( + await readFile(route.source, 'utf8'), + route.provenance.relativePath, + route.source, + )); + } catch { + // Racing deletion is handled by the next source snapshot. + } + } switch (route.kind) { case 'tool': case 'resource': diff --git a/packages/agent-bundle/src/routes/index.ts b/packages/agent-bundle/src/routes/index.ts index 0c397ecf3..85e270285 100644 --- a/packages/agent-bundle/src/routes/index.ts +++ b/packages/agent-bundle/src/routes/index.ts @@ -18,5 +18,23 @@ export type { RouteProvenance, } from './types.ts'; export { generateRouteTypes, routeTypesRelativePath, writeRouteTypes } from './typegen.ts'; -export { validateRouteModuleContract } from './contract.ts'; -export type { AppRouteConfig, PromptConfig, ResourceConfig, RouteSchema, RouteSchemaOutput, ToolConfig, ToolRouteProps } from './public.ts'; +export { validateEventRouteModuleContract, validateRouteModuleContract } from './contract.ts'; +export { canonicalAgentEvents } from './public.ts'; +export type { + AgentEventCanonicalIdentity, + AgentEventDelivery, + AgentEventFallbackMode, + AgentEventNativePayload, + AgentEventProvenance, + AgentEventRouteConfig, + AgentEventRouteProps, + AgentEventRuntimeMode, + AppRouteConfig, + CanonicalAgentEvent, + PromptConfig, + ResourceConfig, + RouteSchema, + RouteSchemaOutput, + ToolConfig, + ToolRouteProps, +} from './public.ts'; diff --git a/packages/agent-bundle/src/routes/public.ts b/packages/agent-bundle/src/routes/public.ts index 024991da1..4f0383c75 100644 --- a/packages/agent-bundle/src/routes/public.ts +++ b/packages/agent-bundle/src/routes/public.ts @@ -5,6 +5,61 @@ export interface RouteSchema { export type RouteSchemaOutput = Schema extends RouteSchema ? Output : never; +/** The event-route families admitted by the recorded #97 v1/G10 decision. */ +export const canonicalAgentEvents = Object.freeze([ + 'session/start', + 'tool/before', + 'tool/after', + 'stop', + 'agent/start', + 'agent/stop', + 'workspace/open', +] as const); + +export type CanonicalAgentEvent = (typeof canonicalAgentEvents)[number]; + +export interface AgentEventProvenance { + readonly host: string; + readonly hostContractRevision: string; + readonly nativeEvent: string; + readonly source: 'native'; +} + +/** Cross-host identity supplied to an event route without fabricated host fields. */ +export interface AgentEventCanonicalIdentity { + readonly event: CanonicalAgentEvent; + readonly idempotencyKey: string; + readonly observedAt: string; + readonly provenance: AgentEventProvenance; + readonly sequence: number; +} + +/** Complete host envelope after the adapter's schema and byte-bound validation. */ +export type AgentEventNativePayload = Readonly>; + +/** Props received by an event route's async default Server Component. */ +export interface AgentEventRouteProps { + readonly canonical: AgentEventCanonicalIdentity; + readonly native: AgentEventNativePayload; + readonly signal: AbortSignal; +} + +export type AgentEventDelivery = 'immediate'; +export type AgentEventRuntimeMode = 'shared' | 'standalone'; +export type AgentEventFallbackMode = 'none' | 'standalone'; + +/** Statically extractable event-route configuration. */ +export interface AgentEventRouteConfig { + readonly delivery?: readonly AgentEventDelivery[]; + readonly fallback?: AgentEventFallbackMode; + readonly runtime?: AgentEventRuntimeMode; + readonly targets?: readonly string[]; + /** Route budget within the adapter's stricter native-host deadline. */ + readonly timeoutMs?: number; + /** Canonical tool selectors for tool/before and tool/after routes. */ + readonly tools?: readonly string[]; +} + /** Props received by every executable MCP route's async default Server Component. */ export interface ToolRouteProps { readonly input: RouteSchemaOutput; diff --git a/packages/agent-bundle/src/routes/typegen.ts b/packages/agent-bundle/src/routes/typegen.ts index 99d16eabf..d8eba3d24 100644 --- a/packages/agent-bundle/src/routes/typegen.ts +++ b/packages/agent-bundle/src/routes/typegen.ts @@ -31,10 +31,16 @@ export const generateRouteTypes = (graph: CompiledRouteGraph): string => { ' input: SchemaOutput;', ' result: SchemaOutput;', '}>;', + 'export type EventRouteContract = Readonly<{', + ' component: Component;', + ' event: Event;', + '}>;', '', 'export interface AgentBundleRoutes {', ...routes.map((route, index) => - ` ${JSON.stringify(route.id)}: RouteContract;`), + route.kind === 'event-route' + ? ` ${JSON.stringify(route.id)}: EventRouteContract;` + : ` ${JSON.stringify(route.id)}: RouteContract;`), '}', '', 'export type RouteId = keyof AgentBundleRoutes;', diff --git a/packages/agent-bundle/src/routes/types.ts b/packages/agent-bundle/src/routes/types.ts index 0b2b2cffb..2d1d353c8 100644 --- a/packages/agent-bundle/src/routes/types.ts +++ b/packages/agent-bundle/src/routes/types.ts @@ -1,4 +1,5 @@ import type { Diagnostic } from '../core/diagnostics.ts'; +import type { CanonicalAgentEvent } from './public.ts'; /** * Every route kind the conventional source tree can declare. Context @@ -39,6 +40,8 @@ export const emptyRouteConfig: Readonly> = Object.freeze export interface CompiledAgentRoute { /** Statically extracted from the module's `export const config` declaration; {@link emptyRouteConfig} when absent or rejected. */ readonly config: Readonly>; + /** Canonical event identity; present only when {@link kind} is `event-route`. */ + readonly event?: CanonicalAgentEvent; readonly id: string; readonly kind: CompiledRouteKind; readonly provenance: RouteProvenance; diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index cb1f773ee..6e3478af7 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -42,7 +42,7 @@ const fixtureConfig = (extra: Readonly> = {}): AgentBund const conventionalTree: Readonly> = { 'src/cli/doctor.tsx': moduleSource, 'src/cli/library/audit.ts': moduleSource, - 'src/events/file/saved.tsx': moduleSource, + 'src/events/workspace/open.tsx': moduleSource, 'src/mcp/curator/apps/dashboard.tsx': `export const config = { resourceUri: 'ui://curator/dashboard.html' }; ${moduleSource}`, 'src/mcp/curator/prompts/curate.tsx': moduleSource, 'src/mcp/curator/resources/catalog.ts': moduleSource, @@ -87,11 +87,12 @@ it('compiles the conventional tree into one frozen graph with a machine-independ ]); expect(curator!.routes.map((route) => route.kind)).toEqual(['app', 'prompt', 'resource', 'tool']); expect(curator!.routes.every((route) => route.serverId === 'mcp:curator')).toBe(true); - expect(graph.events.map((route) => route.id)).toEqual(['event:file/saved']); + expect(graph.events.map((route) => route.id)).toEqual(['event:workspace/open']); expect(graph.events[0]).toMatchObject({ + event: 'workspace/open', kind: 'event-route', - provenance: { kind: 'conventional', relativePath: 'src/events/file/saved.tsx' }, - source: join(root, 'src/events/file/saved.tsx'), + provenance: { kind: 'conventional', relativePath: 'src/events/workspace/open.tsx' }, + source: join(root, 'src/events/workspace/open.tsx'), }); expect(graph.cli).toMatchObject({ mode: 'generated' }); expect(graph.cli!.routes.map((route) => route.id)).toEqual(['cli:doctor', 'cli:library/audit']); @@ -527,3 +528,42 @@ it('validates the single async route-module authoring contract statically', asyn 'AB4811', ]); }); + +it('discovers only the seven v1 event families and validates their component contract', async () => { + const root = await createRoot(); + const eventSource = 'export default async function EventRoute() { return undefined; }\n'; + await writeTree(root, { + 'src/events/agent/start.tsx': eventSource, + 'src/events/agent/stop.tsx': eventSource, + 'src/events/prompt/submit.tsx': eventSource, + 'src/events/session/start.tsx': eventSource, + 'src/events/stop.tsx': eventSource, + 'src/events/tool/after.tsx': eventSource, + 'src/events/tool/before.tsx': 'export default function BeforeTool() { return undefined; }\n', + 'src/events/workspace/open.tsx': eventSource, + }); + + const graph = await compileRouteGraph(root, fixtureConfig()); + + expect(graph.events.map((route) => route.id)).toEqual([ + 'event:agent/start', + 'event:agent/stop', + 'event:session/start', + 'event:stop', + 'event:tool/after', + 'event:tool/before', + 'event:workspace/open', + ]); + expect(graph.events.map((route) => route.event)).toEqual([ + 'agent/start', + 'agent/stop', + 'session/start', + 'stop', + 'tool/after', + 'tool/before', + 'workspace/open', + ]); + expect(graph.diagnostics.map(({ code }) => code)).toEqual(['AB4813', 'AB4810']); + expect(graph.diagnostics[0]?.sourcePath).toBe(join(root, 'src/events/prompt/submit.tsx')); + expect(graph.diagnostics[1]?.sourcePath).toBe(join(root, 'src/events/tool/before.tsx')); +}); diff --git a/packages/rsc-runtime/src/agent-document.ts b/packages/rsc-runtime/src/agent-document.ts index 54d08181f..7b778518d 100644 --- a/packages/rsc-runtime/src/agent-document.ts +++ b/packages/rsc-runtime/src/agent-document.ts @@ -22,6 +22,12 @@ export interface AgentTextNode { readonly text: string; } +/** Guidance intended for the host's immediate additional-context channel. */ +export interface AgentContextNode { + readonly kind: 'context'; + readonly text: string; +} + export interface AgentJsonNode { readonly kind: 'json'; readonly value: JsonValue; @@ -63,6 +69,7 @@ export type AgentDocumentNode = | AgentResultNode | AgentMarkdownNode | AgentTextNode + | AgentContextNode | AgentJsonNode | AgentProgressNode | AgentImageNode @@ -246,6 +253,8 @@ const snapshotNode = (node: AgentDocumentNode, depth: number, state: NodeSnapsho return Object.freeze({ kind: 'markdown', text: text(node.text, 'Agent markdown text') }); case 'text': return Object.freeze({ kind: 'text', text: text(node.text, 'Agent text') }); + case 'context': + return Object.freeze({ kind: 'context', text: text(node.text, 'Agent context text') }); case 'json': return Object.freeze({ kind: 'json', diff --git a/packages/rsc-runtime/src/decode-document.ts b/packages/rsc-runtime/src/decode-document.ts index 3fa0ebf81..363642f58 100644 --- a/packages/rsc-runtime/src/decode-document.ts +++ b/packages/rsc-runtime/src/decode-document.ts @@ -13,6 +13,7 @@ const agentElementTypes = Object.freeze([ 'agent-result', 'agent-markdown', 'agent-text', + 'agent-context', 'agent-json', 'agent-progress', 'agent-image', @@ -71,6 +72,8 @@ const decodeNode = (node: ReactNode, state: DecodeState): AgentDocumentNode => { return { kind: 'markdown', text: textChild(props.children, element.type) }; case 'agent-text': return { kind: 'text', text: textChild(props.children, element.type) }; + case 'agent-context': + return { kind: 'context', text: textChild(props.children, element.type) }; case 'agent-json': return { kind: 'json', value: props.value as JsonValue }; case 'agent-progress': diff --git a/packages/rsc-runtime/src/elements.ts b/packages/rsc-runtime/src/elements.ts index c829ebbfb..2542bf033 100644 --- a/packages/rsc-runtime/src/elements.ts +++ b/packages/rsc-runtime/src/elements.ts @@ -44,6 +44,8 @@ const AgentMarkdown = ({ children }: AgentTextProps): ReactElement => const AgentText = ({ children }: AgentTextProps): ReactElement => createElement('agent-text', null, children); +const AgentContext = ({ children }: AgentTextProps): ReactElement => createElement('agent-context', null, children); + const AgentJson = ({ value }: AgentJsonProps): ReactElement => createElement('agent-json', { value }); const AgentProgress = ({ completed, message, total }: AgentProgressProps): ReactElement => @@ -63,6 +65,7 @@ const AgentError = ({ children, code }: AgentErrorProps): ReactElement => export const Agent = Object.freeze({ Audio: AgentAudio, + Context: AgentContext, Error: AgentError, Image: AgentImage, Json: AgentJson, diff --git a/packages/rsc-runtime/src/index.ts b/packages/rsc-runtime/src/index.ts index 4a464e8fb..c03e9cb30 100644 --- a/packages/rsc-runtime/src/index.ts +++ b/packages/rsc-runtime/src/index.ts @@ -22,6 +22,7 @@ export { export type { AgentAudioNode, AgentContractErrorCode, + AgentContextNode, AgentDocument, AgentDocumentNode, AgentDocumentSnapshot, diff --git a/packages/rsc-runtime/tests/agent-document.test.ts b/packages/rsc-runtime/tests/agent-document.test.ts index 8bb30c3da..47e816b91 100644 --- a/packages/rsc-runtime/tests/agent-document.test.ts +++ b/packages/rsc-runtime/tests/agent-document.test.ts @@ -50,6 +50,7 @@ describe('Agent vocabulary', () => { it('exposes protocol-oriented leaves without DOM elements', () => { expect(Object.keys(Agent)).toEqual([ 'Audio', + 'Context', 'Error', 'Image', 'Json', @@ -60,6 +61,7 @@ describe('Agent vocabulary', () => { 'Text', ]); expect(Object.isFrozen(Agent)).toBe(true); + expect(Agent.Context({ children: 'route guidance' }).type).toBe('agent-context'); expect(Agent.Markdown({ children: '# Ready' }).type).toBe('agent-markdown'); expect(Agent.Resource({ name: 'Catalog', uri: 'catalog://root' }).type).toBe('agent-resource'); expect(Agent.Error({ children: 'represented', code: 'E_DEMO' }).type).toBe('agent-error'); diff --git a/packages/rsc-runtime/tests/dispatcher.test.ts b/packages/rsc-runtime/tests/dispatcher.test.ts index 0a21eb5e6..38efd6ecc 100644 --- a/packages/rsc-runtime/tests/dispatcher.test.ts +++ b/packages/rsc-runtime/tests/dispatcher.test.ts @@ -20,6 +20,7 @@ describe('decodeAgentDocument', () => { const document = decodeAgentDocument(createElement( 'agent-result', { metadata: { source: 'flight' }, value: { ready: true } }, + createElement('agent-context', null, 'route guidance'), createElement('agent-markdown', null, '# Ready'), createElement('agent-error', { code: 'E_REPRESENTED' }, 'Partial result'), )); @@ -27,6 +28,7 @@ describe('decodeAgentDocument', () => { expect(document).toEqual({ root: { children: [ + { kind: 'context', text: 'route guidance' }, { kind: 'markdown', text: '# Ready' }, { code: 'E_REPRESENTED', kind: 'error', message: 'Partial result' }, ],