@tangle-network/traces 0.9.11 → 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-ELWOJS65.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");
@@ -68,6 +72,10 @@ function parseArgs(argv) {
68
72
  harnessExplicit: false,
69
73
  all: false,
70
74
  last: 0,
75
+ current: false,
76
+ latestTurn: false,
77
+ workflow: false,
78
+ maxWorkflowSessions: 100,
71
79
  llm: false,
72
80
  interval: 5,
73
81
  window: 30,
@@ -95,6 +103,18 @@ function parseArgs(argv) {
95
103
  case "--last":
96
104
  a.last = Number(next());
97
105
  break;
106
+ case "--current":
107
+ a.current = true;
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;
98
118
  case "--session":
99
119
  a.session = next();
100
120
  break;
@@ -194,6 +214,57 @@ function parseArgs(argv) {
194
214
  }
195
215
  return a;
196
216
  }
217
+ var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
218
+ "analyze",
219
+ "investigate",
220
+ "improve",
221
+ "convert",
222
+ "index",
223
+ "evidence",
224
+ "stream"
225
+ ]);
226
+ var WORKFLOW_COMMANDS = /* @__PURE__ */ new Set([
227
+ "analyze",
228
+ "investigate",
229
+ "improve",
230
+ "convert",
231
+ "index",
232
+ "evidence"
233
+ ]);
234
+ function applyCurrentSessionSelection(args) {
235
+ if (!args.current) return args;
236
+ if (!CURRENT_SESSION_COMMANDS.has(args.command)) {
237
+ throw new Error(`--current is not supported by ${args.command}`);
238
+ }
239
+ if (args.input) throw new Error("--current cannot be combined with an input file");
240
+ if (args.supervisorRunDir) throw new Error("--current cannot be combined with --supervisor-run-dir");
241
+ if (args.session) throw new Error("--current cannot be combined with --session");
242
+ if (args.last > 0) throw new Error("--current cannot be combined with --last");
243
+ if (args.all) throw new Error("--current cannot be combined with --all");
244
+ const adapter = resolveAdapter(args.harness);
245
+ if (adapter?.harness !== "codex") {
246
+ throw new Error("--current uses CODEX_THREAD_ID and requires --harness codex");
247
+ }
248
+ const sessionId = process.env.CODEX_THREAD_ID?.trim();
249
+ if (!sessionId) throw new Error("--current requires CODEX_THREAD_ID from the active Codex session");
250
+ return { ...args, session: sessionId };
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
+ }
197
268
  async function confirm(question) {
198
269
  if (!process.stdin.isTTY) return false;
199
270
  const rl = (await import("readline/promises")).createInterface({
@@ -239,7 +310,7 @@ async function resolveSelectedSession(args) {
239
310
  sessionId: args.session,
240
311
  path: args.session,
241
312
  // --session is an explicit file; honor --cwd so adoption can find the
242
- // project's .evolve/skill-runs.jsonl (locate() infers cwd in the scan path).
313
+ // project's current or legacy skill-run log (locate() infers cwd in the scan path).
243
314
  cwd: args.cwd ?? null,
244
315
  mtimeMs: st.mtimeMs
245
316
  }
@@ -264,7 +335,7 @@ async function resolveSelectedSession(args) {
264
335
  `session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
265
336
  );
266
337
  }
267
- async function buildAxService() {
338
+ async function buildAxService(model) {
268
339
  const apiKey = process.env.OPENAI_API_KEY;
269
340
  if (!apiKey) {
270
341
  throw new Error(
@@ -272,8 +343,7 @@ async function buildAxService() {
272
343
  );
273
344
  }
274
345
  const apiURL = process.env.OPENAI_BASE_URL || void 0;
275
- const { AxAI } = await import("@ax-llm/ax");
276
- return AxAI.create({ name: "openai", apiKey, ...apiURL ? { apiURL } : {} });
346
+ return createAnalystAi({ apiKey, baseUrl: apiURL, model });
277
347
  }
278
348
  async function cmdList(args) {
279
349
  const groups = await discover({ ...args, last: args.last || 20 });
@@ -297,6 +367,7 @@ function selectedSessionSource(ref, spans, sessionIdOverride) {
297
367
  const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
298
368
  const role = root?.attributes["traces.session.role"];
299
369
  const parentSessionId = root?.attributes["traces.parent_session_id"];
370
+ const relationship = describeSessionRelationship(ref, spans);
300
371
  const corruptionDigest = root?.attributes[ATTR.CORRUPTION_DIGEST];
301
372
  const sessionId = [
302
373
  sessionIdOverride,
@@ -310,6 +381,13 @@ function selectedSessionSource(ref, spans, sessionIdOverride) {
310
381
  subject,
311
382
  role: role === "operator" || role === "child" ? role : "unknown",
312
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 } : {},
313
391
  integrity: ref.integrity?.status ?? "complete",
314
392
  ...ref.integrity ? {
315
393
  corruptionCount: ref.integrity.corruptions.length,
@@ -319,66 +397,67 @@ function selectedSessionSource(ref, spans, sessionIdOverride) {
319
397
  };
320
398
  }
321
399
  async function collectSpans(args) {
322
- if (args.input) return collectImportedSpans(args);
323
- if (args.session) {
324
- const { adapter, ref } = await resolveSelectedSession(args);
325
- const spans2 = await parseSession(adapter, ref);
326
- return {
327
- spans: spans2,
328
- harness: adapter.harness,
329
- cwds: ref.cwd ? [ref.cwd] : [],
330
- sources: [selectedSessionSource(ref, spans2)]
331
- };
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);
332
403
  }
333
- const groups = await discover({ ...args, last: args.last || 1 });
404
+ const selection = await collectSessionRows(args, false, args.last || 1);
334
405
  const spans = [];
335
- const harnesses = [];
406
+ const harnesses = /* @__PURE__ */ new Set();
336
407
  const cwds = [];
337
408
  const sources = [];
338
- for (const { adapter, refs } of groups) {
339
- if (refs.length > 0) harnesses.push(adapter.harness);
340
- for (const ref of refs) {
341
- const parsed = await parseSession(adapter, ref);
342
- appendAll(spans, parsed);
343
- sources.push(selectedSessionSource(ref, parsed));
344
- if (ref.cwd) cwds.push(ref.cwd);
345
- }
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);
346
414
  }
347
- 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
+ };
348
422
  }
349
423
  async function cmdConvert(args) {
350
- const { spans } = await collectSpans(args);
424
+ const { spans, workflow } = await collectSpans(args);
351
425
  if (spans.length === 0) throw new Error("no spans found for the given selection");
426
+ warnIncompleteWorkflow(workflow);
352
427
  const path = await writeOtlpFile(spans, args.otlp);
353
428
  console.log(`wrote ${spans.length} spans \u2192 ${path}`);
354
429
  }
355
- async function sourceSha256(path) {
356
- const hash = createHash("sha256");
357
- for await (const chunk of createReadStream(path)) hash.update(chunk);
358
- return hash.digest("hex");
359
- }
360
- async function collectSessionRows(args, bindExplicitSource = false) {
361
- if (args.session) {
362
- const { adapter, ref } = await resolveSelectedSession(args);
363
- if (!bindExplicitSource) return [{ ref, spans: await parseSession(adapter, ref) }];
364
- const before = await sourceSha256(ref.path);
365
- const spans = await parseSession(adapter, ref);
366
- const after = await sourceSha256(ref.path);
367
- if (before !== after) {
368
- throw new Error(`session source changed while parsing; refusing unbound evidence: ${ref.path}`);
369
- }
370
- 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}`;
371
434
  }
372
- const groups = await discover({ ...args, last: args.last || 20 });
373
- const rows = [];
374
- for (const { adapter, refs } of groups) {
375
- 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"}`;
376
437
  }
377
- 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
+ });
378
455
  }
