Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .evolve/skill-runs.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@
{"skill":"/verify","ts":"2026-07-29T20:20:04Z","project":"agent-eval-trace-tool-descriptors","target":"transport-neutral trace-analysis package export","operatorPrompt":"","durationMin":null,"verdict":"SHIP","dispatchedTo":"/reflect","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/reflect","ts":"2026-07-29T20:23:34Z","project":"agent-eval-trace-tool-descriptors","target":"trace-tool descriptor session n=1","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/handoff","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/handoff","ts":"2026-07-29T20:24:06Z","project":"agent-eval-trace-tool-descriptors","target":"agent-eval trace-tool descriptor implementation","operatorPrompt":"","durationMin":null,"verdict":"READY","dispatchedTo":"/stop","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/critical-audit","ts":"2026-07-29T22:47:03Z","project":"agent-eval-control-integrity","target":"agent-eval PR 487 control-integrity repair, 17 changed paths","operatorPrompt":"","durationMin":null,"verdict":"APPROVE","dispatchedTo":"/verify","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/verify","ts":"2026-07-29T22:47:04Z","project":"agent-eval-control-integrity","target":"agent-eval PR 487: 4244 tests, build, packed exports, runtime import","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/stop","operatorOverride":null,"transcriptPath":null,"traceDir":null}
{"skill":"/simplify","ts":"2026-07-29T22:54:49Z","project":"agent-eval-control-integrity","target":"PR 487 supervisor-run integrity modularization","operatorPrompt":"","durationMin":null,"verdict":"PASS","dispatchedTo":"/stop","operatorOverride":null,"transcriptPath":null,"traceDir":null}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-

---

## [Unreleased]

### Added

- `CONTROL_INTEGRITY_ANALYST` deterministically checks the existing `SupervisorRunSources` and `SupervisorRunTree` for duplicate or detached identities, parent cycles, impossible event order, orphan terminal events, and steer request/acknowledgement mismatches.
Missing transcripts, profile ids, worker logs, and declared tree gaps remain explicitly unavailable and never become a zero or a clean result.
The analyst emits ordinary `AnalystFinding` records and adds no run schema or execution authority.

## [0.136.0] - 2026-07-29 - preserve recursive evidence and complete profile changes

### Fixed
Expand All @@ -16,12 +24,18 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-
- Spawned invocations retain an explicit `supervisor` or `worker` role, and structured verdicts retain their numeric score in rollout rewards and per-worker report rows.
- Accepted-patch counts are unavailable when the source did not retain worker deliverables, even when a worker event claimed patch bytes.
- Manager and worker token totals have independent unavailable reasons, so an uncaptured channel is not reported as zero and a captured zero remains zero.
- Supervisor-run integrity checks share one typed source parse, correlate worker control rows by stable invocation id, and distinguish captured-empty control artifacts from missing artifacts.
- Claude Code child agents remain workers when they delegate, and steer counts use the original tool-use request id.
- Malformed journal and worker-control rows make dependent checks unavailable instead of producing missing-parent claims or clean zero counts.

### Changed — BREAKING

- Custom `SupervisorRunSources` readers must add `managerTokens` and `workerTokens` to `SourceLimits`.
Set each field to `null` only when that role's aggregate token channel is complete; otherwise set the reason it is unavailable.
Readers with no source limitations can continue to use `NO_SOURCE_LIMITS`.
- `SupervisorRunTree.gaps` now contains typed `{ code, message, nodeId?, count? }` records instead of free-form strings.
- `WorkerLogFacts.steersQueued` and `steersDelivered` are now nullable.
`null` means malformed, incomplete, or uncorrelated control rows prevent exact request-id accounting.

