@tangle-network/agent-eval 0.86.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/dist/adapters/http.d.ts +3 -3
- package/dist/adapters/langchain.d.ts +3 -3
- package/dist/adapters/otel.d.ts +6 -6
- package/dist/adversarial-DIVcDoI_.d.ts +88 -0
- package/dist/analyst/index.d.ts +11 -10
- package/dist/analyst/index.js +13 -8
- package/dist/analyst/index.js.map +1 -1
- package/dist/analyze-runs-DwCEkpO_.d.ts +81 -0
- package/dist/belief-state/index.d.ts +4 -4
- package/dist/belief-state/index.js +1 -1
- package/dist/benchmarks/index.d.ts +3 -3
- package/dist/campaign/index.d.ts +165 -18
- package/dist/campaign/index.js +289 -14
- package/dist/campaign/index.js.map +1 -1
- package/dist/chunk-45EEMHTC.js +35 -0
- package/dist/chunk-45EEMHTC.js.map +1 -0
- package/dist/{chunk-FZWAFVAA.js → chunk-4FBZZIYD.js} +2 -2
- package/dist/{chunk-YV7J7X5N.js → chunk-5HRORJQY.js} +22 -12
- package/dist/chunk-5HRORJQY.js.map +1 -0
- package/dist/{chunk-OTYQPHPL.js → chunk-6SOJM3VR.js} +5 -5
- package/dist/chunk-BOD4O7OF.js +40 -0
- package/dist/chunk-BOD4O7OF.js.map +1 -0
- package/dist/{chunk-Z7VFTS2J.js → chunk-CY6U5S3X.js} +2 -2
- package/dist/{chunk-VIDQF3F5.js → chunk-D3V5B42D.js} +5 -34
- package/dist/chunk-D3V5B42D.js.map +1 -0
- package/dist/{chunk-YGYXHNAQ.js → chunk-FIUKOSWI.js} +21 -8
- package/dist/chunk-FIUKOSWI.js.map +1 -0
- package/dist/{chunk-WJL2NJXN.js → chunk-GSH6QNNS.js} +2 -2
- package/dist/{chunk-RBNA5AZT.js → chunk-L3JOU6XM.js} +2 -2
- package/dist/{chunk-IDVBLYCY.js → chunk-LMZQ2Z4U.js} +56 -2
- package/dist/{chunk-IDVBLYCY.js.map → chunk-LMZQ2Z4U.js.map} +1 -1
- package/dist/{chunk-VUINJM5M.js → chunk-QAY5UIJO.js} +2 -193
- package/dist/chunk-QAY5UIJO.js.map +1 -0
- package/dist/{chunk-P2J6SOXT.js → chunk-QG2OVF2D.js} +5 -3
- package/dist/{chunk-P2J6SOXT.js.map → chunk-QG2OVF2D.js.map} +1 -1
- package/dist/chunk-REVYNR6C.js +100 -0
- package/dist/chunk-REVYNR6C.js.map +1 -0
- package/dist/chunk-STGVSCDH.js +202 -0
- package/dist/chunk-STGVSCDH.js.map +1 -0
- package/dist/{chunk-ZZ2HOPME.js → chunk-TWS7AZEY.js} +2 -2
- package/dist/chunk-UHMJT4T7.js +200 -0
- package/dist/chunk-UHMJT4T7.js.map +1 -0
- package/dist/chunk-UMMZHCPB.js +190 -0
- package/dist/chunk-UMMZHCPB.js.map +1 -0
- package/dist/chunk-VZSRQ272.js +149 -0
- package/dist/chunk-VZSRQ272.js.map +1 -0
- package/dist/{chunk-L5G7OUKD.js → chunk-XY4DDNEG.js} +8 -190
- package/dist/chunk-XY4DDNEG.js.map +1 -0
- package/dist/chunk-Y47J2LJ3.js +859 -0
- package/dist/chunk-Y47J2LJ3.js.map +1 -0
- package/dist/{chunk-BABOZOSN.js → chunk-ZFIBGEOL.js} +3 -3
- package/dist/chunk-ZFIBGEOL.js.map +1 -0
- package/dist/{code-agent-session-BRXmavYv.d.ts → code-agent-session-BO8nCnv3.d.ts} +1 -1
- package/dist/contract/index.d.ts +24 -95
- package/dist/contract/index.js +25 -764
- package/dist/contract/index.js.map +1 -1
- package/dist/{control-GeE8OhpN.d.ts → control-_Qb7skHX.d.ts} +2 -2
- package/dist/control.d.ts +5 -5
- package/dist/corpus-BoR-041R.d.ts +560 -0
- package/dist/cost-ledger-DuSqlw5B.d.ts +113 -0
- package/dist/counterfactual-Dwibr5IW.d.ts +85 -0
- package/dist/{dataset-B2kL-fSM.d.ts → dataset-BbGkaN2I.d.ts} +1 -1
- package/dist/{registry-DrEQ3Luj.d.ts → default-registry-zoGHUQEH.d.ts} +29 -2
- package/dist/diagnose.d.ts +251 -0
- package/dist/diagnose.js +381 -0
- package/dist/diagnose.js.map +1 -0
- package/dist/{errors-Dwqw-T_m.d.ts → errors-CzMUYo7b.d.ts} +1 -1
- package/dist/{feedback-trajectory-B3rErRsh.d.ts → feedback-trajectory-D9OVLrg9.d.ts} +1 -1
- package/dist/fuzz.d.ts +484 -0
- package/dist/fuzz.js +613 -0
- package/dist/fuzz.js.map +1 -0
- package/dist/governance/index.d.ts +4 -4
- package/dist/hosted/index.d.ts +6 -6
- package/dist/{index-DE3RXAXD.d.ts → index-Bx3gZ8xl.d.ts} +1 -1
- package/dist/index.d.ts +718 -455
- package/dist/index.js +1604 -793
- package/dist/index.js.map +1 -1
- package/dist/{insight-report-3ADTfClO.d.ts → insight-report-BBwvOh6x.d.ts} +2 -2
- package/dist/{integrity-CJzrpUua.d.ts → integrity-VJ9A7aST.d.ts} +1 -1
- package/dist/{judge-calibration-DilmB3Ml.d.ts → judge-calibration-0p2QcWNE.d.ts} +1 -1
- package/dist/{kind-factory-CVecZZG_.d.ts → kind-factory-5b7xXXOr.d.ts} +2 -2
- package/dist/{llm-client-CuUg2Mn3.d.ts → llm-client-BeEcAokY.d.ts} +1 -1
- package/dist/matrix/index.d.ts +2 -2
- package/dist/meta-eval/index.d.ts +177 -3
- package/dist/meta-eval/index.js +260 -1
- package/dist/meta-eval/index.js.map +1 -1
- package/dist/{multi-layer-verifier-DlWCXuxL.d.ts → multi-layer-verifier-DUZXrPDA.d.ts} +7 -1
- package/dist/multishot/index.d.ts +25 -11
- package/dist/multishot/index.js +36 -7
- package/dist/multishot/index.js.map +1 -1
- package/dist/openapi.json +1 -1
- package/dist/perf/index.d.ts +123 -0
- package/dist/perf/index.js +18 -0
- package/dist/pipelines/index.js +2 -2
- package/dist/{agent-profile-D0PBIWlV.d.ts → pre-registration-DELOEJ8v.d.ts} +144 -4
- package/dist/{provenance-DPpNIOJD.d.ts → provenance-LnqRT0sS.d.ts} +5 -5
- package/dist/{red-team-DW9Ca_tj.d.ts → red-team-BXHil6c8.d.ts} +1 -1
- package/dist/{release-report-hlNtD12q.d.ts → release-report-euXIV_Sk.d.ts} +3 -3
- package/dist/reporting.d.ts +8 -8
- package/dist/reporting.js +3 -3
- package/dist/{researcher-BLPHBbNV.d.ts → researcher-DE6Gpnb4.d.ts} +4 -4
- package/dist/rl.d.ts +194 -656
- package/dist/rl.js +231 -149
- package/dist/rl.js.map +1 -1
- package/dist/{rubric-predictive-validity-CnEl9Jc8.d.ts → rubric-predictive-validity-Cy_W-hWZ.d.ts} +1 -1
- package/dist/{run-campaign-4Y5V5CN3.js → run-campaign-RDGAM5KJ.js} +3 -3
- package/dist/run-campaign-RDGAM5KJ.js.map +1 -0
- package/dist/{run-improvement-loop-CNqQckTj.d.ts → run-improvement-loop-5z_l5zDz.d.ts} +2 -2
- package/dist/{run-record-De9VarXR.d.ts → run-record-e7vj1uZQ.d.ts} +1 -1
- package/dist/{runtime-trajectory-BLRiaifm.d.ts → runtime-trajectory-BDgfGZSr.d.ts} +1 -1
- package/dist/{semantic-concept-judge-DIEgr_6v.d.ts → semantic-concept-judge-Dn8Z6KEG.d.ts} +5 -31
- package/dist/series-convergence-D5OWMBg6.d.ts +33 -0
- package/dist/{statistics-CnC1FMbx.d.ts → statistics-C7PozGrZ.d.ts} +71 -2
- package/dist/{summary-report-Db0dDSWP.d.ts → summary-report-DGmUucwQ.d.ts} +1 -1
- package/dist/traces.d.ts +3 -3
- package/dist/traces.js +8 -6
- package/dist/{types-Cu3u_x59.d.ts → types-2VVIL04s.d.ts} +2 -2
- package/dist/{types-D7lLRYe9.d.ts → types-BU-7W85F.d.ts} +21 -1
- package/dist/{types-CqPax19X.d.ts → types-mn5Aqk7x.d.ts} +1 -1
- package/dist/{verdict-CeEgtjyI.d.ts → verdict-C9MlYujm.d.ts} +3 -0
- package/dist/wire/index.d.ts +3 -3
- package/dist/workflow/index.d.ts +12 -11
- package/dist/workflow/index.js +1 -1
- package/package.json +16 -1
- package/dist/chunk-BABOZOSN.js.map +0 -1
- package/dist/chunk-L5G7OUKD.js.map +0 -1
- package/dist/chunk-SHTXZ4O2.js +0 -113
- package/dist/chunk-SHTXZ4O2.js.map +0 -1
- package/dist/chunk-VIDQF3F5.js.map +0 -1
- package/dist/chunk-VUINJM5M.js.map +0 -1
- package/dist/chunk-YGYXHNAQ.js.map +0 -1
- package/dist/chunk-YV7J7X5N.js.map +0 -1
- /package/dist/{chunk-FZWAFVAA.js.map → chunk-4FBZZIYD.js.map} +0 -0
- /package/dist/{chunk-OTYQPHPL.js.map → chunk-6SOJM3VR.js.map} +0 -0
- /package/dist/{chunk-Z7VFTS2J.js.map → chunk-CY6U5S3X.js.map} +0 -0
- /package/dist/{chunk-WJL2NJXN.js.map → chunk-GSH6QNNS.js.map} +0 -0
- /package/dist/{chunk-RBNA5AZT.js.map → chunk-L3JOU6XM.js.map} +0 -0
- /package/dist/{chunk-ZZ2HOPME.js.map → chunk-TWS7AZEY.js.map} +0 -0
- /package/dist/{run-campaign-4Y5V5CN3.js.map → perf/index.js.map} +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildTrajectory
|
|
3
|
+
} from "./chunk-RZTMDUO7.js";
|
|
4
|
+
import {
|
|
5
|
+
TraceEmitter
|
|
6
|
+
} from "./chunk-TVVP3ZZQ.js";
|
|
7
|
+
import {
|
|
8
|
+
NotFoundError,
|
|
9
|
+
ValidationError
|
|
10
|
+
} from "./chunk-3BFEG2F6.js";
|
|
11
|
+
|
|
12
|
+
// src/counterfactual.ts
|
|
13
|
+
async function runCounterfactual(store, originalRunId, mutation, runner) {
|
|
14
|
+
const originalRun = await store.getRun(originalRunId);
|
|
15
|
+
if (!originalRun) throw new NotFoundError(`counterfactual: run ${originalRunId} not found`);
|
|
16
|
+
const trajectory = await buildTrajectory(store, originalRunId);
|
|
17
|
+
if (mutation.at < 0 || mutation.at >= trajectory.steps.length) {
|
|
18
|
+
throw new ValidationError(
|
|
19
|
+
`counterfactual: mutation.at=${mutation.at} out of range [0, ${trajectory.steps.length})`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const targetStep = trajectory.steps[mutation.at];
|
|
23
|
+
const mutatedStep = applyMutation(targetStep, mutation);
|
|
24
|
+
const cfEmitter = new TraceEmitter(store);
|
|
25
|
+
await cfEmitter.startRun({
|
|
26
|
+
scenarioId: originalRun.scenarioId,
|
|
27
|
+
variantId: originalRun.variantId ? `${originalRun.variantId}+cf:${mutation.kind}@${mutation.at}` : `cf:${mutation.kind}@${mutation.at}`,
|
|
28
|
+
projectId: originalRun.projectId,
|
|
29
|
+
parentRunId: originalRunId,
|
|
30
|
+
layer: "meta",
|
|
31
|
+
tags: { counterfactual: "true", mutationKind: mutation.kind, mutationAt: String(mutation.at) }
|
|
32
|
+
});
|
|
33
|
+
await runner.executeFrom(
|
|
34
|
+
{
|
|
35
|
+
originalRunId,
|
|
36
|
+
originalTrajectory: trajectory,
|
|
37
|
+
prefix: trajectory.steps.slice(0, mutation.at),
|
|
38
|
+
mutation,
|
|
39
|
+
mutatedStep
|
|
40
|
+
},
|
|
41
|
+
cfEmitter
|
|
42
|
+
);
|
|
43
|
+
const counterfactual = await store.getRun(cfEmitter.runId);
|
|
44
|
+
const delta = {
|
|
45
|
+
originalOutcomeScore: originalRun.outcome?.score ?? null,
|
|
46
|
+
counterfactualOutcomeScore: counterfactual?.outcome?.score ?? null,
|
|
47
|
+
deltaScore: originalRun.outcome?.score !== void 0 && counterfactual?.outcome?.score !== void 0 ? counterfactual.outcome.score - originalRun.outcome.score : null
|
|
48
|
+
};
|
|
49
|
+
return { counterfactualRunId: cfEmitter.runId, originalRunId, mutation, delta };
|
|
50
|
+
}
|
|
51
|
+
function applyMutation(step, mutation) {
|
|
52
|
+
if (mutation.kind === "swap-model" && step.span.kind === "llm") {
|
|
53
|
+
const llm = step.span;
|
|
54
|
+
return { ...step, span: { ...llm, model: mutation.newModel } };
|
|
55
|
+
}
|
|
56
|
+
if (mutation.kind === "swap-tool-result" && step.span.kind === "tool") {
|
|
57
|
+
const tool = step.span;
|
|
58
|
+
return { ...step, span: { ...tool, result: mutation.newResult } };
|
|
59
|
+
}
|
|
60
|
+
if (mutation.kind === "inject-system-message" && step.span.kind === "llm") {
|
|
61
|
+
const llm = step.span;
|
|
62
|
+
return {
|
|
63
|
+
...step,
|
|
64
|
+
span: {
|
|
65
|
+
...llm,
|
|
66
|
+
messages: [{ role: "system", content: mutation.content }, ...llm.messages]
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (mutation.kind === "custom") return mutation.apply(step);
|
|
71
|
+
return step;
|
|
72
|
+
}
|
|
73
|
+
function attributeCounterfactuals(results) {
|
|
74
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
const arr = grouped.get(r.mutation.kind) ?? [];
|
|
77
|
+
arr.push(r);
|
|
78
|
+
grouped.set(r.mutation.kind, arr);
|
|
79
|
+
}
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const [kind, items] of grouped) {
|
|
82
|
+
const deltas = items.map((i) => i.delta.deltaScore).filter((d) => typeof d === "number");
|
|
83
|
+
if (deltas.length === 0) continue;
|
|
84
|
+
const meanAbs = deltas.reduce((a, b) => a + Math.abs(b), 0) / deltas.length;
|
|
85
|
+
const meanSigned = deltas.reduce((a, b) => a + b, 0) / deltas.length;
|
|
86
|
+
out.push({
|
|
87
|
+
mutationKind: kind,
|
|
88
|
+
n: deltas.length,
|
|
89
|
+
meanAbsDelta: meanAbs,
|
|
90
|
+
meanSignedDelta: meanSigned
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out.sort((a, b) => b.meanAbsDelta - a.meanAbsDelta);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
runCounterfactual,
|
|
98
|
+
attributeCounterfactuals
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=chunk-REVYNR6C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/counterfactual.ts"],"sourcesContent":["/**\n * Counterfactual replay — \"what would have happened if we'd changed\n * exactly one thing at turn N?\"\n *\n * The framework does NOT drive the agent — it sets up the replay\n * context (prior spans, prior state, mutation spec) and records the\n * resulting divergence. Consumers supply an `executeFrom(ctx)` callback\n * that runs their agent starting from turn N with the mutation applied.\n *\n * Counterfactual runs are recorded as a new Run with `layer='meta'` and\n * `parentRunId = originalRunId`, so downstream diff + correlation\n * pipelines see them natively.\n */\n\nimport { NotFoundError, ValidationError } from './errors'\nimport { TraceEmitter } from './trace/emitter'\nimport type { LlmSpan, Span, ToolSpan } from './trace/schema'\nimport type { TraceStore } from './trace/store'\nimport { buildTrajectory, type Trajectory, type TrajectoryStep } from './trajectory'\n\nexport type CounterfactualMutation =\n | { kind: 'swap-model'; at: number; newModel: string }\n | { kind: 'swap-tool-result'; at: number; newResult: unknown }\n | { kind: 'truncate-after'; at: number }\n | { kind: 'inject-system-message'; at: number; content: string }\n | {\n kind: 'custom'\n at: number\n describe: string\n apply: (step: TrajectoryStep) => TrajectoryStep\n }\n\nexport interface CounterfactualContext {\n originalRunId: string\n originalTrajectory: Trajectory\n /** Steps up to (but not including) the mutation point — the prefix the\n * replayed agent inherits as its prior conversation/tool history. */\n prefix: TrajectoryStep[]\n mutation: CounterfactualMutation\n /** Pre-applied mutation on the step at `mutation.at`. Consumers use this\n * as the FIRST step the replayed agent emits (they decide whether to\n * re-emit it or continue from there). */\n mutatedStep: TrajectoryStep\n}\n\nexport interface CounterfactualResult {\n counterfactualRunId: string\n originalRunId: string\n mutation: CounterfactualMutation\n /** Structured delta summary — caller can extend via scoring. */\n delta: {\n originalOutcomeScore: number | null\n counterfactualOutcomeScore: number | null\n deltaScore: number | null\n }\n}\n\nexport interface CounterfactualRunner {\n /**\n * Execute the agent from `ctx.prefix` with the mutation applied.\n * MUST emit spans into the provided emitter so they become part of\n * the counterfactual run. MUST call emitter.endRun() with a verdict.\n */\n executeFrom: (ctx: CounterfactualContext, emitter: TraceEmitter) => Promise<void>\n}\n\nexport async function runCounterfactual(\n store: TraceStore,\n originalRunId: string,\n mutation: CounterfactualMutation,\n runner: CounterfactualRunner,\n): Promise<CounterfactualResult> {\n const originalRun = await store.getRun(originalRunId)\n if (!originalRun) throw new NotFoundError(`counterfactual: run ${originalRunId} not found`)\n const trajectory = await buildTrajectory(store, originalRunId)\n if (mutation.at < 0 || mutation.at >= trajectory.steps.length) {\n throw new ValidationError(\n `counterfactual: mutation.at=${mutation.at} out of range [0, ${trajectory.steps.length})`,\n )\n }\n const targetStep = trajectory.steps[mutation.at]!\n const mutatedStep = applyMutation(targetStep, mutation)\n\n const cfEmitter = new TraceEmitter(store)\n await cfEmitter.startRun({\n scenarioId: originalRun.scenarioId,\n variantId: originalRun.variantId\n ? `${originalRun.variantId}+cf:${mutation.kind}@${mutation.at}`\n : `cf:${mutation.kind}@${mutation.at}`,\n projectId: originalRun.projectId,\n parentRunId: originalRunId,\n layer: 'meta',\n tags: { counterfactual: 'true', mutationKind: mutation.kind, mutationAt: String(mutation.at) },\n })\n\n await runner.executeFrom(\n {\n originalRunId,\n originalTrajectory: trajectory,\n prefix: trajectory.steps.slice(0, mutation.at),\n mutation,\n mutatedStep,\n },\n cfEmitter,\n )\n\n const counterfactual = await store.getRun(cfEmitter.runId)\n const delta = {\n originalOutcomeScore: originalRun.outcome?.score ?? null,\n counterfactualOutcomeScore: counterfactual?.outcome?.score ?? null,\n deltaScore:\n originalRun.outcome?.score !== undefined && counterfactual?.outcome?.score !== undefined\n ? counterfactual.outcome.score - originalRun.outcome.score\n : null,\n }\n return { counterfactualRunId: cfEmitter.runId, originalRunId, mutation, delta }\n}\n\nfunction applyMutation(step: TrajectoryStep, mutation: CounterfactualMutation): TrajectoryStep {\n if (mutation.kind === 'swap-model' && step.span.kind === 'llm') {\n const llm = step.span as LlmSpan\n return { ...step, span: { ...llm, model: mutation.newModel } }\n }\n if (mutation.kind === 'swap-tool-result' && step.span.kind === 'tool') {\n const tool = step.span as ToolSpan\n return { ...step, span: { ...tool, result: mutation.newResult } }\n }\n if (mutation.kind === 'inject-system-message' && step.span.kind === 'llm') {\n const llm = step.span as LlmSpan\n return {\n ...step,\n span: {\n ...llm,\n messages: [{ role: 'system', content: mutation.content }, ...llm.messages],\n },\n }\n }\n if (mutation.kind === 'custom') return mutation.apply(step)\n // swap-tool-result on non-tool span / swap-model on non-llm / truncate-after: no step-level change.\n return step\n}\n\n/**\n * Aggregate a batch of counterfactuals into a simple attribution table:\n * which mutation kinds move outcomes most? (Useful when you run a grid\n * over the same trajectory — swap-model at every llm span, swap-tool\n * at every tool span — and want a ranked summary.)\n */\nexport function attributeCounterfactuals(results: CounterfactualResult[]): Array<{\n mutationKind: CounterfactualMutation['kind']\n n: number\n meanAbsDelta: number\n meanSignedDelta: number\n}> {\n const grouped = new Map<string, CounterfactualResult[]>()\n for (const r of results) {\n const arr = grouped.get(r.mutation.kind) ?? []\n arr.push(r)\n grouped.set(r.mutation.kind, arr)\n }\n const out: Array<{\n mutationKind: CounterfactualMutation['kind']\n n: number\n meanAbsDelta: number\n meanSignedDelta: number\n }> = []\n for (const [kind, items] of grouped) {\n const deltas = items\n .map((i) => i.delta.deltaScore)\n .filter((d): d is number => typeof d === 'number')\n if (deltas.length === 0) continue\n const meanAbs = deltas.reduce((a, b) => a + Math.abs(b), 0) / deltas.length\n const meanSigned = deltas.reduce((a, b) => a + b, 0) / deltas.length\n out.push({\n mutationKind: kind as CounterfactualMutation['kind'],\n n: deltas.length,\n meanAbsDelta: meanAbs,\n meanSignedDelta: meanSigned,\n })\n }\n return out.sort((a, b) => b.meanAbsDelta - a.meanAbsDelta)\n}\n\n// Re-export Span type for consumer ergonomics.\nexport type { Span }\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/perf/integrity.ts
|
|
2
|
+
function isMissing(value) {
|
|
3
|
+
return value === null || value === void 0;
|
|
4
|
+
}
|
|
5
|
+
function checkRecordIntegrity(records, resolveJourney) {
|
|
6
|
+
const violations = [];
|
|
7
|
+
for (const [recordIndex, record] of records.entries()) {
|
|
8
|
+
if (record.pass !== true) continue;
|
|
9
|
+
const journey = resolveJourney(record);
|
|
10
|
+
if (journey === null) continue;
|
|
11
|
+
for (const field of journey.requiredFields) {
|
|
12
|
+
if (isMissing(record[field])) {
|
|
13
|
+
violations.push({
|
|
14
|
+
recordIndex,
|
|
15
|
+
journeyId: journey.id,
|
|
16
|
+
field,
|
|
17
|
+
reason: "null-required-field",
|
|
18
|
+
detail: `required field '${field}' is ${record[field] === null ? "null" : "undefined"} on a passing '${journey.id}' record`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const field of journey.phaseFields ?? []) {
|
|
23
|
+
if (isMissing(record[field])) {
|
|
24
|
+
violations.push({
|
|
25
|
+
recordIndex,
|
|
26
|
+
journeyId: journey.id,
|
|
27
|
+
field,
|
|
28
|
+
reason: "null-required-field",
|
|
29
|
+
detail: `phase field '${field}' is ${record[field] === null ? "null" : "undefined"} on a passing '${journey.id}' record`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const { field, min } of journey.minimums ?? []) {
|
|
34
|
+
const value = record[field];
|
|
35
|
+
if (isMissing(value)) continue;
|
|
36
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
37
|
+
violations.push({
|
|
38
|
+
recordIndex,
|
|
39
|
+
journeyId: journey.id,
|
|
40
|
+
field,
|
|
41
|
+
reason: "below-minimum",
|
|
42
|
+
detail: `field '${field}' has non-numeric value ${JSON.stringify(value)} on a passing '${journey.id}' record (minimum ${min})`
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (value < min) {
|
|
47
|
+
violations.push({
|
|
48
|
+
recordIndex,
|
|
49
|
+
journeyId: journey.id,
|
|
50
|
+
field,
|
|
51
|
+
reason: "below-minimum",
|
|
52
|
+
detail: `field '${field}' is ${value}, below minimum ${min} on a passing '${journey.id}' record`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { succeeded: violations.length === 0, violations };
|
|
58
|
+
}
|
|
59
|
+
function assertRecordIntegrity(records, resolveJourney) {
|
|
60
|
+
const result = checkRecordIntegrity(records, resolveJourney);
|
|
61
|
+
if (result.succeeded) return;
|
|
62
|
+
const lines = result.violations.map(
|
|
63
|
+
(v) => ` [record ${v.recordIndex}] ${v.journeyId}.${v.field} (${v.reason}): ${v.detail}`
|
|
64
|
+
);
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Record integrity check failed with ${result.violations.length} violation(s):
|
|
67
|
+
${lines.join("\n")}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/perf/journey.ts
|
|
72
|
+
function scenarioKey(journeyId, axes) {
|
|
73
|
+
const parts = Object.keys(axes).sort().map((dim) => `${dim}=${axes[dim]}`);
|
|
74
|
+
return [journeyId, ...parts].join("|");
|
|
75
|
+
}
|
|
76
|
+
function expandMatrix(journeys, axes, filter) {
|
|
77
|
+
const dims = Object.keys(axes).sort();
|
|
78
|
+
let combos = [{}];
|
|
79
|
+
for (const dim of dims) {
|
|
80
|
+
const values = axes[dim];
|
|
81
|
+
const next = [];
|
|
82
|
+
for (const combo of combos) {
|
|
83
|
+
for (const value of values) {
|
|
84
|
+
next.push({ ...combo, [dim]: value });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
combos = next;
|
|
88
|
+
}
|
|
89
|
+
const scenarios = [];
|
|
90
|
+
for (const journey of journeys) {
|
|
91
|
+
for (const combo of combos) {
|
|
92
|
+
if (filter && !filter(journey.id, combo)) continue;
|
|
93
|
+
scenarios.push({ key: scenarioKey(journey.id, combo), journey, axes: combo });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return scenarios;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/perf/ratchet.ts
|
|
100
|
+
function nearestRank(sorted, p) {
|
|
101
|
+
if (sorted.length === 0) throw new Error("nearestRank requires at least one sample");
|
|
102
|
+
const rank = Math.max(1, Math.ceil(p / 100 * sorted.length));
|
|
103
|
+
return sorted[rank - 1];
|
|
104
|
+
}
|
|
105
|
+
function summarizeRecords(records, keyOf, metricFields) {
|
|
106
|
+
const samples = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const record of records) {
|
|
108
|
+
const key = keyOf(record);
|
|
109
|
+
if (key === null) continue;
|
|
110
|
+
let byField = samples.get(key);
|
|
111
|
+
if (!byField) {
|
|
112
|
+
byField = /* @__PURE__ */ new Map();
|
|
113
|
+
samples.set(key, byField);
|
|
114
|
+
}
|
|
115
|
+
for (const field of metricFields) {
|
|
116
|
+
const value = record[field];
|
|
117
|
+
if (typeof value !== "number" || Number.isNaN(value)) continue;
|
|
118
|
+
let values = byField.get(field);
|
|
119
|
+
if (!values) {
|
|
120
|
+
values = [];
|
|
121
|
+
byField.set(field, values);
|
|
122
|
+
}
|
|
123
|
+
values.push(value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const scenarios = {};
|
|
127
|
+
for (const [key, byField] of samples) {
|
|
128
|
+
const stats = {};
|
|
129
|
+
for (const [field, values] of byField) {
|
|
130
|
+
if (values.length === 0) continue;
|
|
131
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
132
|
+
stats[field] = {
|
|
133
|
+
p50: nearestRank(sorted, 50),
|
|
134
|
+
p90: nearestRank(sorted, 90),
|
|
135
|
+
n: sorted.length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
scenarios[key] = stats;
|
|
139
|
+
}
|
|
140
|
+
return { version: 1, scenarios };
|
|
141
|
+
}
|
|
142
|
+
function pctOver(currentValue, baselineValue) {
|
|
143
|
+
if (baselineValue === 0) {
|
|
144
|
+
return currentValue === 0 ? 0 : Number.POSITIVE_INFINITY;
|
|
145
|
+
}
|
|
146
|
+
return (currentValue - baselineValue) / baselineValue * 100;
|
|
147
|
+
}
|
|
148
|
+
function gatePerf(current, baseline, options) {
|
|
149
|
+
const tolerancePct = options?.tolerancePct ?? 10;
|
|
150
|
+
const minSamples = options?.minSamples ?? 3;
|
|
151
|
+
const regressions = [];
|
|
152
|
+
const improvements = [];
|
|
153
|
+
const missing = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const [scenarioKey2, baselineStats] of Object.entries(baseline.scenarios)) {
|
|
155
|
+
const currentStats = current.scenarios[scenarioKey2];
|
|
156
|
+
if (!currentStats) {
|
|
157
|
+
missing.add(scenarioKey2);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const [field, baselineStat] of Object.entries(baselineStats)) {
|
|
161
|
+
const currentStat = currentStats[field];
|
|
162
|
+
if (!currentStat || currentStat.n < minSamples) {
|
|
163
|
+
missing.add(scenarioKey2);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const overBy = {
|
|
167
|
+
p50Pct: pctOver(currentStat.p50, baselineStat.p50),
|
|
168
|
+
p90Pct: pctOver(currentStat.p90, baselineStat.p90)
|
|
169
|
+
};
|
|
170
|
+
const entry = {
|
|
171
|
+
scenarioKey: scenarioKey2,
|
|
172
|
+
field,
|
|
173
|
+
baseline: baselineStat,
|
|
174
|
+
current: currentStat,
|
|
175
|
+
overBy
|
|
176
|
+
};
|
|
177
|
+
if (overBy.p50Pct > tolerancePct || overBy.p90Pct > tolerancePct) {
|
|
178
|
+
regressions.push(entry);
|
|
179
|
+
} else if (overBy.p50Pct < 0 && overBy.p90Pct < 0) {
|
|
180
|
+
improvements.push(entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const newScenarios = Object.keys(current.scenarios).filter((key) => !(key in baseline.scenarios));
|
|
185
|
+
return {
|
|
186
|
+
succeeded: regressions.length === 0,
|
|
187
|
+
regressions,
|
|
188
|
+
improvements,
|
|
189
|
+
missingScenarios: [...missing],
|
|
190
|
+
newScenarios
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
checkRecordIntegrity,
|
|
196
|
+
assertRecordIntegrity,
|
|
197
|
+
scenarioKey,
|
|
198
|
+
expandMatrix,
|
|
199
|
+
summarizeRecords,
|
|
200
|
+
gatePerf
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=chunk-STGVSCDH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/perf/integrity.ts","../src/perf/journey.ts","../src/perf/ratchet.ts"],"sourcesContent":["/**\n * Record-integrity contracts for perf metric records.\n *\n * A record that claims `pass === true` must actually carry the journey's\n * required measurements — a \"passing\" provision run with a null\n * `total_ms` is a lying record, not a pass. Failed records are exempt:\n * a run that errored mid-flight legitimately has nulls.\n */\n\nimport type { JourneySpec } from './journey'\n\nexport interface IntegrityViolation {\n recordIndex: number\n journeyId: string\n field: string\n reason: 'null-required-field' | 'below-minimum'\n detail: string\n}\n\nexport interface IntegrityResult {\n succeeded: boolean\n violations: IntegrityViolation[]\n}\n\nfunction isMissing(value: unknown): boolean {\n return value === null || value === undefined\n}\n\n/**\n * Validates flat metric records (Record<string, unknown> with a boolean\n * `pass` field) against their journey contract. Only records with\n * pass === true are checked — a failed record may legitimately have nulls.\n * resolveJourney maps a record to its JourneySpec (or null to skip).\n */\nexport function checkRecordIntegrity(\n records: ReadonlyArray<Record<string, unknown>>,\n resolveJourney: (record: Record<string, unknown>) => JourneySpec | null,\n): IntegrityResult {\n const violations: IntegrityViolation[] = []\n for (const [recordIndex, record] of records.entries()) {\n if (record.pass !== true) continue\n const journey = resolveJourney(record)\n if (journey === null) continue\n for (const field of journey.requiredFields) {\n if (isMissing(record[field])) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'null-required-field',\n detail: `required field '${field}' is ${record[field] === null ? 'null' : 'undefined'} on a passing '${journey.id}' record`,\n })\n }\n }\n for (const field of journey.phaseFields ?? []) {\n if (isMissing(record[field])) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'null-required-field',\n detail: `phase field '${field}' is ${record[field] === null ? 'null' : 'undefined'} on a passing '${journey.id}' record`,\n })\n }\n }\n for (const { field, min } of journey.minimums ?? []) {\n const value = record[field]\n if (isMissing(value)) continue // null-ness is the required/phase fields' contract\n if (typeof value !== 'number' || Number.isNaN(value)) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'below-minimum',\n detail: `field '${field}' has non-numeric value ${JSON.stringify(value)} on a passing '${journey.id}' record (minimum ${min})`,\n })\n continue\n }\n if (value < min) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'below-minimum',\n detail: `field '${field}' is ${value}, below minimum ${min} on a passing '${journey.id}' record`,\n })\n }\n }\n }\n return { succeeded: violations.length === 0, violations }\n}\n\n/** Throws an Error listing every violation when the result fails. */\nexport function assertRecordIntegrity(\n records: ReadonlyArray<Record<string, unknown>>,\n resolveJourney: (record: Record<string, unknown>) => JourneySpec | null,\n): void {\n const result = checkRecordIntegrity(records, resolveJourney)\n if (result.succeeded) return\n const lines = result.violations.map(\n (v) => ` [record ${v.recordIndex}] ${v.journeyId}.${v.field} (${v.reason}): ${v.detail}`,\n )\n throw new Error(\n `Record integrity check failed with ${result.violations.length} violation(s):\\n${lines.join('\\n')}`,\n )\n}\n","/**\n * Journey × axes matrix for infra performance benchmarks.\n *\n * A journey is one measurable user path (\"provision.cold\", \"chat.ttft\");\n * axes are free-form scenario dimensions (driver, region, image…). The\n * matrix expansion is pure bookkeeping — running the scenarios and\n * recording metrics is the caller's job. This module complements the\n * judge-panel `BenchmarkRunner` (src/benchmark.ts): that one scores\n * QUALITY via judges, this one structures LATENCY / RELIABILITY runs\n * over flat metric records.\n */\n\n/** One measurable user journey (e.g. \"provision.cold\", \"chat.ttft\"). */\nexport interface JourneySpec {\n id: string\n description: string\n /** Needs a real LLM call — schedule nightly, not per-PR. */\n requiresLLM: boolean\n /**\n * Fields that MUST be non-null on a passing record of this journey.\n * A \"passing\" record missing one is an integrity violation, not a pass.\n */\n requiredFields: ReadonlyArray<string>\n /** Numeric floors, e.g. {field: 'event_count', min: 1} for streaming. */\n minimums?: ReadonlyArray<{ field: string; min: number }>\n /** Per-phase breakdown fields expected non-null (subset of requiredFields semantics, reported separately). */\n phaseFields?: ReadonlyArray<string>\n}\n\nexport interface ScenarioAxes {\n /** e.g. driver: ['docker','firecracker'] — every key is a free-form dimension. */\n [dimension: string]: ReadonlyArray<string>\n}\n\nexport interface PerfScenario {\n /** `${journeyId}|${dim1}=${v1}|${dim2}=${v2}` (dims sorted). */\n key: string\n journey: JourneySpec\n axes: Record<string, string>\n}\n\n/** Stable scenario key: journey id then `dim=value` pairs in sorted-dim order. */\nexport function scenarioKey(journeyId: string, axes: Record<string, string>): string {\n const parts = Object.keys(axes)\n .sort()\n .map((dim) => `${dim}=${axes[dim]}`)\n return [journeyId, ...parts].join('|')\n}\n\n/** Cartesian expansion; `filter` lets callers drop invalid combos (e.g. firecracker×resume). */\nexport function expandMatrix(\n journeys: ReadonlyArray<JourneySpec>,\n axes: ScenarioAxes,\n filter?: (journeyId: string, combo: Record<string, string>) => boolean,\n): PerfScenario[] {\n const dims = Object.keys(axes).sort()\n let combos: Record<string, string>[] = [{}]\n for (const dim of dims) {\n const values = axes[dim] as ReadonlyArray<string>\n const next: Record<string, string>[] = []\n for (const combo of combos) {\n for (const value of values) {\n next.push({ ...combo, [dim]: value })\n }\n }\n combos = next\n }\n const scenarios: PerfScenario[] = []\n for (const journey of journeys) {\n for (const combo of combos) {\n if (filter && !filter(journey.id, combo)) continue\n scenarios.push({ key: scenarioKey(journey.id, combo), journey, axes: combo })\n }\n }\n return scenarios\n}\n","/**\n * Percentile ratchet over perf metric records.\n *\n * `summarizeRecords` folds flat records into per-scenario p50/p90 stats;\n * `gatePerf` compares a current summary against a committed baseline and\n * trips on regressions beyond tolerance. Percentiles use nearest-rank on\n * sorted values. Null / non-numeric metric values are excluded from the\n * stat (n reflects only real samples); a field with zero samples is\n * omitted entirely — no fake zeros.\n */\n\nexport interface PerfStat {\n p50: number\n p90: number\n n: number\n}\n\nexport interface PerfBaseline {\n version: 1\n /** key → metric field → stat. */\n scenarios: Record<string, Record<string, PerfStat>>\n}\n\nexport interface PerfRegression {\n scenarioKey: string\n field: string\n baseline: PerfStat\n current: PerfStat\n /** percent over baseline p50 / p90, whichever tripped. */\n overBy: { p50Pct: number; p90Pct: number }\n}\n\nexport interface PerfGateResult {\n succeeded: boolean\n regressions: PerfRegression[]\n /** Negative overBy: strictly better than baseline on both percentiles. */\n improvements: PerfRegression[]\n /** In baseline but absent (or under-sampled, n < minSamples) in current. */\n missingScenarios: string[]\n /** In records but absent from baseline. */\n newScenarios: string[]\n}\n\n/** Nearest-rank percentile over a non-empty ascending-sorted array (p in (0, 100]). */\nfunction nearestRank(sorted: ReadonlyArray<number>, p: number): number {\n if (sorted.length === 0) throw new Error('nearestRank requires at least one sample')\n const rank = Math.max(1, Math.ceil((p / 100) * sorted.length))\n return sorted[rank - 1] as number\n}\n\nexport function summarizeRecords(\n records: ReadonlyArray<Record<string, unknown>>,\n keyOf: (record: Record<string, unknown>) => string | null,\n metricFields: ReadonlyArray<string>,\n): PerfBaseline {\n const samples = new Map<string, Map<string, number[]>>()\n for (const record of records) {\n const key = keyOf(record)\n if (key === null) continue\n let byField = samples.get(key)\n if (!byField) {\n byField = new Map()\n samples.set(key, byField)\n }\n for (const field of metricFields) {\n const value = record[field]\n if (typeof value !== 'number' || Number.isNaN(value)) continue\n let values = byField.get(field)\n if (!values) {\n values = []\n byField.set(field, values)\n }\n values.push(value)\n }\n }\n const scenarios: Record<string, Record<string, PerfStat>> = {}\n for (const [key, byField] of samples) {\n const stats: Record<string, PerfStat> = {}\n for (const [field, values] of byField) {\n if (values.length === 0) continue\n const sorted = [...values].sort((a, b) => a - b)\n stats[field] = {\n p50: nearestRank(sorted, 50),\n p90: nearestRank(sorted, 90),\n n: sorted.length,\n }\n }\n scenarios[key] = stats\n }\n return { version: 1, scenarios }\n}\n\n/** Percent over baseline; baseline 0 → 0% when equal, Infinity when current grew. */\nfunction pctOver(currentValue: number, baselineValue: number): number {\n if (baselineValue === 0) {\n return currentValue === 0 ? 0 : Number.POSITIVE_INFINITY\n }\n return ((currentValue - baselineValue) / baselineValue) * 100\n}\n\nexport function gatePerf(\n current: PerfBaseline,\n baseline: PerfBaseline,\n options?: { tolerancePct?: number; minSamples?: number },\n): PerfGateResult {\n const tolerancePct = options?.tolerancePct ?? 10\n const minSamples = options?.minSamples ?? 3\n const regressions: PerfRegression[] = []\n const improvements: PerfRegression[] = []\n const missing = new Set<string>()\n\n for (const [scenarioKey, baselineStats] of Object.entries(baseline.scenarios)) {\n const currentStats = current.scenarios[scenarioKey]\n if (!currentStats) {\n missing.add(scenarioKey)\n continue\n }\n for (const [field, baselineStat] of Object.entries(baselineStats)) {\n const currentStat = currentStats[field]\n if (!currentStat || currentStat.n < minSamples) {\n // Under-sampled current data cannot gate — surface it instead of\n // pretending the scenario passed.\n missing.add(scenarioKey)\n continue\n }\n const overBy = {\n p50Pct: pctOver(currentStat.p50, baselineStat.p50),\n p90Pct: pctOver(currentStat.p90, baselineStat.p90),\n }\n const entry: PerfRegression = {\n scenarioKey,\n field,\n baseline: baselineStat,\n current: currentStat,\n overBy,\n }\n if (overBy.p50Pct > tolerancePct || overBy.p90Pct > tolerancePct) {\n regressions.push(entry)\n } else if (overBy.p50Pct < 0 && overBy.p90Pct < 0) {\n improvements.push(entry)\n }\n }\n }\n\n const newScenarios = Object.keys(current.scenarios).filter((key) => !(key in baseline.scenarios))\n\n return {\n succeeded: regressions.length === 0,\n regressions,\n improvements,\n missingScenarios: [...missing],\n newScenarios,\n }\n}\n"],"mappings":"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","names":["scenarioKey"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
confidenceInterval
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LMZQ2Z4U.js";
|
|
4
4
|
import {
|
|
5
5
|
AgentEvalError
|
|
6
6
|
} from "./chunk-3BFEG2F6.js";
|
|
@@ -538,4 +538,4 @@ export {
|
|
|
538
538
|
inMemoryCampaignStorage,
|
|
539
539
|
runCampaign
|
|
540
540
|
};
|
|
541
|
-
//# sourceMappingURL=chunk-
|
|
541
|
+
//# sourceMappingURL=chunk-TWS7AZEY.js.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OtlpFileTraceStore,
|
|
3
|
+
TraceFileMissingError,
|
|
4
|
+
buildTraceAnalystTools
|
|
5
|
+
} from "./chunk-QAY5UIJO.js";
|
|
6
|
+
|
|
7
|
+
// src/trace-analyst/prompts.ts
|
|
8
|
+
var TRACE_ANALYST_ACTOR_DESCRIPTION = `You answer questions about an OTLP-shaped JSONL trace dataset using the trace tools provided in the \`traces\` namespace.
|
|
9
|
+
|
|
10
|
+
DISCOVERY \u2192 NARROW \u2192 DEEP-READ protocol \u2014 follow exactly:
|
|
11
|
+
|
|
12
|
+
1. ALWAYS call \`traces.getDatasetOverview({})\` FIRST without a regex_pattern. The result tells you total_traces, raw_jsonl_bytes, services, agents, models, and sample_trace_ids (real ids \u2014 never fabricate one).
|
|
13
|
+
|
|
14
|
+
2. Use raw_jsonl_bytes to gauge how expensive raw scans will be. \`filters.regex_pattern\` is the one scan-heavy filter on getDatasetOverview / queryTraces / countTraces \u2014 narrow with indexed fields (has_errors, model_names, service_names, agent_names, time bounds) BEFORE adding a regex on a large dataset.
|
|
15
|
+
|
|
16
|
+
3. To list more traces than the sample, call \`traces.queryTraces({ filters?, limit, offset? })\`. Each summary carries raw_jsonl_bytes \u2014 use it to choose between viewTrace and searchTrace BEFORE calling either.
|
|
17
|
+
|
|
18
|
+
4. Per-trace inspection:
|
|
19
|
+
- SMALL trace (raw_jsonl_bytes well under 150_000): call \`traces.viewTrace({ trace_id })\`. Returns all spans. Per-attribute payloads are head-capped at ~4KB; large \`input.value\` / \`output.value\` / \`llm.input_messages\` will show a \`[trace-analyst truncated: N bytes]\` marker.
|
|
20
|
+
- LARGE trace (raw_jsonl_bytes near or above 150_000, or you saw an \`oversized\` response): use \`traces.searchTrace({ trace_id, regex_pattern })\` to get bounded SpanMatchRecords (span metadata + matched text + surrounding context). Then call \`traces.viewSpans({ trace_id, span_ids: [...] })\` for surgical reads (~16KB cap, 4\xD7 higher than discovery), or \`traces.searchSpan({ trace_id, span_id, regex_pattern })\` for one large span. Stays bounded regardless of trace size.
|
|
21
|
+
- Useful regex patterns: \`STATUS_CODE_ERROR\` (failures), tool names like \`grep\` or \`view_trace\`, error strings like \`MaxTurnsExceeded\`, model names, attribute keys.
|
|
22
|
+
|
|
23
|
+
5. ONLY call viewTrace / viewSpans / searchTrace / searchSpan with trace/span ids you have already seen in sample_trace_ids, a queryTraces page, or a previous search result. Never invent ids.
|
|
24
|
+
|
|
25
|
+
5a. **Result-shape contract** \u2014 searchTrace and searchSpan return \`{ trace_id, hits, total_matches, has_more }\`. Iterate \`result.hits\` (NOT result.matches). Each hit has \`{ span_id, span_name, span_kind, attribute_path, matched_text, context_before, context_after, match_offset }\`. viewTrace returns \`{ trace_id, spans }\` (or \`oversized\`). viewSpans returns \`{ trace_id, spans, missing_span_ids, truncated_attribute_count }\`. Never assume a field name \u2014 log the result shape first if unsure.
|
|
26
|
+
|
|
27
|
+
6. If viewTrace returns an \`oversized\` summary instead of \`spans\`, DO NOT retry the same call. Read the summary's top_span_names, span_count, span_response_bytes_max, error_span_count to plan a follow-up: switch to searchTrace (or searchSpan for one large span), then viewSpans on a smaller, surgical span_ids set.
|
|
28
|
+
|
|
29
|
+
7. If searchTrace or searchSpan returns has_more=true, REFINE the regex to be more specific rather than blindly raising max_matches.
|
|
30
|
+
|
|
31
|
+
8. If a tool errors (invalid regex, range error), STOP and reconsider \u2014 don't retry with a guessed id or argument. Use the discovery tools above to recover.
|
|
32
|
+
|
|
33
|
+
9. If a ~4KB-truncated payload from viewTrace / searchTrace matters for your answer, first try viewSpans on that span id (~16KB cap). If a 16KB-truncated payload from viewSpans still matters, narrow further with searchSpan against a more specific regex rather than asking for the full payload again.
|
|
34
|
+
|
|
35
|
+
10. If maxDepth > 0 and the question splits into independent semantic branches, delegate well-defined subtasks to subagents using \`await llmQuery(...)\`. Pass narrow context and a focused query. Examples:
|
|
36
|
+
|
|
37
|
+
const reviews = await llmQuery([
|
|
38
|
+
{ query: 'Drill into trace abc123 \u2014 what tool calls preceded the failure?', context: { trace_id: 'abc123' } },
|
|
39
|
+
{ query: 'Drill into trace def456 \u2014 same failure mode?', context: { trace_id: 'def456' } },
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
OBSERVABILITY rules:
|
|
43
|
+
- Each non-final actor turn must emit at least one \`console.log(...)\` for evidence. Up to 3 logs per turn is fine when correlating multiple data sources (e.g. one log for findings list, one for source-file content, one for derived analysis).
|
|
44
|
+
- Do NOT combine \`console.log\` with \`final(...)\` or \`askClarification(...)\` in the same turn \u2014 finish gathering data first, then call final on its own turn.
|
|
45
|
+
- Reuse runtime variables across turns; don't recompute.
|
|
46
|
+
- When done, call \`await final(answer)\` with the fully-formed report. The responder rewrites the answer into output fields; if you only pass a vague summary string the responder has nothing concrete to format.
|
|
47
|
+
|
|
48
|
+
CRITICAL \u2014 \`final()\` payload contract for evidence-grounded analysis tasks:
|
|
49
|
+
- Pass a STRUCTURED object as the second arg with the actual data the responder needs to format the answer. Do NOT pass abstract instructions; pass evidence.
|
|
50
|
+
- Example for per-item verdict tasks:
|
|
51
|
+
\`\`\`js
|
|
52
|
+
await final("Format the per-item verdict report from the evidence below.", {
|
|
53
|
+
findings: [
|
|
54
|
+
{ id: 'sub-1-finding-1', claim: '...', verdict: 'TRUE-POSITIVE', evidence: 'lines 42-45 of contracts/X.sol show ...' },
|
|
55
|
+
...all items
|
|
56
|
+
],
|
|
57
|
+
systemic_summary: '3 sentences I wrote based on the evidence above'
|
|
58
|
+
});
|
|
59
|
+
\`\`\`
|
|
60
|
+
- Calling \`final("answer", {})\` with no evidence is a failure mode \u2014 the responder will hallucinate or echo back the field names. Always include the gathered data.
|
|
61
|
+
- Premature final after a single viewSpans call is INSUFFICIENT for per-finding analysis tasks. Read the requested attributes (e.g. \`spans[i].attributes['redteam.finding.title']\`), and for each one perform the requested cross-reference (e.g. read the source SPAN's \`attributes['source.content']\`).
|
|
62
|
+
|
|
63
|
+
OUTPUT contract \u2014 your final answer must include:
|
|
64
|
+
- A clear prose conclusion answering the user's question.
|
|
65
|
+
- Trace ids and span ids cited as evidence for each claim.
|
|
66
|
+
- Failure modes named in the user's domain language, with frequency and concrete examples.
|
|
67
|
+
|
|
68
|
+
Do NOT invent trace ids, span ids, error messages, or model names. Every fact must be traceable to a tool result.`;
|
|
69
|
+
var TRACE_ANALYST_ACTOR_DESCRIPTION_VERSION = "trace-analyst-actor-v5-2026-05-06";
|
|
70
|
+
var TRACE_ANALYST_SUBAGENT_DESCRIPTION = `You are a trace-analyst subagent. Your parent has delegated a focused trace-inspection question. Use the same DISCOVERY \u2192 NARROW \u2192 DEEP-READ protocol but stay tightly scoped: do exactly what was asked, return a concise compact answer, do NOT spawn further subagents unless the parent's question is genuinely multi-branch.
|
|
71
|
+
|
|
72
|
+
Cite trace ids and span ids for every claim. Do NOT invent ids.`;
|
|
73
|
+
|
|
74
|
+
// src/trace-analyst/analyst.ts
|
|
75
|
+
import { AxJSRuntime, agent } from "@ax-llm/ax";
|
|
76
|
+
async function analyzeTraces(input, options) {
|
|
77
|
+
if (!input.question || typeof input.question !== "string") {
|
|
78
|
+
throw new TypeError("analyzeTraces: input.question must be a non-empty string");
|
|
79
|
+
}
|
|
80
|
+
const store = typeof options.source === "string" ? new OtlpFileTraceStore({ path: options.source }) : options.source;
|
|
81
|
+
if (store instanceof OtlpFileTraceStore) {
|
|
82
|
+
await store.ensureIndexed();
|
|
83
|
+
}
|
|
84
|
+
const tools = buildTraceAnalystTools({ store });
|
|
85
|
+
const turns = [];
|
|
86
|
+
let progressFs;
|
|
87
|
+
if (options.progressLogPath) {
|
|
88
|
+
const { createWriteStream } = await import("fs");
|
|
89
|
+
const { mkdir } = await import("fs/promises");
|
|
90
|
+
const { dirname } = await import("path");
|
|
91
|
+
await mkdir(dirname(options.progressLogPath), { recursive: true });
|
|
92
|
+
progressFs = createWriteStream(options.progressLogPath, { flags: "a" });
|
|
93
|
+
}
|
|
94
|
+
const actorTurnCallback = async (turn) => {
|
|
95
|
+
const snap = {
|
|
96
|
+
turn: turn.turn,
|
|
97
|
+
isError: turn.isError,
|
|
98
|
+
code: turn.code,
|
|
99
|
+
output: turn.output,
|
|
100
|
+
thought: turn.thought
|
|
101
|
+
};
|
|
102
|
+
turns.push(snap);
|
|
103
|
+
if (progressFs) {
|
|
104
|
+
try {
|
|
105
|
+
progressFs.write(`${JSON.stringify({ ...snap, ts: Date.now() })}
|
|
106
|
+
`);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options.onTurn) await options.onTurn(snap);
|
|
111
|
+
};
|
|
112
|
+
const maxDepth = options.maxDepth ?? 1;
|
|
113
|
+
const maxTurns = options.maxTurns ?? 12;
|
|
114
|
+
const maxParallelSubagents = options.maxParallelSubagents ?? 2;
|
|
115
|
+
const maxRuntimeChars = options.maxRuntimeChars ?? 6e3;
|
|
116
|
+
const analyst = agent(
|
|
117
|
+
// `reasoning!` is an internal (Ax `!`) scratchpad field: generated first to
|
|
118
|
+
// force reason-before-conclude, stripped from the returned output — so the
|
|
119
|
+
// consumed shape stays { answer, findings }. Brings the trace-analyst to the
|
|
120
|
+
// same prose-first CoT ordering the kind-factory gets from its `report` field.
|
|
121
|
+
"question:string -> reasoning!:string, answer:string, findings:string[]",
|
|
122
|
+
{
|
|
123
|
+
agentIdentity: {
|
|
124
|
+
name: "TraceAnalyst",
|
|
125
|
+
description: "Analyzes OTLP-shaped JSONL traces using bounded discovery tools to identify systemic failure modes."
|
|
126
|
+
},
|
|
127
|
+
contextFields: ["question"],
|
|
128
|
+
runtime: new AxJSRuntime({
|
|
129
|
+
permissions: [],
|
|
130
|
+
blockDynamicImport: true,
|
|
131
|
+
allowedModules: [],
|
|
132
|
+
freezeIntrinsics: true,
|
|
133
|
+
blockShadowRealm: true,
|
|
134
|
+
// RLM stdout mode relies on runtime bindings persisting across turns.
|
|
135
|
+
preventGlobalThisExtensions: false
|
|
136
|
+
}),
|
|
137
|
+
mode: maxDepth > 0 ? "advanced" : "simple",
|
|
138
|
+
recursionOptions: maxDepth > 0 ? { maxDepth } : void 0,
|
|
139
|
+
maxTurns,
|
|
140
|
+
maxRuntimeChars,
|
|
141
|
+
maxBatchedLlmQueryConcurrency: maxParallelSubagents,
|
|
142
|
+
promptLevel: "detailed",
|
|
143
|
+
// Trace analysis depends on exact prior tool results and runtime variables.
|
|
144
|
+
contextPolicy: { preset: "full", budget: "balanced" },
|
|
145
|
+
functions: { local: tools },
|
|
146
|
+
actorOptions: {
|
|
147
|
+
description: options.actorDescription ?? TRACE_ANALYST_ACTOR_DESCRIPTION,
|
|
148
|
+
...options.model ? { model: options.model } : {},
|
|
149
|
+
// Keep actor messages tool-call/content shaped across reasoning models.
|
|
150
|
+
showThoughts: false,
|
|
151
|
+
thinkingTokenBudget: "none"
|
|
152
|
+
},
|
|
153
|
+
responderOptions: {
|
|
154
|
+
...options.model ? { model: options.model } : {},
|
|
155
|
+
description: options.subagentDescription ?? TRACE_ANALYST_SUBAGENT_DESCRIPTION,
|
|
156
|
+
showThoughts: false
|
|
157
|
+
},
|
|
158
|
+
actorTurnCallback,
|
|
159
|
+
bubbleErrors: [TraceFileMissingError]
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
let result;
|
|
163
|
+
try {
|
|
164
|
+
result = await analyst.forward(options.ai, { question: input.question });
|
|
165
|
+
} finally {
|
|
166
|
+
if (progressFs) {
|
|
167
|
+
await new Promise((resolve) => progressFs.end(() => resolve()));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
answer: typeof result.answer === "string" ? result.answer : String(result.answer ?? ""),
|
|
172
|
+
findings: Array.isArray(result.findings) ? result.findings.filter((s) => typeof s === "string") : [],
|
|
173
|
+
turns,
|
|
174
|
+
turnCount: turns.length,
|
|
175
|
+
usage: normalizeRoleArrays(analyst.getUsage()),
|
|
176
|
+
chatLog: normalizeRoleArrays(analyst.getChatLog()),
|
|
177
|
+
actorPromptVersion: TRACE_ANALYST_ACTOR_DESCRIPTION_VERSION
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function normalizeRoleArrays(value) {
|
|
181
|
+
const record = value && typeof value === "object" ? value : {};
|
|
182
|
+
return {
|
|
183
|
+
actor: normalizeRecordArray(record.actor),
|
|
184
|
+
responder: normalizeRecordArray(record.responder)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function normalizeRecordArray(value) {
|
|
188
|
+
if (!Array.isArray(value)) return [];
|
|
189
|
+
return value.map(
|
|
190
|
+
(item) => item && typeof item === "object" ? { ...item } : { value: item }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
TRACE_ANALYST_ACTOR_DESCRIPTION,
|
|
196
|
+
TRACE_ANALYST_ACTOR_DESCRIPTION_VERSION,
|
|
197
|
+
TRACE_ANALYST_SUBAGENT_DESCRIPTION,
|
|
198
|
+
analyzeTraces
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=chunk-UHMJT4T7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/trace-analyst/prompts.ts","../src/trace-analyst/analyst.ts"],"sourcesContent":["/** Ax RLM prompt for bounded trace discovery and evidence-backed analysis. */\n\nexport const TRACE_ANALYST_ACTOR_DESCRIPTION = `You answer questions about an OTLP-shaped JSONL trace dataset using the trace tools provided in the \\`traces\\` namespace.\n\nDISCOVERY → NARROW → DEEP-READ protocol — follow exactly:\n\n1. ALWAYS call \\`traces.getDatasetOverview({})\\` FIRST without a regex_pattern. The result tells you total_traces, raw_jsonl_bytes, services, agents, models, and sample_trace_ids (real ids — never fabricate one).\n\n2. Use raw_jsonl_bytes to gauge how expensive raw scans will be. \\`filters.regex_pattern\\` is the one scan-heavy filter on getDatasetOverview / queryTraces / countTraces — narrow with indexed fields (has_errors, model_names, service_names, agent_names, time bounds) BEFORE adding a regex on a large dataset.\n\n3. To list more traces than the sample, call \\`traces.queryTraces({ filters?, limit, offset? })\\`. Each summary carries raw_jsonl_bytes — use it to choose between viewTrace and searchTrace BEFORE calling either.\n\n4. Per-trace inspection:\n - SMALL trace (raw_jsonl_bytes well under 150_000): call \\`traces.viewTrace({ trace_id })\\`. Returns all spans. Per-attribute payloads are head-capped at ~4KB; large \\`input.value\\` / \\`output.value\\` / \\`llm.input_messages\\` will show a \\`[trace-analyst truncated: N bytes]\\` marker.\n - LARGE trace (raw_jsonl_bytes near or above 150_000, or you saw an \\`oversized\\` response): use \\`traces.searchTrace({ trace_id, regex_pattern })\\` to get bounded SpanMatchRecords (span metadata + matched text + surrounding context). Then call \\`traces.viewSpans({ trace_id, span_ids: [...] })\\` for surgical reads (~16KB cap, 4× higher than discovery), or \\`traces.searchSpan({ trace_id, span_id, regex_pattern })\\` for one large span. Stays bounded regardless of trace size.\n - Useful regex patterns: \\`STATUS_CODE_ERROR\\` (failures), tool names like \\`grep\\` or \\`view_trace\\`, error strings like \\`MaxTurnsExceeded\\`, model names, attribute keys.\n\n5. ONLY call viewTrace / viewSpans / searchTrace / searchSpan with trace/span ids you have already seen in sample_trace_ids, a queryTraces page, or a previous search result. Never invent ids.\n\n5a. **Result-shape contract** — searchTrace and searchSpan return \\`{ trace_id, hits, total_matches, has_more }\\`. Iterate \\`result.hits\\` (NOT result.matches). Each hit has \\`{ span_id, span_name, span_kind, attribute_path, matched_text, context_before, context_after, match_offset }\\`. viewTrace returns \\`{ trace_id, spans }\\` (or \\`oversized\\`). viewSpans returns \\`{ trace_id, spans, missing_span_ids, truncated_attribute_count }\\`. Never assume a field name — log the result shape first if unsure.\n\n6. If viewTrace returns an \\`oversized\\` summary instead of \\`spans\\`, DO NOT retry the same call. Read the summary's top_span_names, span_count, span_response_bytes_max, error_span_count to plan a follow-up: switch to searchTrace (or searchSpan for one large span), then viewSpans on a smaller, surgical span_ids set.\n\n7. If searchTrace or searchSpan returns has_more=true, REFINE the regex to be more specific rather than blindly raising max_matches.\n\n8. If a tool errors (invalid regex, range error), STOP and reconsider — don't retry with a guessed id or argument. Use the discovery tools above to recover.\n\n9. If a ~4KB-truncated payload from viewTrace / searchTrace matters for your answer, first try viewSpans on that span id (~16KB cap). If a 16KB-truncated payload from viewSpans still matters, narrow further with searchSpan against a more specific regex rather than asking for the full payload again.\n\n10. If maxDepth > 0 and the question splits into independent semantic branches, delegate well-defined subtasks to subagents using \\`await llmQuery(...)\\`. Pass narrow context and a focused query. Examples:\n\n const reviews = await llmQuery([\n { query: 'Drill into trace abc123 — what tool calls preceded the failure?', context: { trace_id: 'abc123' } },\n { query: 'Drill into trace def456 — same failure mode?', context: { trace_id: 'def456' } },\n ]);\n\nOBSERVABILITY rules:\n- Each non-final actor turn must emit at least one \\`console.log(...)\\` for evidence. Up to 3 logs per turn is fine when correlating multiple data sources (e.g. one log for findings list, one for source-file content, one for derived analysis).\n- Do NOT combine \\`console.log\\` with \\`final(...)\\` or \\`askClarification(...)\\` in the same turn — finish gathering data first, then call final on its own turn.\n- Reuse runtime variables across turns; don't recompute.\n- When done, call \\`await final(answer)\\` with the fully-formed report. The responder rewrites the answer into output fields; if you only pass a vague summary string the responder has nothing concrete to format.\n\nCRITICAL — \\`final()\\` payload contract for evidence-grounded analysis tasks:\n- Pass a STRUCTURED object as the second arg with the actual data the responder needs to format the answer. Do NOT pass abstract instructions; pass evidence.\n- Example for per-item verdict tasks:\n \\`\\`\\`js\n await final(\"Format the per-item verdict report from the evidence below.\", {\n findings: [\n { id: 'sub-1-finding-1', claim: '...', verdict: 'TRUE-POSITIVE', evidence: 'lines 42-45 of contracts/X.sol show ...' },\n ...all items\n ],\n systemic_summary: '3 sentences I wrote based on the evidence above'\n });\n \\`\\`\\`\n- Calling \\`final(\"answer\", {})\\` with no evidence is a failure mode — the responder will hallucinate or echo back the field names. Always include the gathered data.\n- Premature final after a single viewSpans call is INSUFFICIENT for per-finding analysis tasks. Read the requested attributes (e.g. \\`spans[i].attributes['redteam.finding.title']\\`), and for each one perform the requested cross-reference (e.g. read the source SPAN's \\`attributes['source.content']\\`).\n\nOUTPUT contract — your final answer must include:\n- A clear prose conclusion answering the user's question.\n- Trace ids and span ids cited as evidence for each claim.\n- Failure modes named in the user's domain language, with frequency and concrete examples.\n\nDo NOT invent trace ids, span ids, error messages, or model names. Every fact must be traceable to a tool result.`\n\nexport const TRACE_ANALYST_ACTOR_DESCRIPTION_VERSION = 'trace-analyst-actor-v5-2026-05-06'\n\n/** Subagent prompt for focused trace-inspection subtasks. */\nexport const TRACE_ANALYST_SUBAGENT_DESCRIPTION = `You are a trace-analyst subagent. Your parent has delegated a focused trace-inspection question. Use the same DISCOVERY → NARROW → DEEP-READ protocol but stay tightly scoped: do exactly what was asked, return a concise compact answer, do NOT spawn further subagents unless the parent's question is genuinely multi-branch.\n\nCite trace ids and span ids for every claim. Do NOT invent ids.`\n","import { type AxActorTurn, type AxAIService, type AxFunction, AxJSRuntime, agent } from '@ax-llm/ax'\nimport {\n TRACE_ANALYST_ACTOR_DESCRIPTION,\n TRACE_ANALYST_ACTOR_DESCRIPTION_VERSION,\n TRACE_ANALYST_SUBAGENT_DESCRIPTION,\n} from './prompts'\nimport type { TraceAnalysisStore } from './store'\nimport { OtlpFileTraceStore, TraceFileMissingError } from './store-otlp'\nimport { buildTraceAnalystTools } from './tools'\n\nexport interface AnalyzeTracesInput {\n /** The user-facing question. Domain framing belongs here, not in the\n * actor description. */\n question: string\n}\n\nexport interface AnalyzeTracesResult {\n /** The responder's prose answer. */\n answer: string\n /** Bulleted findings extracted from the responder's structured output. */\n findings: string[]\n /** Per-actor-turn snapshots captured via `actorTurnCallback`. */\n turns: AnalyzeTracesTurnSnapshot[]\n /** Total turns the actor took. */\n turnCount: number\n /** Token usage by role. */\n usage: TraceAnalystUsage\n /** Full system + assistant + tool message log by role. */\n chatLog: TraceAnalystChatLog\n /** Prompt version that produced this run. */\n actorPromptVersion: string\n}\n\nexport interface TraceAnalystUsage {\n actor: TraceAnalystUsageEntry[]\n responder: TraceAnalystUsageEntry[]\n}\n\nexport interface TraceAnalystUsageEntry {\n [key: string]: unknown\n}\n\nexport interface TraceAnalystChatLog {\n actor: TraceAnalystChatMessage[]\n responder: TraceAnalystChatMessage[]\n}\n\nexport interface TraceAnalystChatMessage {\n [key: string]: unknown\n}\n\nexport interface AnalyzeTracesTurnSnapshot {\n turn: number\n isError: boolean\n /** The JS code the actor produced for this turn. */\n code: string\n /** The formatted action-log entry the actor sees on the next turn. */\n output: string\n /** Provider thought (when `actorOptions.showThoughts` is true and the\n * provider returns it). */\n thought?: string\n}\n\nexport interface AnalyzeTracesOptions {\n /** Trace data source. Pass either an OTLP-JSONL path or a custom store. */\n source: string | TraceAnalysisStore\n /** Caller-provided AxAIService. */\n ai: AxAIService\n /** Model id forwarded to actor + responder. */\n model?: string\n /** Recursion depth. 0 = no sub-agent dispatch. Default 1. */\n maxDepth?: number\n /** Maximum actor turns. Default 12. */\n maxTurns?: number\n /** Maximum parallel sub-agent calls in batched llmQuery. Default 2. */\n maxParallelSubagents?: number\n /** Override the actor description. */\n actorDescription?: string\n /** Override the subagent description. */\n subagentDescription?: string\n /** Per-turn observability hook. */\n onTurn?: (turn: AnalyzeTracesTurnSnapshot) => void | Promise<void>\n /** Override max runtime characters per turn. Default 6000. */\n maxRuntimeChars?: number\n /** When set, every turn's snapshot is appended to this JSONL file\n * immediately. If the analyst crashes mid-loop (provider 503,\n * network error, validator reject) the partial reasoning is still\n * on disk. Replay the file with the responder afterward to recover\n * evidence. */\n progressLogPath?: string\n}\n\n/**\n * Run the trace analyst.\n *\n * Throws:\n * - `TraceFileMissingError` if `source` is a path and doesn't exist.\n * - `AxAgentClarificationError` if the analyst asks for clarification.\n * - Provider errors (auth, rate limits) propagate from the AI service.\n */\nexport async function analyzeTraces(\n input: AnalyzeTracesInput,\n options: AnalyzeTracesOptions,\n): Promise<AnalyzeTracesResult> {\n if (!input.question || typeof input.question !== 'string') {\n throw new TypeError('analyzeTraces: input.question must be a non-empty string')\n }\n\n const store: TraceAnalysisStore =\n typeof options.source === 'string'\n ? new OtlpFileTraceStore({ path: options.source })\n : options.source\n\n // Pre-warm file stores so missing inputs fail before the RLM starts.\n if (store instanceof OtlpFileTraceStore) {\n await store.ensureIndexed()\n }\n\n const tools: AxFunction[] = buildTraceAnalystTools({ store })\n const turns: AnalyzeTracesTurnSnapshot[] = []\n\n // Persist each turn as JSONL so interrupted analyst runs keep useful evidence.\n let progressFs: import('node:fs').WriteStream | undefined\n if (options.progressLogPath) {\n const { createWriteStream } = await import('node:fs')\n const { mkdir } = await import('node:fs/promises')\n const { dirname } = await import('node:path')\n await mkdir(dirname(options.progressLogPath), { recursive: true })\n progressFs = createWriteStream(options.progressLogPath, { flags: 'a' })\n }\n\n const actorTurnCallback = async (turn: AxActorTurn): Promise<void> => {\n const snap: AnalyzeTracesTurnSnapshot = {\n turn: turn.turn,\n isError: turn.isError,\n code: turn.code,\n output: turn.output,\n thought: turn.thought,\n }\n turns.push(snap)\n if (progressFs) {\n try {\n progressFs.write(`${JSON.stringify({ ...snap, ts: Date.now() })}\\n`)\n } catch {\n // Progress logging must never fail the analyst.\n }\n }\n if (options.onTurn) await options.onTurn(snap)\n }\n\n const maxDepth = options.maxDepth ?? 1\n const maxTurns = options.maxTurns ?? 12\n const maxParallelSubagents = options.maxParallelSubagents ?? 2\n const maxRuntimeChars = options.maxRuntimeChars ?? 6000\n\n const analyst = agent<{ question: string }, { answer: string; findings: string[] }>(\n // `reasoning!` is an internal (Ax `!`) scratchpad field: generated first to\n // force reason-before-conclude, stripped from the returned output — so the\n // consumed shape stays { answer, findings }. Brings the trace-analyst to the\n // same prose-first CoT ordering the kind-factory gets from its `report` field.\n 'question:string -> reasoning!:string, answer:string, findings:string[]',\n {\n agentIdentity: {\n name: 'TraceAnalyst',\n description:\n 'Analyzes OTLP-shaped JSONL traces using bounded discovery tools to identify systemic failure modes.',\n },\n contextFields: ['question'],\n runtime: new AxJSRuntime({\n permissions: [],\n blockDynamicImport: true,\n allowedModules: [],\n freezeIntrinsics: true,\n blockShadowRealm: true,\n // RLM stdout mode relies on runtime bindings persisting across turns.\n preventGlobalThisExtensions: false,\n }),\n mode: maxDepth > 0 ? 'advanced' : 'simple',\n recursionOptions: maxDepth > 0 ? { maxDepth } : undefined,\n maxTurns,\n maxRuntimeChars,\n maxBatchedLlmQueryConcurrency: maxParallelSubagents,\n promptLevel: 'detailed',\n // Trace analysis depends on exact prior tool results and runtime variables.\n contextPolicy: { preset: 'full', budget: 'balanced' },\n functions: { local: tools },\n actorOptions: {\n description: options.actorDescription ?? TRACE_ANALYST_ACTOR_DESCRIPTION,\n ...(options.model ? { model: options.model } : {}),\n // Keep actor messages tool-call/content shaped across reasoning models.\n showThoughts: false,\n thinkingTokenBudget: 'none',\n },\n responderOptions: {\n ...(options.model ? { model: options.model } : {}),\n description: options.subagentDescription ?? TRACE_ANALYST_SUBAGENT_DESCRIPTION,\n showThoughts: false,\n },\n actorTurnCallback,\n bubbleErrors: [TraceFileMissingError],\n },\n )\n\n let result: { answer: unknown; findings: unknown }\n try {\n result = await analyst.forward(options.ai, { question: input.question })\n } finally {\n if (progressFs) {\n await new Promise<void>((resolve) => progressFs!.end(() => resolve()))\n }\n }\n\n return {\n answer: typeof result.answer === 'string' ? result.answer : String(result.answer ?? ''),\n findings: Array.isArray(result.findings)\n ? result.findings.filter((s): s is string => typeof s === 'string')\n : [],\n turns,\n turnCount: turns.length,\n usage: normalizeRoleArrays(analyst.getUsage()),\n chatLog: normalizeRoleArrays(analyst.getChatLog()),\n actorPromptVersion: TRACE_ANALYST_ACTOR_DESCRIPTION_VERSION,\n }\n}\n\nfunction normalizeRoleArrays(value: unknown): {\n actor: Record<string, unknown>[]\n responder: Record<string, unknown>[]\n} {\n const record = value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n return {\n actor: normalizeRecordArray(record.actor),\n responder: normalizeRecordArray(record.responder),\n }\n}\n\nfunction normalizeRecordArray(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) return []\n return value.map((item) =>\n item && typeof item === 'object' ? { ...(item as Record<string, unknown>) } : { value: item },\n )\n}\n"],"mappings":"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","names":[]}
|