agent-inspect 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +33 -4
  3. package/docs/ADAPTERS.md +47 -0
  4. package/docs/CLI.md +57 -1
  5. package/docs/LIMITATIONS.md +6 -0
  6. package/docs/SCHEMA.md +1 -0
  7. package/package.json +2 -2
  8. package/packages/cli/dist/index.cjs +1454 -74
  9. package/packages/cli/dist/index.cjs.map +1 -1
  10. package/packages/cli/dist/index.mjs +1455 -75
  11. package/packages/cli/dist/index.mjs.map +1 -1
  12. package/packages/core/dist/advanced.cjs +581 -3
  13. package/packages/core/dist/advanced.cjs.map +1 -1
  14. package/packages/core/dist/advanced.d.cts +172 -8
  15. package/packages/core/dist/advanced.d.ts +172 -8
  16. package/packages/core/dist/advanced.mjs +572 -4
  17. package/packages/core/dist/advanced.mjs.map +1 -1
  18. package/packages/core/dist/checks.d.cts +2 -2
  19. package/packages/core/dist/checks.d.ts +2 -2
  20. package/packages/core/dist/{context-yv2VSDQF.d.ts → context-BDZmi53V.d.ts} +3 -3
  21. package/packages/core/dist/{context-CSKnzpXR.d.cts → context-DjY-jvQk.d.cts} +3 -3
  22. package/packages/core/dist/diff.d.cts +3 -3
  23. package/packages/core/dist/diff.d.ts +3 -3
  24. package/packages/core/dist/exporters.d.cts +3 -3
  25. package/packages/core/dist/exporters.d.ts +3 -3
  26. package/packages/core/dist/index.d.cts +6 -6
  27. package/packages/core/dist/index.d.ts +6 -6
  28. package/packages/core/dist/{inspect-event-CevRYp58.d.cts → inspect-event-CYAV7Hxm.d.cts} +1 -1
  29. package/packages/core/dist/{inspect-event-CevRYp58.d.ts → inspect-event-CYAV7Hxm.d.ts} +1 -1
  30. package/packages/core/dist/{log-config-8aE5Kxtr.d.cts → log-config-BNQ9UTmP.d.cts} +1 -1
  31. package/packages/core/dist/{log-config-DVimQJho.d.ts → log-config-D3Yp1SZN.d.ts} +1 -1
  32. package/packages/core/dist/logs.d.cts +3 -3
  33. package/packages/core/dist/logs.d.ts +3 -3
  34. package/packages/core/dist/{persisted-inspect-event-D-WpXeZX.d.ts → persisted-inspect-event-BLvb0jSX.d.ts} +1 -1
  35. package/packages/core/dist/{persisted-inspect-event-DHcHPUKv.d.cts → persisted-inspect-event-D5SjBz9b.d.cts} +1 -1
  36. package/packages/core/dist/persisted.d.cts +5 -5
  37. package/packages/core/dist/persisted.d.ts +5 -5
  38. package/packages/core/dist/readers.d.cts +2 -2
  39. package/packages/core/dist/readers.d.ts +2 -2
  40. package/packages/core/dist/{types-DgMN3qow.d.cts → types-C-llnPH0.d.cts} +1 -1
  41. package/packages/core/dist/{types-D-Y1kOU-.d.ts → types-CQun9LW6.d.ts} +1 -1
  42. package/packages/core/dist/writers.d.cts +2 -2
  43. package/packages/core/dist/writers.d.ts +2 -2
@@ -23,7 +23,7 @@ var process2__default = /*#__PURE__*/_interopDefault(process2);
23
23
  var tty__default = /*#__PURE__*/_interopDefault(tty);
24
24
 
25
25
  // package.json
26
- var version = "2.3.0";
26
+ var version = "2.5.0";
27
27
 
28
28
  // packages/core/src/correlation-metadata.ts
29
29
  var TRACE_CORRELATION_KEYS = [
@@ -1440,18 +1440,18 @@ var proto = Object.defineProperties(() => {
1440
1440
  }
1441
1441
  }
1442
1442
  });
