@tangle-network/traces 0.23.1 → 1.0.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.
Files changed (74) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-XLS3VANY.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-3I4PJ5Q7.js +25 -0
  6. package/dist/chunk-3T62R7ON.js +5695 -0
  7. package/dist/chunk-4VKPN432.js +138 -0
  8. package/dist/chunk-6CLC7DKP.js +492 -0
  9. package/dist/chunk-7CBV5RJ2.js +535 -0
  10. package/dist/chunk-B62DOFGI.js +273 -0
  11. package/dist/chunk-CL3OQOWX.js +407 -0
  12. package/dist/chunk-DLGAKCQJ.js +782 -0
  13. package/dist/chunk-DU534HAX.js +263 -0
  14. package/dist/chunk-GLUDGUGJ.js +232 -0
  15. package/dist/chunk-J2PM5SVN.js +503 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-NH7HCDLB.js +759 -0
  18. package/dist/chunk-NZ5Y7UEL.js +1217 -0
  19. package/dist/chunk-O52XK7OB.js +19 -0
  20. package/dist/chunk-PYH3PL3N.js +62 -0
  21. package/dist/chunk-RZL5GAWH.js +60 -0
  22. package/dist/chunk-TR6O4S4T.js +208 -0
  23. package/dist/chunk-TW43MR5D.js +259 -0
  24. package/dist/chunk-TYLFUX42.js +228 -0
  25. package/dist/chunk-UOLRCQHC.js +120 -0
  26. package/dist/chunk-UVE5ANVV.js +3921 -0
  27. package/dist/chunk-V2OCGX57.js +53 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-ZZ6KYBPP.js +16 -0
  30. package/dist/improvement-OJQ6365E.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-Y64AJ6MA.js +14 -0
  34. package/dist/mcp-ZPGTJQYQ.js +85 -0
  35. package/dist/report-JAMXRTCW.js +338 -0
  36. package/dist/run-watch-PLWTIRSV.js +649 -0
  37. package/dist/supervisor-report-RMPLLBGZ.js +29 -0
  38. package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
  39. package/dist/upload-R6NMKSGQ.js +20 -0
  40. package/dist/verify-safe-C5ZXZOZ2.js +8 -0
  41. package/docs/analyze.md +76 -0
  42. package/docs/ask.md +88 -0
  43. package/docs/bundle.md +40 -0
  44. package/docs/check.md +77 -0
  45. package/docs/convert.md +44 -0
  46. package/docs/develop.md +32 -0
  47. package/docs/diff.md +38 -0
  48. package/docs/facts.md +114 -0
  49. package/docs/mcp.md +36 -0
  50. package/docs/migrate-1.0.md +33 -0
  51. package/docs/parsers.md +66 -0
  52. package/docs/release-1.0.0.md +147 -0
  53. package/docs/sdk.md +77 -0
  54. package/docs/select.md +70 -0
  55. package/docs/skills.md +46 -0
  56. package/docs/upload.md +47 -0
  57. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  58. package/docs/watch.md +66 -0
  59. package/examples/README.md +9 -0
  60. package/examples/custom-analyst.ts +65 -74
  61. package/install.sh +22 -2
  62. package/package.json +8 -7
  63. package/skills/inspect-agent-traces/SKILL.md +47 -110
  64. package/dist/chunk-7DVZCCZI.js +0 -20983
  65. package/docs/replay-verify.md +0 -123
  66. package/docs/stream-findings.svg +0 -39
  67. package/docs/trace-analysts.md +0 -643
  68. package/examples/custom-backend.ts +0 -21
  69. package/examples/external-engines.ts +0 -62
  70. package/examples/improvement-config.mjs +0 -63
  71. package/examples/observe-and-alert.ts +0 -19
  72. package/examples/register-harness.ts +0 -26
  73. package/skills/build-trace-analyst/SKILL.md +0 -76
  74. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