## [0.135.4] - 2026-07-29 - keep rich source evidence in one schema cohort

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,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` | Model-assisted failure analysis. |
| `@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/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
46 changes: 46 additions & 0 deletions docs/trace-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,52 @@ for (const c of overview.error_clusters) {
See `failureClusters` in [insight-report.md](./insight-report.md) and the
`ErrorCluster` type doc-comments for the field-level contract.

## Recursive control integrity (no LLM)

`CONTROL_INTEGRITY_ANALYST` checks the existing `SupervisorRunSources` or `SupervisorRunTree` directly.
It does not define another run format.
Register it as a custom-input analyst and pass the existing value under its stable id:

```ts
import {
AnalystRegistry,
CONTROL_INTEGRITY_ANALYST,
} from '@tangle-network/agent-eval/analyst'
import {
readLoopsSupervisorRun,
} from '@tangle-network/agent-eval/supervisor-run'

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

const result = await registry.run('run-123', {
custom: { 'control-integrity': sources },
})
```

Pass `SupervisorRunSources` when it is available.
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.

| Question | Current evidence | What the analyst can say |
|---|---|---|
| Is every invocation attached to one unambiguous tree? | `rootId`, `rollout_id`, `parent_rollout_id`, `run_id` | Duplicate ids, missing parents, extra parentless roots, cross-run edges, and ancestry cycles are violations with exact field references. |
| Did invocation roles survive capture? | Explicit journal and `RolloutLine.role` values | The root must remain `supervisor`; non-root roles are consumed as recorded, and workers may spawn workers. |
| Is the causal order possible? | `outcome.metrics.spawned_at`, `started_at`, `settled_at`, `completed_at`, `finished_at` when present | A child before its parent, a child after its parent closed, or a close before a start is a violation; absent timestamps produce no timing claim. |
| Did a queued steer reach the worker? | `SupervisorRunSources.workers[].inbox` and `.events` | Requests and acknowledgements are joined by request id, not compared as totals. Missing, malformed, duplicate, or uncorrelated rows make the affected count unavailable. |
| Can behavior be attributed to an exact profile? | `policy.agent_profile_cell_id` | An absent id is reported as unavailable. |
| Can action authorship or reasoning be inspected? | `messages[]` | Empty gap rows are reported as unavailable. |

An empty finding list means only that no implemented rule fired on the captured fields.
It does not certify that an agent chose the action, that the action was authorized, that a budget or depth limit was enforced, or that a finding caused a later decision.
Those claims require upstream action-decision events carrying `action_id`, `actor_rollout_id`, `target_rollout_id`, `action_kind`, `authority_snapshot_id`, requested and granted resource/depth values, the authorization result, and any `finding_id` or evidence references that caused the action.
Resume integrity additionally requires an explicit prior-session id and resumed-session id rather than a prose summary.

Malformed source rows are excluded from structural claims.
Their count is retained in `SupervisorRunTree.gaps`, so analyzing a projected tree later cannot turn an unreadable parent row into a missing-parent violation.

## Required Trace Shape

Every serious product run should include:
Expand Down
43 changes: 43 additions & 0 deletions scripts/verify-package-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ try {
`
import {
CostLedger,
CONTROL_INTEGRITY_ANALYST as ROOT_CONTROL_INTEGRITY_ANALYST,
analyzeSupervisorRunIntegrity as ROOT_ANALYZE_SUPERVISOR_RUN_INTEGRITY,
InMemoryTraceStore,
type BenchmarkRunnerConfig,
type ChatClient,
Expand All @@ -169,7 +171,10 @@ try {
runTaskScore,
} from '@tangle-network/agent-eval'
import {
CONTROL_INTEGRITY_ANALYST,
ControlIntegrityAnalyst,
RawAnalystFindingSchema,
emitControlIntegrityFindings,
type RawAnalystFinding,
type TraceAnalystGolden,
} from '@tangle-network/agent-eval/analyst'
Expand Down Expand Up @@ -230,6 +235,13 @@ try {
toGrpoRows,
toSftRows,
} from '@tangle-network/agent-eval/rl'
import {
analyzeSupervisorRunIntegrity,
type SupervisorRunIntegrityReport,
type SupervisorRunSources,
type SupervisorRunTree,
type SupervisorRunTreeGap,
} from '@tangle-network/agent-eval/supervisor-run'

const store: TraceAnalysisStore = new OtlpFileTraceStore({ path: 'spans.jsonl' })
const traceToolDescriptors: TraceAnalysisToolDescriptor[] =
Expand Down Expand Up @@ -336,6 +348,13 @@ try {
const removedDatasetRecordInput: Parameters<typeof buildRlDataset>[0] = removedRecordInputs
const canonicalChat = null as unknown as ChatClient
const canonicalJudge = null as unknown as JudgeFn
const controlIntegrityAnalyst: ControlIntegrityAnalyst = CONTROL_INTEGRITY_ANALYST
const controlIntegrityInput = undefined as SupervisorRunSources | SupervisorRunTree | undefined
const controlIntegrityReport = undefined as SupervisorRunIntegrityReport | undefined
const controlTreeGap: SupervisorRunTreeGap = {
code: 'journal-unavailable',
message: 'not captured',
}
const rawFinding: RawAnalystFinding = RawAnalystFindingSchema.parse({
severity: 'info',
claim: 'current',
Expand Down Expand Up @@ -454,6 +473,14 @@ try {
removedDatasetRecordInput,
canonicalChat,
canonicalJudge,
controlIntegrityAnalyst,
controlIntegrityInput,
controlIntegrityReport,
controlTreeGap,
analyzeSupervisorRunIntegrity,
ROOT_ANALYZE_SUPERVISOR_RUN_INTEGRITY,
emitControlIntegrityFindings,
ROOT_CONTROL_INTEGRITY_ANALYST,
rawFinding,
golden,
report,
Expand Down Expand Up @@ -522,6 +549,22 @@ try {
if ('CanonicalRawAnalystFindingSchema' in analyst) {
throw new Error('obsolete analyst export CanonicalRawAnalystFindingSchema')
}
const integrityInput = { rootId: null, nodes: [], gaps: [] }
const integrityReport = root.analyzeSupervisorRunIntegrity(integrityInput)
const integrityFindings = analyst.emitControlIntegrityFindings(
integrityInput,
'2026-07-29T00:00:00.000Z',
)
const integrityCodes = new Set(
integrityFindings.map((finding) => finding.metadata?.integrity_code),
)
if (
integrityReport.input !== 'tree' ||
!integrityCodes.has('root-unavailable') ||
!integrityCodes.has('source-checks-unavailable')
) {
throw new Error('packed control-integrity runtime exports returned an invalid result')
}
for (const name of [
'buildTraceAnalysisToolDescriptors',
'buildTraceAnalystTools',
Expand Down
5 changes: 5 additions & 0 deletions src/analyst/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export {
KNOWLEDGE_GAP_KIND_SPEC,
KNOWLEDGE_POISONING_KIND_SPEC,
} from './kinds'
export {
CONTROL_INTEGRITY_ANALYST,
ControlIntegrityAnalyst,
emitControlIntegrityFindings,
} from './kinds/control-integrity'
export type {
SkillUsageRecord,
SkillUsageReport,
Expand Down
76 changes: 76 additions & 0 deletions src/analyst/kinds/control-integrity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest'
import { fixtureSources } from '../../supervisor-run/fixtures'
import type { SupervisorRunTree } from '../../supervisor-run/types'
import { AnalystRegistry } from '../registry'
import { CONTROL_INTEGRITY_ANALYST, emitControlIntegrityFindings } from './control-integrity'

const AT = '2026-07-29T18:00:00.000Z'

describe('control-integrity analyst adapter', () => {
it('maps typed integrity issues into stable zero-cost analyst findings', async () => {
const input: SupervisorRunTree = {
rootId: null,
nodes: [],
gaps: [{ code: 'journal-unavailable', message: 'journal unavailable' }],
}
const registry = new AnalystRegistry()
registry.register(CONTROL_INTEGRITY_ANALYST)

const result = await registry.run(
'run-1',
{ custom: { 'control-integrity': input } },
{ tags: { producedAt: AT } },
)
const repeated = emitControlIntegrityFindings(input, '2099-01-01T00:00:00.000Z')

expect(result.per_analyst).toEqual([
expect.objectContaining({
analyst_id: 'control-integrity',
status: 'ok',
usage: expect.objectContaining({ cost: { kind: 'observed', usd: 0 } }),
}),
])
expect(result.findings.map((finding) => finding.finding_id)).toEqual(
repeated.map((finding) => finding.finding_id),
)
expect(result.findings.every((finding) => finding.metadata?.integrity_code)).toBe(true)
})

it('passes source input through without dropping source-only checks', () => {
const findings = emitControlIntegrityFindings(
fixtureSources({ workers: null, workersMissingReason: 'not retained' }),
AT,
)

expect(findings.some((finding) => finding.metadata?.integrity_input === 'sources')).toBe(true)
expect(findings.map((finding) => finding.metadata?.integrity_code)).toContain(
'worker-controls-unavailable',
)
expect(findings.map((finding) => finding.metadata?.integrity_code)).not.toContain(
'source-checks-unavailable',
)
})

it('keeps identical local node ids distinct across runs', () => {
const input = (runId: string): SupervisorRunTree => ({
rootId: 'root',
nodes: [
{
...({} as SupervisorRunTree['nodes'][number]),
rollout_id: 'root',
run_id: runId,
},
],
gaps: [],
})

const first = emitControlIntegrityFindings(input('run-a'), AT)
const second = emitControlIntegrityFindings(input('run-b'), AT)

expect(first).not.toHaveLength(0)
expect(second).not.toHaveLength(0)
expect(first[0]?.finding_id).not.toBe(second[0]?.finding_id)
expect(first[0]?.subject).toContain('run-a/')
expect(second[0]?.subject).toContain('run-b/')
})
})
83 changes: 83 additions & 0 deletions src/analyst/kinds/control-integrity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
analyzeSupervisorRunIntegrity,
type SupervisorRunIntegrityEvidence,
} from '../../supervisor-run/integrity'
import type { SupervisorRunSources, SupervisorRunTree } from '../../supervisor-run/types'
import {
type Analyst,
type AnalystContext,
type AnalystFinding,
type EvidenceRef,
makeFinding,
} from '../types'

const ANALYST_ID = 'control-integrity'

function shown(value: unknown): string {
if (value === undefined) return '<absent>'
const encoded = JSON.stringify(value)
return encoded === undefined ? String(value) : encoded
}

function evidenceRef(namespace: string, value: SupervisorRunIntegrityEvidence): EvidenceRef {
return {
kind: 'metric',
uri: `supervisor-run://${encodeURIComponent(namespace)}/${value.path}`,
excerpt: shown(value.value),
}
}

