@tangle-network/traces 0.9.12 → 0.9.13

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/dist/cli.js CHANGED
@@ -6,11 +6,14 @@ import {
6
6
  appendAll,
7
7
  buildPolicyEvidenceRecord,
8
8
  buildSessionIndexFromRows,
9
+ collectSessionSelection,
9
10
  commandAnalyzer,
10
11
  commandRedactor,
12
+ describeSessionRelationship,
11
13
  executeUpload,
12
14
  exportTraceEvidenceFile,
13
15
  haloAnalyzer,
16
+ hodoscopeAnalyzer,
14
17
  indexSessionIdsByTrace,
15
18
  inspectSessionIndex,
16
19
  knownHarnesses,
@@ -40,11 +43,10 @@ import {
40
43
  writePolicyEvidenceFile,
41
44
  writeSessionIndexFile,
42
45
  writeTraceEvidenceExportFile
43
- } from "./chunk-YDUN3M5J.js";
46
+ } from "./chunk-H47X6QWV.js";
44
47
 
45
48
  // src/cli.ts
46
- import { createHash } from "crypto";
47
- import { createReadStream, readFileSync } from "fs";
49
+ import { readFileSync } from "fs";
48
50
  import { readFile, stat } from "fs/promises";
49
51
  import { basename, resolve } from "path";
