diff --git a/examples/rsc-agent-runtime/README.md b/examples/rsc-agent-runtime/README.md index 595f53e25..252440f82 100644 --- a/examples/rsc-agent-runtime/README.md +++ b/examples/rsc-agent-runtime/README.md @@ -11,22 +11,19 @@ This private, opt-in example shows one React Server Components (RSC) runtime sha | RSC render | Hook and MCP result component trees, lowered from Flight | One request | | MCP App UI | Mounted timeline, Refresh, and recoverable row selection | One UI instance | -Native hooks are fresh requests: a process normalizes one host event, invokes the RSC worker through the runtime dispatcher seam, 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. +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. ```tsx -// An Agent Document route reads request-scoped context. -import { Agent, agent } from '@agent-bundle/runtime'; -import type { CanonicalPostToolUse, RuntimeSnapshot } from '../runtime/contracts.js'; - -export async function AfterFileEdit() { - const context = await agent(); - const edit = context.services.edit as CanonicalPostToolUse; - const snapshot = context.services.snapshot as RuntimeSnapshot; +// The semantic event route receives canonical identity plus the complete native payload. +import { Agent } from '@agent-bundle/runtime'; +import type { AgentEventRouteProps } from 'agent-bundle'; + +export const config = { runtime: 'standalone', targets: ['claude', 'codex'] }; + +export default async function AfterFileEdit({ canonical, native }: AgentEventRouteProps) { return ( - - {`Recorded ${edit.path}; ${snapshot.edits.length} edits exist.`} - + {`Recorded ${String(native.tool_name)} from ${canonical.provenance.host}.`} ); } @@ -97,8 +94,8 @@ To exercise one hook manually, give it an explicit state file and native Claude- ```bash AGENT_RUNTIME_STATE_FILE=/tmp/rsc-agent-state.sqlite \ - node examples/rsc-agent-runtime/dist/runtime/hook/index.js --host claude < + + {`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`} + + + ); +} diff --git a/examples/rsc-agent-runtime/tests/fixtures/events/claude-post-tool-use.json b/examples/rsc-agent-runtime/tests/fixtures/events/claude-post-tool-use.json new file mode 100644 index 000000000..3b2f4602b --- /dev/null +++ b/examples/rsc-agent-runtime/tests/fixtures/events/claude-post-tool-use.json @@ -0,0 +1,14 @@ +{ + "cwd": "/tmp/agent-bundle-schema-conformance", + "hook_event_name": "PostToolUse", + "session_id": "schema-conformance-claude", + "tool_input": { + "file_path": "claude-note.txt" + }, + "tool_name": "Write", + "tool_response": { + "success": true + }, + "tool_use_id": "schema-conformance-claude-write", + "transcript_path": "/tmp/agent-bundle-schema-conformance/claude-transcript.jsonl" +} diff --git a/examples/rsc-agent-runtime/tests/fixtures/events/codex-post-tool-use.json b/examples/rsc-agent-runtime/tests/fixtures/events/codex-post-tool-use.json new file mode 100644 index 000000000..cf30f87ef --- /dev/null +++ b/examples/rsc-agent-runtime/tests/fixtures/events/codex-post-tool-use.json @@ -0,0 +1,15 @@ +{ + "cwd": "/tmp/agent-bundle-schema-conformance", + "event_id": "schema-conformance-codex-event", + "hook_event_name": "PostToolUse", + "session_id": "schema-conformance-codex", + "tool_input": { + "command": "*** Begin Patch\n*** Add File: codex-note.txt\n+recorded\n*** End Patch" + }, + "tool_name": "apply_patch", + "tool_response": { + "success": true + }, + "tool_use_id": "schema-conformance-codex-patch", + "transcript_path": "/tmp/agent-bundle-schema-conformance/codex-transcript.jsonl" +} diff --git a/examples/rsc-agent-runtime/tests/host-artifacts.test.ts b/examples/rsc-agent-runtime/tests/host-artifacts.test.ts index 2ae83a387..6157df314 100644 --- a/examples/rsc-agent-runtime/tests/host-artifacts.test.ts +++ b/examples/rsc-agent-runtime/tests/host-artifacts.test.ts @@ -144,10 +144,10 @@ test('materializes self-contained Claude and Codex native plugin artifacts', asy expect(JSON.stringify(codexMcp)).not.toMatch(/\$\{|workspace/i); expect(claudeHooks.hooks.PostToolUse[0]).toMatchObject({ matcher: '^(?:Write|Edit)$' }); expect(claudeHooks.hooks.PostToolUse[0].hooks[0].command).toContain('${CLAUDE_PLUGIN_ROOT}'); - expect(claudeHooks.hooks.PostToolUse[0].hooks[0].command).toContain('--host claude'); + expect(claudeHooks.hooks.PostToolUse[0].hooks[0].command).toContain('hooks/event-route-tool-after.mjs'); expect(codexHooks.hooks.PostToolUse[0]).toMatchObject({ matcher: '^(?:apply_patch|Edit|Write)$' }); expect(codexHooks.hooks.PostToolUse[0].hooks[0].command).toContain('${PLUGIN_ROOT}'); - expect(codexHooks.hooks.PostToolUse[0].hooks[0].command).toContain('--host codex'); + expect(codexHooks.hooks.PostToolUse[0].hooks[0].command).toContain('hooks/event-route-tool-after.mjs'); expect(JSON.stringify({ claudeMcp, claudeHooks, codexMcp, codexHooks })).not.toMatch(/api[ _-]?key/i); const runtimeRoot = join(exampleRoot, 'dist/runtime'); @@ -243,7 +243,7 @@ test('runs the packaged MCP server after its artifact is isolated from the examp } }); -test('runs each packaged native hook from one shell argv path when its plugin root contains spaces and metacharacters', async () => { +test('replays schema-conformance fixtures through each packaged native event route', async () => { await runPackageHosts(); const temporaryRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-hook-root-')); try { @@ -255,33 +255,19 @@ test('runs each packaged native hook from one shell argv path when its plugin ro for (const host of ['claude', 'codex'] as const) { const pluginRoot = join(temporaryRoot, `${host} plugin root ; ordinary`); - const workspace = join(temporaryRoot, `${host}-workspace`); const stateFile = join(temporaryRoot, `${host}-state.sqlite`); const manifestPath = join(pluginRoot, 'hooks/hooks.json'); const rootVariable = host === 'claude' ? 'CLAUDE_PLUGIN_ROOT' : 'PLUGIN_ROOT'; const filename = `${host}-note.txt`; await cp(join(pluginsRoot, host), pluginRoot, { recursive: true }); - await mkdir(workspace); const manifest = await readJson<{ hooks: { PostToolUse: Array<{ hooks: Array<{ command: string }> }> } }>(manifestPath); const command = manifest.hooks.PostToolUse[0]?.hooks[0]?.command; expect(command).toBeTypeOf('string'); - const input = host === 'claude' - ? { - cwd: workspace, - hook_event_name: 'PostToolUse', - session_id: `${host}-session`, - tool_input: { file_path: join(workspace, filename) }, - tool_name: 'Write', - tool_use_id: `${host}-tool`, - } - : { - cwd: workspace, - event_id: `${host}-event`, - hook_event_name: 'PostToolUse', - session_id: `${host}-session`, - tool_input: { command: `*** Begin Patch\n*** Add File: ${filename}\n+recorded\n*** End Patch` }, - tool_name: 'apply_patch', - }; + // These checked-in payloads establish schema conformance only; replay + // through a local command is not evidence of commercial-host dispatch. + const input = await readJson>( + join(exampleRoot, `tests/fixtures/events/${host}-post-tool-use.json`), + ); const result = await runDeclaredHook(command!, { [rootVariable]: pluginRoot, AGENT_RUNTIME_HOOK_ARGV_FILE: argvFile, @@ -299,11 +285,11 @@ test('runs each packaged native hook from one shell argv path when its plugin ro }, }); expect((await readFile(argvFile)).toString('utf8').split('\0').filter(Boolean)).toEqual([ - join(pluginRoot, 'runtime/hook/index.js'), '--host', host, + join(pluginRoot, 'hooks/event-route-tool-after.mjs'), ]); const recorded = await createFileRuntimeKernel({ stateFile }).readSnapshot(); expect(recorded.edits.map((edit) => edit.host)).toEqual([host]); - expect(command).toBe(`node "\${${rootVariable}}/runtime/hook/index.js" --host ${host}`); + expect(command).toBe(`node "\${${rootVariable}}/hooks/event-route-tool-after.mjs"`); expect(command).not.toMatch(/(?:api[ _-]?key|echo|printenv|AGENT_RUNTIME_)/iu); } } finally { diff --git a/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts b/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts index a074fcebf..6449c7b83 100644 --- a/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts +++ b/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts @@ -14,8 +14,8 @@ import { ensureExampleBuilt } from './support/ensure-built.js'; // This is the ordinary-CI micro-eval spot-check (`npm run eval:spot`): one // deterministic pass over the built production artifacts, with no real Claude // or Codex host. It proves the end-to-end runtime path in a small way: a -// native-shaped hook event renders through the RSC worker into the framework -// state kernel's workspace-durable sqlite store (#98), a second hook process +// native-shaped hook event renders through the compiled semantic event route +// into the framework state kernel's workspace-durable sqlite store (#98), a second hook process // replaying the same native tool id commits nothing new, and the MCP server // then RSC-lowers that same shared state for a tool call while linking the // MCP App resource. @@ -32,7 +32,7 @@ test('micro-eval spot-check: built hook and MCP server share one RSC-rendered ru }); const runHookOnce = async (): Promise => { - const hook = spawn(process.execPath, [join(process.cwd(), 'dist/runtime/hook/index.js'), '--host', 'claude'], { + const hook = spawn(process.execPath, [join(process.cwd(), 'dist/plugins/claude/hooks/event-route-tool-after.mjs')], { env: { ...process.env, AGENT_RUNTIME_STATE_FILE: stateFile }, stdio: ['pipe', 'pipe', 'pipe'], }); @@ -44,6 +44,7 @@ test('micro-eval spot-check: built hook and MCP server share one RSC-rendered ru tool_name: 'Write', tool_response: { success: true }, tool_use_id: 'micro-eval-tool-1', + transcript_path: join(workspace, 'transcript.jsonl'), })); const hookStdout: Buffer[] = []; const hookStderr: Buffer[] = [];