379
456
  async function cmdEvidence(args) {
380
- 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);
381
459
  if (rows.length === 0) throw new Error("no spans found for the given selection");
460
+ warnIncompleteWorkflow(selection.workflow);
382
461
  const otlpPath = args.otlp ? await writeOtlpFile(rows.flatMap((row) => row.spans), args.otlp) : void 0;
383
462
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
384
463
  const records = await Promise.all(rows.map(
@@ -398,8 +477,10 @@ async function cmdEvidence(args) {
398
477
  }
399
478
  }
400
479
  async function cmdIndex(args) {
401
- 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);
402
482
  if (rows.length === 0) throw new Error("no spans found for the given selection");
483
+ warnIncompleteWorkflow(selection.workflow);
403
484
  const index = await buildSessionIndexFromRows(rows, {
404
485
  minLoopOccurrences: args.minLoop,
405
486
  selection: {
@@ -409,7 +490,9 @@ async function cmdIndex(args) {
409
490
  last: args.last || void 0,
410
491
  session: args.session,
411
492
  cwd: args.cwd,
412
- since: args.since
493
+ since: args.since,
494
+ latestTurn: args.latestTurn || void 0,
495
+ ...selection.workflow ? { workflow: selection.workflow } : {}
413
496
  }
414
497
  });
415
498
  if (args.out) {
@@ -447,8 +530,8 @@ async function cmdExport(args) {
447
530
  }
448
531
  function traceEvidenceFormat(raw) {
449
532
  const format = raw ?? "auto";
450
- if (format !== "auto" && format !== "policy-evidence" && format !== "sandbox-events" && format !== "openinference" && format !== "intelligence-spans") {
451
- 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");
452
535
  }
453
536
  return format;
454
537
  }
@@ -546,7 +629,7 @@ async function collectImportedSpans(args) {
546
629
  }
547
630
  function externalAnalyzersFromArgs(args) {
548
631
  return args.analyzers.map(
549
- (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] })
550
633
  );
551
634
  }
552
635
  async function cmdInvestigate(args) {
@@ -559,18 +642,20 @@ async function cmdInvestigate(args) {
559
642
  }
560
643
  }
561
644
  async function investigate(args, options = {}) {
562
- const { spans, harness, cwds, sources } = await collectSpans(args);
645
+ const { spans, harness, cwds, sources, workflow } = await collectSpans(args);
563
646
  if (spans.length === 0) throw new Error("no spans found for the given selection");
564
647
  const config = args.config !== void 0 || options.loadDefaultConfig !== false ? await loadTracesConfig(args.config) : void 0;
565
- 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;
566
650
  return runTraceInvestigation(mergeTracesConfig({
567
651
  spans,
568
652
  harness,
569
653
  sources,
654
+ workflow,
570
655
  cwds,
571
656
  minLoopOccurrences: args.minLoop,
572
657
  ai,
573
- model: args.model,
658
+ model: args.llm ? analystModel : args.model,
574
659
  budgetUsd: args.budget,
575
660
  otlpOutPath: args.otlp,
576
661
  externalAnalyzers: externalAnalyzersFromArgs(args),
@@ -580,19 +665,21 @@ async function investigate(args, options = {}) {
580
665
  }, config));
581
666
  }
582
667
  async function cmdImprove(args) {
583
- const { spans, harness, cwds, sources } = await collectSpans(args);
668
+ const { spans, harness, cwds, sources, workflow } = await collectSpans(args);
584
669
  if (spans.length === 0) throw new Error("no spans found for the given selection");
585
670
  const config = await loadTracesConfig(args.config);
586
- 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;
587
673
  const result = await runTraceImprovement({
588
674
  ...mergeTracesConfig({
589
675
  spans,
590
676
  harness,
591
677
  sources,
678
+ workflow,
592
679
  cwds,
593
680
  minLoopOccurrences: args.minLoop,
594
681
  ai,
595
- model: args.model,
682
+ model: args.llm ? analystModel : args.model,
596
683
  budgetUsd: args.budget,
597
684
  otlpOutPath: args.otlp,
598
685
  externalAnalyzers: externalAnalyzersFromArgs(args),
@@ -819,6 +906,7 @@ Input formats:
819
906
  sandbox-events JSON arrays with start/raw/result/done/error events
820
907
  openinference Existing OpenInference JSONL; rewrites through traces redaction
821
908
  intelligence-spans JSONL rows exported from Tangle Intelligence trace spans
909
+ chat-trajectory A message array or objects with a messages array
822
910
  auto Detect the format from the file contents (default)
823
911
 
824
912
  Examples:
@@ -855,7 +943,12 @@ Options:
855
943
  --harness <id> Harness or alias (default: claude-code). Known: ${knownHarnesses().join(", ")}
856
944
  --all Sweep every known harness
857
945
  --last <n> Most-recent N sessions
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
858
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)
859
952
  --cwd <dir> Filter sessions by working directory
860
953
  --supervisor-run-dir <dir>
861
954
  analyze: report a SUPERVISION TREE instead of harness sessions \u2014
@@ -865,7 +958,7 @@ Options:
865
958
  --out <path> Write report to a file
866
959
  --dir <path> improve: write artifacts to this directory
867
960
  --otlp <path> OTLP artifact path (also evidence provenance / dry-run upload preview)
868
- --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
869
962
  --metadata <json> analyze/export file: attach JSON object fields as span attributes
870
963
  --attr <k=v> analyze/export file: attach one span attribute (repeatable)
871
964
  --mode <kind> stream: visualizer | findings | agent (default visualizer)
@@ -873,10 +966,10 @@ Options:
873
966
  --no-spans stream: omit per-span pulse events
874
967
  --no-findings stream: omit semantic live-finding events
875
968
  --llm Enable agentic RLM analysts (needs OPENAI_API_KEY / OPENAI_BASE_URL)
876
- --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})
877
970
  --config <path> investigate/improve/stream: JS config with analysts, liveAnalysts, or external analyzers
878
971
  --budget <usd> USD cap for agentic analysts
879
- --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)
880
973
  --analyzer-prompt <p> analyze: prompt passed to external analyzers (default: diagnose)
881
974
  --interval <s> watch/stream: poll interval seconds (default 5)
882
975
  --window <m> watch/stream: only sessions active in the last N minutes (default 30)
@@ -896,12 +989,13 @@ async function main() {
896
989
  console.log(`traces ${packageVersion()}`);
897
990
  return;
898
991
  }
899
- const args = parseArgs(rawArgs);
900
- if (args.help) {
901
- if (args.command === "export" || args.command === "help" && args.input === "export") usageExport();
992
+ const parsedArgs = parseArgs(rawArgs);
993
+ if (parsedArgs.help) {
994
+ if (parsedArgs.command === "export" || parsedArgs.command === "help" && parsedArgs.input === "export") usageExport();
902
995
  else usage();
903
996
  return;
904
997
  }
998
+ const args = validateWorkflowSelection(applyCurrentSessionSelection(parsedArgs));
905
999
  switch (args.command) {
906
1000
  case "help":
907
1001
  if (args.input === "export") usageExport();