Skip to content

Commit 8c77ce6

Browse files
fix(examples): restore AGENT_RUNTIME_HOOK_PROBE_FILE write in packaged hook route (#196)
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.
1 parent 045b04f commit 8c77ce6

4 files changed

Lines changed: 113 additions & 56 deletions

File tree

‎examples/rsc-agent-runtime/src/events/tool/after.tsx‎

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Agent } from '@agent-bundle/runtime';
44
import type { AgentEventRouteProps } from 'agent-bundle';
55
import * as React from 'react';
66

7+
import { writeEvalProbe } from '../../hook/eval-probe.js';
78
import { normalizeClaudeHook, normalizeCodexHook } from '../../hook/normalize.js';
89
import { createFileRuntimeKernel, resolveImplicitRuntimeStateFile } from '../../runtime/state-file.js';
910

@@ -19,34 +20,40 @@ export default async function AfterFileEdit({
1920
native,
2021
signal,
2122
}: AgentEventRouteProps) {
22-
const host = canonical.provenance.host;
23-
const normalized = host === 'claude'
24-
? normalizeClaudeHook(native)
25-
: host === 'codex'
26-
? normalizeCodexHook(native)
27-
: undefined;
28-
if (normalized === undefined) {
29-
throw new Error(`Unsupported event-route host ${JSON.stringify(host)}`);
30-
}
23+
try {
24+
const host = canonical.provenance.host;
25+
const normalized = host === 'claude'
26+
? normalizeClaudeHook(native)
27+
: host === 'codex'
28+
? normalizeCodexHook(native)
29+
: undefined;
30+
if (normalized === undefined) {
31+
throw new Error(`Unsupported event-route host ${JSON.stringify(host)}`);
32+
}
3133

32-
const configuredStateFile = process.env.AGENT_RUNTIME_STATE_FILE;
33-
const stateFile = configuredStateFile === undefined || configuredStateFile.trim() === ''
34-
? await resolveImplicitRuntimeStateFile(normalized.cwd)
35-
: resolve(configuredStateFile);
36-
const snapshot = await createFileRuntimeKernel({ stateFile }).recordEdit({
37-
host: normalized.host,
38-
idempotencyKey: canonical.idempotencyKey,
39-
path: normalized.path,
40-
sessionId: normalized.sessionId,
41-
toolName: normalized.toolName,
42-
}, { signal });
43-
const editNoun = snapshot.stateVersion === 1 ? 'edit' : 'edits';
34+
const configuredStateFile = process.env.AGENT_RUNTIME_STATE_FILE;
35+
const stateFile = configuredStateFile === undefined || configuredStateFile.trim() === ''
36+
? await resolveImplicitRuntimeStateFile(normalized.cwd)
37+
: resolve(configuredStateFile);
38+
const snapshot = await createFileRuntimeKernel({ stateFile }).recordEdit({
39+
host: normalized.host,
40+
idempotencyKey: canonical.idempotencyKey,
41+
path: normalized.path,
42+
sessionId: normalized.sessionId,
43+
toolName: normalized.toolName,
44+
}, { signal });
45+
const editNoun = snapshot.stateVersion === 1 ? 'edit' : 'edits';
46+
await writeEvalProbe(native, 0);
4447

45-
return (
46-
<Agent.Result>
47-
<Agent.Context>
48-
{`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`}
49-
</Agent.Context>
50-
</Agent.Result>
51-
);
48+
return (
49+
<Agent.Result>
50+
<Agent.Context>
51+
{`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`}
52+
</Agent.Context>
53+
</Agent.Result>
54+
);
55+
} catch (error) {
56+
await writeEvalProbe(native, 1).catch(() => undefined);
57+
throw error;
58+
}
5259
}

‎examples/rsc-agent-runtime/src/hook/cli.ts‎

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,13 @@
1-
import { appendFile } from 'node:fs/promises';
21
import { resolve } from 'node:path';
32

43
import { requestAgentDocument } from '../flight/request-render.js';
54
import { resolveImplicitRuntimeStateFile } from '../runtime/state-file.js';
5+
import { writeEvalProbe } from './eval-probe.js';
66
import { normalizeClaudeHook, normalizeCodexHook } from './normalize.js';
77
import { projectHookDocument } from './project-document.js';
88

99
let probeInput: Record<string, unknown> | undefined;
1010

