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[] = [];