50
52
  import {
@@ -55,6 +57,8 @@ import {
55
57
  renderSupervisorRunMarkdown,
56
58
  rollupSupervisorRuns
57
59
  } from "@tangle-network/agent-eval/supervisor-run";
60
+ import { createAnalystAi } from "@tangle-network/agent-eval/analyst";
61
+ var DEFAULT_ANALYST_MODEL = "gpt-5-mini";
58
62
  function packageVersion() {
59
63
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
60
64
  if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
@@ -69,6 +73,9 @@ function parseArgs(argv) {
69
73
  all: false,
70
74
  last: 0,
71
75
  current: false,
76
+ latestTurn: false,
77
+ workflow: false,
78
+ maxWorkflowSessions: 100,
72
79
  llm: false,
73
80
  interval: 5,
74
81
  window: 30,
@@ -99,6 +106,15 @@ function parseArgs(argv) {
99
106
  case "--current":
100
107
  a.current = true;
101
108
  break;
109
+ case "--latest-turn":
110
+ a.latestTurn = true;
111
+ break;
112
+ case "--workflow":
113
+ a.workflow = true;
114
+ break;
115
+ case "--max-workflow-sessions":
116
+ a.maxWorkflowSessions = Number(next());
117
+ break;
102
118
  case "--session":
103
119
  a.session = next();
104
120
  break;
@@ -207,6 +223,14 @@ var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
207
223
  "evidence",
208
224
  "stream"
209
225
  ]);
226
+ var WORKFLOW_COMMANDS = /* @__PURE__ */ new Set([
227
+ "analyze",
228
+ "investigate",
229
+ "improve",
230
+ "convert",
231
+ "index",
232
+ "evidence"
233
+ ]);
210
234
  function applyCurrentSessionSelection(args) {
211
235
  if (!args.current) return args;
212
236
  if (!CURRENT_SESSION_COMMANDS.has(args.command)) {
@@ -225,6 +249,22 @@ function applyCurrentSessionSelection(args) {
225
249
  if (!sessionId) throw new Error("--current requires CODEX_THREAD_ID from the active Codex session");
226
250
  return { ...args, session: sessionId };
227
251
  }
252
+ function validateWorkflowSelection(args) {
253
+ if (!args.workflow && !args.latestTurn) return args;
254
+ if (!WORKFLOW_COMMANDS.has(args.command)) {
255
+ throw new Error(`${args.workflow ? "--workflow" : "--latest-turn"} is not supported by ${args.command}`);
256
+ }
257
+ if (args.input) throw new Error("--workflow and --latest-turn cannot be combined with an input file");
258
+ if (args.supervisorRunDir) {
259
+ throw new Error("--workflow and --latest-turn cannot be combined with --supervisor-run-dir");
260
+ }
261
+ const selected = resolveAdapter(args.harness);
262
+ const supportsTaskTurns = selected?.harness === "codex" || selected?.harness === "claude-code";
263
+ if (args.latestTurn && (args.all || !supportsTaskTurns)) {
264
+ throw new Error("--latest-turn requires --harness codex or claude-code");
265
+ }
266
+ return args;
267
+ }
228
268
  async function confirm(question) {
229
269
  if (!process.stdin.isTTY) return false;
230
270
  const rl = (await import("readline/promises")).createInterface({
@@ -295,7 +335,7 @@ async function resolveSelectedSession(args) {
295
335
  `session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
296
336
  );
297
337
  }
298
- async function buildAxService() {
338
+ async function buildAxService(model) {
299
339
  const apiKey = process.env.OPENAI_API_KEY;
300
340
  if (!apiKey) {
301
341
  throw new Error(
@@ -303,8 +343,7 @@ async function buildAxService() {
303
343
  );
304
344
  }
305
345
  const apiURL = process.env.OPENAI_BASE_URL || void 0;
306
- const { AxAI } = await import("@ax-llm/ax");
307
- return AxAI.create({ name: "openai", apiKey, ...apiURL ? { apiURL } : {} });
346
+ return createAnalystAi({ apiKey, baseUrl: apiURL, model });
308
347
  }
309
348
  async function cmdList(args) {
310
349
  const groups = await discover({ ...args, last: args.last || 20 });
@@ -328,6 +367,7 @@ function selectedSessionSource(ref, spans, sessionIdOverride) {
328
367
  const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
329
368
  const role = root?.attributes["traces.session.role"];
330
369
  const parentSessionId = root?.attributes["traces.parent_session_id"];
370
+ const relationship = describeSessionRelationship(ref, spans);
331
371
  const corruptionDigest = root?.attributes[ATTR.CORRUPTION_DIGEST];
332
372
  const sessionId = [
333
373
  sessionIdOverride,
@@ -341,6 +381,13 @@ function selectedSessionSource(ref, spans, sessionIdOverride) {
341
381
  subject,
342
382
  role: role === "operator" || role === "child" ? role : "unknown",
343
383
  ...typeof parentSessionId === "string" ? { parentSessionId } : {},
384
+ childSessionIds: relationship.childSessionIds,
385
+ ...relationship.depth !== void 0 ? { depth: relationship.depth } : {},
386
+ ...relationship.agentNickname ? { agentNickname: relationship.agentNickname } : {},
387
+ ...relationship.agentRole ? { agentRole: relationship.agentRole } : {},
388
+ ...relationship.agentPath ? { agentPath: relationship.agentPath } : {},
389
+ ...relationship.taskScope ? { taskScope: relationship.taskScope } : {},
390
+ ...relationship.turnId ? { turnId: relationship.turnId } : {},
344
391
  integrity: ref.integrity?.status ?? "complete",
345
392
  ...ref.integrity ? {
346
393
  corruptionCount: ref.integrity.corruptions.length,
@@ -350,66 +397,67 @@ function selectedSessionSource(ref, spans, sessionIdOverride) {
350
397
  };
351
398
  }
352
399
  async function collectSpans(args) {
353
- if (args.input) return collectImportedSpans(args);
354
- if (args.session) {
355
- const { adapter, ref } = await resolveSelectedSession(args);
356
- const spans2 = await parseSession(adapter, ref);
357
- return {
358
- spans: spans2,
359
- harness: adapter.harness,
360
- cwds: ref.cwd ? [ref.cwd] : [],
361
- sources: [selectedSessionSource(ref, spans2)]
362
- };
400
+ if (args.input) {
401
+ if (args.workflow) throw new Error("--workflow reads discovered sessions and cannot be combined with an input file");
402
+ return collectImportedSpans(args);
363
403
  }
364
- const groups = await discover({ ...args, last: args.last || 1 });
404
+ const selection = await collectSessionRows(args, false, args.last || 1);
365
405
  const spans = [];
366
- const harnesses = [];
406
+ const harnesses = /* @__PURE__ */ new Set();
367
407
  const cwds = [];
368
408
  const sources = [];
369
- for (const { adapter, refs } of groups) {
370
- if (refs.length > 0) harnesses.push(adapter.harness);
371
- for (const ref of refs) {
372
- const parsed = await parseSession(adapter, ref);
373
- appendAll(spans, parsed);
374
- sources.push(selectedSessionSource(ref, parsed));
375
- if (ref.cwd) cwds.push(ref.cwd);
376
- }
409
+ for (const row of selection.rows) {
410
+ harnesses.add(row.adapter.harness);
411
+ appendAll(spans, row.spans);
412
+ sources.push(selectedSessionSource(row.ref, row.spans));
413
+ if (row.ref.cwd) cwds.push(row.ref.cwd);
377
414
  }
378
- return { spans, harness: harnesses.join("+") || args.harness, cwds, sources };
415
+ return {
416
+ spans,
417
+ harness: [...harnesses].join("+") || args.harness,
418
+ cwds,
419
+ sources,
420
+ ...selection.workflow ? { workflow: selection.workflow } : {}
421
+ };
379
422
  }
380
423
  async function cmdConvert(args) {
381
- const { spans } = await collectSpans(args);
424
+ const { spans, workflow } = await collectSpans(args);
382
425
  if (spans.length === 0) throw new Error("no spans found for the given selection");
426
+ warnIncompleteWorkflow(workflow);
383
427
  const path = await writeOtlpFile(spans, args.otlp);
384
428
  console.log(`wrote ${spans.length} spans \u2192 ${path}`);
385
429
  }
386
- async function sourceSha256(path) {
387
- const hash = createHash("sha256");
388
- for await (const chunk of createReadStream(path)) hash.update(chunk);
389
- return hash.digest("hex");
390
- }
391
- async function collectSessionRows(args, bindExplicitSource = false) {
392
- if (args.session) {
393
- const { adapter, ref } = await resolveSelectedSession(args);
394
- if (!bindExplicitSource) return [{ ref, spans: await parseSession(adapter, ref) }];
395
- const before = await sourceSha256(ref.path);
396
- const spans = await parseSession(adapter, ref);
397
- const after = await sourceSha256(ref.path);
398
- if (before !== after) {
399
- throw new Error(`session source changed while parsing; refusing unbound evidence: ${ref.path}`);
400
- }
401
- return [{ ref, spans, sourceSha256: after }];
430
+ function workflowIssueText(issue) {
431
+ if (issue.kind === "cycle") return `cycle: ${issue.sessionIds.join(" -> ")}`;
432
+ if (issue.kind === "unresolved-parent-task") {
433
+ return `cannot resolve parent task for child ${issue.childSessionId} in ${issue.parentSessionId}: ${issue.reason}`;
402
434
  }
403
- const groups = await discover({ ...args, last: args.last || 20 });
404
- const rows = [];
405
- for (const { adapter, refs } of groups) {
406
- for (const ref of refs) rows.push({ ref, spans: await parseSession(adapter, ref) });
435
+ if (issue.kind === "parent-conflict") {
436
+ return `parent conflict for ${issue.sessionId}: declared ${issue.declaredParentSessionId ?? "none"}, referenced by ${issue.referencedParentSessionIds.join(", ") || "none"}`;
407
437
  }
408
- return rows;
438
+ return `${issue.kind} ${issue.sessionId}, referenced as ${issue.relation} by ${issue.referencedBySessionId}`;
439
+ }
440
+ function warnIncompleteWorkflow(workflow) {
441
+ if (!workflow || workflow.complete) return;
442
+ process.stderr.write(
443
+ `warning: session workflow is incomplete (${workflow.issues.length} issue(s)): ${workflow.issues.map(workflowIssueText).join("; ")}
444
+ `
445
+ );
446
+ }
447
+ async function collectSessionRows(args, bindExplicitSource = false, defaultLast = 20) {
448
+ const groups = args.session ? [await resolveSelectedSession(args).then(({ adapter, ref }) => ({ adapter, refs: [ref] }))] : await discover({ ...args, last: args.last || defaultLast });
449
+ return collectSessionSelection(groups, {
450
+ workflow: args.workflow,
451
+ maxWorkflowSessions: args.maxWorkflowSessions,
452
+ taskScope: args.latestTurn ? "latest" : "all",
453
+ bindSources: bindExplicitSource && Boolean(args.session)
454
+ });
409
455
  }
410
456
  async function cmdEvidence(args) {
411
- const rows = (await collectSessionRows(args, true)).filter((row) => row.spans.length > 0);
457
+ const selection = await collectSessionRows(args, true);
458
+ const rows = selection.rows.filter((row) => row.spans.length > 0);
412
459
  if (rows.length === 0) throw new Error("no spans found for the given selection");
460
+ warnIncompleteWorkflow(selection.workflow);
413
461
  const otlpPath = args.otlp ? await writeOtlpFile(rows.flatMap((row) => row.spans), args.otlp) : void 0;
414
462
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
415
463
  const records = await Promise.all(rows.map(
@@ -429,8 +477,10 @@ async function cmdEvidence(args) {
429
477
  }
430
478
  }
431
479
  async function cmdIndex(args) {
432
- const rows = (await collectSessionRows(args)).filter((row) => row.spans.length > 0);
480
+ const selection = await collectSessionRows(args);
481
+ const rows = selection.rows.filter((row) => row.spans.length > 0);
433
482
  if (rows.length === 0) throw new Error("no spans found for the given selection");
483
+ warnIncompleteWorkflow(selection.workflow);
434
484
  const index = await buildSessionIndexFromRows(rows, {
435
485
  minLoopOccurrences: args.minLoop,
436
486
  selection: {
@@ -440,7 +490,9 @@ async function cmdIndex(args) {
440
490
  last: args.last || void 0,
441
491
  session: args.session,
442
492
  cwd: args.cwd,
443
- since: args.since
493
+ since: args.since,
494
+ latestTurn: args.latestTurn || void 0,
495
+ ...selection.workflow ? { workflow: selection.workflow } : {}
444
496
  }
445
497
  });
446
498
  if (args.out) {
@@ -478,8 +530,8 @@ async function cmdExport(args) {
478
530
  }
479
531
  function traceEvidenceFormat(raw) {
480
532
  const format = raw ?? "auto";
481
- if (format !== "auto" && format !== "policy-evidence" && format !== "sandbox-events" && format !== "openinference" && format !== "intelligence-spans") {
482
- throw new Error("--format must be auto, policy-evidence, sandbox-events, openinference, or intelligence-spans");
533
+ if (format !== "auto" && format !== "policy-evidence" && format !== "sandbox-events" && format !== "openinference" && format !== "intelligence-spans" && format !== "chat-trajectory") {
534
+ throw new Error("--format must be auto, policy-evidence, sandbox-events, openinference, intelligence-spans, or chat-trajectory");
483
535
  }
484
536
  return format;
485
537
  }
@@ -577,7 +629,7 @@ async function collectImportedSpans(args) {
577
629
  }
578
630
  function externalAnalyzersFromArgs(args) {
579
631
  return args.analyzers.map(
580
- (spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt, model: args.model }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
632
+ (spec) => spec === "halo" ? haloAnalyzer({ defaultPrompt: args.analyzerPrompt, model: args.model }) : spec === "hodoscope" ? hodoscopeAnalyzer({ summarizeModel: args.model }) : commandAnalyzer({ name: spec, command: spec, args: (p, prompt) => prompt ? [p, prompt] : [p] })
581
633
  );
582
634
  }
583
635
  async function cmdInvestigate(args) {
@@ -590,18 +642,20 @@ async function cmdInvestigate(args) {
590
642
  }
591
643
  }
592
644
  async function investigate(args, options = {}) {
593
- const { spans, harness, cwds, sources } = await collectSpans(args);
645
+ const { spans, harness, cwds, sources, workflow } = await collectSpans(args);
594
646
  if (spans.length === 0) throw new Error("no spans found for the given selection");
595
647
  const config = args.config !== void 0 || options.loadDefaultConfig !== false ? await loadTracesConfig(args.config) : void 0;
596
- const ai = args.llm ? await buildAxService() : void 0;
648
+ const analystModel = args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
649
+ const ai = args.llm ? await buildAxService(analystModel) : void 0;
597
650
  return runTraceInvestigation(mergeTracesConfig({
598
651
  spans,
599
652
  harness,
600
653
  sources,
654
+ workflow,
601
655
  cwds,
602
656
  minLoopOccurrences: args.minLoop,
603
657
  ai,
604
- model: args.model,
658
+ model: args.llm ? analystModel : args.model,
605
659
  budgetUsd: args.budget,
606
660
  otlpOutPath: args.otlp,
607
661
  externalAnalyzers: externalAnalyzersFromArgs(args),
@@ -611,19 +665,21 @@ async function investigate(args, options = {}) {
611
665
  }, config));
612
666
  }
613
667
  async function cmdImprove(args) {
614
- const { spans, harness, cwds, sources } = await collectSpans(args);
668
+ const { spans, harness, cwds, sources, workflow } = await collectSpans(args);
615
669
  if (spans.length === 0) throw new Error("no spans found for the given selection");
616
670
  const config = await loadTracesConfig(args.config);
617
- const ai = args.llm ? await buildAxService() : void 0;
671
+ const analystModel = args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
672
+ const ai = args.llm ? await buildAxService(analystModel) : void 0;
618
673
  const result = await runTraceImprovement({
619
674
  ...mergeTracesConfig({
620
675
  spans,
621
676
  harness,
622
677
  sources,
678
+ workflow,
623
679
  cwds,
624
680
  minLoopOccurrences: args.minLoop,
625
681
  ai,
626
- model: args.model,
682
+ model: args.llm ? analystModel : args.model,
627
683
  budgetUsd: args.budget,
628
684
  otlpOutPath: args.otlp,
629
685
  externalAnalyzers: externalAnalyzersFromArgs(args),
@@ -850,6 +906,7 @@ Input formats:
850
906
  sandbox-events JSON arrays with start/raw/result/done/error events
851
907
  openinference Existing OpenInference JSONL; rewrites through traces redaction
852
908
  intelligence-spans JSONL rows exported from Tangle Intelligence trace spans
909
+ chat-trajectory A message array or objects with a messages array
853
910
  auto Detect the format from the file contents (default)
854
911
 
855
912
  Examples:
@@ -887,7 +944,11 @@ Options:
887
944
  --all Sweep every known harness
888
945
  --last <n> Most-recent N sessions
889
946
  --current Analyze the active Codex session named by CODEX_THREAD_ID
947
+ --latest-turn Limit a resumed Codex or Claude Code session to its latest task turn
890
948
  --session <id|path> Analyze one listed session ID or one explicit harness session file
949
+ --workflow Expand selected sessions through stable parent/child session IDs
950
+ --max-workflow-sessions <n>
951
+ Stop workflow expansion before parsing more than N files (default 100)
891
952
  --cwd <dir> Filter sessions by working directory
892
953
  --supervisor-run-dir <dir>
893
954
  analyze: report a SUPERVISION TREE instead of harness sessions \u2014
@@ -897,7 +958,7 @@ Options:
897
958
  --out <path> Write report to a file
898
959
  --dir <path> improve: write artifacts to this directory
899
960
  --otlp <path> OTLP artifact path (also evidence provenance / dry-run upload preview)
900
- --format <kind> analyze/export file: auto | policy-evidence | sandbox-events | openinference | intelligence-spans
961
+ --format <kind> analyze/export: auto | policy-evidence | sandbox-events | openinference | intelligence-spans | chat-trajectory
901
962
  --metadata <json> analyze/export file: attach JSON object fields as span attributes
902
963
  --attr <k=v> analyze/export file: attach one span attribute (repeatable)
903
964
  --mode <kind> stream: visualizer | findings | agent (default visualizer)
@@ -905,10 +966,10 @@ Options:
905
966
  --no-spans stream: omit per-span pulse events
906
967
  --no-findings stream: omit semantic live-finding events
907
968
  --llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
908
- --model <id> --llm model id (e.g. a router model like glm-5.2); default is agent-eval's
969
+ --model <id> Model for --llm, HALO, and Hodoscope (default for --llm: ${DEFAULT_ANALYST_MODEL})
909
970
  --config <path> investigate/improve/stream: JS config with analysts, liveAnalysts, or external analyzers
910
971
  --budget <usd> USD cap for agentic analysts
911
- --analyzer <cmd> analyze: also run an external engine over the OTLP (repeatable; "halo" or any command)
972
+ --analyzer <id> analyze: also run halo, hodoscope, or an installed command (repeatable)
912
973
  --analyzer-prompt <p> analyze: prompt passed to external analyzers (default: diagnose)
913
974
  --interval <s> watch/stream: poll interval seconds (default 5)
914
975
  --window <m> watch/stream: only sessions active in the last N minutes (default 30)
@@ -934,7 +995,7 @@ async function main() {
934
995
  else usage();
935
996
  return;
936
997
  }
937
- const args = applyCurrentSessionSelection(parsedArgs);
998
+ const args = validateWorkflowSelection(applyCurrentSessionSelection(parsedArgs));
938
999
  switch (args.command) {
939
1000
  case "help":
940
1001
  if (args.input === "export") usageExport();