From 08f83be7099b59cf4737a6a9bea5d126854849ae Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 20:34:40 +0000 Subject: [PATCH] fix(examples): restore AGENT_RUNTIME_HOOK_PROBE_FILE write in packaged hook route PR #182 migrated PostToolUse to the semantic event route but dropped the eval:hosts probe append that the prebuilt hook CLI still performed. Share writeEvalProbe between both paths and cover the route-unit packaged route. --- .../src/events/tool/after.tsx | 63 ++++++++++--------- examples/rsc-agent-runtime/src/hook/cli.ts | 29 +-------- .../rsc-agent-runtime/src/hook/eval-probe.ts | 28 +++++++++ .../tests/route-unit/event-route.test.ts | 49 +++++++++++++++ 4 files changed, 113 insertions(+), 56 deletions(-) create mode 100644 examples/rsc-agent-runtime/src/hook/eval-probe.ts diff --git a/examples/rsc-agent-runtime/src/events/tool/after.tsx b/examples/rsc-agent-runtime/src/events/tool/after.tsx index edfb8cbac..74e5e349b 100644 --- a/examples/rsc-agent-runtime/src/events/tool/after.tsx +++ b/examples/rsc-agent-runtime/src/events/tool/after.tsx @@ -4,6 +4,7 @@ import { Agent } from '@agent-bundle/runtime'; import type { AgentEventRouteProps } from 'agent-bundle'; import * as React from 'react'; +import { writeEvalProbe } from '../../hook/eval-probe.js'; import { normalizeClaudeHook, normalizeCodexHook } from '../../hook/normalize.js'; import { createFileRuntimeKernel, resolveImplicitRuntimeStateFile } from '../../runtime/state-file.js'; @@ -19,34 +20,40 @@ export default async function AfterFileEdit({ native, signal, }: AgentEventRouteProps) { - const host = canonical.provenance.host; - const normalized = host === 'claude' - ? normalizeClaudeHook(native) - : host === 'codex' - ? normalizeCodexHook(native) - : undefined; - if (normalized === undefined) { - throw new Error(`Unsupported event-route host ${JSON.stringify(host)}`); - } + try { + const host = canonical.provenance.host; + const normalized = host === 'claude' + ? normalizeClaudeHook(native) + : host === 'codex' + ? normalizeCodexHook(native) + : undefined; + if (normalized === undefined) { + throw new Error(`Unsupported event-route host ${JSON.stringify(host)}`); + } - const configuredStateFile = process.env.AGENT_RUNTIME_STATE_FILE; - const stateFile = configuredStateFile === undefined || configuredStateFile.trim() === '' - ? await resolveImplicitRuntimeStateFile(normalized.cwd) - : resolve(configuredStateFile); - const snapshot = await createFileRuntimeKernel({ stateFile }).recordEdit({ - host: normalized.host, - idempotencyKey: canonical.idempotencyKey, - path: normalized.path, - sessionId: normalized.sessionId, - toolName: normalized.toolName, - }, { signal }); - const editNoun = snapshot.stateVersion === 1 ? 'edit' : 'edits'; + const configuredStateFile = process.env.AGENT_RUNTIME_STATE_FILE; + const stateFile = configuredStateFile === undefined || configuredStateFile.trim() === '' + ? await resolveImplicitRuntimeStateFile(normalized.cwd) + : resolve(configuredStateFile); + const snapshot = await createFileRuntimeKernel({ stateFile }).recordEdit({ + host: normalized.host, + idempotencyKey: canonical.idempotencyKey, + path: normalized.path, + sessionId: normalized.sessionId, + toolName: normalized.toolName, + }, { signal }); + const editNoun = snapshot.stateVersion === 1 ? 'edit' : 'edits'; + await writeEvalProbe(native, 0); - return ( - - - {`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`} - - - ); + return ( + + + {`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`} + + + ); + } catch (error) { + await writeEvalProbe(native, 1).catch(() => undefined); + throw error; + } } diff --git a/examples/rsc-agent-runtime/src/hook/cli.ts b/examples/rsc-agent-runtime/src/hook/cli.ts index c7e93dcb6..a0681b95a 100644 --- a/examples/rsc-agent-runtime/src/hook/cli.ts +++ b/examples/rsc-agent-runtime/src/hook/cli.ts @@ -1,40 +1,13 @@ -import { appendFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { requestAgentDocument } from '../flight/request-render.js'; import { resolveImplicitRuntimeStateFile } from '../runtime/state-file.js'; +import { writeEvalProbe } from './eval-probe.js'; import { normalizeClaudeHook, normalizeCodexHook } from './normalize.js'; import { projectHookDocument } from './project-document.js'; let probeInput: Record | undefined; -const valueType = (value: unknown): string => { - if (value === null) return 'null'; - if (Array.isArray(value)) return 'array'; - return typeof value; -}; - -const writeEvalProbe = async (input: Record, exitStatus: number): Promise => { - const probeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE; - if (probeFile === undefined || probeFile.trim() === '') return; - - const toolInput = input.tool_input; - const toolInputRecord = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput) - ? toolInput as Record - : undefined; - const topLevelKeys = Object.keys(input).sort(); - const toolInputKeys = toolInputRecord === undefined ? [] : Object.keys(toolInputRecord).sort(); - await appendFile(probeFile, `${JSON.stringify({ - commandLaunched: true, - exitStatus, - toolInputKeys, - toolInputValueTypes: Object.fromEntries(toolInputKeys.map((key) => [key, valueType(toolInputRecord?.[key])])), - toolName: typeof input.tool_name === 'string' ? input.tool_name : undefined, - topLevelKeys, - topLevelValueTypes: Object.fromEntries(topLevelKeys.map((key) => [key, valueType(input[key])])), - })}\n`); -}; - const readInput = async (): Promise> => { let contents = ''; process.stdin.setEncoding('utf8'); diff --git a/examples/rsc-agent-runtime/src/hook/eval-probe.ts b/examples/rsc-agent-runtime/src/hook/eval-probe.ts new file mode 100644 index 000000000..45fc1ec7d --- /dev/null +++ b/examples/rsc-agent-runtime/src/hook/eval-probe.ts @@ -0,0 +1,28 @@ +import { appendFile } from 'node:fs/promises'; + +const valueType = (value: unknown): string => { + if (value === null) return 'null'; + if (Array.isArray(value)) return 'array'; + return typeof value; +}; + +export const writeEvalProbe = async (input: Record, exitStatus: number): Promise => { + const probeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE; + if (probeFile === undefined || probeFile.trim() === '') return; + + const toolInput = input.tool_input; + const toolInputRecord = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput) + ? toolInput as Record + : undefined; + const topLevelKeys = Object.keys(input).sort(); + const toolInputKeys = toolInputRecord === undefined ? [] : Object.keys(toolInputRecord).sort(); + await appendFile(probeFile, `${JSON.stringify({ + commandLaunched: true, + exitStatus, + toolInputKeys, + toolInputValueTypes: Object.fromEntries(toolInputKeys.map((key) => [key, valueType(toolInputRecord?.[key])])), + toolName: typeof input.tool_name === 'string' ? input.tool_name : undefined, + topLevelKeys, + topLevelValueTypes: Object.fromEntries(topLevelKeys.map((key) => [key, valueType(input[key])])), + })}\n`); +}; diff --git a/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts b/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts index ffa5040bc..021a3d5e0 100644 --- a/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts +++ b/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts @@ -17,16 +17,20 @@ const fixture = resolve(import.meta.dirname, '../fixtures/events/claude-post-too let workspace: string; let previousStateFile: string | undefined; +let previousProbeFile: string | undefined; beforeEach(async () => { workspace = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-event-route-')); previousStateFile = process.env.AGENT_RUNTIME_STATE_FILE; + previousProbeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE; process.env.AGENT_RUNTIME_STATE_FILE = join(workspace, 'state.json'); }); afterEach(async () => { if (previousStateFile === undefined) delete process.env.AGENT_RUNTIME_STATE_FILE; else process.env.AGENT_RUNTIME_STATE_FILE = previousStateFile; + if (previousProbeFile === undefined) delete process.env.AGENT_RUNTIME_HOOK_PROBE_FILE; + else process.env.AGENT_RUNTIME_HOOK_PROBE_FILE = previousProbeFile; await rm(workspace, { force: true, recursive: true }); }); @@ -66,3 +70,48 @@ it('renders a native Claude PostToolUse envelope into the document the host proj .toContainContext('Recorded claude-note.txt from claude. Shared state now contains 1 edit.'); expect(rendered.provenance).toMatchObject({ kind: 'event-route', proofLevel: 'route-unit' }); }); + +it('appends a value-free eval hook probe when AGENT_RUNTIME_HOOK_PROBE_FILE is set', async () => { + const probeFile = join(workspace, 'hook-probe.jsonl'); + process.env.AGENT_RUNTIME_HOOK_PROBE_FILE = probeFile; + const native = JSON.parse(await readFile(fixture, 'utf8')) as Record; + + await renderRoute('event:tool/after', { + input: { + canonical: { + event: 'tool/after', + idempotencyKey: 'route-unit-claude-write', + observedAt: '2026-09-01T00:00:00.000Z', + provenance: { + host: 'claude', + hostContractRevision: 'route-unit', + nativeEvent: 'PostToolUse', + source: 'native', + }, + sequence: 1, + }, + native: { ...native, cwd: workspace }, + }, + }); + + const probe = JSON.parse(await readFile(probeFile, 'utf8')); + expect(probe).toEqual({ + commandLaunched: true, + exitStatus: 0, + toolInputKeys: ['file_path'], + toolInputValueTypes: { file_path: 'string' }, + toolName: 'Write', + topLevelKeys: ['cwd', 'hook_event_name', 'session_id', 'tool_input', 'tool_name', 'tool_response', 'tool_use_id', 'transcript_path'], + topLevelValueTypes: { + cwd: 'string', + hook_event_name: 'string', + session_id: 'string', + tool_input: 'object', + tool_name: 'string', + tool_response: 'object', + tool_use_id: 'string', + transcript_path: 'string', + }, + }); + expect(await readFile(probeFile, 'utf8')).not.toContain('claude-note.txt'); +});