@tangle-network/traces 0.12.0 → 0.15.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/README.md +132 -3
- package/dist/{chunk-CV22VGCK.js → chunk-7DOBU7BJ.js} +16040 -12291
- package/dist/cli.js +221 -169
- package/dist/index.d.ts +933 -59
- package/dist/index.js +109 -1
- package/docs/trace-analysts.md +307 -0
- package/install.sh +0 -0
- package/package.json +23 -19
- package/skills/build-trace-analyst/SKILL.md +3 -0
- package/skills/inspect-agent-traces/SKILL.md +22 -20
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_ANALYST_MODEL,
|
|
4
|
+
DEFAULT_QUESTION_MAX_COST_USD,
|
|
3
5
|
DEFAULT_SANDBOX_BASE_URL,
|
|
6
|
+
MAX_TRACE_QUESTION_CHARS,
|
|
7
|
+
analysisEngineFromEnv,
|
|
4
8
|
appendAll,
|
|
5
9
|
assembleSessionBundle,
|
|
10
|
+
assertOutsideSourceBundle,
|
|
6
11
|
buildPolicyEvidenceRecord,
|
|
12
|
+
buildSessionFactsReport,
|
|
7
13
|
buildSessionIndexFromRows,
|
|
8
14
|
cmdReplayVerify,
|
|
9
15
|
cmdReplayVerifyBatch,
|
|
@@ -13,17 +19,21 @@ import {
|
|
|
13
19
|
commandRedactor,
|
|
14
20
|
executeUpload,
|
|
15
21
|
exportTraceEvidenceFile,
|
|
22
|
+
externalFailureMessage,
|
|
16
23
|
fileRunContextSupervisorRunReader,
|
|
24
|
+
findingRejectionDetail,
|
|
17
25
|
haloAnalyzer,
|
|
18
26
|
hodoscopeAnalyzer,
|
|
19
27
|
importCodeTraceBench,
|
|
20
28
|
indexSessionIdsByTrace,
|
|
21
29
|
inspectSessionIndex,
|
|
22
|
-
|
|
30
|
+
isBridgeMismatchError,
|
|
23
31
|
knownHarnesses,
|
|
32
|
+
loadTraceQuestionsFile,
|
|
24
33
|
loadTracesConfig,
|
|
25
34
|
locateSessions,
|
|
26
35
|
mergeTracesConfig,
|
|
36
|
+
normalizeTraceQuestions,
|
|
27
37
|
parseCorpusFlag,
|
|
28
38
|
parseSession,
|
|
29
39
|
parseSince,
|
|
@@ -33,12 +43,14 @@ import {
|
|
|
33
43
|
readOtlpInput,
|
|
34
44
|
readSessionIndexFile,
|
|
35
45
|
renderInspectionReport,
|
|
46
|
+
renderSessionFacts,
|
|
36
47
|
renderValidation,
|
|
37
48
|
renderVerifiedFindingsSection,
|
|
38
49
|
resolveAdapter,
|
|
39
50
|
resolveRunWatchTarget,
|
|
40
51
|
runTraceImprovement,
|
|
41
52
|
runTraceInvestigation,
|
|
53
|
+
runTraceQuestions,
|
|
42
54
|
saveReport,
|
|
43
55
|
selectAdapters,
|
|
44
56
|
serializePolicyEvidence,
|
|
@@ -57,89 +69,23 @@ import {
|
|
|
57
69
|
writeOtlpFile,
|
|
58
70
|
writePolicyEvidenceFile,
|
|
59
71
|
writeSessionIndexFile,
|
|
60
|
-
writeTraceEvidenceExportFile
|
|
61
|
-
|
|
72
|
+
writeTraceEvidenceExportFile,
|
|
73
|
+
writeTraceQuestionsArtifacts
|
|
74
|
+
} from "./chunk-7DOBU7BJ.js";
|
|
62
75
|
|
|
63
76
|
// src/cli.ts
|
|
64
77
|
import { readFileSync } from "fs";
|
|
65
|
-
import {
|
|
78
|
+
import { mkdir, mkdtemp, readFile, stat, writeFile } from "fs/promises";
|
|
79
|
+
import { tmpdir } from "os";
|
|
66
80
|
import { basename, join, resolve } from "path";
|
|
67
81
|
import {
|
|
68
82
|
analyzeSupervisorRun,
|
|
69
83
|
findSupervisorRunDirs,
|
|
70
|
-
|
|
84
|
+
isRuntimeSupervisorRunDir,
|
|
71
85
|
renderSupervisorRollupMarkdown,
|
|
72
86
|
renderSupervisorRunMarkdown,
|
|
73
87
|
rollupSupervisorRuns
|
|
74
88
|
} from "@tangle-network/agent-eval/supervisor-run";
|
|
75
|
-
import { createDspyRlmTraceEngine } from "@tangle-network/agent-eval/analyst";
|
|
76
|
-
|
|
77
|
-
// src/analyst-model-call.ts
|
|
78
|
-
import {
|
|
79
|
-
callLlm,
|
|
80
|
-
costReceiptFromLlm,
|
|
81
|
-
costReceiptFromLlmError,
|
|
82
|
-
LlmCallError
|
|
83
|
-
} from "@tangle-network/agent-eval";
|
|
84
|
-
var PROVIDER_ERROR_BODY_LIMIT = 2e3;
|
|
85
|
-
function createAnalystModelCall(opts) {
|
|
86
|
-
const { apiKey, baseUrl } = opts;
|
|
87
|
-
return async ({ request, callId, signal }) => {
|
|
88
|
-
try {
|
|
89
|
-
const req = structuredClone(request);
|
|
90
|
-
const response = await callLlm(req, {
|
|
91
|
-
apiKey,
|
|
92
|
-
baseUrl,
|
|
93
|
-
signal,
|
|
94
|
-
idempotencyKey: callId
|
|
95
|
-
});
|
|
96
|
-
return {
|
|
97
|
-
succeeded: true,
|
|
98
|
-
response,
|
|
99
|
-
receipt: costReceiptFromLlm(response),
|
|
100
|
-
execution: {
|
|
101
|
-
callId,
|
|
102
|
-
baseUrl,
|
|
103
|
-
requestedModel: req.model,
|
|
104
|
-
servedModel: response.servedModel ?? null,
|
|
105
|
-
finishReason: response.finishReason ?? null,
|
|
106
|
-
durationMs: response.durationMs
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
} catch (error) {
|
|
110
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
111
|
-
const aborted = signal.aborted;
|
|
112
|
-
const message = aborted ? `AbortError: ${err.message}` : err.message;
|
|
113
|
-
const detail = err instanceof LlmCallError ? { status: err.status, body: err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT) } : {};
|
|
114
|
-
return {
|
|
115
|
-
succeeded: false,
|
|
116
|
-
error: message,
|
|
117
|
-
// costReceiptFromLlmError recovers the provider receipt when the
|
|
118
|
-
// response completed but violated the contract; otherwise usage and
|
|
119
|
-
// cost stay explicitly unknown.
|
|
120
|
-
receipt: costReceiptFromLlmError(err) ?? {
|
|
121
|
-
model: request.model,
|
|
122
|
-
inputTokens: 0,
|
|
123
|
-
outputTokens: 0,
|
|
124
|
-
costUnknown: true,
|
|
125
|
-
usageUnknown: true
|
|
126
|
-
},
|
|
127
|
-
execution: {
|
|
128
|
-
callId,
|
|
129
|
-
baseUrl,
|
|
130
|
-
requestedModel: request.model,
|
|
131
|
-
aborted,
|
|
132
|
-
error: message,
|
|
133
|
-
...detail
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// src/cli.ts
|
|
141
|
-
var DEFAULT_ANALYST_MODEL = "gpt-5-mini";
|
|
142
|
-
var TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
|
|
143
89
|
function packageVersion() {
|
|
144
90
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
145
91
|
if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
|
|
@@ -173,7 +119,8 @@ function parseArgs(argv) {
|
|
|
173
119
|
attrs: [],
|
|
174
120
|
concurrency: 4,
|
|
175
121
|
verifyFindings: false,
|
|
176
|
-
replayCorpora: []
|
|
122
|
+
replayCorpora: [],
|
|
123
|
+
questions: []
|
|
177
124
|
};
|
|
178
125
|
for (let i = 1; i < argv.length; i++) {
|
|
179
126
|
const arg = argv[i];
|
|
@@ -231,6 +178,9 @@ function parseArgs(argv) {
|
|
|
231
178
|
case "--otlp":
|
|
232
179
|
a.otlp = next();
|
|
233
180
|
break;
|
|
181
|
+
case "--source-bundle":
|
|
182
|
+
a.sourceBundle = next();
|
|
183
|
+
break;
|
|
234
184
|
case "--otlp-out":
|
|
235
185
|
a.otlpOut = next();
|
|
236
186
|
break;
|
|
@@ -310,6 +260,17 @@ function parseArgs(argv) {
|
|
|
310
260
|
case "--analyzer-prompt":
|
|
311
261
|
a.analyzerPrompt = next();
|
|
312
262
|
break;
|
|
263
|
+
case "--question": {
|
|
264
|
+
const v = next();
|
|
265
|
+
if (v !== void 0) a.questions.push(v);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "--questions":
|
|
269
|
+
a.questionsFile = next();
|
|
270
|
+
break;
|
|
271
|
+
case "--question-budget":
|
|
272
|
+
a.questionBudget = Number(next());
|
|
273
|
+
break;
|
|
313
274
|
case "--redactor":
|
|
314
275
|
a.redactorCmd = next();
|
|
315
276
|
break;
|
|
@@ -332,7 +293,7 @@ function parseArgs(argv) {
|
|
|
332
293
|
}
|
|
333
294
|
return a;
|
|
334
295
|
}
|
|
335
|
-
var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set(["analyze", "investigate", "improve", "stream", "validate"]);
|
|
296
|
+
var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set(["analyze", "facts", "investigate", "improve", "ask", "stream", "validate"]);
|
|
336
297
|
function migrateWritingOtlpFlag(args) {
|
|
337
298
|
if (args.otlp === void 0 || OTLP_INPUT_COMMANDS.has(args.command)) return args;
|
|
338
299
|
if (args.otlpOut !== void 0) {
|
|
@@ -358,8 +319,10 @@ function validateOtlpSelection(raw) {
|
|
|
358
319
|
}
|
|
359
320
|
var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
|
|
360
321
|
"analyze",
|
|
322
|
+
"facts",
|
|
361
323
|
"investigate",
|
|
362
324
|
"improve",
|
|
325
|
+
"ask",
|
|
363
326
|
"convert",
|
|
364
327
|
"index",
|
|
365
328
|
"evidence",
|
|
@@ -368,8 +331,10 @@ var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
368
331
|
]);
|
|
369
332
|
var WORKFLOW_COMMANDS = /* @__PURE__ */ new Set([
|
|
370
333
|
"analyze",
|
|
334
|
+
"facts",
|
|
371
335
|
"investigate",
|
|
372
336
|
"improve",
|
|
337
|
+
"ask",
|
|
373
338
|
"convert",
|
|
374
339
|
"index",
|
|
375
340
|
"evidence"
|
|
@@ -484,41 +449,11 @@ async function resolveSelectedSession(args) {
|
|
|
484
449
|
`session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
|
|
485
450
|
);
|
|
486
451
|
}
|
|
487
|
-
function
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
throw new Error(
|
|
493
|
-
"--llm needs a model key: TANGLE_API_KEY for the Tangle router (the default endpoint), or OPENAI_API_KEY for OpenAI. Set OPENAI_BASE_URL to target any other OpenAI-compatible gateway. Deterministic analysis needs no key."
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
const baseUrl = process.env.OPENAI_BASE_URL || (tangleKey ? TANGLE_ROUTER_BASE_URL : "https://api.openai.com/v1");
|
|
497
|
-
const python = process.env.TRACES_PYTHON;
|
|
498
|
-
return createDspyRlmTraceEngine({
|
|
499
|
-
call: createAnalystModelCall({ apiKey, baseUrl }),
|
|
500
|
-
callRef: `traces-cli:${baseUrl}#${model}`,
|
|
501
|
-
recordExecution: (observation) => {
|
|
502
|
-
analystLog(
|
|
503
|
-
`[analyst] model call ${observation.sequence} ${observation.succeeded ? "ok" : "FAIL"} ${observation.model}`,
|
|
504
|
-
observation.succeeded ? void 0 : { error: observation.error }
|
|
505
|
-
);
|
|
506
|
-
},
|
|
507
|
-
model,
|
|
508
|
-
// Pinned, not defaulted: one findings array from a current coding model
|
|
509
|
-
// needs this cap, and glm-5.2 counts reasoning tokens in
|
|
510
|
-
// completion_tokens — a smaller cap breaches its cost reservation and the
|
|
511
|
-
// fail-closed ledger then refuses every later call in the run.
|
|
512
|
-
maxOutputTokens: 16384,
|
|
513
|
-
// maxCostUsd defaults to $1 per analyst — a proxy-side ceiling separate
|
|
514
|
-
// from --budget. With the larger token cap the per-call reservation grows
|
|
515
|
-
// ~4x, so that default binds before the registry's --budget allocation
|
|
516
|
-
// and kills analysts mid-run. --budget is the operator's spend authority
|
|
517
|
-
// and the registry still splits it across analysts, so forwarding it here
|
|
518
|
-
// only stops the engine's own default from cutting runs short.
|
|
519
|
-
...budgetUsd !== void 0 && Number.isFinite(budgetUsd) && budgetUsd > 0 ? { maxCostUsd: budgetUsd } : {},
|
|
520
|
-
...python ? { runner: { command: python } } : {}
|
|
521
|
-
});
|
|
452
|
+
function llmAnalysisEngine(args, model) {
|
|
453
|
+
return analysisEngineFromEnv({ model, maxCostUsd: args.budget, log: analystLog });
|
|
454
|
+
}
|
|
455
|
+
function analystModelFor(args) {
|
|
456
|
+
return args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
|
|
522
457
|
}
|
|
523
458
|
function requiredBridgeVersion() {
|
|
524
459
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -528,11 +463,14 @@ function requiredBridgeVersion() {
|
|
|
528
463
|
}
|
|
529
464
|
return version;
|
|
530
465
|
}
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
466
|
+
function assertRequestedAnalysesRan(args, result) {
|
|
467
|
+
const messages = [];
|
|
468
|
+
if (args.llm && totalAgenticFailureMessage(result.agenticPerAnalyst)) {
|
|
469
|
+
messages.push(totalAgenticFailureMessage(result.agenticPerAnalyst, { requiredBridgeVersion: requiredBridgeVersion() }));
|
|
470
|
+
}
|
|
471
|
+
const external = externalFailureMessage(result.external);
|
|
472
|
+
if (external) messages.push(external);
|
|
473
|
+
if (messages.length > 0) throw new Error(messages.join("\n"));
|
|
536
474
|
}
|
|
537
475
|
async function cmdList(args) {
|
|
538
476
|
const groups = await discover({ ...args, last: args.last || 20 });
|
|
@@ -582,6 +520,16 @@ async function collectOtlpSpans(path) {
|
|
|
582
520
|
};
|
|
583
521
|
}
|
|
584
522
|
async function collectSpans(args) {
|
|
523
|
+
if (args.sourceBundle) {
|
|
524
|
+
if (!["analyze", "investigate", "improve", "ask"].includes(args.command)) throw new Error("--source-bundle is an analysis option");
|
|
525
|
+
if (args.session || args.otlp || args.input || args.current || args.workflow || args.noContent || args.redactorCmd) {
|
|
526
|
+
throw new Error("--source-bundle cannot be combined with another input, redaction, or metadata-only selection");
|
|
527
|
+
}
|
|
528
|
+
for (const output of [args.out, args.dir, args.otlpOut]) {
|
|
529
|
+
if (output) await assertOutsideSourceBundle(args.sourceBundle, output);
|
|
530
|
+
}
|
|
531
|
+
return collectOtlpSpans(join(args.sourceBundle, "derived", "trace.otlp.jsonl"));
|
|
532
|
+
}
|
|
585
533
|
if (args.otlp) return collectOtlpSpans(args.otlp);
|
|
586
534
|
if (args.input) {
|
|
587
535
|
if (args.workflow) throw new Error("--workflow reads discovered sessions and cannot be combined with an input file");
|
|
@@ -647,6 +595,9 @@ function workflowIssueText(issue) {
|
|
|
647
595
|
if (issue.kind === "parent-conflict") {
|
|
648
596
|
return `parent conflict for ${issue.sessionId}: declared ${issue.declaredParentSessionId ?? "none"}, referenced by ${issue.referencedParentSessionIds.join(", ") || "none"}`;
|
|
649
597
|
}
|
|
598
|
+
if (issue.kind === "unjoined-child") {
|
|
599
|
+
return `unjoined child ${issue.agentPath} spawned by ${issue.parentSessionId}: ${issue.reason}` + (issue.candidates ? ` (${issue.candidates.join(", ")})` : "");
|
|
600
|
+
}
|
|
650
601
|
return `${issue.kind} ${issue.sessionId}, referenced as ${issue.relation} by ${issue.referencedBySessionId}`;
|
|
651
602
|
}
|
|
652
603
|
function warnIncompleteWorkflow(workflow) {
|
|
@@ -754,6 +705,30 @@ async function cmdBundleView(args) {
|
|
|
754
705
|
`${manifest.view} view \u2192 ${result.directory} (${manifest.files.length} file(s) carried, ${manifest.excluded.length} excluded, ${leakCheck.matches} session-text match(es) against ${leakCheck.sourceSignatures} signature(s) from ${leakCheck.comparedSources.length} excluded source(s))`
|
|
755
706
|
);
|
|
756
707
|
}
|
|
708
|
+
async function cmdFacts(args) {
|
|
709
|
+
const format = args.format ?? "json";
|
|
710
|
+
if (format !== "json" && format !== "text") {
|
|
711
|
+
throw new Error(`unknown facts format "${format}" (expected json or text)`);
|
|
712
|
+
}
|
|
713
|
+
const collected = await collectSpans(args);
|
|
714
|
+
if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
|
|
715
|
+
warnIncompleteWorkflow(collected.workflow);
|
|
716
|
+
const report = buildSessionFactsReport(collected.spans, { harness: collected.harness });
|
|
717
|
+
const unread = report.sessions.filter((facts) => facts.recordSpans === 0);
|
|
718
|
+
if (unread.length > 0) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`no records could be read from ${unread.length} selected session(s): ` + unread.map((facts) => `${facts.sessionId ?? facts.traceId} (${facts.unreadRecords.value ?? 0} unread record(s))`).join(", ") + ". The facts sheet would state zeros the spans cannot support."
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const rendered = format === "json" ? `${JSON.stringify(report, null, 2)}
|
|
724
|
+
` : renderSessionFacts(report);
|
|
725
|
+
if (args.out) {
|
|
726
|
+
await writeFile(args.out, rendered, "utf8");
|
|
727
|
+
console.log(`session facts \u2192 ${args.out} (${report.sessions.length} session(s), $0, no model call)`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
process.stdout.write(rendered);
|
|
731
|
+
}
|
|
757
732
|
async function cmdInspect(args) {
|
|
758
733
|
if (!args.input) throw new Error("inspect needs an index file; run `traces index --out session-index.json` first");
|
|
759
734
|
const index = await readSessionIndexFile(args.input);
|
|
@@ -876,7 +851,7 @@ ${renderVerifiedFindingsSection(run)}`;
|
|
|
876
851
|
} else {
|
|
877
852
|
console.log(report);
|
|
878
853
|
}
|
|
879
|
-
|
|
854
|
+
assertRequestedAnalysesRan(args, result);
|
|
880
855
|
}
|
|
881
856
|
async function verifyAnalyzeFindings(args, result) {
|
|
882
857
|
if (args.replayCorpora.length === 0) {
|
|
@@ -898,55 +873,24 @@ async function verifyAnalyzeFindings(args, result) {
|
|
|
898
873
|
});
|
|
899
874
|
}
|
|
900
875
|
async function cmdAnalyzeSupervisorRun(runDir, args) {
|
|
901
|
-
const
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
rollupSupervisorRuns(
|
|
907
|
-
await Promise.all(
|
|
908
|
-
runContextDirs.map((dir) => analyzeSupervisorRun(fileRunContextSupervisorRunReader(dir)))
|
|
909
|
-
)
|
|
910
|
-
),
|
|
911
|
-
`Supervisor rollup \u2014 ${runDir}`
|
|
912
|
-
);
|
|
913
|
-
} else if (runContextDirs.length === 1) {
|
|
914
|
-
markdown = renderSupervisorRunMarkdown(
|
|
915
|
-
await analyzeSupervisorRun(fileRunContextSupervisorRunReader(runContextDirs[0]))
|
|
916
|
-
);
|
|
917
|
-
} else if (nested.length > 0) {
|
|
918
|
-
markdown = renderSupervisorRollupMarkdown(
|
|
919
|
-
rollupSupervisorRuns(await Promise.all(nested.map((dir) => analyzeSupervisorRun(dir)))),
|
|
920
|
-
`Supervisor rollup \u2014 ${runDir}`
|
|
876
|
+
const single = await isRuntimeSupervisorRunDir(runDir);
|
|
877
|
+
const runDirs = single ? [runDir] : await findSupervisorRunDirs(runDir);
|
|
878
|
+
if (runDirs.length === 0) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`no supervisor run found at ${runDir} \u2014 expected a Runtime run containing spawn-journal.jsonl, observer.jsonl, or failure.json, or a parent directory containing such runs`
|
|
921
881
|
);
|
|
922
|
-
} else {
|
|
923
|
-
const report = await analyzeSupervisorRun(runDir);
|
|
924
|
-
if (isUnavailable(report.orchestration.workersSpawned)) {
|
|
925
|
-
throw new Error(
|
|
926
|
-
`no supervisor run found at ${runDir} \u2014 expected <runDir>/spawn-journal.jsonl (agent-runtime createFileRunContext), <runDir>/ws/.agent/supervisor/<id> (or the pre-rename <runDir>/ws/.loops/supervisor/<id>), or a parent directory containing such runs`
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
markdown = renderSupervisorRunMarkdown(report);
|
|
930
882
|
}
|
|
931
|
-
const
|
|
883
|
+
const reports = await Promise.all(
|
|
884
|
+
runDirs.map((dir) => analyzeSupervisorRun(fileRunContextSupervisorRunReader(dir)))
|
|
885
|
+
);
|
|
886
|
+
const markdown = single ? renderSupervisorRunMarkdown(reports[0]) : renderSupervisorRollupMarkdown(rollupSupervisorRuns(reports), `Supervisor rollup \u2014 ${runDir}`);
|
|
932
887
|
if (args.out) {
|
|
933
888
|
await saveReport(args.out, markdown);
|
|
934
|
-
console.log(`supervisor report \u2192 ${args.out} (${
|
|
889
|
+
console.log(`supervisor report \u2192 ${args.out} (${runDirs.length === 1 ? "1 run" : `${runDirs.length} runs`})`);
|
|
935
890
|
} else {
|
|
936
891
|
console.log(markdown);
|
|
937
892
|
}
|
|
938
893
|
}
|
|
939
|
-
async function findFileRunContextDirs(dir) {
|
|
940
|
-
if (await isFileRunContextDir(dir)) return [dir];
|
|
941
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
942
|
-
const found = [];
|
|
943
|
-
for (const entry of entries) {
|
|
944
|
-
if (!entry.isDirectory()) continue;
|
|
945
|
-
const child = join(dir, entry.name);
|
|
946
|
-
if (await isFileRunContextDir(child)) found.push(child);
|
|
947
|
-
}
|
|
948
|
-
return found.sort();
|
|
949
|
-
}
|
|
950
894
|
async function collectImportedSpans(args) {
|
|
951
895
|
if (!args.input) throw new Error("input file missing");
|
|
952
896
|
const attributes = await loadExportAttributes(args);
|
|
@@ -977,6 +921,13 @@ async function collectImportedSpans(args) {
|
|
|
977
921
|
};
|
|
978
922
|
}
|
|
979
923
|
function analystLog(msg, fields) {
|
|
924
|
+
const rejection = findingRejectionDetail(msg, fields);
|
|
925
|
+
if (rejection !== void 0) {
|
|
926
|
+
process.stderr.write(rejection ? `${msg} \u2014 ${rejection}
|
|
927
|
+
` : `${msg}
|
|
928
|
+
`);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
980
931
|
const error = typeof fields?.error === "string" && fields.error ? fields.error : void 0;
|
|
981
932
|
const errorClass = typeof fields?.error_class === "string" && fields.error_class ? `${fields.error_class}: ` : "";
|
|
982
933
|
process.stderr.write(error ? `${msg} \u2014 ${errorClass}${error}
|
|
@@ -996,7 +947,7 @@ async function cmdInvestigate(args) {
|
|
|
996
947
|
} else {
|
|
997
948
|
console.log(result.report);
|
|
998
949
|
}
|
|
999
|
-
|
|
950
|
+
assertRequestedAnalysesRan(args, result);
|
|
1000
951
|
}
|
|
1001
952
|
function conformanceOptions(collected) {
|
|
1002
953
|
return {
|
|
@@ -1014,9 +965,10 @@ async function investigate(args, options = {}) {
|
|
|
1014
965
|
const { spans, harness, cwds, sources, workflow } = collected;
|
|
1015
966
|
if (spans.length === 0) throw new Error("no spans found for the given selection");
|
|
1016
967
|
const config = args.config !== void 0 || options.loadDefaultConfig !== false ? await loadTracesConfig(args.config) : void 0;
|
|
1017
|
-
const analystModel = args
|
|
1018
|
-
const engine = args.llm ?
|
|
968
|
+
const analystModel = analystModelFor(args);
|
|
969
|
+
const engine = args.llm ? llmAnalysisEngine(args, analystModel) : void 0;
|
|
1019
970
|
return runTraceInvestigation(mergeTracesConfig({
|
|
971
|
+
sourceBundle: args.sourceBundle ? { path: args.sourceBundle } : void 0,
|
|
1020
972
|
spans,
|
|
1021
973
|
harness,
|
|
1022
974
|
sources,
|
|
@@ -1038,10 +990,11 @@ async function cmdImprove(args) {
|
|
|
1038
990
|
const { spans, harness, cwds, sources, workflow } = collected;
|
|
1039
991
|
if (spans.length === 0) throw new Error("no spans found for the given selection");
|
|
1040
992
|
const config = await loadTracesConfig(args.config);
|
|
1041
|
-
const analystModel = args
|
|
1042
|
-
const engine = args.llm ?
|
|
993
|
+
const analystModel = analystModelFor(args);
|
|
994
|
+
const engine = args.llm ? llmAnalysisEngine(args, analystModel) : void 0;
|
|
1043
995
|
const result = await runTraceImprovement({
|
|
1044
996
|
...mergeTracesConfig({
|
|
997
|
+
sourceBundle: args.sourceBundle ? { path: args.sourceBundle } : void 0,
|
|
1045
998
|
spans,
|
|
1046
999
|
harness,
|
|
1047
1000
|
sources,
|
|
@@ -1064,7 +1017,76 @@ async function cmdImprove(args) {
|
|
|
1064
1017
|
console.log(
|
|
1065
1018
|
`improvement artifacts \u2192 ${dir} (${result.findings.length} findings with actions and checks, OTLP: ${result.otlpPath})`
|
|
1066
1019
|
);
|
|
1067
|
-
|
|
1020
|
+
assertRequestedAnalysesRan(args, result);
|
|
1021
|
+
}
|
|
1022
|
+
async function cmdAsk(args) {
|
|
1023
|
+
if (args.out) throw new Error("ask writes a directory of artifacts; pass --dir <dir> instead of --out");
|
|
1024
|
+
if (args.llm) throw new Error("ask always uses the model-backed engine; drop --llm");
|
|
1025
|
+
const questions = [
|
|
1026
|
+
...args.questionsFile ? await loadTraceQuestionsFile(args.questionsFile) : [],
|
|
1027
|
+
...args.questions.map((question) => ({ question }))
|
|
1028
|
+
];
|
|
1029
|
+
if (questions.length === 0) {
|
|
1030
|
+
throw new Error('ask needs --question "<text>" (repeatable) or --questions <questions.json>');
|
|
1031
|
+
}
|
|
1032
|
+
normalizeTraceQuestions(questions);
|
|
1033
|
+
if (args.questionBudget !== void 0 && (!Number.isFinite(args.questionBudget) || args.questionBudget <= 0)) {
|
|
1034
|
+
throw new Error("--question-budget must be a positive number of USD");
|
|
1035
|
+
}
|
|
1036
|
+
if (args.budget !== void 0 && (!Number.isFinite(args.budget) || args.budget <= 0)) {
|
|
1037
|
+
throw new Error("--budget must be a positive number of USD");
|
|
1038
|
+
}
|
|
1039
|
+
const engine = analysisEngineFromEnv({
|
|
1040
|
+
model: analystModelFor(args),
|
|
1041
|
+
maxCostUsd: args.questionBudget ?? Math.min(args.budget ?? Infinity, DEFAULT_QUESTION_MAX_COST_USD),
|
|
1042
|
+
log: analystLog
|
|
1043
|
+
});
|
|
1044
|
+
const collected = await collectSpans(args);
|
|
1045
|
+
if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
|
|
1046
|
+
warnIncompleteWorkflow(collected.workflow);
|
|
1047
|
+
const directory = resolve(args.dir ?? await mkdtemp(join(tmpdir(), "traces-ask-")));
|
|
1048
|
+
await mkdir(directory, { recursive: true });
|
|
1049
|
+
const controller = new AbortController();
|
|
1050
|
+
const interrupt = () => controller.abort(new Error("ask interrupted"));
|
|
1051
|
+
process.once("SIGINT", interrupt);
|
|
1052
|
+
let result;
|
|
1053
|
+
try {
|
|
1054
|
+
result = await runTraceQuestions({
|
|
1055
|
+
questions,
|
|
1056
|
+
spans: collected.spans,
|
|
1057
|
+
engine,
|
|
1058
|
+
harness: collected.harness,
|
|
1059
|
+
concurrency: args.concurrency,
|
|
1060
|
+
...args.budget !== void 0 ? { budgetUsd: args.budget } : {},
|
|
1061
|
+
...args.sourceBundle ? { sourceBundle: { path: args.sourceBundle } } : {},
|
|
1062
|
+
otlpOutPath: args.otlpOut ?? join(directory, "traces.otlp.jsonl"),
|
|
1063
|
+
signal: controller.signal,
|
|
1064
|
+
log: analystLog
|
|
1065
|
+
});
|
|
1066
|
+
} finally {
|
|
1067
|
+
process.removeListener("SIGINT", interrupt);
|
|
1068
|
+
}
|
|
1069
|
+
const artifacts = await writeTraceQuestionsArtifacts(result, directory);
|
|
1070
|
+
process.stdout.write(result.report);
|
|
1071
|
+
for (const warning of result.warnings) process.stderr.write(`warning: ${warning}
|
|
1072
|
+
`);
|
|
1073
|
+
process.stderr.write(
|
|
1074
|
+
`ask artifacts \u2192 ${artifacts.directory} (${result.totals.answered}/${result.totals.questions} answered, wall ${(result.totals.wallTimeMs / 1e3).toFixed(1)} s, answers: ${artifacts.result})
|
|
1075
|
+
`
|
|
1076
|
+
);
|
|
1077
|
+
if (!result.ok) {
|
|
1078
|
+
const failed = result.questions.filter((answer) => answer.status === "failed");
|
|
1079
|
+
const lines = [
|
|
1080
|
+
`${failed.length} of ${result.questions.length} question(s) failed; the answers file holds every result.`,
|
|
1081
|
+
...failed.map((answer) => ` ${answer.id}: ${answer.failure?.kind}: ${answer.failure?.message.slice(0, 300)}`)
|
|
1082
|
+
];
|
|
1083
|
+
if (failed.some((answer) => isBridgeMismatchError(answer.failure?.message ?? ""))) {
|
|
1084
|
+
lines.push(
|
|
1085
|
+
`hint: the DSPy bridge protocol is version-locked \u2014 the TRACES_PYTHON interpreter needs agent-eval-rpc[dspy]==${requiredBridgeVersion()} (matching this package's @tangle-network/agent-eval).`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error(lines.join("\n"));
|
|
1089
|
+
}
|
|
1068
1090
|
}
|
|
1069
1091
|
function summarizeFindingEvidence(finding) {
|
|
1070
1092
|
const first = finding.evidence[0];
|
|
@@ -1387,6 +1409,17 @@ Commands:
|
|
|
1387
1409
|
(--supervisor-run-dir <dir> reports a supervision tree instead)
|
|
1388
1410
|
investigate Run typed investigation flow, including BYO config + evidence-backed actions
|
|
1389
1411
|
improve Write findings, evidence, report, and canonical trace artifacts
|
|
1412
|
+
ask Answer free-form questions over the selected sessions with the
|
|
1413
|
+
model-backed engine: questions run concurrently under one budget,
|
|
1414
|
+
every trace:// citation is checked, and answers.json + report.md
|
|
1415
|
+
are written to --dir (exit 1 when any question fails)
|
|
1416
|
+
facts Print the deterministic session-facts sheet for the selected
|
|
1417
|
+
sessions: tool calls excluding synthesized spans, subagent spawns
|
|
1418
|
+
with task names, human turns in order, the final message per task,
|
|
1419
|
+
changed paths, first/last record times, and the harness token total.
|
|
1420
|
+
No model call, no budget, $0. Every fact names the span ids it came
|
|
1421
|
+
from; a fact the spans cannot support is null with its reason.
|
|
1422
|
+
--format json (default) or text (exit 1 when a session cannot be read)
|
|
1390
1423
|
convert Emit OTLP-JSONL only, to --otlp-out (HALO: use analyze --analyzer halo)
|
|
1391
1424
|
index Emit a reusable session index JSON for later investigation
|
|
1392
1425
|
bundle Assemble one session's durable evidence directory: transcript +
|
|
@@ -1435,21 +1468,24 @@ Options:
|
|
|
1435
1468
|
analyze: report a SUPERVISION TREE instead of harness sessions \u2014
|
|
1436
1469
|
steers, spawn waves, concurrency, idle wall, cost by role,
|
|
1437
1470
|
accepted vs rejected. Rolls up when the dir holds many runs.
|
|
1438
|
-
|
|
1439
|
-
|
|
1471
|
+
analyze reads Runtime journals and recorded startup failures.
|
|
1472
|
+
watch reads a live Runtime journal and OTLP span files.
|
|
1440
1473
|
--since <t> upload: window, 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
|
|
1441
1474
|
--out <path> Write report to a file
|
|
1442
|
-
--dir <path> improve: write artifacts to this directory
|
|
1475
|
+
--dir <path> improve/ask: write artifacts to this directory
|
|
1443
1476
|
--otlp <file|dir> READ OTLP-JSONL emitted by any system, skipping the adapters.
|
|
1444
1477
|
A directory reads the OTLP files under it \u2014 only the otlp/
|
|
1445
1478
|
subdirectory when the producer made one \u2014 and names the JSONL
|
|
1446
1479
|
that is not OTLP instead of reading it as broken spans.
|
|
1447
|
-
Supported by: validate, analyze, investigate, improve, stream.
|
|
1480
|
+
Supported by: validate, analyze, investigate, improve, ask, stream.
|
|
1448
1481
|
On a WRITING command it is the deprecated spelling of
|
|
1449
1482
|
--otlp-out; it still works, with a warning, until 0.12.
|
|
1483
|
+
--source-bundle <dir> Analyze a retained full bundle; explicitly grant source-field reads.
|
|
1484
|
+
Available for analyze, investigate, improve, and ask.
|
|
1450
1485
|
--otlp-out <path> WRITE the OTLP-JSONL artifact here (also evidence
|
|
1451
1486
|
provenance / dry-run upload preview)
|
|
1452
1487
|
--format <kind> analyze/export: auto | policy-evidence | sandbox-events | openinference | intelligence-spans | chat-trajectory
|
|
1488
|
+
facts: json (default) | text
|
|
1453
1489
|
--metadata <json> analyze/export file: attach JSON object fields as span attributes
|
|
1454
1490
|
--attr <k=v> analyze/export file: attach one span attribute (repeatable)
|
|
1455
1491
|
--mode <kind> stream: visualizer | findings | agent (default visualizer)
|
|
@@ -1466,10 +1502,20 @@ Options:
|
|
|
1466
1502
|
agent-eval-rpc[dspy] (TRACES_PYTHON selects the interpreter),
|
|
1467
1503
|
version-matched to this package's @tangle-network/agent-eval.
|
|
1468
1504
|
Exits 1 when every agentic analyst fails, with each reason.
|
|
1505
|
+
ask uses the same engine and credentials without --llm.
|
|
1469
1506
|
--model <id> Model for --llm, HALO, and Hodoscope (default for --llm: ${DEFAULT_ANALYST_MODEL})
|
|
1470
1507
|
--config <path> investigate/improve/stream: JS config with analysts, liveAnalysts, or external analyzers
|
|
1471
|
-
--budget <usd> USD cap for agentic analysts
|
|
1472
|
-
--
|
|
1508
|
+
--budget <usd> USD cap for agentic analysts; ask: one ceiling shared by every question
|
|
1509
|
+
--question <text> ask: a question (repeatable, at most ${MAX_TRACE_QUESTION_CHARS} characters)
|
|
1510
|
+
--questions <file> ask: JSON array of questions, strings or
|
|
1511
|
+
{ id?, question, instructions?, answerSchema? }
|
|
1512
|
+
--question-budget <usd>
|
|
1513
|
+
ask: provider ceiling for one question (default: the smaller
|
|
1514
|
+
of --budget and $${DEFAULT_QUESTION_MAX_COST_USD})
|
|
1515
|
+
--concurrency <n> ask: questions running at once (default 4);
|
|
1516
|
+
import-codetracebench: trajectories imported at once
|
|
1517
|
+
--analyzer <id> analyze: also run halo, hodoscope, prime, or an installed command (repeatable);
|
|
1518
|
+
exits 1 after writing the report when any requested analyzer fails
|
|
1473
1519
|
prime posts the full span projection to an OpenAI-compatible bridge
|
|
1474
1520
|
(TRACES_PRIME_BRIDGE_URL, default http://localhost:4181;
|
|
1475
1521
|
TRACES_PRIME_MODEL, default prime/zai/glm-5.2; TRACES_PRIME_TIMEOUT_MS)
|
|
@@ -1551,6 +1597,12 @@ async function main() {
|
|
|
1551
1597
|
case "improve":
|
|
1552
1598
|
await cmdImprove(args);
|
|
1553
1599
|
break;
|
|
1600
|
+
case "ask":
|
|
1601
|
+
await cmdAsk(args);
|
|
1602
|
+
break;
|
|
1603
|
+
case "facts":
|
|
1604
|
+
await cmdFacts(args);
|
|
1605
|
+
break;
|
|
1554
1606
|
case "convert":
|
|
1555
1607
|
await cmdConvert(args);
|
|
1556
1608
|
break;
|