/** Translate typed supervisor-run integrity issues into the shared analyst envelope. */
export function emitControlIntegrityFindings(
input: SupervisorRunSources | SupervisorRunTree,
producedAt: string,
): AnalystFinding[] {
const report = analyzeSupervisorRunIntegrity(input, { capturedAt: producedAt })
return report.issues.map((issue) =>
makeFinding({
analyst_id: ANALYST_ID,
produced_at: producedAt,
area: issue.area,
severity: issue.severity,
subject: `${report.runRef}/${issue.subject}`,
claim: issue.claim,
rationale: issue.detail,
evidence_refs: issue.evidence.map((value) => evidenceRef(report.runRef, value)),
recommended_action: issue.recommendedAction,
validation_plan:
'Re-run this deterministic analyst on the retained SupervisorRunSources or SupervisorRunTree after correcting the producer.',
confidence: 1,
metadata: {
integrity_code: issue.code,
integrity_input: report.input,
integrity_run_ref: report.runRef,
integrity_subject: issue.subject,
...issue.metadata,
},
}),
)
}

/** Deterministic Analyst adapter for `SupervisorRunSources | SupervisorRunTree`. */
export class ControlIntegrityAnalyst implements Analyst<SupervisorRunSources | SupervisorRunTree> {
readonly id = ANALYST_ID
readonly description =
'Deterministic supervisor-run integrity checks with explicit unavailable evidence.'
readonly inputKind = 'custom' as const
readonly cost = { kind: 'deterministic' as const, est_usd_per_run: 0 }
readonly version = '2.0.0'

async analyze(
input: SupervisorRunSources | SupervisorRunTree,
ctx: AnalystContext,
): Promise<AnalystFinding[]> {
const producedAt = ctx.tags?.producedAt ?? new Date().toISOString()
const findings = emitControlIntegrityFindings(input, producedAt)
ctx.log?.(`control-integrity: ${findings.length} finding(s)`, {
input: 'nodes' in input ? 'SupervisorRunTree' : 'SupervisorRunSources',
})
return findings
}
}

export const CONTROL_INTEGRITY_ANALYST = new ControlIntegrityAnalyst()
5 changes: 5 additions & 0 deletions src/analyst/kinds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
* to pass each completed kind's findings to the kinds that follow it.
*/

export {
CONTROL_INTEGRITY_ANALYST,
ControlIntegrityAnalyst,
emitControlIntegrityFindings,
} from './control-integrity'
export { FAILURE_MODE_KIND_SPEC } from './failure-mode'
export { IMPROVEMENT_KIND_SPEC } from './improvement'
export { KNOWLEDGE_GAP_KIND_SPEC } from './knowledge-gap'
Expand Down
Loading