1443
- var createStyler = (open2, close, parent) => {
1443
+ var createStyler = (open3, close, parent) => {
1444
1444
  let openAll;
1445
1445
  let closeAll;
1446
1446
  if (parent === void 0) {
1447
- openAll = open2;
1447
+ openAll = open3;
1448
1448
  closeAll = close;
1449
1449
  } else {
1450
- openAll = parent.openAll + open2;
1450
+ openAll = parent.openAll + open3;
1451
1451
  closeAll = close + parent.closeAll;
1452
1452
  }
1453
1453
  return {
1454
- open: open2,
1454
+ open: open3,
1455
1455
  close,
1456
1456
  openAll,
1457
1457
  closeAll,
@@ -1516,15 +1516,15 @@ function getStatusIcon(status) {
1516
1516
  if (status === "error") return source_default.red("\u2716");
1517
1517
  return source_default.yellow("\u23F3");
1518
1518
  }
1519
- function renderStepLine(name, durationMs, status, depth) {
1519
+ function renderStepLine(name, durationMs2, status, depth) {
1520
1520
  try {
1521
1521
  const nm = formatTerminalName(name);
1522
1522
  const ind = getIndent(depth ?? 0);
1523
- if (status === "running" && durationMs === void 0) {
1523
+ if (status === "running" && durationMs2 === void 0) {
1524
1524
  return `${ind}${source_default.yellow("\u23F3")} ${nm}`;
1525
1525
  }
1526
- const hasDur = durationMs !== void 0 && Number.isFinite(durationMs);
1527
- const dur = hasDur ? formatDuration2(durationMs) : void 0;
1526
+ const hasDur = durationMs2 !== void 0 && Number.isFinite(durationMs2);
1527
+ const dur = hasDur ? formatDuration2(durationMs2) : void 0;
1528
1528
  if (status === "running") {
1529
1529
  return dur !== void 0 ? `${ind}${source_default.yellow("\u23F3")} ${nm} (${dur})` : `${ind}${source_default.yellow("\u23F3")} ${nm}`;
1530
1530
  }
@@ -1677,14 +1677,14 @@ async function extractMetadata(filePath, _quickScan) {
1677
1677
  } else {
1678
1678
  status = "unknown";
1679
1679
  }
1680
- const durationMs = explicitDurationMs ?? (startedAt !== void 0 && endedAt !== void 0 && Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt >= startedAt ? endedAt - startedAt : void 0);
1680
+ const durationMs2 = explicitDurationMs ?? (startedAt !== void 0 && endedAt !== void 0 && Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt >= startedAt ? endedAt - startedAt : void 0);
1681
1681
  return {
1682
1682
  runId: resolvedRunId,
1683
1683
  name,
1684
1684
  status,
1685
1685
  startedAt,
1686
1686
  endedAt,
1687
- durationMs,
1687
+ durationMs: durationMs2,
1688
1688
  eventCount: parsedTrace.sourceEventCount,
1689
1689
  filePath,
1690
1690
  fileSize: stats.size,
@@ -1705,7 +1705,7 @@ function buildRunSummary(events) {
1705
1705
  const runId = started?.runId ?? events.find((e) => typeof e.runId === "string")?.runId ?? "unknown-run";
1706
1706
  const name = typeof started?.name === "string" && started.name.trim() !== "" ? started.name : void 0;
1707
1707
  const status = lastCompleted ? lastCompleted.status : started ? "running" : "unknown";
1708
- const durationMs = lastCompleted && isFiniteNumber(lastCompleted.durationMs) ? lastCompleted.durationMs : void 0;
1708
+ const durationMs2 = lastCompleted && isFiniteNumber(lastCompleted.durationMs) ? lastCompleted.durationMs : void 0;
1709
1709
  started && isFiniteNumber(started.startTime) ? started.startTime : void 0;
1710
1710
  const steps = /* @__PURE__ */ new Map();
1711
1711
  for (const e of events) {
@@ -1795,7 +1795,7 @@ function buildRunSummary(events) {
1795
1795
  runId,
1796
1796
  name,
1797
1797
  status,
1798
- durationMs,
1798
+ durationMs: durationMs2,
1799
1799
  totalSteps,
1800
1800
  llmSteps,
1801
1801
  toolSteps,
@@ -2540,10 +2540,12 @@ async function searchTraces(metas, options) {
2540
2540
  durationFilter = parseDurationFilter(options.duration);
2541
2541
  }
2542
2542
  const limit = options.limit ?? 50;
2543
+ const sessionId = options.session?.trim();
2543
2544
  const hasContentFilter = Boolean(
2544
2545
  options.status || stepTypeFilter || nameQuery || toolQuery || durationFilter
2545
2546
  );
2546
2547
  const results = [];
2548
+ const sessionLabel = sessionId && sessionId !== "" ? sessionId : void 0;
2547
2549
  if (!hasContentFilter) {
2548
2550
  for (const m of filtered) {
2549
2551
  results.push({
@@ -2552,9 +2554,10 @@ async function searchTraces(metas, options) {
2552
2554
  runStatus: m.status,
2553
2555
  timestamp: m.startedAt,
2554
2556
  durationMs: m.durationMs,
2555
- matchReason: "trace in directory",
2556
- matchedFields: ["run"],
2557
- filePath: m.filePath
2557
+ matchReason: sessionLabel ? `trace in session ${sessionLabel}` : "trace in directory",
2558
+ matchedFields: sessionLabel ? ["run", "session"] : ["run"],
2559
+ filePath: m.filePath,
2560
+ ...sessionLabel ? { sessionId: sessionLabel } : {}
2558
2561
  });
2559
2562
  }
2560
2563
  return results.slice(0, limit);
@@ -2693,6 +2696,478 @@ async function loadTraceMetadataList(_traceDir, fileNames, getPath) {
2693
2696
  }
2694
2697
  return metas;
2695
2698
  }
2699
+
2700
+ // packages/core/src/sessions/metadata.ts
2701
+ function isNonEmptyString3(value) {
2702
+ return typeof value === "string" && value.trim() !== "";
2703
+ }
2704
+ function finitePositiveInt(value) {
2705
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
2706
+ return void 0;
2707
+ }
2708
+ return Math.trunc(value);
2709
+ }
2710
+ function extractSessionWorkflowMetadata(record) {
2711
+ if (!record) return void 0;
2712
+ const out = {};
2713
+ let found = false;
2714
+ const assignString = (key, value) => {
2715
+ if (isNonEmptyString3(value)) {
2716
+ out[key] = value.trim();
2717
+ found = true;
2718
+ }
2719
+ };
2720
+ assignString("sessionId", record.sessionId);
2721
+ assignString("conversationId", record.conversationId);
2722
+ assignString("groupId", record.groupId);
2723
+ assignString("parentGroupId", record.parentGroupId);
2724
+ assignString("retryOf", record.retryOf);
2725
+ assignString("retryReason", record.retryReason);
2726
+ assignString("handoffFrom", record.handoffFrom);
2727
+ assignString("handoffTo", record.handoffTo);
2728
+ assignString("subAgentId", record.subAgentId);
2729
+ assignString("subAgentName", record.subAgentName);
2730
+ assignString("jobId", record.jobId);
2731
+ assignString("queueName", record.queueName);
2732
+ assignString("workflowName", record.workflowName);
2733
+ assignString("workflowStep", record.workflowStep);
2734
+ assignString("toolCallId", record.toolCallId);
2735
+ assignString("mcpToolCallId", record.mcpToolCallId);
2736
+ assignString("linkedStepId", record.linkedStepId);
2737
+ assignString("correlationId", record.correlationId);
2738
+ assignString("requestId", record.requestId);
2739
+ assignString("decisionId", record.decisionId);
2740
+ const attempt = finitePositiveInt(record.attempt);
2741
+ if (attempt !== void 0) {
2742
+ out.attempt = attempt;
2743
+ found = true;
2744
+ }
2745
+ return found ? out : void 0;
2746
+ }
2747
+ function sessionKeyForRun(meta, options) {
2748
+ if (meta?.sessionId) return meta.sessionId;
2749
+ if (options?.correlateByGroupId && meta?.groupId) {
2750
+ return `group:${meta.groupId}`;
2751
+ }
2752
+ return void 0;
2753
+ }
2754
+
2755
+ // packages/core/src/sessions/load.ts
2756
+ async function enrichSessionRunRecord(meta) {
2757
+ let metadata;
2758
+ try {
2759
+ const events = await readTraceEventsFromFile(meta.filePath);
2760
+ for (const event of events) {
2761
+ if (event.event !== "run_started") continue;
2762
+ if (event.metadata && typeof event.metadata === "object") {
2763
+ metadata = event.metadata;
2764
+ }
2765
+ break;
2766
+ }
2767
+ } catch {
2768
+ }
2769
+ return {
2770
+ runId: meta.runId,
2771
+ name: meta.name,
2772
+ status: meta.status,
2773
+ startedAt: meta.startedAt,
2774
+ endedAt: meta.endedAt,
2775
+ durationMs: meta.durationMs,
2776
+ filePath: meta.filePath,
2777
+ metadata
2778
+ };
2779
+ }
2780
+ async function loadSessionRunRecords(metas) {
2781
+ const out = [];
2782
+ for (const meta of metas) {
2783
+ out.push(await enrichSessionRunRecord(meta));
2784
+ }
2785
+ return out;
2786
+ }
2787
+
2788
+ // packages/core/src/sessions/scope.ts
2789
+ function filterMetasBySessionScope(metas, records, options) {
2790
+ const sessionId = options.sessionId?.trim();
2791
+ const groupId = options.groupId?.trim();
2792
+ const warnings = [];
2793
+ if (sessionId) {
2794
+ const index = buildSessionIndex(records, {
2795
+ correlateByGroupId: options.correlateByGroupId === true
2796
+ });
2797
+ warnings.push(...index.warnings);
2798
+ const session = index.sessions.find((item) => item.sessionId === sessionId);
2799
+ if (!session) {
2800
+ return {
2801
+ metas: [],
2802
+ scopeLabel: sessionId,
2803
+ scopeKind: "session",
2804
+ runIds: [],
2805
+ warnings,
2806
+ notFound: true
2807
+ };
2808
+ }
2809
+ const runIdSet = new Set(session.runIds);
2810
+ const filtered = metas.filter((meta) => runIdSet.has(meta.runId));
2811
+ return {
2812
+ metas: filtered,
2813
+ scopeLabel: sessionId,
2814
+ scopeKind: "session",
2815
+ runIds: session.runIds,
2816
+ warnings,
2817
+ notFound: false
2818
+ };
2819
+ }
2820
+ if (groupId) {
2821
+ const runIds = records.filter((run) => extractSessionWorkflowMetadata(run.metadata)?.groupId === groupId).map((run) => run.runId).sort();
2822
+ if (runIds.length === 0) {
2823
+ return {
2824
+ metas: [],
2825
+ scopeLabel: groupId,
2826
+ scopeKind: "group",
2827
+ runIds: [],
2828
+ warnings,
2829
+ notFound: true
2830
+ };
2831
+ }
2832
+ const runIdSet = new Set(runIds);
2833
+ return {
2834
+ metas: metas.filter((meta) => runIdSet.has(meta.runId)),
2835
+ scopeLabel: groupId,
2836
+ scopeKind: "group",
2837
+ runIds,
2838
+ warnings,
2839
+ notFound: false
2840
+ };
2841
+ }
2842
+ return {
2843
+ metas: [...metas],
2844
+ scopeLabel: "",
2845
+ scopeKind: "session",
2846
+ runIds: [],
2847
+ warnings,
2848
+ notFound: false
2849
+ };
2850
+ }
2851
+
2852
+ // packages/core/src/sessions/checks.ts
2853
+ function emptySummary() {
2854
+ return { passed: 0, failed: 0, warnings: 0, errors: 0 };
2855
+ }
2856
+ function mergeSummary(target, source) {
2857
+ return {
2858
+ passed: target.passed + source.passed,
2859
+ failed: target.failed + source.failed,
2860
+ warnings: target.warnings + source.warnings,
2861
+ errors: target.errors + source.errors
2862
+ };
2863
+ }
2864
+ function sessionDiagnostic(code, message) {
2865
+ return { code, message, severity: "error" };
2866
+ }
2867
+ function aggregateSessionCheckResults(perRun, scope) {
2868
+ if (scope.notFound) {
2869
+ return {
2870
+ ok: false,
2871
+ status: "error",
2872
+ format: perRun[0]?.format ?? "agent-inspect-jsonl",
2873
+ scopeKind: scope.scopeKind,
2874
+ scopeLabel: scope.scopeLabel,
2875
+ runIds: [],
2876
+ runResults: [],
2877
+ summary: { ...emptySummary(), errors: 1 },
2878
+ findings: [],
2879
+ diagnostics: [
2880
+ sessionDiagnostic(
2881
+ "AI_CHECK_INVALID_ARGUMENTS",
2882
+ `${scope.scopeKind} not found: ${scope.scopeLabel}`
2883
+ )
2884
+ ],
2885
+ ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
2886
+ };
2887
+ }
2888
+ if (scope.empty || perRun.length === 0) {
2889
+ return {
2890
+ ok: false,
2891
+ status: "error",
2892
+ format: perRun[0]?.format ?? "agent-inspect-jsonl",
2893
+ scopeKind: scope.scopeKind,
2894
+ scopeLabel: scope.scopeLabel,
2895
+ runIds: scope.runIds,
2896
+ runResults: [],
2897
+ summary: { ...emptySummary(), errors: 1 },
2898
+ findings: [],
2899
+ diagnostics: [
2900
+ sessionDiagnostic(
2901
+ "AI_CHECK_TRACE_UNREADABLE",
2902
+ `No readable traces in ${scope.scopeKind}: ${scope.scopeLabel}`
2903
+ )
2904
+ ],
2905
+ ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
2906
+ };
2907
+ }
2908
+ let summary = emptySummary();
2909
+ const findings = [];
2910
+ const diagnostics = [];
2911
+ const runResults = [];
2912
+ for (const result of perRun) {
2913
+ summary = mergeSummary(summary, result.summary);
2914
+ findings.push(...result.findings);
2915
+ diagnostics.push(...result.diagnostics);
2916
+ if (result.runId) {
2917
+ runResults.push({ runId: result.runId, status: result.status });
2918
+ }
2919
+ }
2920
+ runResults.sort((a, b) => a.runId.localeCompare(b.runId));
2921
+ findings.sort((a, b) => {
2922
+ const runCmp = (a.evidence[0]?.runId ?? "").localeCompare(
2923
+ b.evidence[0]?.runId ?? ""
2924
+ );
2925
+ if (runCmp !== 0) return runCmp;
2926
+ return a.ruleId.localeCompare(b.ruleId);
2927
+ });
2928
+ const hasErrors = diagnostics.some((item) => item.severity === "error");
2929
+ const status = hasErrors ? "error" : summary.failed > 0 ? "fail" : "pass";
2930
+ return {
2931
+ ok: status === "pass",
2932
+ status,
2933
+ format: perRun[0]?.format ?? "agent-inspect-jsonl",
2934
+ scopeKind: scope.scopeKind,
2935
+ scopeLabel: scope.scopeLabel,
2936
+ runIds: scope.runIds,
2937
+ runResults,
2938
+ summary,
2939
+ findings,
2940
+ diagnostics,
2941
+ ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
2942
+ };
2943
+ }
2944
+
2945
+ // packages/core/src/sessions/index.ts
2946
+ function compareRuns(a, b) {
2947
+ const aStart = a.startedAt ?? 0;
2948
+ const bStart = b.startedAt ?? 0;
2949
+ if (aStart !== bStart) return aStart - bStart;
2950
+ return a.runId.localeCompare(b.runId);
2951
+ }
2952
+ function buildGroups(runIds, metaByRunId) {
2953
+ const byGroup = /* @__PURE__ */ new Map();
2954
+ for (const runId of runIds) {
2955
+ const groupId = metaByRunId.get(runId)?.groupId;
2956
+ if (!groupId) continue;
2957
+ const existing = byGroup.get(groupId);
2958
+ if (existing) {
2959
+ existing.runIds.push(runId);
2960
+ } else {
2961
+ byGroup.set(groupId, {
2962
+ groupId,
2963
+ parentGroupId: metaByRunId.get(runId)?.parentGroupId,
2964
+ runIds: [runId]
2965
+ });
2966
+ }
2967
+ }
2968
+ return [...byGroup.values()].map((group) => ({
2969
+ ...group,
2970
+ runIds: [...group.runIds].sort((a, b) => a.localeCompare(b))
2971
+ }));
2972
+ }
2973
+ function buildHandoffs(runIds, metaByRunId, warnings, sessionId) {
2974
+ const edges = [];
2975
+ const seen = /* @__PURE__ */ new Set();
2976
+ const pushEdge = (edge) => {
2977
+ const key = `${edge.from}->${edge.to}:${edge.confidence}`;
2978
+ if (seen.has(key)) return;
2979
+ seen.add(key);
2980
+ edges.push(edge);
2981
+ };
2982
+ for (const runId of runIds) {
2983
+ const meta = metaByRunId.get(runId);
2984
+ if (!meta) continue;
2985
+ if (meta.handoffFrom && meta.handoffTo) {
2986
+ pushEdge({
2987
+ from: meta.handoffFrom,
2988
+ to: meta.handoffTo,
2989
+ source: "manual",
2990
+ confidence: "explicit"
2991
+ });
2992
+ continue;
2993
+ }
2994
+ if (meta.handoffFrom) {
2995
+ pushEdge({
2996
+ from: meta.handoffFrom,
2997
+ to: runId,
2998
+ source: "manual",
2999
+ confidence: "explicit"
3000
+ });
3001
+ }
3002
+ if (meta.handoffTo) {
3003
+ pushEdge({
3004
+ from: runId,
3005
+ to: meta.handoffTo,
3006
+ source: "manual",
3007
+ confidence: "explicit"
3008
+ });
3009
+ }
3010
+ if (meta.subAgentId && meta.parentGroupId && !meta.handoffFrom && !meta.handoffTo) {
3011
+ pushEdge({
3012
+ from: meta.parentGroupId,
3013
+ to: meta.subAgentId,
3014
+ source: "inferred",
3015
+ confidence: "correlated"
3016
+ });
3017
+ warnings.push({
3018
+ code: "ambiguous-handoff-endpoints",
3019
+ message: "Handoff inferred from parentGroupId and subAgentId without explicit handoffFrom/handoffTo.",
3020
+ runId,
3021
+ sessionId
3022
+ });
3023
+ }
3024
+ }
3025
+ return edges.sort((a, b) => {
3026
+ const from = a.from.localeCompare(b.from);
3027
+ if (from !== 0) return from;
3028
+ return a.to.localeCompare(b.to);
3029
+ });
3030
+ }
3031
+ function buildRetries(runIds, metaByRunId, warnings, sessionId) {
3032
+ const retries = [];
3033
+ for (const runId of runIds) {
3034
+ const meta = metaByRunId.get(runId);
3035
+ if (!meta) continue;
3036
+ if (meta.retryOf) {
3037
+ retries.push({
3038
+ runId,
3039
+ retryOf: meta.retryOf,
3040
+ attempt: meta.attempt,
3041
+ source: "manual",
3042
+ confidence: "explicit"
3043
+ });
3044
+ continue;
3045
+ }
3046
+ if (meta.attempt !== void 0 && meta.attempt > 1) {
3047
+ retries.push({
3048
+ runId,
3049
+ attempt: meta.attempt,
3050
+ source: "inferred",
3051
+ confidence: "correlated"
3052
+ });
3053
+ warnings.push({
3054
+ code: "ambiguous-retry-link",
3055
+ message: "attempt > 1 without retryOf; retry link is correlated only.",
3056
+ runId,
3057
+ sessionId
3058
+ });
3059
+ }
3060
+ }
3061
+ return retries.sort((a, b) => a.runId.localeCompare(b.runId));
3062
+ }
3063
+ function buildCriticalPath(runs, handoffs) {
3064
+ const runById = new Map(runs.map((run) => [run.runId, run]));
3065
+ const explicitTargets = new Set(
3066
+ handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.to)
3067
+ );
3068
+ const explicitSources = new Set(
3069
+ handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.from)
3070
+ );
3071
+ const ordered = [...runs].sort(compareRuns);
3072
+ const path15 = [];
3073
+ const visited = /* @__PURE__ */ new Set();
3074
+ const pushRun = (run, confidence, source) => {
3075
+ if (visited.has(run.runId)) return;
3076
+ visited.add(run.runId);
3077
+ path15.push({
3078
+ runId: run.runId,
3079
+ name: run.name,
3080
+ startedAt: run.startedAt,
3081
+ durationMs: run.durationMs,
3082
+ confidence,
3083
+ source
3084
+ });
3085
+ };
3086
+ for (const edge of handoffs) {
3087
+ if (edge.confidence !== "explicit") continue;
3088
+ const fromRun = [...runById.values()].find(
3089
+ (run) => run.runId === edge.from || metaRunIdMatches(run, edge.from, runById)
3090
+ );
3091
+ const toRun = [...runById.values()].find(
3092
+ (run) => run.runId === edge.to || metaRunIdMatches(run, edge.to, runById)
3093
+ );
3094
+ if (fromRun) pushRun(fromRun, "explicit", "manual");
3095
+ if (toRun) pushRun(toRun, "explicit", "manual");
3096
+ }
3097
+ for (const run of ordered) {
3098
+ if (visited.has(run.runId)) continue;
3099
+ const confidence = explicitTargets.has(run.runId) || explicitSources.has(run.runId) ? "explicit" : "correlated";
3100
+ pushRun(run, confidence, confidence === "explicit" ? "manual" : "inferred");
3101
+ }
3102
+ return path15;
3103
+ }
3104
+ function metaRunIdMatches(run, token, runById) {
3105
+ const meta = extractSessionWorkflowMetadata(run.metadata);
3106
+ return meta?.subAgentId === token || meta?.groupId === token || runById.has(token);
3107
+ }
3108
+ function buildSessionIndex(inputRuns, options = {}) {
3109
+ const warnings = [];
3110
+ const runs = [...inputRuns].sort(compareRuns);
3111
+ const metaByRunId = /* @__PURE__ */ new Map();
3112
+ for (const run of runs) {
3113
+ metaByRunId.set(run.runId, extractSessionWorkflowMetadata(run.metadata));
3114
+ }
3115
+ const sessionsByKey = /* @__PURE__ */ new Map();
3116
+ const unscopedRunIds = [];
3117
+ for (const run of runs) {
3118
+ const meta = metaByRunId.get(run.runId);
3119
+ const key = sessionKeyForRun(meta, {
3120
+ correlateByGroupId: options.correlateByGroupId === true
3121
+ });
3122
+ if (!key) {
3123
+ unscopedRunIds.push(run.runId);
3124
+ continue;
3125
+ }
3126
+ const bucket = sessionsByKey.get(key) ?? [];
3127
+ bucket.push(run);
3128
+ sessionsByKey.set(key, bucket);
3129
+ }
3130
+ const sessions = [...sessionsByKey.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([sessionId, sessionRuns]) => {
3131
+ const runIds = sessionRuns.map((run) => run.runId).sort();
3132
+ const handoffs = buildHandoffs(runIds, metaByRunId, warnings, sessionId);
3133
+ const retries = buildRetries(runIds, metaByRunId, warnings, sessionId);
3134
+ const groups = buildGroups(runIds, metaByRunId);
3135
+ const criticalPath = buildCriticalPath(sessionRuns, handoffs);
3136
+ const confidences = new Set(handoffs.map((edge) => edge.confidence));
3137
+ if (confidences.has("explicit") && confidences.has("correlated")) {
3138
+ warnings.push({
3139
+ code: "mixed-confidence-group",
3140
+ message: "Session aggregates explicit and correlated handoff edges.",
3141
+ sessionId
3142
+ });
3143
+ }
3144
+ return {
3145
+ sessionId,
3146
+ runIds,
3147
+ groups,
3148
+ handoffs,
3149
+ retries,
3150
+ criticalPath
3151
+ };
3152
+ });
3153
+ if (sessions.length === 0 && runs.length > 0) {
3154
+ warnings.push({
3155
+ code: "missing-session-id",
3156
+ message: "No sessionId (or correlated groupId) found on input runs."
3157
+ });
3158
+ }
3159
+ warnings.sort((a, b) => {
3160
+ const code = a.code.localeCompare(b.code);
3161
+ if (code !== 0) return code;
3162
+ return (a.runId ?? "").localeCompare(b.runId ?? "");
3163
+ });
3164
+ return {
3165
+ runs,
3166
+ sessions,
3167
+ unscopedRunIds: unscopedRunIds.sort(),
3168
+ warnings
3169
+ };
3170
+ }
2696
3171
  var KNOWN_EVENTS = /* @__PURE__ */ new Set([
2697
3172
  "run_started",
2698
3173
  "run_completed",
@@ -2751,10 +3226,10 @@ function statusIcon(status) {
2751
3226
  if (status === "running") return "\u23F3";
2752
3227
  return "?";
2753
3228
  }
2754
- function durationCell(status, durationMs) {
3229
+ function durationCell(status, durationMs2) {
2755
3230
  if (status === "running" || status === "unknown") return "-";
2756
- if (durationMs !== void 0 && Number.isFinite(durationMs)) {
2757
- return formatDuration2(durationMs);
3231
+ if (durationMs2 !== void 0 && Number.isFinite(durationMs2)) {
3232
+ return formatDuration2(durationMs2);
2758
3233
  }
2759
3234
  return "-";
2760
3235
  }
@@ -3411,7 +3886,7 @@ var TreeBuilder = class {
3411
3886
  const startedAt = sorted.length > 0 ? sorted[0].timestamp : void 0;
3412
3887
  const endedAt = sorted.length > 0 ? sorted[sorted.length - 1].timestamp : void 0;
3413
3888
  const status = computeRunStatus(sorted);
3414
- const durationMs = startedAt !== void 0 && endedAt !== void 0 && Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt >= startedAt && status !== "running" ? endedAt - startedAt : void 0;
3889
+ const durationMs2 = startedAt !== void 0 && endedAt !== void 0 && Number.isFinite(startedAt) && Number.isFinite(endedAt) && endedAt >= startedAt && status !== "running" ? endedAt - startedAt : void 0;
3415
3890
  const name = sorted.find((e) => e.kind === "RUN")?.name;
3416
3891
  out.push({
3417
3892
  runId,
@@ -3419,7 +3894,7 @@ var TreeBuilder = class {
3419
3894
  status,
3420
3895
  startedAt,
3421
3896
  endedAt: status === "running" ? void 0 : endedAt,
3422
- durationMs,
3897
+ durationMs: durationMs2,
3423
3898
  children: roots,
3424
3899
  metadata: {
3425
3900
  totalEvents: sorted.length,
@@ -3714,13 +4189,13 @@ function persistedEventsForParsedTrace(parsed) {
3714
4189
  function isRecord8(value) {
3715
4190
  return typeof value === "object" && value !== null && !Array.isArray(value);
3716
4191
  }
3717
- function isNonEmptyString3(value) {
4192
+ function isNonEmptyString4(value) {
3718
4193
  return typeof value === "string" && value.trim() !== "";
3719
4194
  }
3720
4195
  function readStringField(record, keys) {
3721
4196
  for (const key of keys) {
3722
4197
  const value = record[key];
3723
- if (isNonEmptyString3(value)) return value;
4198
+ if (isNonEmptyString4(value)) return value;
3724
4199
  }
3725
4200
  return void 0;
3726
4201
  }
@@ -3854,7 +4329,7 @@ function parseUnixNanoToIso(value) {
3854
4329
  return void 0;
3855
4330
  }
3856
4331
  function parseIsoTime(value) {
3857
- if (!isNonEmptyString3(value)) return void 0;
4332
+ if (!isNonEmptyString4(value)) return void 0;
3858
4333
  const ms = Date.parse(value);
3859
4334
  if (!Number.isFinite(ms)) return void 0;
3860
4335
  return new Date(ms).toISOString();
@@ -4114,9 +4589,9 @@ function mapOpenInferenceSpan(span, index, version2) {
4114
4589
  if (endedAt !== void 0) {
4115
4590
  event.endedAt = endedAt;
4116
4591
  }
4117
- const durationMs = durationBetweenIso(startedAt, endedAt);
4118
- if (durationMs !== void 0) {
4119
- event.durationMs = durationMs;
4592
+ const durationMs2 = durationBetweenIso(startedAt, endedAt);
4593
+ if (durationMs2 !== void 0) {
4594
+ event.durationMs = durationMs2;
4120
4595
  }
4121
4596
  if (tokenUsage !== void 0) {
4122
4597
  event.tokenUsage = tokenUsage;
@@ -4701,9 +5176,9 @@ function mapOtlpSpan(context) {
4701
5176
  if (endedAt !== void 0) {
4702
5177
  event.endedAt = endedAt;
4703
5178
  }
4704
- const durationMs = durationBetweenIso(startedAt, endedAt);
4705
- if (durationMs !== void 0) {
4706
- event.durationMs = durationMs;
5179
+ const durationMs2 = durationBetweenIso(startedAt, endedAt);
5180
+ if (durationMs2 !== void 0) {
5181
+ event.durationMs = durationMs2;
4707
5182
  }
4708
5183
  if (tokenUsage !== void 0) {
4709
5184
  event.tokenUsage = tokenUsage;
@@ -5708,13 +6183,13 @@ var EventNormalizer = class {
5708
6183
  const timestampMissing = parsedTs === void 0;
5709
6184
  const parentIdKey = cfg.parentIdKey;
5710
6185
  const parentId = parentIdKey ? safeString(raw[parentIdKey]) : void 0;
5711
- let durationMs;
6186
+ let durationMs2;
5712
6187
  const durationKey = cfg.durationKey;
5713
6188
  if (durationKey) {
5714
6189
  const v = raw[durationKey];
5715
- if (isFiniteNumber2(v)) durationMs = v;
6190
+ if (isFiniteNumber2(v)) durationMs2 = v;
5716
6191
  } else if (isFiniteNumber2(raw.durationMs)) {
5717
- durationMs = raw.durationMs;
6192
+ durationMs2 = raw.durationMs;
5718
6193
  }
5719
6194
  let status;
5720
6195
  const statusKey = cfg.statusKey;
@@ -5761,7 +6236,7 @@ var EventNormalizer = class {
5761
6236
  kind,
5762
6237
  timestamp,
5763
6238
  ...status ? { status } : {},
5764
- ...durationMs !== void 0 ? { durationMs } : {},
6239
+ ...durationMs2 !== void 0 ? { durationMs: durationMs2 } : {},
5765
6240
  ...Object.keys(attributes).length > 0 ? { attributes } : {},
5766
6241
  confidence,
5767
6242
  source: {
@@ -7349,7 +7824,7 @@ function manualTraceEventsToRunTree(events) {
7349
7824
  }
7350
7825
  const startedAt = started.startTime;
7351
7826
  const endedAt = lastCompleted !== void 0 && runStatus !== "running" ? lastCompleted.endTime : void 0;
7352
- const durationMs = lastCompleted !== void 0 && Number.isFinite(lastCompleted.durationMs) ? lastCompleted.durationMs : void 0;
7827
+ const durationMs2 = lastCompleted !== void 0 && Number.isFinite(lastCompleted.durationMs) ? lastCompleted.durationMs : void 0;
7353
7828
  const steps = /* @__PURE__ */ new Map();
7354
7829
  for (const e of events) {
7355
7830
  if (e.event !== "step_started") continue;
@@ -7438,7 +7913,7 @@ function manualTraceEventsToRunTree(events) {
7438
7913
  status: runStatus,
7439
7914
  startedAt,
7440
7915
  endedAt,
7441
- durationMs,
7916
+ durationMs: durationMs2,
7442
7917
  children: roots,
7443
7918
  metadata: {
7444
7919
  totalEvents: inspectNodes.size,
@@ -7751,7 +8226,7 @@ function manualTraceEventsToComparableRun(events) {
7751
8226
  let runStatus;
7752
8227
  if (lastCompleted === void 0) runStatus = "running";
7753
8228
  else runStatus = lastCompleted.status;
7754
- const durationMs = lastCompleted !== void 0 && Number.isFinite(lastCompleted.durationMs) ? lastCompleted.durationMs : void 0;
8229
+ const durationMs2 = lastCompleted !== void 0 && Number.isFinite(lastCompleted.durationMs) ? lastCompleted.durationMs : void 0;
7755
8230
  const steps = /* @__PURE__ */ new Map();
7756
8231
  let order = 0;
7757
8232
  for (const e of events) {
@@ -7822,7 +8297,7 @@ function manualTraceEventsToComparableRun(events) {
7822
8297
  runId,
7823
8298
  name: rs.name,
7824
8299
  status: runStatus,
7825
- durationMs,
8300
+ durationMs: durationMs2,
7826
8301
  steps: roots
7827
8302
  };
7828
8303
  }
@@ -8408,11 +8883,29 @@ async function searchCommand(options = {}) {
8408
8883
  parseDurationFilter(options.duration);
8409
8884
  }
8410
8885
  const files = await td.list();
8411
- const metas = await loadTraceMetadataList(
8886
+ let metas = await loadTraceMetadataList(
8412
8887
  traceDir,
8413
8888
  files,
8414
8889
  (f) => td.getPath(f)
8415
8890
  );
8891
+ const sessionId = options.session?.trim();
8892
+ if (sessionId) {
8893
+ const records = await loadSessionRunRecords(metas);
8894
+ const scoped = filterMetasBySessionScope(metas, records, {
8895
+ sessionId,
8896
+ correlateByGroupId: options.correlateGroup === true
8897
+ });
8898
+ if (scoped.notFound) {
8899
+ if (options.json) {
8900
+ console.log(JSON.stringify([], null, 2));
8901
+ } else {
8902
+ console.log(`Session not found: ${sessionId}`);
8903
+ console.log(`Trace directory: ${traceDir}`);
8904
+ }
8905
+ return;
8906
+ }
8907
+ metas = scoped.metas;
8908
+ }
8416
8909
  const status = options.status === "success" || options.status === "error" || options.status === "running" || options.status === "unknown" ? options.status : void 0;
8417
8910
  const results = await searchTraces(metas, {
8418
8911
  traceDir,
@@ -8423,7 +8916,9 @@ async function searchCommand(options = {}) {
8423
8916
  name: options.name,
8424
8917
  tool: options.tool,
8425
8918
  duration: options.duration,
8426
- limit: parseLimit2(options.limit)
8919
+ limit: parseLimit2(options.limit),
8920
+ ...sessionId ? { session: sessionId } : {},
8921
+ ...options.correlateGroup ? { correlateGroup: true } : {}
8427
8922
  });
8428
8923
  if (options.json) {
8429
8924
  console.log(JSON.stringify(results, null, 2));
@@ -8451,6 +8946,184 @@ async function searchCommand(options = {}) {
8451
8946
  }
8452
8947
  }
8453
8948
 
8949
+ // packages/cli/src/sessions.ts
8950
+ async function loadSessionIndex(traceDir, correlateGroup) {
8951
+ const td = new TraceDirectory({ dir: traceDir });
8952
+ const files = await td.list();
8953
+ const metas = await loadTraceMetadataList(
8954
+ traceDir,
8955
+ files,
8956
+ (fileName) => td.getPath(fileName)
8957
+ );
8958
+ const runs = await loadSessionRunRecords(metas);
8959
+ return buildSessionIndex(runs, { correlateByGroupId: correlateGroup === true });
8960
+ }
8961
+ function findSession(index, sessionId) {
8962
+ return index.sessions.find((session) => session.sessionId === sessionId);
8963
+ }
8964
+ function renderSessionsHuman(index, traceDir) {
8965
+ if (index.sessions.length === 0) {
8966
+ console.log("No sessions found");
8967
+ console.log(`Trace directory: ${traceDir}`);
8968
+ if (index.unscopedRunIds.length > 0) {
8969
+ console.log(
8970
+ `Unscoped runs (no sessionId): ${index.unscopedRunIds.join(", ")}`
8971
+ );
8972
+ }
8973
+ return;
8974
+ }
8975
+ console.log("Sessions:");
8976
+ for (const session of index.sessions) {
8977
+ const firstRun = index.runs.find(
8978
+ (run) => session.runIds.includes(run.runId)
8979
+ );
8980
+ const workflowName = firstRun?.metadata && typeof firstRun.metadata.workflowName === "string" ? firstRun.metadata.workflowName : void 0;
8981
+ const suffix = workflowName ? ` workflow=${workflowName}` : "";
8982
+ console.log(
8983
+ ` ${session.sessionId} (${session.runIds.length} run${session.runIds.length === 1 ? "" : "s"})${suffix}`
8984
+ );
8985
+ }
8986
+ if (index.unscopedRunIds.length > 0) {
8987
+ console.log("");
8988
+ console.log(
8989
+ `Unscoped runs (no sessionId): ${index.unscopedRunIds.join(", ")}`
8990
+ );
8991
+ }
8992
+ }
8993
+ function renderSessionHuman(session, index, options) {
8994
+ console.log(`Session: ${session.sessionId}`);
8995
+ console.log(`Runs: ${session.runIds.join(", ")}`);
8996
+ if (session.handoffs.length > 0) {
8997
+ console.log("");
8998
+ console.log("Handoffs:");
8999
+ for (const edge of session.handoffs) {
9000
+ console.log(
9001
+ ` ${edge.from} -> ${edge.to} (${edge.confidence}, ${edge.source})`
9002
+ );
9003
+ }
9004
+ }
9005
+ if (session.retries.length > 0) {
9006
+ console.log("");
9007
+ console.log("Retries:");
9008
+ for (const retry of session.retries) {
9009
+ const attempt = retry.attempt !== void 0 ? ` attempt=${retry.attempt}` : "";
9010
+ const of = retry.retryOf !== void 0 ? ` retryOf=${retry.retryOf}` : "";
9011
+ console.log(
9012
+ ` ${retry.runId}${attempt}${of} (${retry.confidence})`
9013
+ );
9014
+ }
9015
+ }
9016
+ if (options.criticalPath && session.criticalPath.length > 0) {
9017
+ console.log("");
9018
+ console.log("Critical path:");
9019
+ for (const step of session.criticalPath) {
9020
+ const duration = step.durationMs !== void 0 ? ` ${step.durationMs}ms` : "";
9021
+ console.log(
9022
+ ` ${step.runId}${step.name ? ` (${step.name})` : ""}${duration} [${step.confidence}]`
9023
+ );
9024
+ }
9025
+ }
9026
+ if (options.diagnostics && index.warnings.length > 0) {
9027
+ const scoped = index.warnings.filter(
9028
+ (warning) => warning.sessionId === session.sessionId || !warning.sessionId
9029
+ );
9030
+ if (scoped.length > 0) {
9031
+ console.log("");
9032
+ console.log("Diagnostics:");
9033
+ for (const warning of scoped) {
9034
+ const run = warning.runId ? ` run=${warning.runId}` : "";
9035
+ console.log(` [${warning.code}]${run} ${warning.message}`);
9036
+ }
9037
+ }
9038
+ }
9039
+ }
9040
+ async function sessionsCommand(options = {}) {
9041
+ try {
9042
+ const traceDir = resolveTraceDir({ dir: options.dir });
9043
+ const index = await loadSessionIndex(traceDir, options.correlateGroup);
9044
+ if (options.json) {
9045
+ console.log(
9046
+ JSON.stringify(
9047
+ {
9048
+ traceDir,
9049
+ sessions: index.sessions,
9050
+ unscopedRunIds: index.unscopedRunIds,
9051
+ warnings: index.warnings
9052
+ },
9053
+ null,
9054
+ 2
9055
+ )
9056
+ );
9057
+ return;
9058
+ }
9059
+ renderSessionsHuman(index, traceDir);
9060
+ } catch (e) {
9061
+ const msg = e instanceof Error ? e.message : String(e);
9062
+ console.error(`[AgentInspect] sessions failed: ${msg}`);
9063
+ process.exitCode = 1;
9064
+ }
9065
+ }
9066
+ async function sessionCommand(sessionId, options = {}) {
9067
+ const id = typeof sessionId === "string" && sessionId.trim() !== "" ? sessionId.trim() : "";
9068
+ if (id === "") {
9069
+ console.error("Session id is required");
9070
+ process.exitCode = 1;
9071
+ return;
9072
+ }
9073
+ try {
9074
+ const traceDir = resolveTraceDir({ dir: options.dir });
9075
+ const index = await loadSessionIndex(traceDir);
9076
+ const session = findSession(index, id);
9077
+ if (!session) {
9078
+ console.log(`Session not found: ${id}`);
9079
+ console.log(`Trace directory: ${traceDir}`);
9080
+ process.exitCode = 1;
9081
+ return;
9082
+ }
9083
+ if (options.json) {
9084
+ const timelines = {};
9085
+ if (options.timeline) {
9086
+ for (const runId of session.runIds) {
9087
+ const result = await readRunTraceEvents(runId, traceDir);
9088
+ if (result && result.events.length > 0) {
9089
+ timelines[runId] = buildRunTimeline(result.events);
9090
+ }
9091
+ }
9092
+ }
9093
+ console.log(
9094
+ JSON.stringify(
9095
+ {
9096
+ traceDir,
9097
+ session,
9098
+ warnings: index.warnings.filter(
9099
+ (warning) => warning.sessionId === id || warning.sessionId === void 0
9100
+ ),
9101
+ ...options.timeline ? { timelines } : {}
9102
+ },
9103
+ null,
9104
+ 2
9105
+ )
9106
+ );
9107
+ return;
9108
+ }
9109
+ renderSessionHuman(session, index, options);
9110
+ if (options.timeline) {
9111
+ for (const runId of session.runIds) {
9112
+ const result = await readRunTraceEvents(runId, traceDir);
9113
+ if (!result || result.events.length === 0) continue;
9114
+ const timeline = buildRunTimeline(result.events);
9115
+ console.log("");
9116
+ console.log(`Timeline: ${runId}`);
9117
+ console.log(renderTimeline(timeline));
9118
+ }
9119
+ }
9120
+ } catch (e) {
9121
+ const msg = e instanceof Error ? e.message : String(e);
9122
+ console.error(`[AgentInspect] session failed: ${msg}`);
9123
+ process.exitCode = 1;
9124
+ }
9125
+ }
9126
+
8454
9127
  // packages/cli/src/what.ts
8455
9128
  async function whatCommand(runId, options = {}) {
8456
9129
  const id = typeof runId === "string" && runId.trim() !== "" ? runId.trim() : "";
@@ -9597,7 +10270,7 @@ function diagnostic(code, message, ruleId) {
9597
10270
  ...ruleId ? { ruleId } : {}
9598
10271
  };
9599
10272
  }
9600
- function emptySummary() {
10273
+ function emptySummary2() {
9601
10274
  return {
9602
10275
  passed: 0,
9603
10276
  failed: 0,
@@ -9612,7 +10285,7 @@ function errorResult(input3, diagnostics, selectedRun) {
9612
10285
  format: input3.read.format,
9613
10286
  ...selectedRun ? { runId: selectedRun.runId } : {},
9614
10287
  summary: {
9615
- ...emptySummary(),
10288
+ ...emptySummary2(),
9616
10289
  errors: diagnostics.filter((item) => item.severity === "error").length
9617
10290
  },
9618
10291
  findings: [],
@@ -10518,7 +11191,7 @@ function createSafetyRawContentRule(options = {}) {
10518
11191
  }
10519
11192
  function createSafetySecretPatternRule(options = {}) {
10520
11193
  const patterns = options.patterns ?? DEFAULT_SECRET_PATTERNS;
10521
- const maxStringLength = options.maxStringLength ?? 4096;
11194
+ const maxStringLength2 = options.maxStringLength ?? 4096;
10522
11195
  return {
10523
11196
  id: "safety.secretPattern",
10524
11197
  category: "safety",
@@ -10528,7 +11201,7 @@ function createSafetySecretPatternRule(options = {}) {
10528
11201
  for (const event of context.events) {
10529
11202
  for (const entry of eventValueEntries(event, { includeSummaries: true, includeError: true })) {
10530
11203
  if (typeof entry.value !== "string") continue;
10531
- const sample = entry.value.slice(0, maxStringLength);
11204
+ const sample = entry.value.slice(0, maxStringLength2);
10532
11205
  for (const pattern of patterns) {
10533
11206
  pattern.pattern.lastIndex = 0;
10534
11207
  if (!pattern.pattern.test(sample)) continue;
@@ -10784,6 +11457,621 @@ function runTraceChecks(input3, options = {}) {
10784
11457
  diagnostics
10785
11458
  };
10786
11459
  }
11460
+ var ALL_RULES = [
11461
+ "circuit.same-tool-repetition",
11462
+ "circuit.same-args-repetition",
11463
+ "circuit.max-loop-iterations",
11464
+ "circuit.max-retries",
11465
+ "circuit.tool-timeout",
11466
+ "circuit.runaway-llm-loop",
11467
+ "circuit.excessive-branch-width"
11468
+ ];
11469
+ function closed(ruleId, message) {
11470
+ return { ruleId, status: "closed", severity: "info", message, evidence: [] };
11471
+ }
11472
+ function open2(ruleId, message, evidence, severity = "error") {
11473
+ return {
11474
+ ruleId,
11475
+ status: severity === "warning" ? "warn" : "open",
11476
+ severity,
11477
+ message,
11478
+ evidence
11479
+ };
11480
+ }
11481
+ function isToolEvent(event) {
11482
+ const name = event.name.toLowerCase();
11483
+ return event.kind === "tool" || name.startsWith("tool:") || name.startsWith("function:") || name.includes(".tool.") || name.startsWith("mcp:");
11484
+ }
11485
+ function isLlmEvent(event) {
11486
+ const name = event.name.toLowerCase();
11487
+ return event.kind === "llm" || name.startsWith("llm:") || name.includes(".llm.") || name.includes("generation");
11488
+ }
11489
+ function toolLabel(event) {
11490
+ const attrs = event.attributes ?? {};
11491
+ const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;
11492
+ if (typeof fromAttr === "string" && fromAttr.length > 0) return fromAttr;
11493
+ return event.name.replace(/^(tool:|function:|mcp:)/i, "");
11494
+ }
11495
+ function argsHash(toolName2, args) {
11496
+ return crypto.createHash("sha256").update(`${toolName2}:${JSON.stringify(args ?? null)}`).digest("hex").slice(0, 16);
11497
+ }
11498
+ function toolArgs(event) {
11499
+ const attrs = event.attributes ?? {};
11500
+ return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;
11501
+ }
11502
+ function durationMs(event) {
11503
+ if (typeof event.durationMs === "number") return event.durationMs;
11504
+ const attrs = event.attributes ?? {};
11505
+ const fromAttr = attrs.durationMs ?? attrs.duration;
11506
+ return typeof fromAttr === "number" ? fromAttr : void 0;
11507
+ }
11508
+ function attemptNumber(event) {
11509
+ const attrs = event.attributes ?? {};
11510
+ const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;
11511
+ return typeof value === "number" ? value : void 0;
11512
+ }
11513
+ function evaluateSameToolRepetition(events, maxRepeats) {
11514
+ const ruleId = "circuit.same-tool-repetition";
11515
+ const counts = /* @__PURE__ */ new Map();
11516
+ for (const event of events.filter(isToolEvent)) {
11517
+ const label = toolLabel(event);
11518
+ counts.set(label, (counts.get(label) ?? 0) + 1);
11519
+ }
11520
+ const evidence = [];
11521
+ for (const [toolName2, count] of counts) {
11522
+ if (count > maxRepeats) {
11523
+ evidence.push({ ruleId, toolName: toolName2, count, threshold: maxRepeats });
11524
+ }
11525
+ }
11526
+ if (evidence.length === 0) {
11527
+ return closed(ruleId, "Tool repetition within threshold.");
11528
+ }
11529
+ return open2(ruleId, "Same tool repeated beyond threshold.", evidence);
11530
+ }
11531
+ function evaluateSameArgsRepetition(events, maxRepeats) {
11532
+ const ruleId = "circuit.same-args-repetition";
11533
+ const counts = /* @__PURE__ */ new Map();
11534
+ for (const event of events.filter(isToolEvent)) {
11535
+ const label = toolLabel(event);
11536
+ const hash = argsHash(label, toolArgs(event));
11537
+ const key = `${label}:${hash}`;
11538
+ const current = counts.get(key) ?? { toolName: label, count: 0 };
11539
+ current.count += 1;
11540
+ counts.set(key, current);
11541
+ }
11542
+ const evidence = [];
11543
+ for (const entry of counts.values()) {
11544
+ if (entry.count > maxRepeats) {
11545
+ evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });
11546
+ }
11547
+ }
11548
+ if (evidence.length === 0) {
11549
+ return closed(ruleId, "Tool argument repetition within threshold.");
11550
+ }
11551
+ return open2(ruleId, "Same tool arguments repeated beyond threshold.", evidence);
11552
+ }
11553
+ function evaluateMaxLoopIterations(events, maxIterations) {
11554
+ const ruleId = "circuit.max-loop-iterations";
11555
+ const iterationEvents = events.filter((event) => {
11556
+ const attrs = event.attributes ?? {};
11557
+ return typeof attrs.iteration === "number" || event.name.toLowerCase().includes("loop");
11558
+ });
11559
+ const maxSeen = iterationEvents.reduce((max, event) => {
11560
+ const attrs = event.attributes ?? {};
11561
+ const iteration = typeof attrs.iteration === "number" ? attrs.iteration : max;
11562
+ return Math.max(max, iteration);
11563
+ }, iterationEvents.length);
11564
+ if (maxSeen <= maxIterations) {
11565
+ return closed(ruleId, "Loop iterations within threshold.");
11566
+ }
11567
+ return open2(ruleId, "Loop iterations exceeded threshold.", [
11568
+ { ruleId, count: maxSeen, threshold: maxIterations }
11569
+ ]);
11570
+ }
11571
+ function evaluateMaxRetries(events, maxRetries) {
11572
+ const ruleId = "circuit.max-retries";
11573
+ const attempts = events.map(attemptNumber).filter((value) => value !== void 0);
11574
+ const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;
11575
+ if (maxAttempt <= maxRetries) {
11576
+ return closed(ruleId, "Retry count within threshold.");
11577
+ }
11578
+ return open2(ruleId, "Retry count exceeded threshold.", [
11579
+ { ruleId, count: maxAttempt, threshold: maxRetries }
11580
+ ]);
11581
+ }
11582
+ function evaluateToolTimeout(events, maxDurationMs) {
11583
+ const ruleId = "circuit.tool-timeout";
11584
+ const evidence = [];
11585
+ for (const event of events.filter(isToolEvent)) {
11586
+ const duration = durationMs(event);
11587
+ if (duration !== void 0 && duration > maxDurationMs) {
11588
+ evidence.push({
11589
+ ruleId,
11590
+ toolName: toolLabel(event),
11591
+ count: duration,
11592
+ threshold: maxDurationMs,
11593
+ eventId: event.eventId
11594
+ });
11595
+ }
11596
+ }
11597
+ if (evidence.length === 0) {
11598
+ return closed(ruleId, "Tool durations within timeout.");
11599
+ }
11600
+ return open2(ruleId, "Tool call exceeded configured timeout.", evidence, "warning");
11601
+ }
11602
+ function evaluateRunawayLlmLoop(events, maxLlmCalls) {
11603
+ const ruleId = "circuit.runaway-llm-loop";
11604
+ const llmCount = events.filter(isLlmEvent).length;
11605
+ const hasTerminal = events.some((event) => {
11606
+ const status = (event.status ?? event.attributes?.status ?? "").toString().toLowerCase();
11607
+ return status === "ok" || status === "success" || status === "completed";
11608
+ });
11609
+ if (llmCount <= maxLlmCalls || hasTerminal) {
11610
+ return closed(ruleId, "LLM call count within threshold or run completed.");
11611
+ }
11612
+ return open2(ruleId, "Runaway LLM loop detected.", [
11613
+ { ruleId, count: llmCount, threshold: maxLlmCalls }
11614
+ ]);
11615
+ }
11616
+ function evaluateExcessiveBranchWidth(events, maxWidth) {
11617
+ const ruleId = "circuit.excessive-branch-width";
11618
+ const children = /* @__PURE__ */ new Map();
11619
+ for (const event of events) {
11620
+ const parentId = event.parentId;
11621
+ if (!parentId) continue;
11622
+ children.set(parentId, (children.get(parentId) ?? 0) + 1);
11623
+ }
11624
+ const evidence = [];
11625
+ for (const [parentId, count] of children) {
11626
+ if (count > maxWidth) {
11627
+ evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });
11628
+ }
11629
+ }
11630
+ if (evidence.length === 0) {
11631
+ return closed(ruleId, "Branch width within threshold.");
11632
+ }
11633
+ return open2(ruleId, "Excessive parallel branch width detected.", evidence, "warning");
11634
+ }
11635
+ function runRule(ruleId, events, options) {
11636
+ switch (ruleId) {
11637
+ case "circuit.same-tool-repetition":
11638
+ if (options.sameToolRepetition === void 0) return void 0;
11639
+ return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);
11640
+ case "circuit.same-args-repetition":
11641
+ if (options.sameArgsRepetition === void 0) return void 0;
11642
+ return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);
11643
+ case "circuit.max-loop-iterations":
11644
+ if (options.maxLoopIterations === void 0) return void 0;
11645
+ return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);
11646
+ case "circuit.max-retries":
11647
+ if (options.maxRetries === void 0) return void 0;
11648
+ return evaluateMaxRetries(events, options.maxRetries.maxRetries);
11649
+ case "circuit.tool-timeout":
11650
+ if (options.toolTimeout === void 0) return void 0;
11651
+ return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);
11652
+ case "circuit.runaway-llm-loop":
11653
+ if (options.runawayLlmLoop === void 0) return void 0;
11654
+ return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);
11655
+ case "circuit.excessive-branch-width":
11656
+ if (options.excessiveBranchWidth === void 0) return void 0;
11657
+ return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);
11658
+ default:
11659
+ return void 0;
11660
+ }
11661
+ }
11662
+ function runCircuits(events, options = {}) {
11663
+ const selected = options.rules ?? ALL_RULES;
11664
+ const results = [];
11665
+ for (const ruleId of selected) {
11666
+ const result = runRule(ruleId, events, options);
11667
+ if (result) results.push(result);
11668
+ }
11669
+ const ok = !results.some((result) => result.status === "open" && result.severity === "error");
11670
+ return { ok, results };
11671
+ }
11672
+
11673
+ // packages/guardrails/src/rules.ts
11674
+ var DEFAULT_INJECTION_PATTERNS = [
11675
+ "ignore previous instructions",
11676
+ "ignore all prior",
11677
+ "disregard your instructions",
11678
+ "system prompt",
11679
+ "you are now",
11680
+ "jailbreak"
11681
+ ];
11682
+ function pass(ruleId, message) {
11683
+ return { ruleId, status: "pass", severity: "info", message, evidence: [] };
11684
+ }
11685
+ function fail(ruleId, message, evidence, severity = "error") {
11686
+ return { ruleId, status: severity === "warning" ? "warn" : "fail", severity, message, evidence };
11687
+ }
11688
+ function boundedPreview(value, max = 80) {
11689
+ if (value.length <= max) return value;
11690
+ return `${value.slice(0, max - 3)}...`;
11691
+ }
11692
+ function evaluateBannedPhrase(text, options) {
11693
+ const ruleId = "guardrail.banned-phrase";
11694
+ const haystack = options.caseInsensitive !== false ? text.toLowerCase() : text;
11695
+ const evidence = [];
11696
+ for (const phrase of options.phrases) {
11697
+ const needle = options.caseInsensitive !== false ? phrase.toLowerCase() : phrase;
11698
+ if (needle.length > 0 && haystack.includes(needle)) {
11699
+ evidence.push({ ruleId, match: phrase, preview: boundedPreview(text) });
11700
+ }
11701
+ }
11702
+ if (evidence.length === 0) {
11703
+ return pass(ruleId, "No banned phrases matched.");
11704
+ }
11705
+ return fail(ruleId, `Matched ${evidence.length} banned phrase(s).`, evidence);
11706
+ }
11707
+ var SEVERITY_RANK2 = { info: 0, warning: 1, error: 2 };
11708
+ function evaluatePiiLeak(value, options = {}) {
11709
+ const ruleId = "guardrail.pii-leak";
11710
+ const minSeverity = options.minSeverity ?? "warning";
11711
+ const result = redact(value, { profile: options.profile ?? "share", collectFindings: true });
11712
+ const findings = result.findings.filter(
11713
+ (finding) => SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2[minSeverity]
11714
+ );
11715
+ if (findings.length === 0) {
11716
+ return pass(ruleId, "No PII-style redaction findings.");
11717
+ }
11718
+ const evidence = findings.map((finding) => ({
11719
+ ruleId,
11720
+ path: finding.path,
11721
+ detector: finding.detector,
11722
+ preview: finding.preview
11723
+ }));
11724
+ return fail(ruleId, `Detected ${findings.length} PII-style finding(s).`, evidence);
11725
+ }
11726
+ function measureDepth(value) {
11727
+ if (value === null || typeof value !== "object") return 0;
11728
+ if (Array.isArray(value)) {
11729
+ return 1 + Math.max(0, ...value.map((item) => measureDepth(item)));
11730
+ }
11731
+ const depths = Object.values(value).map((item) => measureDepth(item));
11732
+ return 1 + (depths.length === 0 ? 0 : Math.max(...depths));
11733
+ }
11734
+ function maxStringLength(value) {
11735
+ if (typeof value === "string") return value.length;
11736
+ if (value === null || typeof value !== "object") return 0;
11737
+ if (Array.isArray(value)) {
11738
+ return Math.max(0, ...value.map((item) => maxStringLength(item)));
11739
+ }
11740
+ return Math.max(0, ...Object.values(value).map((item) => maxStringLength(item)));
11741
+ }
11742
+ function evaluateUnsafeToolArgs(toolName2, toolArgs2, options = {}) {
11743
+ const ruleId = "guardrail.unsafe-tool-args";
11744
+ const blocked = new Set((options.blockedTools ?? []).map((name) => name.toLowerCase()));
11745
+ const evidence = [];
11746
+ if (blocked.has(toolName2.toLowerCase())) {
11747
+ evidence.push({ ruleId, preview: toolName2, match: toolName2 });
11748
+ }
11749
+ const maxDepth = options.maxDepth ?? 12;
11750
+ const depth = measureDepth(toolArgs2);
11751
+ if (depth > maxDepth) {
11752
+ evidence.push({ ruleId, path: "args", preview: `depth=${depth}` });
11753
+ }
11754
+ const maxLen = options.maxStringLength ?? 16384;
11755
+ const longest = maxStringLength(toolArgs2);
11756
+ if (longest > maxLen) {
11757
+ evidence.push({ ruleId, path: "args", preview: `maxStringLength=${longest}` });
11758
+ }
11759
+ if (evidence.length === 0) {
11760
+ return pass(ruleId, "Tool arguments within configured bounds.");
11761
+ }
11762
+ return fail(ruleId, "Unsafe or oversized tool arguments detected.", evidence);
11763
+ }
11764
+ function evaluatePromptInjection(text, options = {}) {
11765
+ const ruleId = "guardrail.prompt-injection";
11766
+ const patterns = options.patterns ?? DEFAULT_INJECTION_PATTERNS;
11767
+ const haystack = text.toLowerCase();
11768
+ const evidence = [];
11769
+ for (const pattern of patterns) {
11770
+ const needle = pattern.toLowerCase();
11771
+ if (needle.length > 0 && haystack.includes(needle)) {
11772
+ evidence.push({ ruleId, match: pattern, preview: boundedPreview(text) });
11773
+ }
11774
+ }
11775
+ if (evidence.length === 0) {
11776
+ return pass(ruleId, "No prompt-injection patterns matched.");
11777
+ }
11778
+ return fail(ruleId, `Matched ${evidence.length} injection pattern(s).`, evidence, "warning");
11779
+ }
11780
+ function validateSchemaField(value, field, path15, evidence) {
11781
+ const ruleId = "guardrail.structured-output";
11782
+ if (field.type) {
11783
+ const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
11784
+ if (actual !== field.type) {
11785
+ evidence.push({ ruleId, path: path15, preview: `expected ${field.type}, got ${actual}` });
11786
+ return;
11787
+ }
11788
+ }
11789
+ if (field.enum && !field.enum.some((item) => Object.is(item, value))) {
11790
+ evidence.push({ ruleId, path: path15, preview: "value not in enum" });
11791
+ }
11792
+ if (field.type === "object" && field.required && value && typeof value === "object" && !Array.isArray(value)) {
11793
+ const record = value;
11794
+ for (const key of field.required) {
11795
+ if (!(key in record)) {
11796
+ evidence.push({ ruleId, path: `${path15}.${key}`, preview: "missing required key" });
11797
+ }
11798
+ }
11799
+ }
11800
+ }
11801
+ function evaluateStructuredOutput(value, options) {
11802
+ const ruleId = "guardrail.structured-output";
11803
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11804
+ return fail(ruleId, "Structured output must be an object.", [
11805
+ { ruleId, preview: typeof value }
11806
+ ]);
11807
+ }
11808
+ const record = value;
11809
+ const evidence = [];
11810
+ for (const [key, field] of Object.entries(options.schema)) {
11811
+ validateSchemaField(record[key], field, key, evidence);
11812
+ }
11813
+ if (evidence.length === 0) {
11814
+ return pass(ruleId, "Structured output matches schema subset.");
11815
+ }
11816
+ return fail(ruleId, "Structured output schema violation.", evidence);
11817
+ }
11818
+ function evaluateOversizeOutput(value, options = {}) {
11819
+ const ruleId = "guardrail.oversize-output";
11820
+ const text = typeof value === "string" ? value : JSON.stringify(value);
11821
+ const maxLength = options.maxLength ?? options.maxSerializedLength ?? 32768;
11822
+ if (text.length <= maxLength) {
11823
+ return pass(ruleId, "Output within size limits.");
11824
+ }
11825
+ return fail(ruleId, `Output exceeds max length (${text.length} > ${maxLength}).`, [
11826
+ { ruleId, preview: `length=${text.length}` }
11827
+ ]);
11828
+ }
11829
+ function evaluateRequiredJsonShape(value, options) {
11830
+ const ruleId = "guardrail.required-json-shape";
11831
+ let parsed = value;
11832
+ if (typeof value === "string") {
11833
+ try {
11834
+ parsed = JSON.parse(value);
11835
+ } catch {
11836
+ return fail(ruleId, "Value is not valid JSON.", [{ ruleId, preview: boundedPreview(value) }]);
11837
+ }
11838
+ }
11839
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11840
+ return fail(ruleId, "JSON value must be an object.", [{ ruleId, preview: typeof parsed }]);
11841
+ }
11842
+ const record = parsed;
11843
+ const evidence = [];
11844
+ for (const key of options.requiredKeys) {
11845
+ if (!(key in record)) {
11846
+ evidence.push({ ruleId, path: key, preview: "missing required key" });
11847
+ }
11848
+ }
11849
+ if (evidence.length === 0) {
11850
+ return pass(ruleId, "Required JSON keys present.");
11851
+ }
11852
+ return fail(ruleId, "Missing required JSON keys.", evidence);
11853
+ }
11854
+
11855
+ // packages/guardrails/src/run.ts
11856
+ var ALL_RULES2 = [
11857
+ "guardrail.banned-phrase",
11858
+ "guardrail.pii-leak",
11859
+ "guardrail.unsafe-tool-args",
11860
+ "guardrail.prompt-injection",
11861
+ "guardrail.structured-output",
11862
+ "guardrail.oversize-output",
11863
+ "guardrail.required-json-shape"
11864
+ ];
11865
+ function isErrorFailure(result) {
11866
+ return result.status === "fail" && result.severity === "error";
11867
+ }
11868
+ function runRule2(ruleId, input3, options) {
11869
+ switch (ruleId) {
11870
+ case "guardrail.banned-phrase": {
11871
+ if (!options.bannedPhrase || input3.text === void 0) return void 0;
11872
+ return evaluateBannedPhrase(input3.text, options.bannedPhrase);
11873
+ }
11874
+ case "guardrail.pii-leak": {
11875
+ if (input3.value === void 0 && input3.text === void 0) return void 0;
11876
+ return evaluatePiiLeak(input3.value ?? input3.text, options.piiLeak);
11877
+ }
11878
+ case "guardrail.unsafe-tool-args": {
11879
+ if (!input3.toolName) return void 0;
11880
+ return evaluateUnsafeToolArgs(input3.toolName, input3.toolArgs ?? {}, options.unsafeToolArgs);
11881
+ }
11882
+ case "guardrail.prompt-injection": {
11883
+ if (input3.text === void 0) return void 0;
11884
+ return evaluatePromptInjection(input3.text, options.promptInjection);
11885
+ }
11886
+ case "guardrail.structured-output": {
11887
+ if (!options.structuredOutput || input3.value === void 0) return void 0;
11888
+ return evaluateStructuredOutput(input3.value, options.structuredOutput);
11889
+ }
11890
+ case "guardrail.oversize-output": {
11891
+ if (input3.value === void 0 && input3.text === void 0) return void 0;
11892
+ return evaluateOversizeOutput(input3.value ?? input3.text, options.oversizeOutput);
11893
+ }
11894
+ case "guardrail.required-json-shape": {
11895
+ if (!options.requiredJsonShape || input3.value === void 0 && input3.text === void 0) return void 0;
11896
+ return evaluateRequiredJsonShape(input3.value ?? input3.text, options.requiredJsonShape);
11897
+ }
11898
+ default:
11899
+ return void 0;
11900
+ }
11901
+ }
11902
+ function runGuardrails(input3, options = {}) {
11903
+ const selected = options.rules ?? ALL_RULES2;
11904
+ const results = [];
11905
+ for (const ruleId of selected) {
11906
+ const result = runRule2(ruleId, input3, options);
11907
+ if (result) results.push(result);
11908
+ }
11909
+ const ok = !results.some(isErrorFailure);
11910
+ return { ok, results };
11911
+ }
11912
+
11913
+ // packages/cli/src/safety-extensions.ts
11914
+ var GUARDRAIL_ALIASES = {
11915
+ "banned-phrase": "guardrail.banned-phrase",
11916
+ "pii-leak": "guardrail.pii-leak",
11917
+ "unsafe-tool-args": "guardrail.unsafe-tool-args",
11918
+ "prompt-injection": "guardrail.prompt-injection",
11919
+ "structured-output": "guardrail.structured-output",
11920
+ "oversize-output": "guardrail.oversize-output",
11921
+ "required-json-shape": "guardrail.required-json-shape"
11922
+ };
11923
+ var CIRCUIT_ALIASES = {
11924
+ "same-tool-repetition": "circuit.same-tool-repetition",
11925
+ "same-args-repetition": "circuit.same-args-repetition",
11926
+ "max-loop-iterations": "circuit.max-loop-iterations",
11927
+ "max-retries": "circuit.max-retries",
11928
+ "tool-timeout": "circuit.tool-timeout",
11929
+ "runaway-llm-loop": "circuit.runaway-llm-loop",
11930
+ "excessive-branch-width": "circuit.excessive-branch-width"
11931
+ };
11932
+ function parseGuardrailRules(values) {
11933
+ if (!values?.length) return void 0;
11934
+ return values.map((value) => {
11935
+ const rule = GUARDRAIL_ALIASES[value] ?? value;
11936
+ return rule;
11937
+ });
11938
+ }
11939
+ function parseCircuitRules(values) {
11940
+ if (!values?.length) return void 0;
11941
+ return values.map((value) => {
11942
+ const rule = CIRCUIT_ALIASES[value] ?? value;
11943
+ return rule;
11944
+ });
11945
+ }
11946
+ function toSeverity(severity) {
11947
+ return severity;
11948
+ }
11949
+ function guardrailFinding(result) {
11950
+ return {
11951
+ ruleId: result.ruleId,
11952
+ severity: toSeverity(result.severity),
11953
+ status: result.status === "pass" ? "pass" : result.status === "warn" ? "warning" : "fail",
11954
+ message: result.message,
11955
+ evidence: result.evidence.map(
11956
+ (item) => ({
11957
+ path: item.preview ? `${item.path ?? "value"} (${item.preview})` : item.path
11958
+ })
11959
+ )
11960
+ };
11961
+ }
11962
+ function circuitFinding(result) {
11963
+ return {
11964
+ ruleId: result.ruleId,
11965
+ severity: toSeverity(result.severity),
11966
+ status: result.status === "closed" ? "pass" : result.status === "warn" ? "warning" : "fail",
11967
+ message: result.message,
11968
+ evidence: result.evidence.map(
11969
+ (item) => ({
11970
+ runId: item.runId,
11971
+ eventId: item.eventId,
11972
+ path: item.path,
11973
+ name: item.toolName
11974
+ })
11975
+ ),
11976
+ actual: result.evidence[0]?.count,
11977
+ expected: result.evidence[0]?.threshold
11978
+ };
11979
+ }
11980
+ function eventToCircuit(event) {
11981
+ return {
11982
+ eventId: event.eventId,
11983
+ runId: event.runId,
11984
+ name: event.name,
11985
+ kind: event.kind,
11986
+ parentId: event.parentId,
11987
+ startedAt: event.startedAt,
11988
+ endedAt: event.endedAt,
11989
+ durationMs: event.durationMs,
11990
+ attributes: event.attributes,
11991
+ status: event.status
11992
+ };
11993
+ }
11994
+ function collectGuardrailInputs(read) {
11995
+ const inputs = [];
11996
+ for (const event of read.events) {
11997
+ const attrs = event.attributes ?? {};
11998
+ for (const key of ["output", "answer", "text", "content", "message"]) {
11999
+ const value = attrs[key];
12000
+ if (typeof value === "string") inputs.push({ text: value });
12001
+ else if (value !== void 0) inputs.push({ value });
12002
+ }
12003
+ if (event.kind === "TOOL" || event.name.startsWith("tool:")) {
12004
+ inputs.push({
12005
+ toolName: String(attrs.toolName ?? attrs.tool ?? event.name),
12006
+ toolArgs: attrs.arguments ?? attrs.args ?? attrs.input
12007
+ });
12008
+ }
12009
+ }
12010
+ return inputs;
12011
+ }
12012
+ var DEFAULT_GUARDRAIL_OPTIONS = {
12013
+ bannedPhrase: { phrases: ["delete all data", "ignore all instructions"] },
12014
+ promptInjection: {},
12015
+ piiLeak: { profile: "share" }
12016
+ };
12017
+ var DEFAULT_CIRCUIT_OPTIONS = {
12018
+ sameToolRepetition: { maxRepeats: 3 },
12019
+ sameArgsRepetition: { maxRepeats: 2 },
12020
+ maxLoopIterations: { maxIterations: 20 },
12021
+ maxRetries: { maxRetries: 3 },
12022
+ toolTimeout: { maxDurationMs: 6e4 },
12023
+ runawayLlmLoop: { maxLlmCalls: 12 },
12024
+ excessiveBranchWidth: { maxWidth: 8 }
12025
+ };
12026
+ function mergeSafetyExtensions(result, read, options) {
12027
+ const findings = [...result.findings];
12028
+ let failed = result.summary.failed;
12029
+ let warnings = result.summary.warnings;
12030
+ let passed = result.summary.passed;
12031
+ const guardrailRules = parseGuardrailRules(options.guardrails);
12032
+ if (guardrailRules) {
12033
+ for (const input3 of collectGuardrailInputs(read)) {
12034
+ const run = runGuardrails(input3, {
12035
+ ...DEFAULT_GUARDRAIL_OPTIONS,
12036
+ rules: guardrailRules
12037
+ });
12038
+ for (const item of run.results) {
12039
+ const finding = guardrailFinding(item);
12040
+ findings.push(finding);
12041
+ if (finding.status === "fail") failed += 1;
12042
+ else if (finding.status === "warning") warnings += 1;
12043
+ else passed += 1;
12044
+ }
12045
+ }
12046
+ }
12047
+ const circuitRules = parseCircuitRules(options.circuits);
12048
+ if (circuitRules) {
12049
+ const circuitRun = runCircuits(
12050
+ read.events.map(eventToCircuit),
12051
+ { ...DEFAULT_CIRCUIT_OPTIONS, rules: circuitRules }
12052
+ );
12053
+ for (const item of circuitRun.results) {
12054
+ const finding = circuitFinding(item);
12055
+ findings.push(finding);
12056
+ if (finding.status === "fail") failed += 1;
12057
+ else if (finding.status === "warning") warnings += 1;
12058
+ else passed += 1;
12059
+ }
12060
+ }
12061
+ const ok = failed === 0 && result.summary.errors === 0;
12062
+ return {
12063
+ ...result,
12064
+ ok,
12065
+ status: failed > 0 ? "fail" : result.status,
12066
+ summary: {
12067
+ passed,
12068
+ failed,
12069
+ warnings,
12070
+ errors: result.summary.errors
12071
+ },
12072
+ findings
12073
+ };
12074
+ }
10787
12075
 
10788
12076
  // packages/cli/src/check.ts
10789
12077
  var DEFAULT_SELECT = ["run.status"];
@@ -10953,6 +12241,13 @@ function printJson(result) {
10953
12241
  console.log(JSON.stringify(stable2(result), null, 2));
10954
12242
  }
10955
12243
  function printHuman(result) {
12244
+ const scoped = result;
12245
+ if (scoped.scopeLabel) {
12246
+ console.log(`Scope: ${scoped.scopeKind} ${scoped.scopeLabel}`);
12247
+ if (scoped.runIds?.length) {
12248
+ console.log(`Runs: ${scoped.runIds.join(", ")}`);
12249
+ }
12250
+ }
10956
12251
  console.log(`Check status: ${result.status}`);
10957
12252
  console.log(`Format: ${result.format}`);
10958
12253
  if (result.runId !== void 0) console.log(`Run: ${result.runId}`);
@@ -10964,7 +12259,9 @@ function printHuman(result) {
10964
12259
  }
10965
12260
  for (const finding of result.findings) {
10966
12261
  const path15 = finding.evidence[0]?.path;
10967
- console.log(`- ${finding.ruleId}: ${finding.message}${path15 ? ` (${path15})` : ""}`);
12262
+ const run = finding.evidence[0]?.runId;
12263
+ const runPrefix = run ? `[${run}] ` : "";
12264
+ console.log(`- ${runPrefix}${finding.ruleId}: ${finding.message}${path15 ? ` (${path15})` : ""}`);
10968
12265
  }
10969
12266
  }
10970
12267
  function readErrorResult(error) {
@@ -10980,24 +12277,84 @@ function readErrorResult(error) {
10980
12277
  async function checkCommand(target, options = {}, stdin = process.stdin) {
10981
12278
  let result;
10982
12279
  let phase = "config";
12280
+ const sessionId = options.session?.trim();
12281
+ const groupId = options.group?.trim();
12282
+ const useSessionScope = Boolean(sessionId || groupId);
10983
12283
  try {
10984
12284
  const config = await loadConfig(options.config);
10985
12285
  const built = buildRules(config, options);
10986
12286
  if (built.diagnostics.some((item) => item.severity === "error")) {
10987
12287
  result = errorResult2("AI_CHECK_INVALID_CONFIG", "Invalid check configuration.");
10988
12288
  result.diagnostics = [...built.diagnostics];
12289
+ } else if (useSessionScope) {
12290
+ phase = "read";
12291
+ const traceDir = resolveTraceDir({ dir: options.dir });
12292
+ const td = new TraceDirectory({ dir: traceDir });
12293
+ const files = await td.list();
12294
+ const metas = await loadTraceMetadataList(
12295
+ traceDir,
12296
+ files,
12297
+ (fileName) => td.getPath(fileName)
12298
+ );
12299
+ const records = await loadSessionRunRecords(metas);
12300
+ const scoped = filterMetasBySessionScope(metas, records, {
12301
+ ...sessionId ? { sessionId } : {},
12302
+ ...groupId ? { groupId } : {},
12303
+ correlateByGroupId: options.correlateGroup === true
12304
+ });
12305
+ const perRun = [];
12306
+ for (const meta of scoped.metas) {
12307
+ const read = await openTrace(
12308
+ { type: "file", path: meta.filePath },
12309
+ {
12310
+ ...options.format !== void 0 ? { format: options.format } : {}
12311
+ }
12312
+ );
12313
+ perRun.push(
12314
+ mergeSafetyExtensions(
12315
+ runTraceChecks(
12316
+ { read },
12317
+ {
12318
+ rules: built.rules,
12319
+ select: built.select,
12320
+ runId: meta.runId
12321
+ }
12322
+ ),
12323
+ read,
12324
+ {
12325
+ ...options.guardrails ? { guardrails: options.guardrails } : {},
12326
+ ...options.circuit ? { circuits: options.circuit } : {}
12327
+ }
12328
+ )
12329
+ );
12330
+ }
12331
+ result = aggregateSessionCheckResults(perRun, {
12332
+ scopeKind: scoped.scopeKind,
12333
+ scopeLabel: scoped.scopeLabel,
12334
+ runIds: scoped.runIds,
12335
+ sessionWarnings: scoped.warnings,
12336
+ notFound: scoped.notFound,
12337
+ empty: scoped.metas.length === 0
12338
+ });
10989
12339
  } else {
10990
12340
  phase = "read";
10991
12341
  const input3 = await inputFromTarget(target, options, stdin);
10992
12342
  const read = await openTrace(input3, {
10993
12343
  ...options.format !== void 0 ? { format: options.format } : {}
10994
12344
  });
10995
- result = runTraceChecks(
10996
- { read },
12345
+ result = mergeSafetyExtensions(
12346
+ runTraceChecks(
12347
+ { read },
12348
+ {
12349
+ rules: built.rules,
12350
+ select: built.select,
12351
+ ...options.run !== void 0 ? { runId: options.run } : {}
12352
+ }
12353
+ ),
12354
+ read,
10997
12355
  {
10998
- rules: built.rules,
10999
- select: built.select,
11000
- ...options.run !== void 0 ? { runId: options.run } : {}
12356
+ ...options.guardrails ? { guardrails: options.guardrails } : {},
12357
+ ...options.circuit ? { circuits: options.circuit } : {}
11001
12358
  }
11002
12359
  );
11003
12360
  }
@@ -11201,7 +12558,7 @@ function evidenceForEvent(event, path15) {
11201
12558
  }
11202
12559
  ];
11203
12560
  }
11204
- function fail(ruleId, message, evidence, expected, actual) {
12561
+ function fail2(ruleId, message, evidence, expected, actual) {
11205
12562
  return {
11206
12563
  ruleId,
11207
12564
  status: "fail",
@@ -11397,7 +12754,7 @@ var checks = {
11397
12754
  "eval.requireSuccess",
11398
12755
  "run",
11399
12756
  (context) => context.run.status === "ok" ? [] : [
11400
- fail(
12757
+ fail2(
11401
12758
  "eval.requireSuccess",
11402
12759
  "Run did not complete successfully.",
11403
12760
  evidenceForRun(context.run, "status"),
@@ -11412,7 +12769,7 @@ var checks = {
11412
12769
  return createRule("eval.requiredTools", "tool", (context) => {
11413
12770
  const tools = new Set(nodeNames(context.nodes, "TOOL"));
11414
12771
  return expected.filter((name) => !tools.has(name)).map(
11415
- (name) => fail(
12772
+ (name) => fail2(
11416
12773
  "eval.requiredTools",
11417
12774
  `Required tool ${name} did not appear.`,
11418
12775
  evidenceForRun(context.run, "children"),
@@ -11428,7 +12785,7 @@ var checks = {
11428
12785
  "eval.forbiddenTools",
11429
12786
  "tool",
11430
12787
  (context) => context.nodes.filter((node) => node.event.kind === "TOOL" && blocked.includes(node.event.name)).map(
11431
- (node) => fail(
12788
+ (node) => fail2(
11432
12789
  "eval.forbiddenTools",
11433
12790
  `Forbidden tool ${node.event.name} appeared.`,
11434
12791
  evidenceForEvent(node.event, "name"),
@@ -11443,7 +12800,7 @@ var checks = {
11443
12800
  "eval.maxDurationMs",
11444
12801
  "run",
11445
12802
  (context) => context.run.durationMs !== void 0 && context.run.durationMs > maxDurationMs ? [
11446
- fail(
12803
+ fail2(
11447
12804
  "eval.maxDurationMs",
11448
12805
  `Run duration exceeded ${maxDurationMs}ms.`,
11449
12806
  evidenceForRun(context.run, "durationMs"),
@@ -11460,7 +12817,7 @@ var checks = {
11460
12817
  void 0
11461
12818
  );
11462
12819
  return deepest !== void 0 && deepest.depth > maxDepth ? [
11463
- fail(
12820
+ fail2(
11464
12821
  "eval.maxDepth",
11465
12822
  `Run tree depth exceeded ${maxDepth}.`,
11466
12823
  evidenceForEvent(deepest.event, "depth"),
@@ -11477,7 +12834,7 @@ var checks = {
11477
12834
  (context) => context.nodes.flatMap((node) => {
11478
12835
  const retries = numericAttribute(node, ["retryCount", "retries", "attempt"]);
11479
12836
  return retries !== void 0 && retries > maxRetries ? [
11480
- fail(
12837
+ fail2(
11481
12838
  "eval.maxRetries",
11482
12839
  `Retry count exceeded ${maxRetries}.`,
11483
12840
  evidenceForEvent(node.event, "attributes.retryCount"),
@@ -11492,7 +12849,7 @@ var checks = {
11492
12849
  return createRule("eval.maxTotalTokens", "llm", (context) => {
11493
12850
  const total = totalTokenCount(context.events);
11494
12851
  return total > maxTotalTokens ? [
11495
- fail(
12852
+ fail2(
11496
12853
  "eval.maxTotalTokens",
11497
12854
  `Total token usage exceeded ${maxTotalTokens}.`,
11498
12855
  evidenceForRun(context.run, "tokenUsage.total"),
@@ -11507,7 +12864,7 @@ var checks = {
11507
12864
  "eval.noFailedSteps",
11508
12865
  "run",
11509
12866
  (context) => context.nodes.filter((node) => node.event.status === "error" || node.event.kind === "ERROR").map(
11510
- (node) => fail(
12867
+ (node) => fail2(
11511
12868
  "eval.noFailedSteps",
11512
12869
  "Run contains a failed step or error node.",
11513
12870
  evidenceForEvent(node.event, "status"),
@@ -11525,7 +12882,7 @@ var checks = {
11525
12882
  (node, index) => index < firstLlmIndex && node.event.kind === "RETRIEVER"
11526
12883
  );
11527
12884
  return retrievalIndex === -1 ? [
11528
- fail(
12885
+ fail2(
11529
12886
  "eval.requiredRetrievalBeforeGeneration",
11530
12887
  "No retrieval step appeared before the first LLM generation.",
11531
12888
  evidenceForEvent(context.nodes[firstLlmIndex].event, "kind"),
@@ -11541,7 +12898,7 @@ var checks = {
11541
12898
  const decisions = context.nodes.filter((node) => node.event.kind === "DECISION");
11542
12899
  if (decisions.length === 0) {
11543
12900
  return [
11544
- fail(
12901
+ fail2(
11545
12902
  "eval.requiredDecisionMetadata",
11546
12903
  "No decision node is available for required metadata.",
11547
12904
  evidenceForRun(context.run, "children"),
@@ -11552,7 +12909,7 @@ var checks = {
11552
12909
  }
11553
12910
  return decisions.flatMap(
11554
12911
  (node) => required.filter((key) => !hasAttribute(node, key)).map(
11555
- (key) => fail(
12912
+ (key) => fail2(
11556
12913
  "eval.requiredDecisionMetadata",
11557
12914
  `Decision metadata ${key} is missing.`,
11558
12915
  evidenceForEvent(node.event, `attributes.${key}`),
@@ -11573,7 +12930,7 @@ var checks = {
11573
12930
  const contexts = collectTextFields(context.nodes, contextKeys, ["RETRIEVER", "TOOL"]);
11574
12931
  if (answers.length === 0 || contexts.length === 0) {
11575
12932
  return [
11576
- fail(
12933
+ fail2(
11577
12934
  "eval.contextOverlap",
11578
12935
  "Answer and context text are required for overlap evaluation.",
11579
12936
  firstEvidence(answers.length > 0 ? answers : contexts, context.run, "children"),
@@ -11587,7 +12944,7 @@ var checks = {
11587
12944
  const sharedTerms = [...answerTerms].filter((term) => contextTerms.has(term)).length;
11588
12945
  const overlap = answerTerms.size === 0 ? 0 : sharedTerms / answerTerms.size;
11589
12946
  return sharedTerms < minSharedTerms || overlap < minOverlap ? [
11590
- fail(
12947
+ fail2(
11591
12948
  "eval.contextOverlap",
11592
12949
  "Answer text did not sufficiently overlap retrieved context.",
11593
12950
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -11615,7 +12972,7 @@ var checks = {
11615
12972
  const quotes = quotedSnippets(answerText, minQuoteLength);
11616
12973
  if (quotes.length === 0) {
11617
12974
  return requireQuote ? [
11618
- fail(
12975
+ fail2(
11619
12976
  "eval.quoteOverlap",
11620
12977
  "Answer did not contain a quote for overlap evaluation.",
11621
12978
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -11626,7 +12983,7 @@ var checks = {
11626
12983
  }
11627
12984
  const missing = quotes.filter((quote) => !contextText.includes(quote.toLowerCase()));
11628
12985
  return missing.length > 0 ? [
11629
- fail(
12986
+ fail2(
11630
12987
  "eval.quoteOverlap",
11631
12988
  "Quoted answer text did not appear in retrieved context.",
11632
12989
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -11644,7 +13001,7 @@ var checks = {
11644
13001
  const citations = collectTextFields(context.nodes, citationKeys);
11645
13002
  const count = citationCount(answers.map((field) => field.text).join(" "), citations);
11646
13003
  return count === 0 ? [
11647
- fail(
13004
+ fail2(
11648
13005
  "eval.citationPresence",
11649
13006
  "Answer did not include citations or source references.",
11650
13007
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -11662,7 +13019,7 @@ var checks = {
11662
13019
  const availableSet = new Set(available);
11663
13020
  const missing = expected.filter((id) => !availableSet.has(id));
11664
13021
  return missing.length > 0 ? [
11665
- fail(
13022
+ fail2(
11666
13023
  "eval.requiredSourceIds",
11667
13024
  "Required source IDs were not present in trace context or citations.",
11668
13025
  evidenceForRun(context.run, "children"),
@@ -11682,7 +13039,7 @@ var checks = {
11682
13039
  const tooShort = options.minCharacters !== void 0 && characters < options.minCharacters || options.minWords !== void 0 && words < options.minWords;
11683
13040
  const tooLong = options.maxCharacters !== void 0 && characters > options.maxCharacters || options.maxWords !== void 0 && words > options.maxWords;
11684
13041
  return answer.length === 0 || tooShort || tooLong ? [
11685
- fail(
13042
+ fail2(
11686
13043
  "eval.answerLengthBounds",
11687
13044
  "Answer length fell outside required bounds.",
11688
13045
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -11705,7 +13062,7 @@ var checks = {
11705
13062
  const answer = answers.map((field) => field.text).join(" ").toLowerCase();
11706
13063
  const matches = banned.filter((phrase) => answer.includes(phrase));
11707
13064
  return matches.length > 0 ? [
11708
- fail(
13065
+ fail2(
11709
13066
  "eval.bannedUnsupportedPhrases",
11710
13067
  "Answer contained banned unsupported-answer phrasing.",
11711
13068
  firstEvidence(answers, context.run, "attributes.answer"),
@@ -12083,7 +13440,7 @@ function invalidArgumentResult(command, error) {
12083
13440
  });
12084
13441
  }
12085
13442
  function buildSafetyRules(options) {
12086
- const maxStringLength = parseLimit3(options.maxStringLength, "--max-string-length") ?? DEFAULT_MAX_STRING_LENGTH;
13443
+ const maxStringLength2 = parseLimit3(options.maxStringLength, "--max-string-length") ?? DEFAULT_MAX_STRING_LENGTH;
12087
13444
  const maxArrayLength = parseLimit3(options.maxArrayLength, "--max-array-length") ?? DEFAULT_MAX_ARRAY_LENGTH;
12088
13445
  const maxObjectKeys = parseLimit3(options.maxObjectKeys, "--max-object-keys") ?? DEFAULT_MAX_OBJECT_KEYS;
12089
13446
  const maxSerializedBytes = parseLimit3(options.maxSerializedBytes, "--max-serialized-bytes") ?? DEFAULT_MAX_SERIALIZED_BYTES;
@@ -12092,7 +13449,7 @@ function buildSafetyRules(options) {
12092
13449
  createSafetyRedactionRule(),
12093
13450
  createSafetySecretPatternRule(),
12094
13451
  createSafetyOversizedAttributeRule({
12095
- maxStringLength,
13452
+ maxStringLength: maxStringLength2,
12096
13453
  maxArrayLength,
12097
13454
  maxObjectKeys,
12098
13455
  maxSerializedBytes
@@ -12973,7 +14330,18 @@ function createCliProgram() {
12973
14330
  ]).option("--allowed-model <model>", "allow an LLM model (repeatable)", (value, previous = []) => [
12974
14331
  ...previous,
12975
14332
  value
12976
- ]).option("--max-total-tokens <number>", "add llm.usage with a max total-token budget").action((target, opts) => {
14333
+ ]).option("--max-total-tokens <number>", "add llm.usage with a max total-token budget").option("--session <id>", "check all runs in a workflow session (requires --dir)").option("--group <id>", "check all runs sharing a groupId (requires --dir)").option(
14334
+ "--correlate-group",
14335
+ "when using --session, also match synthetic group: session keys"
14336
+ ).option(
14337
+ "--guardrails <rule>",
14338
+ "run optional guardrail rules (repeatable): banned-phrase, pii-leak, prompt-injection, ...",
14339
+ (value, previous = []) => [...previous, value]
14340
+ ).option(
14341
+ "--circuit <rule>",
14342
+ "run optional circuit rules (repeatable): same-tool-repetition, max-retries, ...",
14343
+ (value, previous = []) => [...previous, value]
14344
+ ).action((target, opts) => {
12977
14345
  runCommand(() => checkCommand(target, opts));
12978
14346
  });
12979
14347
  program.command("eval").description("Run deterministic local evals against a trace").argument("<trace-path-or-run-id>", "trace file, directory, stdin -, or run id").option("--dir <path>", "trace directory for run-id lookup").addOption(
@@ -13077,9 +14445,21 @@ function createCliProgram() {
13077
14445
  ).option("--kind <kind>", "filter by step kind/type (llm, tool, logic, \u2026)").option("--type <type>", "alias for --kind on manual trace step type").option("--name <query>", "substring match on run or step name").option("--tool <query>", "substring match on tool step name or metadata.toolName").option(
13078
14446
  "--duration <expr>",
13079
14447
  "duration filter on run or step (e.g. >5s, >=500ms)"
13080
- ).option("--limit <number>", "max results (default 50)").option("--json", "print results as JSON").action((opts) => {
14448
+ ).option("--limit <number>", "max results (default 50)").option("--session <id>", "limit search to one workflow session").option(
14449
+ "--correlate-group",
14450
+ "when using --session, also match synthetic group: keys"
14451
+ ).option("--json", "print results as JSON").action((opts) => {
13081
14452
  runCommand(() => searchCommand(opts));
13082
14453
  });
14454
+ program.command("sessions").description("List workflow sessions grouped from local trace metadata (read-only)").option("--dir <path>", "trace directory").option(
14455
+ "--correlate-group",
14456
+ "treat shared groupId as a synthetic session when sessionId is absent"
14457
+ ).option("--json", "print sessions index as JSON").action((opts) => {
14458
+ runCommand(() => sessionsCommand(opts));
14459
+ });
14460
+ program.command("session").description("Inspect one workflow session: runs, handoffs, retries (read-only)").argument("<session-id>", "session id (from sessions output)").option("--dir <path>", "trace directory").option("--timeline", "include per-run timelines").option("--critical-path", "include critical path section").option("--diagnostics", "include ambiguity warnings").option("--json", "print session view as JSON").action((sessionId, opts) => {
14461
+ runCommand(() => sessionCommand(sessionId, opts));
14462
+ });
13083
14463
  program.command("what").description("Concise human-readable summary of a single run (local JSONL)").argument("<run-id>", "run id (e.g. from list output)").option("--dir <path>", "trace directory").option("--json", "print structured summary as JSON").option("--no-correlation", "omit correlation metadata from human output").action((runId, opts) => {
13084
14464
  runCommand(() => whatCommand(runId, opts));
13085
14465
  });