@tangle-network/traces 0.23.1 → 1.0.1

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 (75) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-2KF4KSPB.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-2XXABIRL.js +25 -0
  6. package/dist/chunk-4VKPN432.js +138 -0
  7. package/dist/chunk-6GOG5KIK.js +53 -0
  8. package/dist/chunk-7USMM4MA.js +5695 -0
  9. package/dist/chunk-CG5QHQO4.js +492 -0
  10. package/dist/chunk-CQ6UYZG6.js +1217 -0
  11. package/dist/chunk-DU534HAX.js +263 -0
  12. package/dist/chunk-FC4V25CG.js +503 -0
  13. package/dist/chunk-GLUDGUGJ.js +232 -0
  14. package/dist/chunk-GNRXKQEQ.js +120 -0
  15. package/dist/chunk-K2KH2HHD.js +228 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-O52XK7OB.js +19 -0
  18. package/dist/chunk-PYH3PL3N.js +62 -0
  19. package/dist/chunk-QLVP37YB.js +782 -0
  20. package/dist/chunk-QUHUMZ4W.js +759 -0
  21. package/dist/chunk-REO2JQJY.js +535 -0
  22. package/dist/chunk-RZL5GAWH.js +60 -0
  23. package/dist/chunk-TOCNGML5.js +273 -0
  24. package/dist/chunk-UQSAGXPH.js +208 -0
  25. package/dist/chunk-VFOLC4OX.js +407 -0
  26. package/dist/chunk-WF7PTDCZ.js +3921 -0
  27. package/dist/chunk-YAX72TST.js +259 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-J4RAIOXO.js +16 -0
  30. package/dist/improvement-LCCVGGUS.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-XXL7VZWB.js +14 -0
  34. package/dist/mcp-EYIDKBEK.js +85 -0
  35. package/dist/report-E3G2T5TJ.js +338 -0
  36. package/dist/run-watch-RAOOAXOF.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-3R7GRSKS.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/release-1.0.1.md +4 -0
  54. package/docs/sdk.md +77 -0
  55. package/docs/select.md +70 -0
  56. package/docs/skills.md +46 -0
  57. package/docs/upload.md +47 -0
  58. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  59. package/docs/watch.md +66 -0
  60. package/examples/README.md +9 -0
  61. package/examples/custom-analyst.ts +65 -74
  62. package/install.sh +22 -2
  63. package/package.json +10 -9
  64. package/skills/inspect-agent-traces/SKILL.md +47 -110
  65. package/dist/chunk-7DVZCCZI.js +0 -20983
  66. package/docs/replay-verify.md +0 -123
  67. package/docs/stream-findings.svg +0 -39
  68. package/docs/trace-analysts.md +0 -643
  69. package/examples/custom-backend.ts +0 -21
  70. package/examples/external-engines.ts +0 -62
  71. package/examples/improvement-config.mjs +0 -63
  72. package/examples/observe-and-alert.ts +0 -19
  73. package/examples/register-harness.ts +0 -26
  74. package/skills/build-trace-analyst/SKILL.md +0 -76
  75. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
