Skip to content

Commit 2e59d6e

Browse files
chore(examples): use built-in framework surface — hooks-and-scripts, host-test, mcp-app, skills-starter (#471)
* chore(examples/host-test): record request.lineage through the public context member request.lineage is a first-class Observed<AgentLineage> on AgentRequestContext since #444, so the probe reads it directly instead of casting for an optional member and records it on every line (available or unavailable with reason). renderLineage types the serialized value against the public AgentLineage and Observed types. The route-unit suite asserts a mounted lineage is recorded and rendered. * chore(examples/skills-starter): type outcome graders against agent-bundle/eval Both graders declare themselves as the public EvalGraderFunction instead of restating the grader context and outcome shapes by hand. The README names the build output directory the CLI actually writes (artifact/, not dist/). * chore(examples/mcp-app): type the outcome grader against agent-bundle/eval The status grader declares itself as the public EvalGraderFunction instead of restating the grader context and outcome shapes. The README's mcp run example reuses the build output the CLI actually writes (--artifact artifact).
1 parent 833e48f commit 2e59d6e

9 files changed

Lines changed: 111 additions & 49 deletions

File tree

‎examples/host-test/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
A probing plugin. Install it into a Claude Code, Codex, or Cursor home, drive
44
one agent session, and read back exactly what that host sent to every plugin
55
hook and MCP call — the raw envelope, the framework request context each
6-
handler saw, and (once the framework resolves it) the conversation lineage.
6+
handler saw, and the conversation lineage the runtime resolved for it.
77
It is the acceptance vehicle for `request.lineage` and the evidence source for
88
`docs/audits/*-host-lineage-matrix.md`.
99

@@ -25,7 +25,7 @@ bounded summary into the durable state kernel (`src/state.ts`,
2525
| --- | --- |
2626
| `event.native` | The complete host payload, byte for byte, with secret-looking values replaced by `[redacted]`. |
2727
| `event.canonical` | The framework's canonical identity (`event`, `idempotencyKey`, `observedAt`, `provenance`). |
28-
| `request` | `(await agent())` as the route saw it: `invocation`, `host`, `session`, `actor`, `workspace`, `capabilities`, provider keys, whether state and notices were mounted, and `lineage` when the runtime supplies it. |
28+
| `request` | `(await agent())` as the route saw it: `invocation`, `host`, `session`, `actor`, `workspace`, `capabilities`, `lineage`, provider keys, and whether state and notices were mounted. `lineage` is always present: `available` with the resolved tree position, or `unavailable` with the runtime's per-host reason. |
2929
| `ids` | Every identity-shaped native field (`conversation_id`, `generation_id`, `session_id`, `subagent_id`, `tool_call_id`, `agent_id`, `turn_id`, `user_email`, …) lifted out for filtering. |
3030
| `process` | `pid`, `ppid`, `cwd`, `execPath`, entry file, uptime — of the process that ran the route. |
3131
| `runtime` | `shared-runtime` when the hook reached the warm MCP-hosted runtime, `standalone-hook` when it fell back to the hook process, `mcp-server`, or `cli`. |

‎examples/host-test/src/capture.ts‎

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -135,25 +135,27 @@ const detectRuntime = (context: AgentRequestContext, argv: readonly string[]): C
135135
}
136136
};
137137

138-
/** The framework request context as the route observed it, minus non-data members. */
139-
export const snapshotRequest = (context: AgentRequestContext): JsonObject => {
140-
const lineage = (context as AgentRequestContext & { readonly lineage?: unknown }).lineage;
141-
return {
142-
actor: asJson(context.actor),
143-
capabilities: asJson(context.capabilities),
144-
hasNotices: context.notices !== undefined,
145-
hasState: context.state !== undefined,
146-
host: asJson(context.host),
147-
invocation: asJson(context.invocation),
148-
...(lineage === undefined ? {} : { lineage: asJson(lineage) }),
149-
providers: {
150-
keys: Object.keys(context.providers).sort((left, right) => left.localeCompare(right)),
151-
processLifetime: asJson(context.providers.processLifetime),
152-
},
153-
session: asJson(context.session),
154-
workspace: asJson(context.workspace),
155-
};
156-
};
138+
/**
139+
* The framework request context as the route observed it, minus non-data
140+
* members. `lineage` is a first-class `Observed` member of the context, so it
141+
* is recorded on every line: `available` with the resolved tree position, or
142+
* `unavailable` with the runtime's per-host reason.
143+
*/
144+
export const snapshotRequest = (context: AgentRequestContext): JsonObject => ({
145+
actor: asJson(context.actor),
146+
capabilities: asJson(context.capabilities),
147+
hasNotices: context.notices !== undefined,
148+
hasState: context.state !== undefined,
149+
host: asJson(context.host),
150+
invocation: asJson(context.invocation),
151+
lineage: asJson(context.lineage),
152+
providers: {
153+
keys: Object.keys(context.providers).sort((left, right) => left.localeCompare(right)),
154+
processLifetime: asJson(context.providers.processLifetime),
155+
},
156+
session: asJson(context.session),
157+
workspace: asJson(context.workspace),
158+
});
157159