11-
const valueType = (value: unknown): string => {
12-
if (value === null) return 'null';
13-
if (Array.isArray(value)) return 'array';
14-
return typeof value;
15-
};
16-
17-
const writeEvalProbe = async (input: Record<string, unknown>, exitStatus: number): Promise<void> => {
18-
const probeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
19-
if (probeFile === undefined || probeFile.trim() === '') return;
20-
21-
const toolInput = input.tool_input;
22-
const toolInputRecord = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)
23-
? toolInput as Record<string, unknown>
24-
: undefined;
25-
const topLevelKeys = Object.keys(input).sort();
26-
const toolInputKeys = toolInputRecord === undefined ? [] : Object.keys(toolInputRecord).sort();
27-
await appendFile(probeFile, `${JSON.stringify({
28-
commandLaunched: true,
29-
exitStatus,
30-
toolInputKeys,
31-
toolInputValueTypes: Object.fromEntries(toolInputKeys.map((key) => [key, valueType(toolInputRecord?.[key])])),
32-
toolName: typeof input.tool_name === 'string' ? input.tool_name : undefined,
33-
topLevelKeys,
34-
topLevelValueTypes: Object.fromEntries(topLevelKeys.map((key) => [key, valueType(input[key])])),
35-
})}\n`);
36-
};
37-
3811
const readInput = async (): Promise<Record<string, unknown>> => {
3912
let contents = '';
4013
process.stdin.setEncoding('utf8');
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { appendFile } from 'node:fs/promises';
2+
3+
const valueType = (value: unknown): string => {
4+
if (value === null) return 'null';
5+
if (Array.isArray(value)) return 'array';
6+
return typeof value;
7+
};
8+
9+
export const writeEvalProbe = async (input: Record<string, unknown>, exitStatus: number): Promise<void> => {
10+
const probeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
11+
if (probeFile === undefined || probeFile.trim() === '') return;
12+
13+
const toolInput = input.tool_input;
14+
const toolInputRecord = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)
15+
? toolInput as Record<string, unknown>
16+
: undefined;
17+
const topLevelKeys = Object.keys(input).sort();
18+
const toolInputKeys = toolInputRecord === undefined ? [] : Object.keys(toolInputRecord).sort();
19+
await appendFile(probeFile, `${JSON.stringify({
20+
commandLaunched: true,
21+
exitStatus,
22+
toolInputKeys,
23+
toolInputValueTypes: Object.fromEntries(toolInputKeys.map((key) => [key, valueType(toolInputRecord?.[key])])),
24+
toolName: typeof input.tool_name === 'string' ? input.tool_name : undefined,
25+
topLevelKeys,
26+
topLevelValueTypes: Object.fromEntries(topLevelKeys.map((key) => [key, valueType(input[key])])),
27+
})}\n`);
28+
};

‎examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ const fixture = resolve(import.meta.dirname, '../fixtures/events/claude-post-too
1717

1818
let workspace: string;
1919
let previousStateFile: string | undefined;
20+
let previousProbeFile: string | undefined;
2021

2122
beforeEach(async () => {
2223
workspace = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-event-route-'));
2324
previousStateFile = process.env.AGENT_RUNTIME_STATE_FILE;
25+
previousProbeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
2426
process.env.AGENT_RUNTIME_STATE_FILE = join(workspace, 'state.json');
2527
});
2628

2729
afterEach(async () => {
2830
if (previousStateFile === undefined) delete process.env.AGENT_RUNTIME_STATE_FILE;
2931
else process.env.AGENT_RUNTIME_STATE_FILE = previousStateFile;
32+
if (previousProbeFile === undefined) delete process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
33+
else process.env.AGENT_RUNTIME_HOOK_PROBE_FILE = previousProbeFile;
3034
await rm(workspace, { force: true, recursive: true });
3135
});
3236

@@ -66,3 +70,48 @@ it('renders a native Claude PostToolUse envelope into the document the host proj
6670
.toContainContext('Recorded claude-note.txt from claude. Shared state now contains 1 edit.');
6771
expect(rendered.provenance).toMatchObject({ kind: 'event-route', proofLevel: 'route-unit' });
6872
});
73+
74+
it('appends a value-free eval hook probe when AGENT_RUNTIME_HOOK_PROBE_FILE is set', async () => {
75+
const probeFile = join(workspace, 'hook-probe.jsonl');
76+
process.env.AGENT_RUNTIME_HOOK_PROBE_FILE = probeFile;
77+
const native = JSON.parse(await readFile(fixture, 'utf8')) as Record<string, unknown>;
78+
79+
await renderRoute('event:tool/after', {
80+
input: {
81+
canonical: {
82+
event: 'tool/after',
83+
idempotencyKey: 'route-unit-claude-write',
84+
observedAt: '2026-09-01T00:00:00.000Z',
85+
provenance: {
86+
host: 'claude',
87+
hostContractRevision: 'route-unit',
88+
nativeEvent: 'PostToolUse',
89+
source: 'native',
90+
},
91+
sequence: 1,
92+
},
93+
native: { ...native, cwd: workspace },
94+
},
95+
});
96+
97+
const probe = JSON.parse(await readFile(probeFile, 'utf8'));
98+
expect(probe).toEqual({
99+
commandLaunched: true,
100+
exitStatus: 0,
101+
toolInputKeys: ['file_path'],
102+
toolInputValueTypes: { file_path: 'string' },
103+
toolName: 'Write',
104+
topLevelKeys: ['cwd', 'hook_event_name', 'session_id', 'tool_input', 'tool_name', 'tool_response', 'tool_use_id', 'transcript_path'],
105+
topLevelValueTypes: {
106+
cwd: 'string',
107+
hook_event_name: 'string',
108+
session_id: 'string',
109+
tool_input: 'object',
110+
tool_name: 'string',
111+
tool_response: 'object',
112+
tool_use_id: 'string',
113+
transcript_path: 'string',
114+
},
115+
});
116+
expect(await readFile(probeFile, 'utf8')).not.toContain('claude-note.txt');
117+
});

0 commit comments

Comments
 (0)