@@ -0,0 +1,503 @@
1
+ import {
2
+ classifyLiveActions,
3
+ createPipelineAccumulator,
4
+ ruleContext,
5
+ runRules
6
+ } from "./chunk-7CBV5RJ2.js";
7
+ import {
8
+ conformanceOfSpans,
9
+ skippedAnalyses
10
+ } from "./chunk-TW43MR5D.js";
11
+ import {
12
+ metadataOnly
13
+ } from "./chunk-V2OCGX57.js";
14
+ import {
15
+ exportBytes,
16
+ openDeterministicTraceStore
17
+ } from "./chunk-3I4PJ5Q7.js";
18
+ import {
19
+ CONTEXT_BANDS,
20
+ createSessionFactsReportBuilder,
21
+ modelCallMeasurement,
22
+ priceReport
23
+ } from "./chunk-UVE5ANVV.js";
24
+ import {
25
+ appendAll
26
+ } from "./chunk-NZ5Y7UEL.js";
27
+
28
+ // src/improvement.ts
29
+ import { createHash } from "crypto";
30
+ import { mkdir, writeFile } from "fs/promises";
31
+ import { join, resolve } from "path";
32
+ import { makeFinding } from "@tangle-network/agent-eval/analyst";
33
+
34
+ // src/analyze.ts
35
+ import {
36
+ buildDefaultAnalystRegistry
37
+ } from "@tangle-network/agent-eval/analyst";
38
+ var MIN_TOOL_CALLS_FOR_TOOL_SIGNALS = 10;
39
+ var TOOL_SIGNALS = /* @__PURE__ */ new Set(["single-tool-dependency", "no-self-verification"]);
40
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "exec_command", "exec", "shell", "local_shell", "run_shell_command", "terminal"]);
41
+ function evidenceOf(finding, traceId) {
42
+ const ref = finding.evidence_refs.find((evidence) => evidence.uri.includes(`/trace/${encodeURIComponent(traceId)}/`));
43
+ if (!ref?.excerpt) return null;
44
+ try {
45
+ return JSON.parse(ref.excerpt);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function toolSignalHolds(finding, traceId, checked) {
51
+ const evidence = evidenceOf(finding, traceId);
52
+ const calls = evidence?.calls ?? evidence?.tool_calls;
53
+ if (typeof calls !== "number" || calls < MIN_TOOL_CALLS_FOR_TOOL_SIGNALS) return false;
54
+ if (finding.subject === "single-tool-dependency") return !(typeof evidence?.tool === "string" && SHELL_TOOLS.has(evidence.tool.toLowerCase()));
55
+ return !checked.has(traceId);
56
+ }
57
+ function withHeldToolSignals(result, checked) {
58
+ const findings = result.findings.flatMap((finding) => {
59
+ if (!finding.subject || !TOOL_SIGNALS.has(finding.subject)) return [finding];
60
+ const traces = Array.isArray(finding.metadata?.evidence_trace_ids) ? finding.metadata.evidence_trace_ids : [];
61
+ const kept = traces.filter((traceId) => toolSignalHolds(finding, traceId, checked));
62
+ if (kept.length === 0) return [];
63
+ if (kept.length === traces.length) return [finding];
64
+ return [{
65
+ ...finding,
66
+ claim: finding.claim.replace(/^\d+\//u, `${kept.length}/`),
67
+ evidence_refs: finding.evidence_refs.filter((evidence) => kept.some((traceId) => evidence.uri.includes(`/trace/${encodeURIComponent(traceId)}/`))),
68
+ metadata: { ...finding.metadata, evidence_trace_ids: kept, observed_trace_count: kept.length }
69
+ }];
70
+ });
71
+ const dropped = result.findings.length - findings.length;
72
+ return dropped === 0 ? result : {
73
+ ...result,
74
+ findings,
75
+ per_analyst: result.per_analyst.map((summary) => ({
76
+ ...summary,
77
+ findings_count: findings.filter((finding) => finding.analyst_id === summary.analyst_id).length
78
+ }))
79
+ };
80
+ }
81
+ var BEHAVIORAL_DEFAULT_MAX_TRACES = 1e3;
82
+ function createSpanAnalysis(opts = {}) {
83
+ const chunks = [];
84
+ const traces = /* @__PURE__ */ new Set();
85
+ const checked = /* @__PURE__ */ new Set();
86
+ return {
87
+ add(spans) {
88
+ if (spans.length === 0) return;
89
+ chunks.push(exportBytes(opts.registry ? spans : metadataOnly(spans).spans));
90
+ if (opts.registry) return;
91
+ const byTrace2 = /* @__PURE__ */ new Map();
92
+ for (const span of spans) {
93
+ traces.add(span.trace_id);
94
+ const group = byTrace2.get(span.trace_id);
95
+ if (group) group.push(span);
96
+ else byTrace2.set(span.trace_id, [span]);
97
+ }
98
+ for (const [traceId, group] of byTrace2) if (classifyLiveActions(group).some((action) => action.kind === "verify")) checked.add(traceId);
99
+ },
100
+ async run() {
101
+ if (chunks.length === 0) throw new Error("analyzeSpans: no spans to analyze");
102
+ opts.signal?.throwIfAborted();
103
+ const runId = opts.runId ?? `traces-${Date.now()}`;
104
+ const store = openDeterministicTraceStore(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks));
105
+ chunks.length = 0;
106
+ if (opts.registry) {
107
+ const result2 = await opts.registry.run(runId, { traceStore: store }, { signal: opts.signal });
108
+ opts.signal?.throwIfAborted();
109
+ return { result: result2 };
110
+ }
111
+ opts.signal?.throwIfAborted();
112
+ const registry = buildDefaultAnalystRegistry({
113
+ registry: { log: opts.log },
114
+ behavioral: { maxTraces: Math.max(BEHAVIORAL_DEFAULT_MAX_TRACES, traces.size) }
115
+ });
116
+ const result = await registry.run(runId, { traceStore: store }, { signal: opts.signal });
117
+ opts.signal?.throwIfAborted();
118
+ return { result: withHeldToolSignals(result, checked) };
119
+ }
120
+ };
121
+ }
122
+ async function analyzeSpans(spans, opts = {}) {
123
+ if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
124
+ opts.signal?.throwIfAborted();
125
+ const analysis = createSpanAnalysis(opts);
126
+ analysis.add(spans);
127
+ return analysis.run();
128
+ }
129
+
130
+ // src/improvement.ts
131
+ function spanEvidenceUri(traceId, spanId) {
132
+ return `trace://${encodeURIComponent(traceId)}/span/${encodeURIComponent(spanId)}`;
133
+ }
134
+ var severityRank = {
135
+ critical: 0,
136
+ high: 1,
137
+ medium: 2,
138
+ low: 3,
139
+ info: 4
140
+ };
141
+ function rankFindings(findings) {
142
+ return [...findings].sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || a.area.localeCompare(b.area) || a.claim.localeCompare(b.claim));
143
+ }
144
+ function renderFindingPacket(packet, title = "Trace finding packet") {
145
+ const lines = [`# ${title}`, ""];
146
+ lines.push(`${packet.findings.length} evidence-backed finding(s) with actions and checks.`);
147
+ lines.push("");
148
+ if (packet.findings.length > 0) {
149
+ lines.push("## findings");
150
+ lines.push("");
151
+ for (const finding of rankFindings(packet.findings)) {
152
+ lines.push(`### ${finding.severity.toUpperCase()} \u2014 ${finding.claim}`);
153
+ lines.push("");
154
+ lines.push(`- **Area:** ${finding.area}`);
155
+ lines.push(`- **Analyst:** ${finding.analyst_id}`);
156
+ if (finding.recommended_action) lines.push(`- **Action:** ${finding.recommended_action}`);
157
+ if (finding.validation_plan) lines.push(`- **Check:** ${finding.validation_plan}`);
158
+ for (const ref of finding.evidence_refs.slice(0, 3)) {
159
+ lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
160
+ }
161
+ lines.push("");
162
+ }
163
+ }
164
+ return lines.join("\n");
165
+ }
166
+ function buildTraceFindingPacket(opts) {
167
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
168
+ const packet = {
169
+ schemaVersion: 1,
170
+ kind: "traces.finding_packet",
171
+ generatedAt,
172
+ source: opts.source ?? "traces",
173
+ findings: opts.findings
174
+ };
175
+ return { ...packet, report: renderFindingPacket(packet, opts.title) };
176
+ }
177
+ var RULE_AREA = {
178
+ "same-failing-command": "tool-use",
179
+ "verification-without-change": "verification",
180
+ "high-tool-error-rate": "tool-use"
181
+ };
182
+ var DEMOTED = /* @__PURE__ */ new Set(["no-self-verification"]);
183
+ var LOWER = {
184
+ critical: "high",
185
+ high: "medium",
186
+ medium: "low",
187
+ low: "low",
188
+ info: "info"
189
+ };
190
+ function measuredSeverity(finding) {
191
+ return finding.subject !== void 0 && DEMOTED.has(finding.subject) ? { ...finding, severity: LOWER[finding.severity] } : finding;
192
+ }
193
+ function ruleFinding(live, traceId) {
194
+ const spanIds = [...new Set(live.evidence.flatMap((item) => item.spanIds ?? []))];
195
+ return makeFinding({
196
+ analyst_id: "traces-rules",
197
+ area: RULE_AREA[live.ruleId] ?? "tool-use",
198
+ subject: live.ruleId,
199
+ claim: live.claim,
200
+ severity: live.severity,
201
+ rationale: live.title,
202
+ evidence_refs: [
203
+ ...spanIds.slice(0, 3).map((spanId) => ({
204
+ kind: "span",
205
+ uri: spanEvidenceUri(traceId, spanId),
206
+ excerpt: live.evidence.find((item) => item.spanIds?.includes(spanId))?.value.slice(0, 240) ?? live.title
207
+ })),
208
+ ...live.evidence.filter((item) => item.kind === "metric").map((item) => ({
209
+ kind: "metric",
210
+ uri: `metric://trace/${encodeURIComponent(traceId)}/rules/${live.ruleId}/${item.label}`,
211
+ excerpt: item.value
212
+ }))
213
+ ],
214
+ recommended_action: live.action,
215
+ validation_plan: live.check,
216
+ confidence: 1,
217
+ metadata: { source: "traces.rules", ruleId: live.ruleId, sessionId: live.session.sessionId, traceId, spanIds },
218
+ id_basis: `${traceId}:${live.fingerprint}`
219
+ });
220
+ }
221
+ function terminalFailureFinding(pipelines, spans) {
222
+ const failedRuns = pipelines.failureClusters.totalFailures;
223
+ if (failedRuns === 0) return void 0;
224
+ const top = pipelines.failureClusters.clusters[0];
225
+ const root = spans.find((item) => item.parent_span_id === null && item.status.code === "ERROR" && (!top?.exampleRunId || item.trace_id === top.exampleRunId));
226
+ return makeFinding({
227
+ analyst_id: "traces-deterministic",
228
+ area: "reliability",
229
+ subject: "terminal-failure",
230
+ claim: `${failedRuns}/${pipelines.failureClusters.totalRuns} session(s) ended in an error`,
231
+ severity: "high",
232
+ rationale: "A session whose root span failed did not finish its work.",
233
+ evidence_refs: [
234
+ ...root ? [{ kind: "span", uri: spanEvidenceUri(root.trace_id, root.span_id), excerpt: `${root.name}: ${root.status.message ?? "root span ended with ERROR"}`.slice(0, 240) }] : [],
235
+ {
236
+ kind: "metric",
237
+ uri: `metric://pipelines/failure_cluster/${top?.failureClass ?? "unknown"}`,
238
+ excerpt: top ? `${top.runCount} session(s); example: ${top.exampleError ?? "error details not captured"}` : `${failedRuns} failed session(s)`
239
+ }
240
+ ],
241
+ recommended_action: "Read the error the session ended on and fix its cause before rerunning the task.",
242
+ validation_plan: "Rerun the task and require the session to end without an error.",
243
+ confidence: 1,
244
+ metadata: { source: "agent-eval.failureClusterView", failedRuns, totalRuns: pipelines.failureClusters.totalRuns, topFailureClass: top?.failureClass }
245
+ });
246
+ }
247
+ var ERROR_TEXT_CAP = 80;
248
+ var CLUSTERS_KEPT = 5;
249
+ function errorLine(span) {
250
+ const output = span.attributes["output.value"];
251
+ const text = span.status.message ?? (typeof output === "string" ? output : "");
252
+ const line = text.split("\n").map((part) => part.replace(/\s+/gu, " ").trim()).find((part) => part.length > 0) ?? "no error text recorded";
253
+ return line.length > ERROR_TEXT_CAP ? `${line.slice(0, ERROR_TEXT_CAP - 1)}\u2026` : line;
254
+ }
255
+ function toolErrorsOf(sessionId, tools, errored) {
256
+ const clusters = /* @__PURE__ */ new Map();
257
+ for (const span of errored) {
258
+ const tool = typeof span.attributes["tool.name"] === "string" ? span.attributes["tool.name"] : span.name.replace(/^tool\./u, "");
259
+ const error = errorLine(span);
260
+ const key = `${tool}\0${error}`;
261
+ const cluster = clusters.get(key) ?? { tool, error, count: 0, spanIds: [] };
262
+ cluster.count += 1;
263
+ if (cluster.spanIds.length < 3) cluster.spanIds.push(span.span_id);
264
+ clusters.set(key, cluster);
265
+ }
266
+ return {
267
+ sessionId,
268
+ calls: tools.length,
269
+ errors: errored.length,
270
+ clusters: [...clusters.values()].sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, CLUSTERS_KEPT)
271
+ };
272
+ }
273
+ function byTrace(spans) {
274
+ const groups = /* @__PURE__ */ new Map();
275
+ for (const span of spans) {
276
+ const group = groups.get(span.trace_id);
277
+ if (group) group.push(span);
278
+ else groups.set(span.trace_id, [span]);
279
+ }
280
+ return groups;
281
+ }
282
+ var FACT_FIELDS = ["cwd", "repo", "toolCalls", "subagents", "pullRequests", "humanTurns", "finalMessages", "changedFiles", "firstRecordAt", "lastRecordAt"];
283
+ function factsCannotAnswer(facts) {
284
+ const sessionId = facts.sessionId ?? facts.traceId;
285
+ const out = [];
286
+ for (const field of FACT_FIELDS) {
287
+ const fact = facts[field];
288
+ if (fact.value === null && fact.unavailable) out.push({ analysis: `facts.${field}`, reason: fact.unavailable, sessionId });
289
+ }
290
+ if (facts.tokens.input === null && facts.tokens.unavailable) out.push({ analysis: "facts.tokens", reason: facts.tokens.unavailable, sessionId });
291
+ else if (facts.cost.usd === null && facts.cost.unavailable) out.push({ analysis: "facts.cost", reason: facts.cost.unavailable, sessionId });
292
+ if (facts.firstFailure.status === "unknown") out.push({ analysis: "facts.firstFailure", reason: facts.firstFailure.reason, sessionId });
293
+ const waste = facts.measurements.waste;
294
+ if (waste.minutes === null && waste.unavailable) out.push({ analysis: "facts.measurements.waste", reason: waste.unavailable, sessionId });
295
+ return out;
296
+ }
297
+ function argumentsCannotAnswer(pipelines) {
298
+ const calls = pipelines.toolUse.reduce((total, run) => total + run.totalCalls, 0);
299
+ const captured = pipelines.toolUse.reduce((total, run) => total + run.callsWithCapturedArgs, 0);
300
+ if (calls === 0 || captured === calls) return [];
301
+ return [{
302
+ analysis: "argument comparisons: repeated commands, loops, blind retries and failing streaks",
303
+ reason: `${calls - captured}/${calls} tool call(s) kept no arguments, so whether two calls were the same cannot be compared`
304
+ }];
305
+ }
306
+ var CONFORMANCE_SPANS_READ = 25e4;
307
+ var CONFORMANCE_TEXT_KEPT = 256;
308
+ function conformanceView(span) {
309
+ let attributes;
310
+ for (const [key, value] of Object.entries(span.attributes)) {
311
+ if (typeof value !== "string" || value.length <= CONFORMANCE_TEXT_KEPT) continue;
312
+ attributes ??= { ...span.attributes };
313
+ attributes[key] = `sha256:${createHash("sha256").update(value).digest("hex")}`;
314
+ }
315
+ return attributes ? { ...span, attributes } : span;
316
+ }
317
+ var perThousand = (count, calls) => Math.round(count * 1e3 / calls * 100) / 100;
318
+ function guardRefusalRates(toolCalls, byGuard) {
319
+ const refusals = [...byGuard.values()].reduce((sum, count) => sum + count, 0);
320
+ return {
321
+ toolCalls,
322
+ refusals,
323
+ perThousandCalls: toolCalls === 0 ? null : perThousand(refusals, toolCalls),
324
+ byGuard: [...byGuard].sort(([a, x], [b, y]) => y - x || a.localeCompare(b)).map(([guard, count]) => ({ guard, refusals: count, perThousandCalls: toolCalls === 0 ? 0 : perThousand(count, toolCalls) }))
325
+ };
326
+ }
327
+ function createTraceInvestigation(opts) {
328
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
329
+ let selected;
330
+ const contexts = [];
331
+ const latencies = Array.from({ length: CONTEXT_BANDS }, () => []);
332
+ const builder = createSessionFactsReportBuilder({
333
+ ...opts.facts,
334
+ harness: opts.harness,
335
+ generatedAt,
336
+ onSession: (reading) => {
337
+ selected = reading.facts;
338
+ appendAll(contexts, reading.operating.contextTokens);
339
+ reading.operating.latencyMs.forEach((values, band) => appendAll(latencies[band], values));
340
+ }
341
+ });
342
+ let toolCalls = 0;
343
+ const byGuard = /* @__PURE__ */ new Map();
344
+ const limitKinds = /* @__PURE__ */ new Map();
345
+ let terminalFailures = 0;
346
+ let terminalAtLimit = 0;
347
+ const pipelines = createPipelineAccumulator({ minLoopOccurrences: opts.minLoopOccurrences });
348
+ const analysis = createSpanAnalysis({
349
+ ...opts.registry ? { registry: opts.registry } : {},
350
+ runId: `traces-analysis-${Date.parse(generatedAt) || Date.now()}`,
351
+ ...opts.signal ? { signal: opts.signal } : {},
352
+ ...opts.log ? { log: opts.log } : {}
353
+ });
354
+ const rules = /* @__PURE__ */ new Map();
355
+ const failedRoots = [];
356
+ const conformanceSpans = [];
357
+ let spanCount = 0;
358
+ let empty = true;
359
+ return {
360
+ async add(spans, batch = {}) {
361
+ opts.signal?.throwIfAborted();
362
+ if (spans.length === 0) return;
363
+ empty = false;
364
+ spanCount += spans.length;
365
+ const groups = byTrace(spans);
366
+ for (const [traceId, group] of groups) {
367
+ selected = void 0;
368
+ builder.add(group);
369
+ const sheet = selected;
370
+ const failed = group.find((item) => item.parent_span_id === null && item.status.code === "ERROR");
371
+ if (failed) failedRoots.push({ ...failed, attributes: {} });
372
+ if (!sheet) continue;
373
+ const { guardRefusals, limitStop } = sheet.measurements;
374
+ toolCalls += sheet.toolCalls.value ?? 0;
375
+ for (const [guard, count] of Object.entries(guardRefusals.byGuard)) byGuard.set(guard, (byGuard.get(guard) ?? 0) + count);
376
+ if (limitStop) limitKinds.set(limitStop.kind, (limitKinds.get(limitStop.kind) ?? 0) + 1);
377
+ if (failed) {
378
+ terminalFailures += 1;
379
+ if (limitStop) terminalAtLimit += 1;
380
+ }
381
+ const sessionId = sheet.sessionId ?? sheet.traceId;
382
+ const context = ruleContext(group, {
383
+ session: { harness: sheet.harness ?? batch.harness ?? opts.harness, sessionId, cwd: sheet.cwd.value },
384
+ generatedAt,
385
+ facts: [sheet]
386
+ });
387
+ rules.set(traceId, {
388
+ findings: runRules(context).map((live) => ruleFinding(live, sheet.traceId)),
389
+ toolErrors: toolErrorsOf(sessionId, context.tools, context.erroredTools)
390
+ });
391
+ }
392
+ await pipelines.add(spans);
393
+ analysis.add(spans);
394
+ if (!opts.conformance) {
395
+ for (const span of spans) {
396
+ if (conformanceSpans.length >= CONFORMANCE_SPANS_READ) break;
397
+ conformanceSpans.push(conformanceView(span));
398
+ }
399
+ }
400
+ },
401
+ addUnread(entry) {
402
+ builder.addUnread(entry);
403
+ },
404
+ async finish(end = {}) {
405
+ if (empty) throw new Error("runTraceInvestigation: no spans to analyze");
406
+ opts.signal?.throwIfAborted();
407
+ const harness = end.harness ?? opts.harness;
408
+ for (const entry of opts.unread ?? []) builder.addUnread(entry);
409
+ const facts = await priceReport({ ...builder.build(), harness });
410
+ opts.signal?.throwIfAborted();
411
+ const measures = pipelines.report();
412
+ const findings = [];
413
+ const toolErrors = [];
414
+ for (const sheet of facts.sessions) {
415
+ const held = rules.get(sheet.traceId);
416
+ if (!held) continue;
417
+ appendAll(findings, held.findings);
418
+ toolErrors.push(held.toolErrors);
419
+ }
420
+ const terminal = terminalFailureFinding(measures, failedRoots);
421
+ if (terminal) findings.push(terminal);
422
+ const result = await analysis.run();
423
+ appendAll(findings, result.result.findings);
424
+ for (const summary of result.result.per_analyst) {
425
+ if (summary.status !== "ok") opts.log?.(`[analyst] ${summary.status.toUpperCase()} ${summary.analyst_id}`, { error: summary.error?.message ?? summary.reason });
426
+ }
427
+ let conformance = end.conformance ?? opts.conformance;
428
+ if (!conformance) {
429
+ const views = conformanceSpans;
430
+ if (spanCount > views.length) views.length = spanCount;
431
+ conformance = conformanceOfSpans(views);
432
+ }
433
+ const cannotAnswer = [
434
+ ...skippedAnalyses(conformance).map((skipped) => ({ analysis: `${skipped.capability}: ${skipped.analysis}`, reason: skipped.reason })),
435
+ ...argumentsCannotAnswer(measures),
436
+ ...result.result.per_analyst.filter((summary) => summary.status !== "ok").map((summary) => ({ analysis: `analyst ${summary.analyst_id}`, reason: summary.error?.message ?? summary.reason ?? summary.status })),
437
+ ...(facts.selection?.unreadSessions ?? []).map((entry) => ({
438
+ analysis: "every question about this session",
439
+ reason: entry.reason,
440
+ sessionId: entry.sessionId ?? entry.traceId ?? entry.source ?? "unknown"
441
+ })),
442
+ ...facts.sessions.flatMap(factsCannotAnswer)
443
+ ];
444
+ return {
445
+ schemaVersion: 1,
446
+ kind: "traces.analysis",
447
+ generatedAt,
448
+ harness,
449
+ facts,
450
+ packet: buildTraceFindingPacket({
451
+ findings: rankFindings(findings.map(measuredSeverity)),
452
+ generatedAt,
453
+ source: "traces analyze",
454
+ title: "traces analyze findings"
455
+ }),
456
+ measures: {
457
+ ...measures,
458
+ toolErrors,
459
+ operating: {
460
+ modelCalls: modelCallMeasurement({ contextTokens: contexts, latencyMs: latencies }),
461
+ guardRefusals: guardRefusalRates(toolCalls, byGuard),
462
+ limitStops: {
463
+ sessions: [...limitKinds.values()].reduce((sum, count) => sum + count, 0),
464
+ byKind: Object.fromEntries([...limitKinds].sort(([a], [b]) => a.localeCompare(b))),
465
+ terminalFailures,
466
+ terminalFailuresAtLimit: terminalAtLimit
467
+ }
468
+ }
469
+ },
470
+ conformance,
471
+ cannotAnswer
472
+ };
473
+ }
474
+ };
475
+ }
476
+ async function runTraceInvestigation(opts) {
477
+ if (opts.spans.length === 0) throw new Error("runTraceInvestigation: no spans to analyze");
478
+ const { spans, ...rest } = opts;
479
+ const investigation = createTraceInvestigation(rest);
480
+ await investigation.add(spans);
481
+ return investigation.finish();
482
+ }
483
+ async function writeAnalysisArtifacts(report, packet, outDir) {
484
+ const directory = resolve(outDir);
485
+ await mkdir(directory, { recursive: true });
486
+ const paths = { directory, report: join(directory, "report.md"), findings: join(directory, "findings.json") };
487
+ await Promise.all([
488
+ writeFile(paths.report, report, "utf8"),
489
+ writeFile(paths.findings, `${JSON.stringify(packet, null, 2)}
490
+ `, "utf8")
491
+ ]);
492
+ return paths;
493
+ }
494
+
495
+ export {
496
+ analyzeSpans,
497
+ spanEvidenceUri,
498
+ rankFindings,
499
+ buildTraceFindingPacket,
500
+ createTraceInvestigation,
501
+ runTraceInvestigation,
502
+ writeAnalysisArtifacts
503
+ };