158160
export interface CaptureInput {
159161
readonly event?: AgentEventRouteProps;

‎examples/host-test/src/dump.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {
22
agent,
3+
type AgentLineage,
34
type AgentStateHandle,
45
type JsonObject,
56
type JsonValue,
7+
type Observed,
68
} from '@agent-bundle/runtime';
79
import { z } from 'zod';
810

@@ -155,9 +157,11 @@ export const renderDumpMarkdown = (result: DumpResult): string => {
155157
/** One cell: `depth N · <conversation> ← <parent> (resolution)` or the typed unavailable reason. */
156158
export const renderLineage = (lineage: JsonValue | undefined): string => {
157159
if (lineage === undefined || lineage === null || typeof lineage !== 'object' || Array.isArray(lineage)) return 'not recorded';
158-
const observed = lineage as { readonly state?: string; readonly reason?: string; readonly value?: JsonValue };
159-
if (observed.state !== 'available') return `unavailable · ${observed.reason ?? 'unknown'}`;
160-
const value = (observed.value ?? {}) as { readonly conversation?: string; readonly depth?: number; readonly parent?: string; readonly resolution?: string; readonly root?: string };
161-
const parent = value.parent === undefined ? '' : ` ← ${value.parent}`;
162-
return `depth ${String(value.depth ?? '?')} · ${value.conversation ?? '?'}${parent} (${value.resolution ?? '?'})`;
160+
// The serialized `Observed<AgentLineage>` the capture wrote from `request.lineage`.
161+
const observed = lineage as unknown as Observed<AgentLineage> | { readonly state?: undefined };
162+
if (observed.state !== 'available') {
163+
return `unavailable · ${observed.state === 'unavailable' ? observed.reason : 'unknown'}`;
164+
}
165+
const { conversation, depth, parent, resolution } = observed.value;
166+
return `depth ${String(depth)} · ${conversation}${parent === undefined ? '' : ` ← ${parent}`} (${resolution})`;
163167
};

‎examples/host-test/tests/route-unit/routes.test.ts‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

55
import { afterEach, beforeEach, expect, it } from '@rstest/core';
6-
import { available } from '@agent-bundle/runtime';
6+
import { available, type AgentLineage } from '@agent-bundle/runtime';
77
import {
88
createGeneratedRuntimeState,
99
type GeneratedRuntimeState,
@@ -46,12 +46,19 @@ const eventInput = (
4646
native,
4747
});
4848

49-
const render = async (route: string, input: unknown, sessionId = 'root-session', host = 'claude') => {
49+
const render = async (
50+
route: string,
51+
input: unknown,
52+
sessionId = 'root-session',
53+
host = 'claude',
54+
lineage?: AgentLineage,
55+
) => {
5056
const bindings = await runtimeState.requestBindings();
5157
try {
5258
return await renderRoute(route, {
5359
context: {
5460
host: available({ name: host }, 'native'),
61+
...(lineage === undefined ? {} : { lineage: available(lineage, 'native') }),
5562
noticeLedger: bindings.noticeLedger,
5663
session: available({ sessionId }, 'native'),
5764
state: bindings.state,
@@ -125,6 +132,9 @@ it('records the complete native envelope, the request context, and env names for
125132
hasState: true,
126133
host: { state: 'available', value: { name: 'claude' } },
127134
invocation: { kind: 'event' },
135+
// `request.lineage` is recorded on every line; the route-unit context
136+
// mounts none, so the runtime's unavailable reason is the evidence.
137+
lineage: { state: 'unavailable' },
128138
session: { state: 'available', value: { sessionId: 'root-session' } },
129139
},
130140
});
@@ -133,6 +143,36 @@ it('records the complete native envelope, the request context, and env names for
133143
expect(JSON.stringify(record)).not.toContain(logDir.replace('captures.ndjson', 'value-should-not-appear'));
134144
});
135145

146+
it('records the mounted request.lineage verbatim and renders it in the dump table', async () => {
147+
const lineage: AgentLineage = {
148+
conversation: 'agent-1',
149+
depth: 1,
150+
parent: 'root-session',
151+
resolution: 'registry',
152+
root: 'root-session',
153+
subagent: { id: 'agent-1' },
154+
};
155+
await render('event:tool/before', eventInput('tool/before', {
156+
agent_id: 'agent-1',
157+
cwd: '/repo',
158+
hook_event_name: 'PreToolUse',
159+
session_id: 'root-session',
160+
tool_input: { command: 'pwd' },
161+
tool_name: 'Bash',
162+
tool_use_id: 'toolu_02',
163+
}), 'root-session', 'claude', lineage);
164+
165+
const [record] = await readLogLines();
166+
expect(record).toMatchObject({ request: { lineage: { source: 'native', state: 'available', value: lineage } } });
167+
168+
const dumped = await render('tool:host-test/dump', { conversation: 'agent-1' });
169+
expect(dumped.document.value).toMatchObject({
170+
matched: 1,
171+
records: [expect.objectContaining({ lineage: { source: 'native', state: 'available', value: lineage } })],
172+
});
173+
expectDocument(dumped).toContainMarkdown('depth 1 · agent-1 ← root-session (registry)');
174+
});
175+
136176
it('redacts secret-looking native values but keeps ids intact', async () => {
137177
await render('event:tool/before', eventInput('tool/before', {
138178
cwd: '/repo',

‎examples/mcp-app/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pnpm exec agent-bundle mcp run --server status --target portable
8484
```
8585

8686
The command resolves the generated entry from the portable target's MCP
87-
manifest, building a temporary artifact first; pass `--artifact dist` to
87+
manifest, building a temporary artifact first; pass `--artifact artifact` to
8888
reuse the `pnpm build` output instead. Closing stdin exits 0 and Ctrl-C
8989
exits 130, and per-server state persists under
9090
`.agent-bundle/mcp-run/portable/status`.
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { readFile } from 'node:fs/promises';
22
import { join } from 'node:path';
33

4+
import type { EvalGraderFunction } from 'agent-bundle/eval';
5+
46
import { isHealthyCompilerFixture } from '../../src/compiler-status-contract.ts';
57

6-
export default async ({ fixturePath }: { readonly fixturePath: string }) => {
8+
const grade: EvalGraderFunction = async ({ fixturePath }) => {
79
const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as unknown;
810
return isHealthyCompilerFixture(result)
9-
? { detail: 'The compiler service is healthy.', outcome: 'pass' as const }
10-
: { detail: 'The compiler service did not report a healthy status.', outcome: 'fail' as const };
11+
? { detail: 'The compiler service is healthy.', outcome: 'pass' }
12+
: { detail: 'The compiler service did not report a healthy status.', outcome: 'fail' };
1113
};
14+
15+
export default grade;

‎examples/skills-starter/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ pnpm dev
6464

6565
Use `pnpm check` when you want validation and a build without starting the
6666
Workbench; run the deterministic eval command separately when you need the
67-
release-readiness verdict. Generated output is written to `dist/`; its root
68-
contract is `dist/agent-bundle.manifest.json`. The `.agent-bundle/` directory
67+
release-readiness verdict. Generated output is written to `artifact/`; its root
68+
contract is `artifact/agent-bundle.manifest.json`. The `.agent-bundle/` directory
6969
contains development state and is not source material.
Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import { readFile } from 'node:fs/promises';
22
import { join } from 'node:path';
33

4-
export default async ({ fixturePath }: { readonly fixturePath: string }) => {
5-
const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as {
6-
readonly evidence?: unknown;
7-
readonly outcome?: string;
8-
readonly rollbackOrStopCondition?: string;
9-
};
4+
import type { EvalGraderFunction } from 'agent-bundle/eval';
5+
6+
interface OperationsResult {
7+
readonly evidence?: unknown;
8+
readonly outcome?: string;
9+
readonly rollbackOrStopCondition?: string;
10+
}
11+
12+
const grade: EvalGraderFunction = async ({ fixturePath }) => {
13+
const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as OperationsResult;
1014
const complete = result.outcome === 'ready'
1115
&& Array.isArray(result.evidence)
1216
&& result.evidence.length >= 2
1317
&& typeof result.rollbackOrStopCondition === 'string'
1418
&& result.rollbackOrStopCondition.length > 0;
1519
return complete
16-
? { detail: 'The operational handoff includes evidence and a rollback or stop condition.', outcome: 'pass' as const }
17-
: { detail: 'The operational handoff is missing evidence or a rollback or stop condition.', outcome: 'fail' as const };
20+
? { detail: 'The operational handoff includes evidence and a rollback or stop condition.', outcome: 'pass' }
21+
: { detail: 'The operational handoff is missing evidence or a rollback or stop condition.', outcome: 'fail' };
1822
};
23+
24+
export default grade;
Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { readFile } from 'node:fs/promises';
22
import { join } from 'node:path';
33

4-
export default async ({ fixturePath }: { readonly fixturePath: string }) => {
5-
const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as {
6-
readonly blockers?: unknown;
7-
readonly verdict?: string;
8-
};
4+
import type { EvalGraderFunction } from 'agent-bundle/eval';
5+
6+
interface ReleaseResult {
7+
readonly blockers?: unknown;
8+
readonly verdict?: string;
9+
}
10+
11+
const grade: EvalGraderFunction = async ({ fixturePath }) => {
12+
const result = JSON.parse(await readFile(join(fixturePath, 'result.json'), 'utf8')) as ReleaseResult;
913
return result.verdict === 'ready' && Array.isArray(result.blockers) && result.blockers.length === 0
10-
? { detail: 'The release artifact is ready with no blockers.', outcome: 'pass' as const }
11-
: { detail: 'The release artifact is not ready or has unresolved blockers.', outcome: 'fail' as const };
14+
? { detail: 'The release artifact is ready with no blockers.', outcome: 'pass' }
15+
: { detail: 'The release artifact is not ready or has unresolved blockers.', outcome: 'fail' };
1216
};
17+
18+
export default grade;

0 commit comments

Comments
 (0)