@@ -0,0 +1,759 @@
1
+ import {
2
+ createFindingRejectionTally,
3
+ formatFindingRejections,
4
+ totalFindingRejections
5
+ } from "./chunk-PYH3PL3N.js";
6
+ import {
7
+ ASK_QUESTION_HEADER,
8
+ DEFAULT_ASK_CONCURRENCY,
9
+ MAX_TRACE_QUESTION_CHARS,
10
+ QUESTION_LABEL
11
+ } from "./chunk-O52XK7OB.js";
12
+ import {
13
+ exportBytes,
14
+ openAgenticTraceStore
15
+ } from "./chunk-2XXABIRL.js";
16
+ import {
17
+ createSessionFactsReportBuilder,
18
+ renderSessionFactsContext
19
+ } from "./chunk-WF7PTDCZ.js";
20
+ import {
21
+ indexSessionIdsByTrace
22
+ } from "./chunk-UQSAGXPH.js";
23
+ import {
24
+ appendAll
25
+ } from "./chunk-CQ6UYZG6.js";
26
+
27
+ // src/ask.ts
28
+ import { mkdir, readFile, writeFile } from "fs/promises";
29
+ import { join, resolve } from "path";
30
+ import { performance } from "perf_hooks";
31
+ import { CostCeilingReachedError, CostLedger } from "@tangle-network/agent-eval";
32
+ import {
33
+ defineTraceAnalyst,
34
+ runTraceAnalyst
35
+ } from "@tangle-network/agent-eval/analyst";
36
+
37
+ // src/answer-schema.ts
38
+ var SUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
39
+ "type",
40
+ "properties",
41
+ "required",
42
+ "additionalProperties",
43
+ "items",
44
+ "enum",
45
+ "const",
46
+ "title",
47
+ "description"
48
+ ]);
49
+ var JSON_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
50
+ var MAX_SCHEMA_DEPTH = 32;
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function assertAnswerSchema(schema, path = "answerSchema", depth = 0) {
55
+ if (depth > MAX_SCHEMA_DEPTH) throw new TypeError(`${path}: schema nesting exceeds ${MAX_SCHEMA_DEPTH} levels`);
56
+ if (!isRecord(schema)) throw new TypeError(`${path} must be a JSON Schema object`);
57
+ for (const key of Object.keys(schema)) {
58
+ if (!SUPPORTED_KEYWORDS.has(key)) {
59
+ throw new TypeError(
60
+ `${path}: unsupported JSON Schema keyword "${key}"; supported: ${[...SUPPORTED_KEYWORDS].join(", ")}`
61
+ );
62
+ }
63
+ }
64
+ if (schema.type !== void 0) {
65
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
66
+ if (types.length === 0 || !types.every((type) => typeof type === "string" && JSON_TYPES.has(type))) {
67
+ throw new TypeError(`${path}.type must be one of ${[...JSON_TYPES].join(", ")}, or an array of them`);
68
+ }
69
+ }
70
+ const declared = schema.type === void 0 ? void 0 : Array.isArray(schema.type) ? schema.type : [schema.type];
71
+ for (const keyword of ["properties", "required", "additionalProperties"]) {
72
+ if (schema[keyword] !== void 0 && !declared?.includes("object")) {
73
+ throw new TypeError(`${path}: "${keyword}" is checked only for an object; declare "type": "object" alongside it`);
74
+ }
75
+ }
76
+ if (schema.items !== void 0 && !declared?.includes("array")) {
77
+ throw new TypeError(`${path}: "items" is checked only for an array; declare "type": "array" alongside it`);
78
+ }
79
+ if (schema.properties !== void 0) {
80
+ if (!isRecord(schema.properties)) throw new TypeError(`${path}.properties must be an object`);
81
+ for (const [name, child] of Object.entries(schema.properties)) {
82
+ assertAnswerSchema(child, `${path}.properties.${name}`, depth + 1);
83
+ }
84
+ }
85
+ if (schema.required !== void 0) {
86
+ if (!Array.isArray(schema.required) || !schema.required.every((name) => typeof name === "string")) {
87
+ throw new TypeError(`${path}.required must be an array of property names`);
88
+ }
89
+ }
90
+ if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") {
91
+ throw new TypeError(`${path}.additionalProperties must be a boolean`);
92
+ }
93
+ if (schema.items !== void 0) assertAnswerSchema(schema.items, `${path}.items`, depth + 1);
94
+ if (schema.enum !== void 0 && (!Array.isArray(schema.enum) || schema.enum.length === 0)) {
95
+ throw new TypeError(`${path}.enum must be a non-empty array`);
96
+ }
97
+ for (const key of ["title", "description"]) {
98
+ if (schema[key] !== void 0 && typeof schema[key] !== "string") {
99
+ throw new TypeError(`${path}.${key} must be a string`);
100
+ }
101
+ }
102
+ }
103
+ function jsonType(value) {
104
+ if (value === null) return "null";
105
+ if (Array.isArray(value)) return "array";
106
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
107
+ return typeof value;
108
+ }
109
+ function typeMatches(value, type) {
110
+ const actual = jsonType(value);
111
+ return actual === type || type === "number" && actual === "integer";
112
+ }
113
+ function sameJson(left, right) {
114
+ return JSON.stringify(left) === JSON.stringify(right);
115
+ }
116
+ function answerSchemaErrors(value, schema, path = "$") {
117
+ const errors = [];
118
+ if (schema.type !== void 0) {
119
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
120
+ if (!types.some((type) => typeMatches(value, type))) {
121
+ errors.push(`${path}: expected ${types.join(" or ")}, got ${jsonType(value)}`);
122
+ return errors;
123
+ }
124
+ }
125
+ if (schema.const !== void 0 && !sameJson(value, schema.const)) {
126
+ errors.push(`${path}: expected ${JSON.stringify(schema.const)}`);
127
+ }
128
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => sameJson(value, option))) {
129
+ errors.push(`${path}: expected one of ${schema.enum.map((option) => JSON.stringify(option)).join(", ")}`);
130
+ }
131
+ if (isRecord(value)) {
132
+ const properties = isRecord(schema.properties) ? schema.properties : {};
133
+ for (const name of schema.required ?? []) {
134
+ if (!(name in value)) errors.push(`${path}.${name}: required property is missing`);
135
+ }
136
+ for (const [name, child] of Object.entries(value)) {
137
+ const childSchema = properties[name];
138
+ if (childSchema !== void 0) {
139
+ errors.push(...answerSchemaErrors(child, childSchema, `${path}.${name}`));
140
+ } else if (schema.additionalProperties === false) {
141
+ errors.push(`${path}.${name}: property is not allowed`);
142
+ }
143
+ }
144
+ }
145
+ if (Array.isArray(value) && schema.items !== void 0) {
146
+ value.forEach((entry, index) => {
147
+ errors.push(...answerSchemaErrors(entry, schema.items, `${path}[${index}]`));
148
+ });
149
+ }
150
+ return errors;
151
+ }
152
+ function parseJsonAnswer(text) {
153
+ const trimmed = text.trim();
154
+ const candidates = [trimmed];
155
+ const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/.exec(trimmed);
156
+ if (fenced) candidates.push(fenced[1].trim());
157
+ for (const candidate of candidates) {
158
+ try {
159
+ return { ok: true, value: JSON.parse(candidate) };
160
+ } catch {
161
+ }
162
+ }
163
+ return { ok: false, error: "answer is not one JSON value" };
164
+ }
165
+
166
+ // src/ask.ts
167
+ var BRIDGE_MISMATCH_PATTERN = /must contain exactly|No module named ['"]?agent_eval_rpc|could not start/;
168
+ function isBridgeMismatchError(message) {
169
+ return BRIDGE_MISMATCH_PATTERN.test(message);
170
+ }
171
+ var ASK_DEFINITION_VERSION = "1.1.0";
172
+ var ASK_RULES = [
173
+ "TRACES ASK RULES",
174
+ "1. Answer QUESTION only from trace tool results of this run.",
175
+ "2. PREPARED CONTEXT ends analyst_instructions: the trace list JSON, then a SESSION FACTS sheet. Copy IDs from both.",
176
+ // The rules must fit the DSPy preview head, so this one does not also legislate
177
+ // formatting: `traceCitationsInText` reads a citation the model emphasised.
178
+ "3. Cite each fact as trace://<trace_id>/span/<span_id>: span ids the sheet names, never the sheet.",
179
+ "4. Quote excerpts from viewSpans output, never searchTrace hits.",
180
+ '5. If the trace does not record a fact, say "not in trace".'
181
+ ].join("\n");
182
+ var ANSWER_SCHEMA_RULE = "6. Answer with one JSON value matching ANSWER SCHEMA below, nothing else.";
183
+ var MAX_CONTEXT_TRACES = 200;
184
+ var QUESTION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
185
+ var QUESTION_KEYS = /* @__PURE__ */ new Set(["id", "question", "instructions", "answerSchema"]);
186
+ function defaultQuestionId(index, reserved, taken) {
187
+ let n = index + 1;
188
+ while (reserved.has(`q${n}`) || taken.has(`q${n}`)) n += 1;
189
+ return `q${n}`;
190
+ }
191
+ function normalizeTraceQuestions(questions) {
192
+ if (questions.length === 0) throw new Error("ask needs at least one question");
193
+ const reserved = new Set(questions.flatMap((entry) => typeof entry.id === "string" ? [entry.id] : []));
194
+ const seen = /* @__PURE__ */ new Set();
195
+ return questions.map((entry, index) => {
196
+ const id = entry.id ?? defaultQuestionId(index, reserved, seen);
197
+ if (!QUESTION_ID.test(id)) {
198
+ throw new Error(`question ID "${id}" must match ${QUESTION_ID} (letters, digits, dot, underscore, hyphen)`);
199
+ }
200
+ if (seen.has(id)) throw new Error(`duplicate question ID "${id}"`);
201
+ seen.add(id);
202
+ const question = typeof entry.question === "string" ? entry.question.trim() : "";
203
+ if (!question) throw new Error(`question ${id} is empty`);
204
+ if (question.length > MAX_TRACE_QUESTION_CHARS) {
205
+ throw new Error(
206
+ `question ${id} has ${question.length} characters; the limit is ${MAX_TRACE_QUESTION_CHARS} so the model sees it whole. Move detail into the entry's "instructions" field.`
207
+ );
208
+ }
209
+ if (entry.instructions !== void 0 && typeof entry.instructions !== "string") {
210
+ throw new Error(`question ${id}: instructions must be a string`);
211
+ }
212
+ if (entry.answerSchema !== void 0) assertAnswerSchema(entry.answerSchema, `question ${id} answerSchema`);
213
+ return {
214
+ id,
215
+ question,
216
+ ...entry.instructions?.trim() ? { instructions: entry.instructions.trim() } : {},
217
+ ...entry.answerSchema !== void 0 ? { answerSchema: entry.answerSchema } : {}
218
+ };
219
+ });
220
+ }
221
+ async function loadTraceQuestionsFile(path) {
222
+ let parsed;
223
+ try {
224
+ parsed = JSON.parse(await readFile(path, "utf8"));
225
+ } catch (error) {
226
+ throw new Error(`questions file ${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
227
+ }
228
+ const list = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.questions) ? parsed.questions : void 0;
229
+ if (!list) throw new Error(`questions file ${path} must hold a JSON array or an object with a "questions" array`);
230
+ return list.map((entry, index) => {
231
+ if (typeof entry === "string") return { question: entry };
232
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
233
+ throw new Error(`questions file ${path}: entry ${index + 1} must be a string or an object`);
234
+ }
235
+ for (const key of Object.keys(entry)) {
236
+ if (!QUESTION_KEYS.has(key)) {
237
+ throw new Error(`questions file ${path}: entry ${index + 1} has unknown key "${key}"`);
238
+ }
239
+ }
240
+ const record = entry;
241
+ if (typeof record.question !== "string") {
242
+ throw new Error(`questions file ${path}: entry ${index + 1} needs a "question" string`);
243
+ }
244
+ if (record.id !== void 0 && typeof record.id !== "string") {
245
+ throw new Error(`questions file ${path}: entry ${index + 1} "id" must be a string`);
246
+ }
247
+ return {
248
+ question: record.question,
249
+ ...record.id !== void 0 ? { id: record.id } : {},
250
+ ...record.instructions !== void 0 ? { instructions: record.instructions } : {},
251
+ ...record.answerSchema !== void 0 ? { answerSchema: record.answerSchema } : {}
252
+ };
253
+ });
254
+ }
255
+ function renderTraceQuestionPrompt(question) {
256
+ return `${ASK_QUESTION_HEADER}${QUESTION_LABEL}${question}`;
257
+ }
258
+ function renderTraceQuestionInstructions(question) {
259
+ const rules = question.answerSchema ? `${ASK_RULES}
260
+ ${ANSWER_SCHEMA_RULE}` : ASK_RULES;
261
+ return [
262
+ rules,
263
+ question.instructions ? `QUESTION GUIDANCE:
264
+ ${question.instructions}` : "",
265
+ question.answerSchema ? `ANSWER SCHEMA:
266
+ ${JSON.stringify(question.answerSchema)}` : ""
267
+ ].filter(Boolean).join("\n\n");
268
+ }
269
+ function describeTraces(spans) {
270
+ const { sessionByTrace } = indexSessionIdsByTrace(spans);
271
+ const byTrace = /* @__PURE__ */ new Map();
272
+ for (const span of spans) {
273
+ const entry = byTrace.get(span.trace_id) ?? { count: 0, start: Infinity, end: -Infinity, root: null };
274
+ entry.count += 1;
275
+ const start = Date.parse(span.start_time);
276
+ const end = Date.parse(span.end_time);
277
+ if (Number.isFinite(start)) entry.start = Math.min(entry.start, start);
278
+ if (Number.isFinite(end)) entry.end = Math.max(entry.end, end);
279
+ if (span.parent_span_id === null && entry.root === null) entry.root = span.name;
280
+ byTrace.set(span.trace_id, entry);
281
+ }
282
+ return [...byTrace].map(([traceId, entry]) => ({
283
+ traceId,
284
+ sessionId: sessionByTrace.get(traceId) ?? null,
285
+ spanCount: entry.count,
286
+ startTime: Number.isFinite(entry.start) ? new Date(entry.start).toISOString() : null,
287
+ endTime: Number.isFinite(entry.end) ? new Date(entry.end).toISOString() : null,
288
+ rootSpan: entry.root
289
+ }));
290
+ }
291
+ function preparedContext(traces, sheet) {
292
+ const list = JSON.stringify({
293
+ traces: traces.slice(0, MAX_CONTEXT_TRACES).map((trace) => ({
294
+ trace_id: trace.traceId,
295
+ session_id: trace.sessionId,
296
+ spans: trace.spanCount,
297
+ start: trace.startTime,
298
+ end: trace.endTime,
299
+ root: trace.rootSpan
300
+ })),
301
+ omitted_traces: Math.max(0, traces.length - MAX_CONTEXT_TRACES)
302
+ });
303
+ return sheet ? `${list}
304
+
305
+ ${sheet}` : list;
306
+ }
307
+ function createTraceQuestionCorpus() {
308
+ const chunks = [];
309
+ const traces = [];
310
+ const facts = createSessionFactsReportBuilder();
311
+ let spanCount = 0;
312
+ return {
313
+ add(spans) {
314
+ if (spans.length === 0) return;
315
+ spanCount += spans.length;
316
+ chunks.push(exportBytes(spans));
317
+ appendAll(traces, describeTraces(spans));
318
+ facts.add(spans);
319
+ },
320
+ finish() {
321
+ const store = openAgenticTraceStore(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks));
322
+ chunks.length = 0;
323
+ const report = facts.build();
324
+ return {
325
+ store,
326
+ traces,
327
+ context: preparedContext(traces, report.sessions.length === 0 ? void 0 : renderSessionFactsContext(report)),
328
+ spanCount
329
+ };
330
+ }
331
+ };
332
+ }
333
+ var TRACE_URI = /trace:\/\/[^\s/"'`<>()[\]{}]+\/span\/[^\s/"'`<>()[\]{},;]+/g;
334
+ var TRAILING_PROSE = /[.,:;!?*_~]+$/;
335
+ function traceCitationsInText(text) {
336
+ const seen = /* @__PURE__ */ new Set();
337
+ const out = [];
338
+ for (const match of text.matchAll(TRACE_URI)) {
339
+ const uri = match[0].replace(TRAILING_PROSE, "");
340
+ if (seen.has(uri)) continue;
341
+ seen.add(uri);
342
+ const parts = /^trace:\/\/([^/]+)\/span\/([^/]+)$/.exec(uri);
343
+ let traceId = null;
344
+ let spanId = null;
345
+ if (parts) {
346
+ try {
347
+ traceId = decodeURIComponent(parts[1]);
348
+ spanId = decodeURIComponent(parts[2]);
349
+ } catch {
350
+ traceId = null;
351
+ spanId = null;
352
+ }
353
+ }
354
+ out.push({ uri, traceId, spanId });
355
+ }
356
+ return out;
357
+ }
358
+ async function verifyCitations(text, store) {
359
+ const citations = traceCitationsInText(text);
360
+ const wanted = /* @__PURE__ */ new Map();
361
+ for (const citation of citations) {
362
+ if (!citation.traceId || !citation.spanId) continue;
363
+ const spans = wanted.get(citation.traceId) ?? /* @__PURE__ */ new Set();
364
+ spans.add(citation.spanId);
365
+ wanted.set(citation.traceId, spans);
366
+ }
367
+ const found = /* @__PURE__ */ new Map();
368
+ for (const [traceId, spanIds] of wanted) {
369
+ const existing = await store.hasSpans({ trace_id: traceId, span_ids: [...spanIds] });
370
+ found.set(traceId, new Set(existing));
371
+ }
372
+ return citations.map((citation) => ({
373
+ ...citation,
374
+ resolved: Boolean(citation.traceId && citation.spanId && found.get(citation.traceId)?.has(citation.spanId))
375
+ }));
376
+ }
377
+ function isBudgetRefusal(error) {
378
+ if (error instanceof CostCeilingReachedError) return true;
379
+ const message = error instanceof Error ? error.message : String(error);
380
+ return /would exceed ceiling|model cost limit reached/.test(message);
381
+ }
382
+ function ledgerIsExhausted(ledger, budgetUsd, floorUsd) {
383
+ if (budgetUsd === void 0 || floorUsd === void 0) return false;
384
+ const settled = ledger.summary({ channel: "analyst" }).totalCostUsd;
385
+ return Number.isFinite(settled) && budgetUsd - settled < floorUsd;
386
+ }
387
+ var ENGINE_NO_ANSWER = /returned no answer/;
388
+ function questionFailureKind(error, state) {
389
+ if (state.aborted) return "aborted";
390
+ if (isBudgetRefusal(error)) return "budget-refused";
391
+ const message = errorMessage(error);
392
+ if (isBridgeMismatchError(message)) return "error";
393
+ if (ENGINE_NO_ANSWER.test(message)) return "no-answer";
394
+ return state.ledgerExhausted ? "budget-refused" : "error";
395
+ }
396
+ function errorMessage(error) {
397
+ return error instanceof Error ? `${error.constructor.name}: ${error.message}` : String(error);
398
+ }
399
+ function engineCallReservationFloorUsd(engine) {
400
+ const config = engine.executionConfig;
401
+ const pricing = config.pricing;
402
+ const outputRate = pricing?.outputUsdPerMillion;
403
+ const maxOutput = config.max_output_tokens;
404
+ const maxReasoning = config.max_reasoning_tokens ?? 0;
405
+ if (typeof outputRate !== "number" || typeof maxOutput !== "number" || typeof maxReasoning !== "number") {
406
+ return void 0;
407
+ }
408
+ return (maxOutput + maxReasoning) * outputRate / 1e6;
409
+ }
410
+ function formatUsd(value) {
411
+ return `$${value < 0.01 && value > 0 ? value.toPrecision(2) : value.toFixed(2)}`;
412
+ }
413
+ function unrunAnswer(question, failure, model) {
414
+ const at = (/* @__PURE__ */ new Date()).toISOString();
415
+ return {
416
+ id: question.id,
417
+ question: question.question,
418
+ status: "failed",
419
+ failure,
420
+ answer: null,
421
+ citations: [],
422
+ findings: [],
423
+ rejectedFindings: {},
424
+ model,
425
+ modelCalls: null,
426
+ toolCalls: null,
427
+ usage: null,
428
+ startedAt: at,
429
+ endedAt: at,
430
+ latencyMs: 0
431
+ };
432
+ }
433
+ function sumOrNull(values) {
434
+ return values.some((value) => value === null) ? null : values.reduce((sum, value) => sum + (value ?? 0), 0);
435
+ }
436
+ async function runTraceQuestions(opts) {
437
+ if ((opts.corpus?.spanCount ?? opts.spans?.length ?? 0) === 0) throw new Error("runTraceQuestions: no spans to ask about");
438
+ const questions = normalizeTraceQuestions(opts.questions);
439
+ const concurrency = opts.concurrency ?? DEFAULT_ASK_CONCURRENCY;
440
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new RangeError("concurrency must be an integer >= 1");
441
+ const budgetUsd = opts.budgetUsd;
442
+ if (budgetUsd !== void 0 && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
443
+ throw new RangeError("budgetUsd must be a positive number");
444
+ }
445
+ const warnings = [];
446
+ const floor = engineCallReservationFloorUsd(opts.engine);
447
+ const effectiveConcurrency = Math.min(concurrency, questions.length);
448
+ if (budgetUsd !== void 0 && floor !== void 0) {
449
+ if (budgetUsd < floor) {
450
+ throw new RangeError(
451
+ `budget ${formatUsd(budgetUsd)} is below one model call's reservation of at least ${formatUsd(floor)} for ${opts.engine.model ?? opts.engine.id}; every question would be refused before it ran`
452
+ );
453
+ }
454
+ if (budgetUsd < floor * effectiveConcurrency) {
455
+ warnings.push(
456
+ `budget ${formatUsd(budgetUsd)} covers at most ${Math.floor(budgetUsd / floor)} concurrent model call(s) at ${formatUsd(floor)} reserved each; questions wait for reservations, and later calls are refused once settled spend leaves less than one reservation`
457
+ );
458
+ }
459
+ }
460
+ opts.signal?.throwIfAborted();
461
+ const runStarted = performance.now();
462
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
463
+ const corpus = opts.corpus ?? (() => {
464
+ const builder = createTraceQuestionCorpus();
465
+ builder.add(opts.spans);
466
+ return builder.finish();
467
+ })();
468
+ const { store, traces, context } = corpus;
469
+ const runId = `traces-ask-${Date.parse(generatedAt) || Date.now()}`;
470
+ const ledger = new CostLedger(budgetUsd);
471
+ let active = 0;
472
+ let peakConcurrency = 0;
473
+ const askOne = async (question) => {
474
+ const startedAt = /* @__PURE__ */ new Date();
475
+ const started = performance.now();
476
+ const rejections = createFindingRejectionTally(question.id);
477
+ let usage = null;
478
+ let completed;
479
+ let failure;
480
+ const log = (msg, fields) => {
481
+ rejections.record(msg, fields);
482
+ opts.log?.(`[${question.id}] ${msg}`, fields);
483
+ };
484
+ const definition = defineTraceAnalyst({
485
+ // Namespaced so a question ID can never select a built-in kind's subject rules.
486
+ id: `ask.${question.id}`,
487
+ description: `traces ask question ${question.id}`,
488
+ area: "question",
489
+ version: ASK_DEFINITION_VERSION,
490
+ question: renderTraceQuestionPrompt(question.question),
491
+ instructions: renderTraceQuestionInstructions(question),
492
+ toolGroup: "all",
493
+ prepareContext: () => context,
494
+ ...opts.limits ? { limits: opts.limits } : {}
495
+ });
496
+ if (opts.signal?.aborted) {
497
+ failure = { kind: "aborted", message: "run aborted before the question started" };
498
+ } else {
499
+ active += 1;
500
+ peakConcurrency = Math.max(peakConcurrency, active);
501
+ try {
502
+ completed = await runTraceAnalyst({
503
+ definition,
504
+ engine: opts.engine,
505
+ store,
506
+ context: {
507
+ runId,
508
+ correlationId: `${runId}:${question.id}`,
509
+ costLedger: ledger,
510
+ costPhase: "trace-question",
511
+ log,
512
+ recordUsage: (receipt) => {
513
+ usage = receipt;
514
+ },
515
+ ...opts.signal ? { signal: opts.signal } : {}
516
+ }
517
+ });
518
+ } catch (error) {
519
+ failure = {
520
+ kind: questionFailureKind(error, {
521
+ aborted: opts.signal?.aborted ?? false,
522
+ ledgerExhausted: ledgerIsExhausted(ledger, budgetUsd, floor)
523
+ }),
524
+ message: errorMessage(error)
525
+ };
526
+ } finally {
527
+ active -= 1;
528
+ }
529
+ }
530
+ const answer = completed && completed.answer.trim() ? completed.answer : null;
531
+ let parsedAnswer;
532
+ let hasParsedAnswer = false;
533
+ let citations = [];
534
+ try {
535
+ if (answer !== null) {
536
+ citations = await verifyCitations(answer, store);
537
+ if (question.answerSchema) {
538
+ const parsed = parseJsonAnswer(answer);
539
+ const problems = parsed.ok ? answerSchemaErrors(parsed.value, question.answerSchema) : [parsed.error];
540
+ if (parsed.ok) {
541
+ parsedAnswer = parsed.value;
542
+ hasParsedAnswer = true;
543
+ }
544
+ if (problems.length > 0 && !failure) {
545
+ failure = { kind: "invalid-answer", message: problems.slice(0, 5).join("; ") };
546
+ }
547
+ }
548
+ const unresolved = citations.filter((citation) => !citation.resolved);
549
+ if (unresolved.length > 0 && !failure) {
550
+ failure = {
551
+ kind: "unresolved-citations",
552
+ message: `${unresolved.length} cited span(s) do not exist: ${unresolved.slice(0, 3).map((c) => c.uri).join(", ")}`
553
+ };
554
+ }
555
+ } else if (completed && !failure) {
556
+ failure = { kind: "no-answer", message: "the engine returned an empty answer" };
557
+ }
558
+ } catch (error) {
559
+ if (!failure) {
560
+ failure = {
561
+ kind: opts.signal?.aborted ? "aborted" : "error",
562
+ message: `checking the answer failed: ${errorMessage(error)}`
563
+ };
564
+ }
565
+ }
566
+ const endedAt = /* @__PURE__ */ new Date();
567
+ return {
568
+ id: question.id,
569
+ question: question.question,
570
+ status: failure ? "failed" : "answered",
571
+ ...failure ? { failure } : {},
572
+ answer,
573
+ ...hasParsedAnswer ? { parsedAnswer } : {},
574
+ citations,
575
+ findings: completed?.findings ?? [],
576
+ rejectedFindings: rejections.counts()[question.id] ?? {},
577
+ model: opts.engine.model ?? null,
578
+ modelCalls: completed?.modelCalls ?? null,
579
+ toolCalls: completed?.toolCalls ?? null,
580
+ usage,
581
+ startedAt: startedAt.toISOString(),
582
+ endedAt: endedAt.toISOString(),
583
+ latencyMs: Math.round(performance.now() - started),
584
+ ...completed ? { trajectory: completed.trajectory } : {}
585
+ };
586
+ };
587
+ const answers = new Array(questions.length);
588
+ let next = 0;
589
+ const setupTimeMs = Math.round(performance.now() - runStarted);
590
+ const settled = await Promise.allSettled(Array.from({ length: effectiveConcurrency }, async () => {
591
+ while (next < questions.length) {
592
+ const index = next;
593
+ next += 1;
594
+ answers[index] = await askOne(questions[index]);
595
+ }
596
+ }));
597
+ const failedWorker = settled.find((worker) => worker.status === "rejected");
598
+ const wallTimeMs = Math.round(performance.now() - runStarted);
599
+ if (failedWorker) {
600
+ const message = errorMessage(failedWorker.reason);
601
+ warnings.push(`the question pool stopped early: ${message}`);
602
+ const kind = opts.signal?.aborted ? "aborted" : "error";
603
+ for (const [index, question] of questions.entries()) {
604
+ answers[index] ??= unrunAnswer(question, { kind, message }, opts.engine.model ?? null);
605
+ }
606
+ }
607
+ const summary = ledger.summary({ channel: "analyst" });
608
+ const answered = answers.filter((answer) => answer.status === "answered").length;
609
+ const totals = {
610
+ questions: answers.length,
611
+ answered,
612
+ failed: answers.length - answered,
613
+ modelCalls: sumOrNull(answers.map((answer) => answer.modelCalls)),
614
+ toolCalls: sumOrNull(answers.map((answer) => answer.toolCalls)),
615
+ providerCalls: summary.totalCalls + summary.pendingCalls,
616
+ cost: summary.costProvenance,
617
+ wallTimeMs,
618
+ setupTimeMs,
619
+ questionTimeMs: answers.reduce((sum, answer) => sum + answer.latencyMs, 0),
620
+ peakConcurrency
621
+ };
622
+ const partial = {
623
+ schemaVersion: 1,
624
+ kind: "traces.ask",
625
+ generatedAt,
626
+ harness: opts.harness ?? "unknown",
627
+ engine: { id: opts.engine.id, version: opts.engine.version, model: opts.engine.model ?? null },
628
+ concurrency,
629
+ effectiveConcurrency,
630
+ budgetUsd: budgetUsd ?? null,
631
+ questionBudgetUsd: typeof opts.engine.executionConfig.max_cost_usd === "number" ? opts.engine.executionConfig.max_cost_usd : null,
632
+ questionTimeoutMs: typeof opts.engine.executionConfig.timeout_ms === "number" ? opts.engine.executionConfig.timeout_ms : null,
633
+ spanCount: corpus.spanCount,
634
+ traces,
635
+ questions: answers,
636
+ totals,
637
+ warnings,
638
+ ok: answered === answers.length
639
+ };
640
+ return { ...partial, report: renderTraceQuestionsReport(partial) };
641
+ }
642
+ function costText(cost) {
643
+ if (!cost) return "not captured";
644
+ return cost.kind === "uncaptured" ? "uncaptured" : `${formatUsd(cost.usd)} ${cost.kind}`;
645
+ }
646
+ function seconds(ms) {
647
+ return `${(ms / 1e3).toFixed(1)} s`;
648
+ }
649
+ function cell(value) {
650
+ return value.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
651
+ }
652
+ function countText(value) {
653
+ return value === null ? "unknown" : String(value);
654
+ }
655
+ var ANSWER_SECTION_LEVEL = 2;
656
+ function nestAnswerHeadings(text) {
657
+ let fence = null;
658
+ return text.split("\n").map((line) => {
659
+ const fenced = /^ {0,3}(`{3,}|~{3,})/.exec(line);
660
+ if (fenced) {
661
+ const marker = fenced[1][0];
662
+ const length = fenced[1].length;
663
+ if (fence === null) fence = { marker, length };
664
+ else if (fence.marker === marker && length >= fence.length) fence = null;
665
+ return line;
666
+ }
667
+ if (fence !== null) return line;
668
+ const heading = /^(#{1,6})(?=\s)/.exec(line);
669
+ if (!heading) return line;
670
+ const level = Math.min(6, Math.max(ANSWER_SECTION_LEVEL + 1, heading[1].length));
671
+ return `${"#".repeat(level)}${line.slice(heading[1].length)}`;
672
+ }).join("\n");
673
+ }
674
+ function renderTraceQuestionsReport(result) {
675
+ const { totals } = result;
676
+ const lines = ["# traces ask", ""];
677
+ const effective = result.effectiveConcurrency === result.concurrency ? "" : ` (${result.effectiveConcurrency} effective)`;
678
+ lines.push(
679
+ `${totals.questions} question(s) over ${result.traces.length} trace(s) (${result.spanCount} spans, ${result.harness}). Engine \`${result.engine.id}\`${result.engine.model ? `, model \`${result.engine.model}\`` : ""}; concurrency ${result.concurrency}${effective}; budget ${result.budgetUsd === null ? "uncapped" : `${formatUsd(result.budgetUsd)} shared`}${result.questionBudgetUsd === null ? "" : `, ${formatUsd(result.questionBudgetUsd)} per question`}.`
680
+ );
681
+ lines.push("");
682
+ lines.push(
683
+ `**${totals.answered} answered, ${totals.failed} failed.** Wall time ${seconds(totals.wallTimeMs)} (${seconds(totals.setupTimeMs)} of it building the trace store) against ${seconds(totals.questionTimeMs)} of question time (peak ${totals.peakConcurrency} at once). Cost ${costText(totals.cost)} over ${totals.providerCalls} provider call(s); ${countText(totals.modelCalls)} model call(s), ${countText(totals.toolCalls)} tool call(s).`
684
+ );
685
+ lines.push("");
686
+ for (const warning of result.warnings) {
687
+ lines.push(`> Warning: ${warning}`);
688
+ lines.push("");
689
+ }
690
+ lines.push("| ID | Status | Citations | Rejected findings | Model calls | Tool calls | Cost | Time |");
691
+ lines.push("|---|---|---|---|---|---|---|---|");
692
+ for (const answer of result.questions) {
693
+ const resolved = answer.citations.filter((citation) => citation.resolved).length;
694
+ lines.push(
695
+ `| \`${cell(answer.id)}\` | ${answer.status === "answered" ? "answered" : `failed: ${answer.failure?.kind}`} | ${resolved}/${answer.citations.length} resolved | ${totalFindingRejections(answer.rejectedFindings)} | ${countText(answer.modelCalls)} | ${countText(answer.toolCalls)} | ${costText(answer.usage?.cost)} | ${seconds(answer.latencyMs)} |`
696
+ );
697
+ }
698
+ lines.push("");
699
+ for (const answer of result.questions) {
700
+ lines.push(`## ${answer.id}: ${answer.question.replace(/\s+/g, " ").trim()}`);
701
+ lines.push("");
702
+ if (answer.failure) {
703
+ lines.push(`**Failed (${answer.failure.kind}):** ${answer.failure.message.trim()}`);
704
+ lines.push("");
705
+ }
706
+ if (answer.answer !== null) {
707
+ lines.push(nestAnswerHeadings(answer.answer.trim()));
708
+ lines.push("");
709
+ }
710
+ const notes = [];
711
+ const unresolved = answer.citations.filter((citation) => !citation.resolved);
712
+ if (answer.citations.length > 0) {
713
+ notes.push(
714
+ `- **Citations:** ${answer.citations.length - unresolved.length} of ${answer.citations.length} resolve to a span in the trace.`
715
+ );
716
+ for (const citation of unresolved) notes.push(`- **Unresolved:** ${citation.uri}`);
717
+ } else if (answer.answer !== null) {
718
+ notes.push("- **Citations:** none in the answer text.");
719
+ }
720
+ if (answer.findings.length > 0) {
721
+ notes.push(`- **Accepted findings:** ${answer.findings.length}`);
722
+ for (const finding of answer.findings.slice(0, 5)) notes.push(` - ${finding.severity}: ${finding.claim}`);
723
+ }
724
+ const rejected = formatFindingRejections(answer.rejectedFindings);
725
+ if (rejected) notes.push(`- **Rejected by the evidence gate:** ${rejected}`);
726
+ if (notes.length > 0) lines.push(...notes, "");
727
+ }
728
+ return lines.join("\n");
729
+ }
730
+ async function writeTraceQuestionsArtifacts(result, outDir) {
731
+ const directory = resolve(outDir);
732
+ await mkdir(directory, { recursive: true });
733
+ const paths = {
734
+ directory,
735
+ result: join(directory, "answers.json"),
736
+ report: join(directory, "report.md")
737
+ };
738
+ const { report, ...machine } = result;
739
+ await Promise.all([
740
+ writeFile(paths.result, `${JSON.stringify(machine, null, 2)}
741
+ `, "utf8"),
742
+ writeFile(paths.report, report, "utf8")
743
+ ]);
744
+ return paths;
745
+ }
746
+
747
+ export {
748
+ isBridgeMismatchError,
749
+ normalizeTraceQuestions,
750
+ loadTraceQuestionsFile,
751
+ renderTraceQuestionPrompt,
752
+ renderTraceQuestionInstructions,
753
+ createTraceQuestionCorpus,
754
+ traceCitationsInText,
755
+ engineCallReservationFloorUsd,
756
+ runTraceQuestions,
757
+ renderTraceQuestionsReport,
758
+ writeTraceQuestionsArtifacts
759
+ };