Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/refresh-scaffold-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-agent-bundle": patch
---

Update the `minimal`, `mcp-server`, and `cli-tool` templates to `@types/node` 26.5.1, update the routed templates to React 19.3.0, and move the `mcp-server` template to `@types/react` 19.3.0 while preserving the `zod` 4.5.4 runtime peer floor. (#808)
1 change: 0 additions & 1 deletion examples/hooks-and-scripts/agent-bundle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default defineConfig({
plugin: {
description: 'Hook simulation, script traces, logs, and recovery.',
name: 'hooks-and-scripts',
version: '1.0.0',
},
// verify-release ships by convention: unclaimed plain scripts under
// src/scripts/ are discovered. detect-risk stays explicitly configured
Expand Down
1 change: 1 addition & 0 deletions examples/hooks-and-scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@agent-bundle-example/hooks-and-scripts",
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
1 change: 0 additions & 1 deletion examples/mcp-app/agent-bundle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default defineConfig({
plugin: {
description: 'A unified service-readiness assistant with MCP, Skills, Hooks, scripts, and evaluation.',
name: 'mcp-app-example',
version: '1.0.0',
},
scripts: {
'check-service-fixture': './src/scripts/check-service-fixture.ts',
Expand Down
1 change: 1 addition & 0 deletions examples/mcp-app/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@agent-bundle-example/mcp-app",
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
22 changes: 10 additions & 12 deletions examples/rsc-agent-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This private, opt-in example shows one React Server Components (RSC) runtime sha
| --- | --- | --- |
| Definition | Static hook matchers, tool schemas, resource URIs, and metadata | Build/startup |
| Kernel | Framework state kernel (#98): typed events, monotonic revisions, workspace-durable `node:sqlite` storage | Cross-process |
| RSC render | Hook and MCP result component trees, lowered from Flight | One request |
| RSC render | Agent Document component trees projected from Flight | One request |
| MCP App UI | Mounted timeline, Refresh, and recoverable row selection | One UI instance |

Native hooks are fresh requests: the compiler-generated client validates one host event, invokes `src/events/tool/after.tsx` in its explicit standalone mode, projects the final Agent Document, and exits. The durable kernel—not a Node module cache or React state—connects later hook processes and MCP calls.
Expand All @@ -32,14 +32,14 @@ export default async function AfterFileEdit({ canonical }: AgentEventRouteProps<
```

```tsx
// An MCP JSX route describes protocol blocks, not browser HTML.
import { Mcp } from '@agent-bundle/runtime';
// An Agent Document route describes protocol-neutral output, not browser HTML.
import { Agent } from '@agent-bundle/runtime';

export function RenderTimeline({ snapshot }: { snapshot: { edits: unknown[]; stateVersion: number } }) {
export function RenderTimeline({ snapshot }: { snapshot: { edits: Array<Record<string, string>>; stateVersion: number } }) {
return (
<Mcp.Result structuredContent={snapshot}>
<Mcp.Text>{`Showing ${snapshot.edits.length} edits.`}</Mcp.Text>
</Mcp.Result>
<Agent.Result value={snapshot}>
<Agent.Text>{`Showing ${snapshot.edits.length} edits.`}</Agent.Text>
</Agent.Result>
);
}
```
Expand Down Expand Up @@ -150,12 +150,10 @@ AGENT_RUNTIME_STATE_FILE=/tmp/rsc-agent-state.sqlite PORT=3000 \
| Static surface | Result |
| --- | --- |
| `recent_edits` | Text plus `structuredContent` snapshot |
| `render_edit_timeline` | RSC-lowered text and snapshot; links the versioned timeline resource |
| `runtime_status` | RSC-lowered text, image, and structured status |
| `render_edit_timeline` | Agent Document text and snapshot; links the versioned timeline resource |
| `runtime_status` | Agent Document text, image, and structured status |
| `ui://rsc-agent-runtime/edit-timeline-v1.html` | `text/html;profile=mcp-app` self-contained React timeline |

`Mcp.Text`, `Mcp.Image`, `Mcp.Audio`, `Mcp.ResourceLink`, and `Mcp.EmbeddedResource` lower to their matching MCP content blocks. `Mcp.Result` supplies ordered `content`, optional `structuredContent`, optional `isError`, and optional serializable `_meta`.

## Native packages and evaluation

Claude Code has a native compact manifest, `${CLAUDE_PLUGIN_ROOT}` paths, and a
Expand Down Expand Up @@ -185,7 +183,7 @@ through the compiled `src/events/tool/after.tsx` route (Agent Document projectio
state-kernel commit), replays the same native tool id from a second hook
process to prove cross-process idempotency, then connects a real stdio MCP
client to the built server over the same state file and asserts the
RSC-lowered `render_edit_timeline` result, the shared edit snapshot, and the
Agent Document `render_edit_timeline` result, the shared edit snapshot, and the
linked MCP App resource. It contacts no real host and needs no credentials.

To exercise the Claude package manually when an external host run is separately
Expand Down
10 changes: 6 additions & 4 deletions examples/rsc-agent-runtime/src/dev/invocation-worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { requestAgentDocumentWithFlight, requestFlightRenderWithFlight } from '../flight/request-render.js';
import { writeSync } from 'node:fs';
import { lowerMcpResult } from '@agent-bundle/runtime';

import { documentToCallToolResult } from '@agent-bundle/runtime';

import { requestAgentDocumentWithFlight } from '../flight/request-render.js';
import type {
DevRuntimeInspectionRequest,
DevRuntimeInspectionResponse,
Expand Down Expand Up @@ -188,11 +190,11 @@ const invoke = async (signal?: AbortSignal): Promise<InvocationOutput> => {
});
}

const rendered = await requestFlightRenderWithFlight(renderRequest, {
const rendered = await requestAgentDocumentWithFlight(renderRequest, {
maximumFlightBytes: maximumInvocationFlightBytes,
signal,
});
const protocol = lowerMcpResult(rendered.node);
const protocol = documentToCallToolResult(rendered.document);
return Object.freeze({
flight: Buffer.from(rendered.flight),
response: Object.freeze({
Expand Down
22 changes: 12 additions & 10 deletions examples/rsc-agent-runtime/src/flight/request-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@ import { createAgentRenderDispatcher, type AgentDocument, type AgentRenderInvoca
import type { RenderRequest } from '../runtime/contracts.js';
import { redactInspectionDiagnostics } from '../dev/inspection-security.js';

export const maximumFlightRenderBytes = 4 * 1024 * 1024;
export const maximumFlightRenderStderrBytes = 256 * 1024;
export const maximumFlightRenderMetadataBytes = 128;
const maximumFlightRenderBytes = 4 * 1024 * 1024;
const maximumFlightRenderStderrBytes = 256 * 1024;
const maximumFlightRenderMetadataBytes = 128;

const terminationGraceMs = 100;

export interface FlightRenderResult {
interface FlightRenderResult {
readonly flight: Uint8Array;
readonly node: ReactNode;
/** Exact durable state identity captured by the render worker; never user-visible. */
readonly stateVersion: number;
}

export interface AgentDocumentFlightRenderResult extends FlightRenderResult {
interface AgentDocumentFlightRenderResult extends FlightRenderResult {
readonly document: AgentDocument;
}

export interface FlightRenderOptions {
interface FlightRenderOptions {
readonly maximumFlightBytes?: number;
readonly signal?: AbortSignal;
}
Expand Down Expand Up @@ -71,7 +71,7 @@ const parseSnapshotMetadata = (metadata: Buffer): number => {
return stateVersion;
};

export const requestFlightRenderWithFlight = async (
const requestFlightRenderWithFlight = async (
request: RenderRequest,
options: FlightRenderOptions = {},
): Promise<FlightRenderResult> => {
Expand Down Expand Up @@ -188,9 +188,6 @@ export const requestFlightRenderWithFlight = async (
});
};

export const requestFlightRender = async (request: RenderRequest): Promise<ReactNode> =>
(await requestFlightRenderWithFlight(request)).node;

const renderInvocationFor = (request: RenderRequest): AgentRenderInvocation => {
switch (request.type) {
case 'hook/after-file-edit':
Expand Down Expand Up @@ -239,6 +236,11 @@ export const requestAgentDocumentWithFlight = async (
rendered = await requestFlightRenderWithFlight(request, { ...options, signal: dispatch.signal });
return Readable.toWeb(Readable.from([rendered.flight])) as ReadableStream<Uint8Array>;
},
}, {
limits: {
maxDocumentBytes: maximumFlightRenderBytes,
maxEventBytes: maximumFlightRenderBytes + 1_024,
},
});
const document = await dispatcher.dispatch({ invocation: renderInvocationFor(request), signal });
if (rendered === undefined) throw new Error('Flight execution host returned no render result');
Expand Down
11 changes: 9 additions & 2 deletions examples/rsc-agent-runtime/src/hook/project-document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { AgentDocument, NativePostToolUseOutput } from '@agent-bundle/runtime';
import type { AgentDocument } from '@agent-bundle/runtime';

export const projectHookDocument = (document: AgentDocument): NativePostToolUseOutput => {
interface ProjectedPostToolUseOutput {
readonly hookSpecificOutput: {
readonly additionalContext: string;
readonly hookEventName: 'PostToolUse';
};
}

export const projectHookDocument = (document: AgentDocument): ProjectedPostToolUseOutput => {
if (document.status === 'failed' || document.root.kind !== 'result') {
throw new Error('Hook render requires a successful Agent.Result document');
}
Expand Down
13 changes: 7 additions & 6 deletions examples/rsc-agent-runtime/src/mcp/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createFileRuntimeKernel } from '../runtime/state-file.js';
import { lowerMcpResult } from '@agent-bundle/runtime';
import { requestFlightRender } from '../flight/request-render.js';
import { documentToCallToolResult } from '@agent-bundle/runtime';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';

import { createFileRuntimeKernel } from '../runtime/state-file.js';
import { requestAgentDocument } from '../flight/request-render.js';

import { resolveStateFile, type McpRequestExtra, type ResolveStateOptions } from './resolve-state.js';

type ToolInput = { limit?: number };
Expand All @@ -22,12 +23,12 @@ export const createMcpHandlers = (options: ResolveStateOptions): Record<string,
render_edit_timeline: async (input, extra) => {
const stateFile = await resolveStateFile(options, extra);
const snapshot = await createFileRuntimeKernel({ stateFile }).readSnapshot({ limit: input.limit });
return lowerMcpResult(
await requestFlightRender({ snapshot, stateFile, type: 'mcp/render-timeline' }),
return documentToCallToolResult(
await requestAgentDocument({ snapshot, stateFile, type: 'mcp/render-timeline' }),
);
},
runtime_status: async (_input, extra) => {
const stateFile = await resolveStateFile(options, extra);
return lowerMcpResult(await requestFlightRender({ stateFile, type: 'mcp/runtime-status' }));
return documentToCallToolResult(await requestAgentDocument({ stateFile, type: 'mcp/runtime-status' }));
},
});
16 changes: 8 additions & 8 deletions examples/rsc-agent-runtime/src/rsc/components.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { basename } from 'node:path';

import { Agent, Mcp, agent } from '@agent-bundle/runtime';
import { Agent, agent } from '@agent-bundle/runtime';
import type { CanonicalPostToolUse, RuntimeSnapshot } from '../runtime/contracts.js';

const hookServices = async (): Promise<{ edit: CanonicalPostToolUse; snapshot: RuntimeSnapshot }> => {
Expand Down Expand Up @@ -31,9 +31,9 @@ export const AfterFileEdit = async () => {
};

export const RenderEditTimeline = ({ snapshot }: { snapshot: RuntimeSnapshot }) => (
<Mcp.Result structuredContent={{ edits: snapshot.edits, stateVersion: snapshot.stateVersion }}>
<Mcp.Text>{`Showing ${snapshot.edits.length} recorded edits.`}</Mcp.Text>
</Mcp.Result>
<Agent.Result value={{ edits: snapshot.edits.map((edit) => ({ ...edit })), stateVersion: snapshot.stateVersion }}>
<Agent.Text>{`Showing ${snapshot.edits.length} recorded edits.`}</Agent.Text>
</Agent.Result>
);

const STATUS_PNG_BASE64 =
Expand All @@ -44,9 +44,9 @@ export const RuntimeStatus = ({ snapshot }: { snapshot: RuntimeSnapshot }) => {
const editNoun = editCount === 1 ? 'edit' : 'edits';

return (
<Mcp.Result structuredContent={{ editCount, stateVersion: snapshot.stateVersion }}>
<Mcp.Text>{`Runtime state contains ${editCount} ${editNoun}.`}</Mcp.Text>
<Mcp.Image data={STATUS_PNG_BASE64} mimeType="image/png" />
</Mcp.Result>
<Agent.Result value={{ editCount, stateVersion: snapshot.stateVersion }}>
<Agent.Text>{`Runtime state contains ${editCount} ${editNoun}.`}</Agent.Text>
<Agent.Image data={STATUS_PNG_BASE64} mimeType="image/png" />
</Agent.Result>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,12 @@ const event = (eventId: string) => ({
toolName: 'Write',
});

const oversizedMcpWorker = (payloadBytes: number): string => {
const oversizedAgentDocumentWorker = (payloadBytes: number): string => {
return `const { writeSync } = require('node:fs');
const payload = 'x'.repeat(${payloadBytes});
const model = ['$', 'mcp-result', null, {
_meta: '$undefined',
isError: '$undefined',
structuredContent: { payload, stateVersion: 0 },
children: [['$', 'mcp-text', null, { children: 'ok' }]],
const model = ['$', 'agent-result', null, {
value: { payload, stateVersion: 0 },
children: [['$', 'agent-text', null, { children: 'ok' }]],
}];
writeSync(3, Buffer.from('{"stateVersion":0}'));
process.stdout.end(\`0:\${JSON.stringify(model)}\\n\`);
Expand Down Expand Up @@ -553,7 +551,7 @@ test('caps inspection stdout independently after Flight leaves its response enve
const compilerRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-invoke-'));
try {
const entry = await buildInvocationEntry(compilerRoot);
await writeFile(join(compilerRoot, 'rsc', 'rsc', 'index.js'), oversizedMcpWorker(2_100_000));
await writeFile(join(compilerRoot, 'rsc', 'rsc', 'index.js'), oversizedAgentDocumentWorker(2_100_000));
const result = await invoke(entry, {
stateFile: join(compilerRoot, 'events.jsonl'),
stateStoreId: 'fixture-state',
Expand Down
Loading
Loading