Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .evolve/skill-runs.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@
{"skill":"/agent-eval","ts":"2026-07-30T11:07:02Z","project":"agent-eval-analyst-proof-20260730","target":"crash-safe resumable public analyst benchmark command","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/verify","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/verify","ts":"2026-07-30T11:07:07Z","project":"agent-eval-analyst-proof-20260730","target":"benchmark command recovery, artifacts, and AgentRx metrics","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/stop","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/semgrep","ts":"2026-07-30T15:47:18Z","project":"agent-eval-analyst-proof-20260730","target":"agent-eval public analyst benchmark, 664 files, 492 rules","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/stop","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/agent-eval","ts":"2026-07-30T17:36:56Z","project":"agent-eval-runtime-run-reader","target":"Runtime FileRunContext supervisor-run reader","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/verify","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/verify","ts":"2026-07-30T17:36:56Z","project":"agent-eval-runtime-run-reader","target":"Runtime FileRunContext reader, real traces CLI path, and package","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/stop","operatorOverride":null,"transcriptPath":null,"traceDir":null}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-

### Fixed

- Supervisor-run analysis now reads agent-runtime `createFileRunContext(dir)` directories directly.
It composes nested journal envelopes by exact node identity, preserves profile digests, runtime tags, failures, and metered spend, and leaves unrecorded roles, terminal times, policy, transcripts, verification, and judge evidence unavailable.
A direct run path produces one report, while a parent directory containing multiple direct runs produces a rollup.
- Canonical `trace://<trace>/span/<span>` evidence is classified as span evidence instead of artifact evidence.
- Public model output selects positive integer assistant step ids.
The runner builds canonical trace URIs and exact action excerpts from those spans, and rejects missing, non-assistant, or empty steps.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ See [concepts](./docs/concepts.md), [customer paths](./docs/customer-journeys.md
| `@tangle-network/agent-eval/reporting` | Statistical comparisons and report rendering. |
| `@tangle-network/agent-eval/analyst` | Built-in and custom trace analysts, labeled comparison, costs, and reports. |
| `@tangle-network/agent-eval/traces` | Store, replay, and inspect structured traces. |
| `@tangle-network/agent-eval/supervisor-run` | Read and analyze recursive supervisor runs without collapsing missing measurements to zero. |
| `@tangle-network/agent-eval/supervisor-run` | Read loops or agent-runtime recursive run directories without collapsing missing measurements to zero. |
| `@tangle-network/agent-eval/benchmarks` | Benchmark adapters and retrieval metrics. |
| `@tangle-network/agent-eval/rl` | Export rewards, preferences, and training rows. |
| `@tangle-network/agent-eval/wire` | HTTP and RPC schemas for other languages. |
Expand Down
13 changes: 11 additions & 2 deletions docs/trace-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ for (const c of overview.error_clusters) {

`CONTROL_INTEGRITY_ANALYST` checks the existing `SupervisorRunSources` or `SupervisorRunTree` directly.
It does not define another run format.
`analyzeSupervisorRun(runDir)` detects both the loops layout and an agent-runtime `createFileRunContext(dir)` layout.
Runtime directories contain `spawn-journal.jsonl` plus optional `result.json` and `trajectory.json`.
The Runtime reader unwraps `{ kind: 'begin' | 'event', root, event }` storage envelopes and feeds the normalized events into the same source parser and analyzer used by every other reader.
Nested tree root markers are joined to their parent spawn by exact node id.
Profile digests, runtime tags, terminal reasons, structured verdicts, and metered spend are retained exactly when recorded.
The reader does not derive invocation roles, outcome quality, completion time, provider policy, worker transcripts, or verification results from labels or artifacts.
Those fields remain unavailable when their source is absent.

Register it as a custom-input analyst and pass the existing value under its stable id:

```ts
Expand All @@ -129,10 +137,10 @@ import {
CONTROL_INTEGRITY_ANALYST,
} from '@tangle-network/agent-eval/analyst'
import {
readLoopsSupervisorRun,
readRuntimeSupervisorRun,
} from '@tangle-network/agent-eval/supervisor-run'

const sources = await readLoopsSupervisorRun(runDir)
const sources = await readRuntimeSupervisorRun(runDir)
const registry = new AnalystRegistry()
registry.register(CONTROL_INTEGRITY_ANALYST)

Expand All @@ -142,6 +150,7 @@ const result = await registry.run('run-123', {
```

Pass `SupervisorRunSources` when it is available.
Use `readLoopsSupervisorRun()` when the input is explicitly the loops layout.
A `SupervisorRunTree` does not retain raw journal multiplicity or worker request and acknowledgement rows, so tree input explicitly reports those checks as unavailable.

The deterministic pass can prove only facts represented by these two existing surfaces.
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,12 +557,15 @@ export {
analyzeSupervisorRunIntegrity,
analyzeSupervisorRunSources,
claudeCodeSupervisorRunReader,
isRuntimeSupervisorRunDir,
isUnavailable,
type Measured,
readClaudeCodeSupervisorRun,
readRuntimeSupervisorRun,
renderSupervisorRunHeadline,
renderSupervisorRunMarkdown,
rollupSupervisorRuns,
runtimeSupervisorRunReader,
type SourceLimits,
SUPERVISOR_RUN_INTEGRITY_SCHEMA,
SUPERVISOR_RUN_SCHEMA,
Expand Down
47 changes: 37 additions & 10 deletions src/supervisor-run/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export function analyzeSupervisorRunSources(
const result = parseJson(src.result)
const judge = parseJson(src.judge)
const { rootId, workerSpawns, workerCloses, startedAt, completedAt } = tree
const rootSpawn =
rootId === null ? null : (tree.spawns.find((spawn) => spawn.id === rootId) ?? null)
const spawnById = new Map(workerSpawns.map((spawn) => [spawn.id, spawn]))
const spawnsByLabel = new Map<string, SpawnRow[]>()
for (const spawn of workerSpawns) {
Expand Down Expand Up @@ -410,9 +412,13 @@ export function analyzeSupervisorRunSources(
}

// ── economics ──────────────────────────────────────────────────────────
const journalWorkerIn = workerCloses.reduce((a, c) => a + c.spend.tokens.input, 0)
const journalWorkerOut = workerCloses.reduce((a, c) => a + c.spend.tokens.output, 0)
const journalWorkerUsd = workerCloses.reduce((a, c) => a + c.spend.usd, 0)
const rootChildIds = new Set(
workerSpawns.filter((spawn) => spawn.parent === rootId).map((spawn) => spawn.id),
)
const rootChildCloses = workerCloses.filter((close) => rootChildIds.has(close.id))
const journalWorkerIn = rootChildCloses.reduce((a, c) => a + c.spend.tokens.input, 0)
const journalWorkerOut = rootChildCloses.reduce((a, c) => a + c.spend.tokens.output, 0)
const journalWorkerUsd = rootChildCloses.reduce((a, c) => a + c.spend.usd, 0)
const workerUsdById = new Map<string, number>()
for (const c of workerCloses) {
workerUsdById.set(c.id, (workerUsdById.get(c.id) ?? 0) + c.spend.usd)
Expand Down Expand Up @@ -466,13 +472,29 @@ export function analyzeSupervisorRunSources(
const close = spawn === null ? null : (closeById.get(spawn.id) ?? null)
const passed = close?.valid ?? f?.passed ?? null
const matchingRoles = new Set(matchingSpawns.map((candidate) => candidate.role))
const matchingRuntimes = new Set(matchingSpawns.map((candidate) => candidate.runtime))
const matchingProfiles = new Set(matchingSpawns.map((candidate) => candidate.profileDigest))
const journalWallMs =
spawn?.at !== null &&
spawn?.at !== undefined &&
close?.at !== null &&
close?.at !== undefined &&
close.at >= spawn.at
? close.at - spawn.at
: null
return {
workerId: w.workerId ?? null,
worker: w.label,
role: matchingRoles.size === 1 ? (matchingSpawns[0]?.role ?? null) : null,
wallMs: f?.started != null && f.finishedAt != null ? f.finishedAt - f.started : null,
tokensIn: w.tokensIn ?? null,
tokensOut: w.tokensOut ?? null,
runtime: matchingRuntimes.size === 1 ? (matchingSpawns[0]?.runtime ?? null) : null,
profileDigest:
matchingProfiles.size === 1 ? (matchingSpawns[0]?.profileDigest ?? null) : null,
status: close?.status ?? null,
failure: close?.reason ?? null,
infra: close?.infra ?? null,
wallMs: f?.started != null && f.finishedAt != null ? f.finishedAt - f.started : journalWallMs,
tokensIn: w.tokensIn ?? (close?.hasSpend ? close.spend.tokens.input : null),
tokensOut: w.tokensOut ?? (close?.hasSpend ? close.spend.tokens.output : null),
usd:
usdLimit !== null
? null
Expand Down Expand Up @@ -535,9 +557,10 @@ export function analyzeSupervisorRunSources(
src.brainLog === null
? gap(
'brain.brainTruncations',
src.supRunDir === null
? 'no supervisor run dir under <ws>/.loops/supervisor'
: 'brain.jsonl absent — loops predates the brain-call tap, so truncation cannot be ruled out',
src.brainLogMissingReason ??
(src.supRunDir === null
? 'no supervisor run dir under <ws>/.loops/supervisor'
: 'brain.jsonl absent — loops predates the brain-call tap, so truncation cannot be ruled out'),
)
: brainCalls.filter((c) => c.finish_reason === 'length').length,
workers: {
Expand Down Expand Up @@ -573,7 +596,7 @@ export function analyzeSupervisorRunSources(
usdLimit !== null
? usdLimit
: stateUsd !== null
? `state.json result.spentUsd${journalWorkerUsd === 0 ? ' — brain-priced only; worker CLI inference is unpriced (see worker token counts)' : ''}`
? `state.json result.spentUsd${rootChildCloses.length > 0 && journalWorkerUsd === 0 ? ' — brain-priced only; worker CLI inference is unpriced (see worker token counts)' : ''}`
: haveJournal
? 'journal metered + settled usd'
: journalMissing,
Expand Down Expand Up @@ -663,6 +686,10 @@ export function analyzeSupervisorRunSources(
instanceId: src.instanceId,
arm: src.arm,
supervisorId: rootId !== null ? rootId : unavailable(journalMissing),
supervisorProfileDigest:
rootSpawn?.profileDigest !== null && rootSpawn?.profileDigest !== undefined
? rootSpawn.profileDigest
: gap('supervisorProfileDigest', 'root spawned event has no profile digest'),
generatedAt: new Date(now()).toISOString(),
orchestration,
decision,
Expand Down
2 changes: 2 additions & 0 deletions src/supervisor-run/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function fixtureJournal(plan: JournalPlan): string {
kind: 'spawned',
id: root,
label: 'root',
role: 'supervisor',
budget: {},
runtime: 'inline',
seq: 0,
Expand All @@ -42,6 +43,7 @@ export function fixtureJournal(plan: JournalPlan): string {
id,
parent: root,
label,
role: 'worker',
budget: {},
runtime: 'inline',
seq: i,
Expand Down
16 changes: 15 additions & 1 deletion src/supervisor-run/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { describe, expect, it } from 'vitest'
import type { SupervisorRunNodeRole as RootSupervisorRunNodeRole } from '../index'
import {
type SupervisorRunNodeRole as RootSupervisorRunNodeRole,
isRuntimeSupervisorRunDir as rootIsRuntimeSupervisorRunDir,
readRuntimeSupervisorRun as rootReadRuntimeSupervisorRun,
runtimeSupervisorRunReader as rootRuntimeSupervisorRunReader,
} from '../index'
import {
isRuntimeSupervisorRunDir,
NO_SOURCE_LIMITS,
readRuntimeSupervisorRun,
runtimeSupervisorRunReader,
type SourceLimits,
type SupervisorRunNodeRole,
type WorkerLogSource,
Expand All @@ -23,5 +31,11 @@ describe('supervisor-run public contract', () => {
expect(rootRole).toBe('supervisor')
expect(worker.workerId).toBe('worker-1')
expect(limits).toMatchObject({ managerTokens: null, workerTokens: null })
expect(typeof readRuntimeSupervisorRun).toBe('function')
expect(typeof runtimeSupervisorRunReader).toBe('function')
expect(typeof isRuntimeSupervisorRunDir).toBe('function')
expect(rootReadRuntimeSupervisorRun).toBe(readRuntimeSupervisorRun)
expect(rootRuntimeSupervisorRunReader).toBe(runtimeSupervisorRunReader)
expect(rootIsRuntimeSupervisorRunDir).toBe(isRuntimeSupervisorRunDir)
})
})
5 changes: 5 additions & 0 deletions src/supervisor-run/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export {
renderSupervisorRunMarkdown,
} from './render'
export { type SupervisorRolloutOptions, supervisorRunRolloutLines } from './rollout-nodes'
export {
isRuntimeSupervisorRunDir,
readRuntimeSupervisorRun,
runtimeSupervisorRunReader,
} from './runtime-reader'
export {
type DecisionMetrics,
type EconomicsMetrics,
Expand Down
36 changes: 26 additions & 10 deletions src/supervisor-run/loops-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
renderSupervisorRunHeadline,
renderSupervisorRunMarkdown,
} from './render'
import { isRuntimeSupervisorRunDir, readRuntimeSupervisorRun } from './runtime-reader'
import {
NO_SOURCE_LIMITS,
type SupervisorRunReader,
Expand Down Expand Up @@ -270,7 +271,11 @@ export async function analyzeSupervisorRun(
opts: LoopsReaderOptions = {},
): Promise<SupervisorRunReport> {
if (typeof input === 'string') {
return analyzeSupervisorRunSources(await readLoopsSupervisorRun(input, opts))
return analyzeSupervisorRunSources(
(await isRuntimeSupervisorRunDir(input))
? await readRuntimeSupervisorRun(input)
: await readLoopsSupervisorRun(input, opts),
)
}
if (isReader(input)) return analyzeSupervisorRunSources(await input.read())
return analyzeSupervisorRunSources(input)
Expand Down Expand Up @@ -303,7 +308,9 @@ export async function writeSupervisorRunReport(
runDir: string,
opts: WriteSupervisorRunOptions = {},
): Promise<SupervisorRunReport> {
const sources = await readLoopsSupervisorRun(runDir, opts)
const sources = (await isRuntimeSupervisorRunDir(runDir))
? await readRuntimeSupervisorRun(runDir)
: await readLoopsSupervisorRun(runDir, opts)
const report = analyzeSupervisorRunSources(sources)
const md = renderSupervisorRunMarkdown(report)
const dest = opts.reportDir ?? runDir
Expand Down Expand Up @@ -389,8 +396,19 @@ export async function reportSupervisorRound(
return rollup
}

/** Every `<...>/runs/<iid>/<arm>` directory under `root`. */
/**
* Every loops or Runtime supervisor run below `root`.
*
* When `root` itself is one run, return no children so callers can distinguish
* a single report from a parent-directory rollup.
*/
export async function findSupervisorRunDirs(root: string): Promise<string[]> {
if (
(await isRuntimeSupervisorRunDir(root)) ||
(await findSupervisorRunDirIn(join(root, 'ws'))) !== null
) {
return []
}
const found: string[] = []
const walk = async (dir: string, depth: number): Promise<void> => {
if (depth > 8) return
Expand All @@ -399,13 +417,11 @@ export async function findSupervisorRunDirs(root: string): Promise<string[]> {
if (!e.isDirectory()) continue
if (e.name === 'node_modules' || e.name === '.git') continue
const full = join(dir, e.name)
if (e.name === 'runs') {
const iids = await readdir(full, { withFileTypes: true }).catch(() => [])
for (const iid of iids) {
if (!iid.isDirectory()) continue
const arms = await readdir(join(full, iid.name), { withFileTypes: true }).catch(() => [])
for (const arm of arms) if (arm.isDirectory()) found.push(join(full, iid.name, arm.name))
}
if (
(await isRuntimeSupervisorRunDir(full)) ||
(await findSupervisorRunDirIn(join(full, 'ws'))) !== null
) {
found.push(full)
continue
}
await walk(full, depth + 1)
Expand Down
7 changes: 4 additions & 3 deletions src/supervisor-run/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function renderSupervisorRunMarkdown(r: SupervisorRunReport): string {
out.push('')
out.push(`- Run: \`${r.runRef}\``)
out.push(`- Supervisor: \`${showMeasured(r.supervisorId)}\``)
out.push(`- Supervisor profile: \`${showMeasured(r.supervisorProfileDigest)}\``)
out.push(`- Generated: ${r.generatedAt}`)
out.push('')

Expand Down Expand Up @@ -161,12 +162,12 @@ export function renderSupervisorRunMarkdown(r: SupervisorRunReport): string {
out.push('')
if (!isUnavailable(e.perWorker) && e.perWorker.length > 0) {
out.push(
'| Worker id | Label | Role | Wall | Tokens in | Tokens out | Patch bytes | Verify passed | Score |',
'| Worker id | Label | Role | Runtime | Profile digest | Status | Failure | Infra | Wall | Tokens in | Tokens out | Patch bytes | Verify passed | Score |',
)
out.push('|---|---|---|---:|---:|---:|---:|---|---:|')
out.push('|---|---|---|---|---|---|---|---|---:|---:|---:|---:|---|---:|')
for (const w of e.perWorker) {
out.push(
`| ${w.workerId === null ? 'unavailable — legacy label join' : `\`${w.workerId}\``} | \`${w.worker}\` | ${w.role ?? 'unavailable — no journal join'} | ${w.wallMs === null ? 'unavailable — no start/finish pair' : fmtMs(w.wallMs)} | ${w.tokensIn ?? 'unavailable — store does not attribute tokens per worker'} | ${w.tokensOut ?? 'unavailable — store does not attribute tokens per worker'} | ${w.patchBytes ?? 'unavailable — no worker patch file'} | ${w.passed === null ? 'unavailable — no verdict' : String(w.passed)} | ${w.score ?? 'unavailable — no numeric score'} |`,
`| ${w.workerId === null ? 'unavailable — legacy label join' : `\`${w.workerId}\``} | \`${w.worker}\` | ${w.role ?? 'unavailable — source recorded no role'} | ${w.runtime ?? 'unavailable — source recorded no runtime'} | ${w.profileDigest === null ? 'unavailable — source recorded no profile digest' : `\`${w.profileDigest}\``} | ${w.status ?? 'unavailable — no terminal event'} | ${w.failure ?? 'none recorded'} | ${w.infra ?? 'unavailable'} | ${w.wallMs === null ? 'unavailable — no spawn/finish pair' : fmtMs(w.wallMs)} | ${w.tokensIn ?? 'unavailable — store does not attribute tokens per worker'} | ${w.tokensOut ?? 'unavailable — store does not attribute tokens per worker'} | ${w.patchBytes ?? 'unavailable — no worker patch file'} | ${w.passed === null ? 'unavailable — no verdict' : String(w.passed)} | ${w.score ?? 'unavailable — no numeric score'} |`,
)
}
out.push('')
Expand Down
Loading