agent-inspect 6.31.9 → 6.31.11

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 (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/NESTJS.md +56 -18
  4. package/package.json +1 -1
  5. package/packages/cli/dist/{chunk-SWUZRRDT.mjs → chunk-MQYHJL3W.mjs} +2216 -1938
  6. package/packages/cli/dist/chunk-MQYHJL3W.mjs.map +1 -0
  7. package/packages/cli/dist/index.cjs +975 -671
  8. package/packages/cli/dist/index.cjs.map +1 -1
  9. package/packages/cli/dist/index.mjs +32 -9
  10. package/packages/cli/dist/index.mjs.map +1 -1
  11. package/packages/cli/dist/{src-XFIPYODW.mjs → src-76R6E2LQ.mjs} +3 -3
  12. package/packages/cli/dist/{src-XFIPYODW.mjs.map → src-76R6E2LQ.mjs.map} +1 -1
  13. package/packages/core/dist/advanced.cjs +2079 -1879
  14. package/packages/core/dist/advanced.cjs.map +1 -1
  15. package/packages/core/dist/advanced.d.cts +56 -4
  16. package/packages/core/dist/advanced.d.ts +56 -4
  17. package/packages/core/dist/advanced.mjs +188 -23
  18. package/packages/core/dist/advanced.mjs.map +1 -1
  19. package/packages/core/dist/checks.cjs +126 -9
  20. package/packages/core/dist/checks.cjs.map +1 -1
  21. package/packages/core/dist/checks.d.cts +3 -3
  22. package/packages/core/dist/checks.d.ts +3 -3
  23. package/packages/core/dist/checks.mjs +3 -2
  24. package/packages/core/dist/{chunk-L5WLVBW4.mjs → chunk-AHUZNHFI.mjs} +4 -55
  25. package/packages/core/dist/chunk-AHUZNHFI.mjs.map +1 -0
  26. package/packages/core/dist/{chunk-2CCRUI42.mjs → chunk-CCYKNAOD.mjs} +119 -12
  27. package/packages/core/dist/chunk-CCYKNAOD.mjs.map +1 -0
  28. package/packages/core/dist/{chunk-7YYS742B.mjs → chunk-M3RJD3BP.mjs} +5 -5
  29. package/packages/core/dist/{chunk-7YYS742B.mjs.map → chunk-M3RJD3BP.mjs.map} +1 -1
  30. package/packages/core/dist/{chunk-2IRRUNIN.mjs → chunk-OAUNS5CS.mjs} +3 -3
  31. package/packages/core/dist/{chunk-2IRRUNIN.mjs.map → chunk-OAUNS5CS.mjs.map} +1 -1
  32. package/packages/core/dist/{chunk-IJJSPJXV.mjs → chunk-QOC3PJO3.mjs} +4 -4
  33. package/packages/core/dist/{chunk-IJJSPJXV.mjs.map → chunk-QOC3PJO3.mjs.map} +1 -1
  34. package/packages/core/dist/chunk-S6LQBLX5.mjs +67 -0
  35. package/packages/core/dist/chunk-S6LQBLX5.mjs.map +1 -0
  36. package/packages/core/dist/{chunk-3SPBELJB.mjs → chunk-SSG3ICI4.mjs} +16 -2
  37. package/packages/core/dist/chunk-SSG3ICI4.mjs.map +1 -0
  38. package/packages/core/dist/exporters.cjs +3 -3
  39. package/packages/core/dist/exporters.cjs.map +1 -1
  40. package/packages/core/dist/exporters.mjs +1 -1
  41. package/packages/core/dist/{index-EQihlEei.d.cts → index-DACU4Dy1.d.cts} +12 -0
  42. package/packages/core/dist/{index-CgOAGuM5.d.ts → index-b4tIGrzq.d.ts} +12 -0
  43. package/packages/core/dist/index.cjs +77 -0
  44. package/packages/core/dist/index.cjs.map +1 -1
  45. package/packages/core/dist/index.mjs +3 -2
  46. package/packages/core/dist/index.mjs.map +1 -1
  47. package/packages/core/dist/persisted.cjs +11 -0
  48. package/packages/core/dist/persisted.cjs.map +1 -1
  49. package/packages/core/dist/persisted.mjs +4 -3
  50. package/packages/core/dist/persisted.mjs.map +1 -1
  51. package/packages/core/dist/{programmatic-gZTWLObB.d.cts → programmatic-BLlC7Ytq.d.cts} +1 -1
  52. package/packages/core/dist/{programmatic-gZTWLObB.d.ts → programmatic-BLlC7Ytq.d.ts} +1 -1
  53. package/packages/core/dist/readers.cjs +11 -0
  54. package/packages/core/dist/readers.cjs.map +1 -1
  55. package/packages/core/dist/readers.d.cts +1 -1
  56. package/packages/core/dist/readers.d.ts +1 -1
  57. package/packages/core/dist/readers.mjs +4 -3
  58. package/packages/cli/dist/chunk-SWUZRRDT.mjs.map +0 -1
  59. package/packages/core/dist/chunk-2CCRUI42.mjs.map +0 -1
  60. package/packages/core/dist/chunk-3SPBELJB.mjs.map +0 -1
  61. package/packages/core/dist/chunk-L5WLVBW4.mjs.map +0 -1
@@ -2483,7 +2483,7 @@ function stepMixLine(summary) {
2483
2483
  if (summary.logicSteps > 0) parts.push(`${summary.logicSteps} logic`);
2484
2484
  return parts.length > 0 ? parts.join(", ") : "none";
2485
2485
  }
2486
- function outcomeLine(summary) {
2486
+ function executionLine(summary) {
2487
2487
  if (summary.status === "success") {
2488
2488
  return summary.errorSteps > 0 ? "Completed with step errors recorded." : "Completed successfully.";
2489
2489
  }
@@ -2501,7 +2501,7 @@ function outcomeLine(summary) {
2501
2501
  if (summary.status === "running") {
2502
2502
  return "Run is still in progress (no run_completed).";
2503
2503
  }
2504
- return "Outcome unknown \u2014 inspect events may be incomplete.";
2504
+ return "Execution status unknown \u2014 inspect events may be incomplete.";
2505
2505
  }
2506
2506
  function buildRunWhatSummary(events) {
2507
2507
  const base = buildRunSummary(events);
@@ -2592,7 +2592,7 @@ function renderRunWhat(summary, options = {}) {
2592
2592
  lines.push(`Correlation: ${parts.join(", ")}`);
2593
2593
  }
2594
2594
  }
2595
- lines.push(`Outcome: ${outcomeLine(summary)}`);
2595
+ lines.push(`Execution: ${executionLine(summary)}`);
2596
2596
  if (summary.longestStep && summary.totalSteps > 0) {
2597
2597
  lines.push(
2598
2598
  `Slowest: ${summary.longestStep.name} (${formatDuration2(summary.longestStep.durationMs)}, ${summary.longestStep.type})`
@@ -2604,2110 +2604,2169 @@ function renderRunWhat(summary, options = {}) {
2604
2604
  return lines.join("\n");
2605
2605
  }
2606
2606
 
2607
- // packages/core/src/explain.ts
2608
- function toDisplayStatus(status) {
2609
- if (status === "ok") return "success";
2610
- if (status === "error") return "error";
2611
- if (status === "running") return "running";
2612
- return "unknown";
2613
- }
2614
- function flatten(nodes, out = []) {
2615
- for (const node of nodes) {
2616
- out.push({ node, index: out.length + 1 });
2617
- flatten(node.children, out);
2607
+ // packages/core/src/diagnostics/programmatic.ts
2608
+ var PROGRAMMATIC_DIAGNOSTIC_SPECS = Object.freeze({
2609
+ AI_TRACE_INPUT_INVALID: {
2610
+ code: "AI_TRACE_INPUT_INVALID",
2611
+ summary: 'Expected { type: "file", path }, { type: "directory", path }, { type: "string", content }, { type: "buffer", content }, or { type: "stdin" }.',
2612
+ remediation: "For a file path, use openTraceFile(path).",
2613
+ relatedCodes: ["invalid_input"]
2614
+ },
2615
+ AI_TRACE_FORMAT_UNSUPPORTED: {
2616
+ code: "AI_TRACE_FORMAT_UNSUPPORTED",
2617
+ summary: "No trace reader could detect the input format.",
2618
+ remediation: "Pass an AgentInspect JSONL file via openTraceFile, or set options.format to a registered reader.",
2619
+ relatedCodes: ["unsupported_format"]
2620
+ },
2621
+ AI_TRACE_FORMAT_AMBIGUOUS: {
2622
+ code: "AI_TRACE_FORMAT_AMBIGUOUS",
2623
+ summary: "Multiple trace readers matched the input with equal confidence.",
2624
+ remediation: "Set options.format explicitly to disambiguate the reader.",
2625
+ relatedCodes: ["ambiguous_format"]
2626
+ },
2627
+ AI_TRACE_FACTS_INPUT_NOT_NORMALIZED: {
2628
+ code: "AI_TRACE_FACTS_INPUT_NOT_NORMALIZED",
2629
+ summary: "TraceFacts requires TraceReadResult or PersistedInspectEvent[].",
2630
+ remediation: "Use openTraceFile() to normalize a JSONL trace first."
2631
+ },
2632
+ AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED: {
2633
+ code: "AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED",
2634
+ summary: "Multiple runs are available; select a run before executing checks.",
2635
+ remediation: "Pass options.runId or TraceCheckInput.selectedRun.",
2636
+ relatedCodes: ["AI_CHECK_RUN_SELECTION_REQUIRED"]
2637
+ },
2638
+ AI_TRACE_RELATIONSHIP_SELF_PARENT: {
2639
+ code: "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2640
+ summary: "A parentId equals its own event/step id.",
2641
+ remediation: "Reject at capture (AI_LANGGRAPH_SELF_PARENT_REJECTED) or drop via logical projection; do not invent replacement parents.",
2642
+ relatedCodes: [
2643
+ "AI_LANGGRAPH_SELF_PARENT_REJECTED",
2644
+ "AI_LOGICAL_SELF_PARENT_REMOVED"
2645
+ ]
2646
+ },
2647
+ AI_TRACE_RELATIONSHIP_CYCLE: {
2648
+ code: "AI_TRACE_RELATIONSHIP_CYCLE",
2649
+ summary: "Trace contains a parentId cycle.",
2650
+ remediation: "Use visibility-first tree linking for legacy fixtures; prefer acyclic capture for new adapter output.",
2651
+ relatedCodes: ["structure.cycle"]
2652
+ },
2653
+ AI_TRACE_CONTRACT_INPUT_INVALID: {
2654
+ code: "AI_TRACE_CONTRACT_INPUT_INVALID",
2655
+ summary: "evaluateTraceContract expects ({ read }, contract) or use evaluateTraceContractRead(read, contract).",
2656
+ remediation: "Pass TraceCheckInput as the first argument: evaluateTraceContract({ read }, contract).",
2657
+ relatedCodes: ["AI_CHECK_INVALID_ARGUMENTS"]
2658
+ },
2659
+ AI_INSPECT_RUN_UNSUPPORTED_OPTION: {
2660
+ code: "AI_INSPECT_RUN_UNSUPPORTED_OPTION",
2661
+ summary: 'inspectRun does not accept a "writer" option.',
2662
+ remediation: "Use createInspector({ writer }) then inspector.run(...). inspectRun persists via the global JSONL path (schema 0.1)."
2618
2663
  }
2619
- return out;
2620
- }
2621
- function redactValue(redactor, key, value) {
2622
- return redactor.redactValue(key, value);
2623
- }
2624
- function fact(id, label, value, redactor) {
2625
- return {
2626
- id,
2627
- label,
2628
- value: redactValue(redactor, id.split(".").at(-1) ?? id, value),
2629
- source: "trace",
2630
- confidence: "observed"
2631
- };
2664
+ });
2665
+ function formatProgrammaticDiagnostic(code, detail) {
2666
+ const spec = PROGRAMMATIC_DIAGNOSTIC_SPECS[code];
2667
+ const summary = detail?.trim() ? detail.trim() : spec.summary;
2668
+ return `${code}: ${summary} Remediation: ${spec.remediation}`;
2632
2669
  }
2633
- function topKinds(run) {
2634
- return Object.entries(run.metadata.kinds).filter(([, count]) => count > 0).sort((a, b) => {
2635
- if (b[1] !== a[1]) return b[1] - a[1];
2636
- return a[0].localeCompare(b[0]);
2637
- }).slice(0, 5).map(([kind, count]) => `${kind}:${count}`);
2670
+
2671
+ // packages/core/src/checks/logical-events.ts
2672
+ function isRecord6(value) {
2673
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2638
2674
  }
2639
- function countErrorNodes(nodes) {
2640
- return nodes.filter((entry) => entry.node.event.status === "error").length;
2675
+ function legacyEvent(event) {
2676
+ const value = event.attributes?.legacyEvent;
2677
+ return typeof value === "string" ? value : void 0;
2641
2678
  }
2642
- function slowestNode(nodes) {
2643
- return nodes.filter(
2644
- (entry) => entry.node.event.durationMs !== void 0 && // The RUN boundary spans the whole run, so its duration always dominates
2645
- // and shadows every real step. Rank actual work nodes only, matching the
2646
- // stats slowest-step ranking, which never counts the run envelope.
2647
- entry.node.event.kind !== "RUN"
2648
- ).sort((a, b) => {
2649
- const delta = (b.node.event.durationMs ?? 0) - (a.node.event.durationMs ?? 0);
2650
- return delta !== 0 ? delta : a.index - b.index;
2651
- })[0];
2679
+ function stepIdOf(event) {
2680
+ const value = event.attributes?.stepId;
2681
+ if (typeof value === "string" && value.trim() !== "") return value;
2682
+ return void 0;
2652
2683
  }
2653
- function attributeFacts(nodes, redactor) {
2654
- const facts = [];
2655
- for (const entry of nodes) {
2656
- const attrs = entry.node.event.attributes;
2657
- if (attrs === void 0) continue;
2658
- for (const key of Object.keys(attrs).sort()) {
2659
- facts.push({
2660
- id: `node.${entry.index}.attributes.${key}`,
2661
- label: `${entry.node.event.name} attribute ${key}`,
2662
- value: redactValue(redactor, key, attrs[key]),
2663
- source: "trace",
2664
- confidence: "observed"
2665
- });
2666
- if (facts.length >= 8) return facts;
2667
- }
2668
- }
2669
- return facts;
2684
+ function cloneEvent(event) {
2685
+ return {
2686
+ ...event,
2687
+ ...event.attributes !== void 0 ? { attributes: { ...event.attributes } } : {},
2688
+ ...event.error !== void 0 ? { error: { ...event.error } } : {},
2689
+ ...event.tokenUsage !== void 0 ? { tokenUsage: { ...event.tokenUsage } } : {},
2690
+ ...event.source !== void 0 ? { source: { ...event.source } } : {}
2691
+ };
2670
2692
  }
2671
- function buildFacts(run, redactor) {
2672
- const nodes = flatten(run.children);
2673
- const facts = [
2674
- fact("run.id", "Run id", run.runId, redactor),
2675
- fact("run.name", "Run name", run.name ?? run.runId, redactor),
2676
- fact("run.status", "Run status", toDisplayStatus(run.status), redactor),
2677
- fact("run.totalEvents", "Total events", run.metadata.totalEvents, redactor),
2678
- fact("run.stepCount", "Top-level step count", run.children.length, redactor),
2679
- fact("run.nodeCount", "Total node count", nodes.length, redactor),
2680
- fact("run.errorNodeCount", "Error node count", countErrorNodes(nodes), redactor),
2681
- fact("run.kinds", "Observed kind mix", topKinds(run), redactor)
2682
- ];
2683
- if (run.durationMs !== void 0) {
2684
- facts.push(fact("run.durationMs", "Run duration milliseconds", run.durationMs, redactor));
2685
- }
2686
- const slowest = slowestNode(nodes);
2687
- if (slowest !== void 0) {
2688
- facts.push(
2689
- fact("run.slowestNode", "Slowest observed node", {
2690
- name: slowest.node.event.name,
2691
- kind: slowest.node.event.kind,
2692
- durationMs: slowest.node.event.durationMs
2693
- }, redactor)
2694
- );
2693
+ function mergeAttributes(start, complete) {
2694
+ const merged = {
2695
+ ...isRecord6(complete.attributes) ? complete.attributes : {},
2696
+ ...isRecord6(start.attributes) ? start.attributes : {}
2697
+ };
2698
+ merged.legacyEvent = start.attributes?.legacyEvent ?? complete.attributes?.legacyEvent;
2699
+ merged.legacyCompleteEvent = complete.attributes?.legacyEvent;
2700
+ if (complete.attributes?.errorStack !== void 0) {
2701
+ merged.errorStack = complete.attributes.errorStack;
2695
2702
  }
2696
- facts.push(...attributeFacts(nodes, redactor));
2697
- return facts;
2703
+ return Object.keys(merged).length > 0 ? merged : void 0;
2698
2704
  }
2699
- function buildInferences(run, facts) {
2700
- const inferences = [];
2701
- const errorFact = facts.find((item) => item.id === "run.errorNodeCount");
2702
- const kindFact = facts.find((item) => item.id === "run.kinds");
2703
- const durationFact = facts.find((item) => item.id === "run.durationMs");
2704
- const errorNodeCount = typeof errorFact?.value === "number" ? errorFact.value : 0;
2705
- if (run.status === "error" || errorNodeCount > 0) {
2706
- inferences.push({
2707
- id: "outcome.error",
2708
- label: "Outcome",
2709
- text: "The run recorded an error status or at least one error node.",
2710
- evidence: ["run.status", "run.errorNodeCount"],
2711
- confidence: "deterministic"
2712
- });
2713
- } else if (run.status === "ok") {
2714
- inferences.push({
2715
- id: "outcome.success",
2716
- label: "Outcome",
2717
- text: "The run completed without observed error nodes.",
2718
- evidence: ["run.status", "run.errorNodeCount"],
2719
- confidence: "deterministic"
2720
- });
2721
- }
2722
- if (kindFact !== void 0) {
2723
- inferences.push({
2724
- id: "shape.kind-mix",
2725
- label: "Trace shape",
2726
- text: "The explanation is based on the observed event kind mix, not generated content.",
2727
- evidence: [kindFact.id],
2728
- confidence: "deterministic"
2729
- });
2730
- }
2731
- if (durationFact !== void 0) {
2732
- inferences.push({
2733
- id: "timing.duration",
2734
- label: "Timing",
2735
- text: "Timing claims are limited to persisted duration fields in the trace.",
2736
- evidence: [durationFact.id],
2737
- confidence: "deterministic"
2738
- });
2739
- }
2740
- return inferences;
2705
+ function pairStartComplete(start, complete) {
2706
+ const attributes = mergeAttributes(start, complete);
2707
+ const paired = {
2708
+ ...cloneEvent(start),
2709
+ status: complete.status,
2710
+ timestamp: complete.timestamp ?? start.timestamp,
2711
+ ...complete.endedAt !== void 0 ? { endedAt: complete.endedAt } : {},
2712
+ ...complete.durationMs !== void 0 ? { durationMs: complete.durationMs } : {},
2713
+ ...complete.error !== void 0 ? { error: { ...complete.error } } : {},
2714
+ ...complete.tokenUsage !== void 0 && start.tokenUsage === void 0 ? { tokenUsage: { ...complete.tokenUsage } } : {},
2715
+ ...attributes !== void 0 ? { attributes } : {}
2716
+ };
2717
+ return {
2718
+ ...paired,
2719
+ sourceEventIds: Object.freeze([start.eventId, complete.eventId]),
2720
+ projection: {
2721
+ paired: true,
2722
+ absorbedEventIds: Object.freeze([complete.eventId]),
2723
+ parentNormalized: false,
2724
+ ...start.parentId !== void 0 ? { originalParentId: start.parentId } : {}
2725
+ }
2726
+ };
2741
2727
  }
2742
- function buildLocalExplanation(run, options = {}) {
2743
- const redactionProfile = options.redactionProfile ?? "local";
2744
- const resolved = resolveRedactionProfile(redactionProfile);
2745
- const redactor = new Redactor({ extraKeys: resolved.extraKeys });
2746
- const mode = options.mode ?? "local";
2747
- const facts = buildFacts(run, redactor);
2728
+ function asLogical(event, extras) {
2748
2729
  return {
2749
- mode,
2750
- runId: String(redactValue(redactor, "runId", run.runId)),
2751
- ...run.name !== void 0 ? { name: String(redactValue(redactor, "name", run.name)) } : {},
2752
- ...run.status !== void 0 ? { status: toDisplayStatus(run.status) } : {},
2753
- redactionProfile,
2754
- facts,
2755
- inferences: mode === "dry-run" ? [] : buildInferences(run, facts),
2756
- notes: [
2757
- "Generated locally without provider or network calls.",
2758
- "Facts are observed from normalized trace data; inferences are deterministic labels."
2759
- ]
2730
+ ...cloneEvent(event),
2731
+ sourceEventIds: Object.freeze([event.eventId]),
2732
+ projection: {
2733
+ paired: false,
2734
+ absorbedEventIds: Object.freeze([]),
2735
+ parentNormalized: extras?.parentNormalized === true,
2736
+ ...{}
2737
+ }
2760
2738
  };
2761
2739
  }
2762
-
2763
- // packages/core/src/outcomes/types.ts
2764
- var OBSERVED_OUTCOME_STATUSES = [
2765
- "passed",
2766
- "failed",
2767
- "unknown",
2768
- "skipped"
2769
- ];
2770
- var OBSERVED_OUTCOME_METHODS = [
2771
- "dom",
2772
- "accessibility",
2773
- "snapshot",
2774
- "network",
2775
- "storage",
2776
- "filesystem",
2777
- "database",
2778
- "queue",
2779
- "custom"
2780
- ];
2781
- var OUTCOME_ATTRIBUTE_STATUS_KEY = "outcomeStatus";
2782
- var OUTCOME_ATTRIBUTE_EXPECTATION_KEY = "expectation";
2783
- var OUTCOME_ATTRIBUTE_METHOD_KEY = "method";
2784
- var OUTCOME_ATTRIBUTE_OBSERVED_AT_KEY = "observedAt";
2785
- var OUTCOME_LEGACY_EVENT = "outcome_observed";
2786
-
2787
- // packages/core/src/outcomes/validate.ts
2788
- function parseObservedOutcomeStatus(value) {
2789
- const trimmed = value.trim().toLowerCase();
2790
- if (OBSERVED_OUTCOME_STATUSES.includes(trimmed)) {
2791
- return trimmed;
2792
- }
2793
- throw new Error(
2794
- `Unsupported observation status "${value}". Use passed, failed, unknown, or skipped.`
2795
- );
2796
- }
2797
-
2798
- // packages/core/src/outcomes/extract.ts
2799
- function isOutcomeStatus(value) {
2800
- return value === "passed" || value === "failed" || value === "unknown" || value === "skipped";
2801
- }
2802
- function parseMethod(value) {
2803
- if (typeof value !== "string" || value.trim() === "") return void 0;
2804
- return value;
2805
- }
2806
- function fromOutcomeObservedEvent(event) {
2807
- return {
2808
- outcomeId: event.outcomeId,
2809
- runId: event.runId,
2810
- ...event.parentId !== void 0 ? { parentId: event.parentId } : {},
2811
- name: event.name,
2812
- expectation: event.expectation,
2813
- status: event.status,
2814
- ...event.method !== void 0 ? { method: event.method } : {},
2815
- ...event.actual !== void 0 ? { actual: event.actual } : {},
2816
- ...event.evidence !== void 0 ? { evidence: event.evidence } : {},
2817
- observedAt: event.observedAt
2818
- };
2819
- }
2820
- function fromPersistedOutcome(event) {
2821
- if (event.kind !== "OUTCOME") return void 0;
2822
- const attrs = event.attributes ?? {};
2823
- const statusRaw = attrs[OUTCOME_ATTRIBUTE_STATUS_KEY];
2824
- if (!isOutcomeStatus(statusRaw)) return void 0;
2825
- const expectation = typeof attrs[OUTCOME_ATTRIBUTE_EXPECTATION_KEY] === "string" ? attrs[OUTCOME_ATTRIBUTE_EXPECTATION_KEY] : event.name;
2826
- const observedAtRaw = attrs[OUTCOME_ATTRIBUTE_OBSERVED_AT_KEY];
2827
- const observedAt = typeof observedAtRaw === "string" ? Date.parse(observedAtRaw) : typeof observedAtRaw === "number" && Number.isFinite(observedAtRaw) ? observedAtRaw : Date.parse(event.timestamp);
2828
- return {
2829
- outcomeId: event.eventId,
2830
- runId: event.runId,
2831
- ...event.parentId !== void 0 ? { parentId: event.parentId } : {},
2832
- name: event.name,
2833
- expectation,
2834
- status: statusRaw,
2835
- ...parseMethod(attrs[OUTCOME_ATTRIBUTE_METHOD_KEY]) !== void 0 ? { method: parseMethod(attrs[OUTCOME_ATTRIBUTE_METHOD_KEY]) } : {},
2836
- ...event.outputSummary !== void 0 ? { actual: event.outputSummary } : {},
2837
- ...attrs.evidence !== void 0 ? { evidence: attrs.evidence } : {},
2838
- observedAt: Number.isFinite(observedAt) ? observedAt : Date.parse(event.timestamp)
2839
- };
2840
- }
2841
- function extractOutcomesFromTraceEvents(events) {
2842
- const out = [];
2843
- for (const event of events) {
2844
- if (event.event === OUTCOME_LEGACY_EVENT) {
2845
- out.push(fromOutcomeObservedEvent(event));
2846
- }
2847
- }
2848
- return out.sort((a, b) => a.observedAt - b.observedAt || a.name.localeCompare(b.name));
2849
- }
2850
- function extractOutcomesFromPersistedEvents(events) {
2851
- const out = [];
2740
+ function projectLogicalEvents(events) {
2741
+ const diagnostics = [];
2742
+ const byRun = /* @__PURE__ */ new Map();
2852
2743
  for (const event of events) {
2853
- const outcome = fromPersistedOutcome(event);
2854
- if (outcome) out.push(outcome);
2855
- }
2856
- return out.sort((a, b) => a.observedAt - b.observedAt || a.name.localeCompare(b.name));
2857
- }
2858
- function summarizeObservedOutcomes(outcomes) {
2859
- const summary = {
2860
- total: outcomes.length,
2861
- passed: 0,
2862
- failed: 0,
2863
- unknown: 0,
2864
- skipped: 0,
2865
- outcomes: [...outcomes]
2866
- };
2867
- for (const outcome of outcomes) {
2868
- summary[outcome.status] += 1;
2869
- }
2870
- return summary;
2871
- }
2872
- function outcomesMatchingStatus(outcomes, statuses) {
2873
- const set = new Set(statuses);
2874
- return outcomes.filter((outcome) => set.has(outcome.status));
2875
- }
2876
- function parseObservationFilter(value) {
2877
- if (value === void 0 || value.trim() === "") return void 0;
2878
- return parseObservedOutcomeStatus(value);
2879
- }
2880
-
2881
- // packages/core/src/outcomes/render.ts
2882
- function renderObservedOutcomesMarkdown(summary) {
2883
- if (summary.total === 0) {
2884
- return "No observed outcomes recorded for this run.";
2885
- }
2886
- const lines = [
2887
- `Total: ${summary.total} (passed ${summary.passed}, failed ${summary.failed}, unknown ${summary.unknown}, skipped ${summary.skipped})`,
2888
- "",
2889
- "| Name | Status | Expectation | Method |",
2890
- "| --- | --- | --- | --- |"
2891
- ];
2892
- for (const outcome of summary.outcomes) {
2893
- lines.push(
2894
- `| ${outcome.name} | ${outcome.status} | ${outcome.expectation.replace(/\|/g, "\\|")} | ${outcome.method ?? "-"} |`
2895
- );
2896
- }
2897
- return lines.join("\n");
2898
- }
2899
- function renderObservedOutcomesHtml(summary) {
2900
- if (summary.total === 0) {
2901
- return "<p>No observed outcomes recorded for this run.</p>";
2902
- }
2903
- const rows = summary.outcomes.map((outcome) => {
2904
- const esc = (v) => v.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2905
- return `<tr><td>${esc(outcome.name)}</td><td>${esc(outcome.status)}</td><td>${esc(outcome.expectation)}</td><td>${esc(outcome.method ?? "-")}</td></tr>`;
2906
- }).join("");
2907
- return `<p>Total: ${summary.total} (passed ${summary.passed}, failed ${summary.failed}, unknown ${summary.unknown}, skipped ${summary.skipped})</p><table><thead><tr><th>Name</th><th>Status</th><th>Expectation</th><th>Method</th></tr></thead><tbody>${rows}</tbody></table>`;
2908
- }
2909
-
2910
- // packages/core/src/search.ts
2911
- function parseDurationFilter(expr) {
2912
- const raw = expr.trim();
2913
- const m = raw.match(/^(>=|<=|>|<)\s*(.+)$/);
2914
- if (!m) {
2915
- throw new Error(
2916
- `Invalid --duration "${expr}". Use forms like >5s, >=500ms, <2m.`
2917
- );
2918
- }
2919
- const op = m[1];
2920
- const ms = parseDuration(m[2].trim());
2921
- return { op, ms };
2922
- }
2923
- function durationMatches(valueMs, filter) {
2924
- if (valueMs === void 0 || !Number.isFinite(valueMs)) return false;
2925
- switch (filter.op) {
2926
- case ">":
2927
- return valueMs > filter.ms;
2928
- case ">=":
2929
- return valueMs >= filter.ms;
2930
- case "<":
2931
- return valueMs < filter.ms;
2932
- case "<=":
2933
- return valueMs <= filter.ms;
2934
- default:
2935
- return false;
2936
- }
2937
- }
2938
- function normalizeStepTypeFilter(kind, type) {
2939
- const v = (kind ?? type)?.trim().toLowerCase();
2940
- return v && v !== "" ? v : void 0;
2941
- }
2942
- function nameMatches(hay, needle) {
2943
- return hay.toLowerCase().includes(needle.toLowerCase());
2944
- }
2945
- async function searchTraces(metas, options) {
2946
- let filtered = filterTraces(metas, { since: options.since });
2947
- const stepTypeFilter = normalizeStepTypeFilter(options.kind, options.type);
2948
- const nameQuery = options.name?.trim();
2949
- const toolQuery = options.tool?.trim();
2950
- let durationFilter;
2951
- if (options.duration) {
2952
- durationFilter = parseDurationFilter(options.duration);
2953
- }
2954
- const limit = options.limit ?? 50;
2955
- const sessionId = options.session?.trim();
2956
- const observationStatus2 = parseObservationFilter(options.observation);
2957
- const hasContentFilter = Boolean(
2958
- options.status || stepTypeFilter || nameQuery || toolQuery || durationFilter || observationStatus2
2959
- );
2960
- const results = [];
2961
- const sessionLabel = sessionId && sessionId !== "" ? sessionId : void 0;
2962
- if (!hasContentFilter) {
2963
- for (const m of filtered) {
2964
- results.push({
2965
- runId: m.runId,
2966
- runName: m.name,
2967
- runStatus: m.status,
2968
- timestamp: m.startedAt,
2969
- durationMs: m.durationMs,
2970
- matchReason: sessionLabel ? `trace in session ${sessionLabel}` : "trace in directory",
2971
- matchedFields: sessionLabel ? ["run", "session"] : ["run"],
2972
- filePath: m.filePath,
2973
- ...sessionLabel ? { sessionId: sessionLabel } : {}
2974
- });
2975
- }
2976
- results.sort((a, b) => {
2977
- const ta = a.timestamp ?? 0;
2978
- const tb = b.timestamp ?? 0;
2979
- if (ta !== tb) return tb - ta;
2980
- return a.runId.localeCompare(b.runId);
2981
- });
2982
- return results.slice(0, limit);
2744
+ const list = byRun.get(event.runId) ?? [];
2745
+ list.push(event);
2746
+ byRun.set(event.runId, list);
2983
2747
  }
2984
- for (const m of filtered) {
2985
- if (options.status && m.status !== options.status) continue;
2986
- let events = [];
2987
- try {
2988
- events = await readTraceEventsFromFile(m.filePath);
2989
- } catch {
2990
- continue;
2748
+ const absorbedIds = /* @__PURE__ */ new Set();
2749
+ const logicalByRawId = /* @__PURE__ */ new Map();
2750
+ const stepIdToLogicalId = /* @__PURE__ */ new Map();
2751
+ const logical = [];
2752
+ const orderedRuns = [...byRun.keys()].sort((a, b) => a.localeCompare(b));
2753
+ for (const runId of orderedRuns) {
2754
+ const runEvents = byRun.get(runId) ?? [];
2755
+ const starts = [];
2756
+ const completes = [];
2757
+ const others = [];
2758
+ for (const event of runEvents) {
2759
+ const legacy = legacyEvent(event);
2760
+ if (legacy === "step_started" || legacy === "run_started" && event.status === "running") {
2761
+ starts.push(event);
2762
+ } else if (legacy === "step_completed" || legacy === "run_completed") {
2763
+ completes.push(event);
2764
+ } else {
2765
+ others.push(event);
2766
+ }
2991
2767
  }
2992
- if (events.length === 0) continue;
2993
- const runMatches = matchRunLevel(m, {
2994
- stepTypeFilter,
2995
- nameQuery,
2996
- toolQuery,
2997
- durationFilter,
2998
- statusFilter: options.status
2999
- });
3000
- results.push(...runMatches);
3001
- const stepMatches = matchStepLevel(m, events, {
3002
- stepTypeFilter,
3003
- nameQuery,
3004
- toolQuery,
3005
- durationFilter,
3006
- statusFilter: options.status
3007
- });
3008
- results.push(...stepMatches);
3009
- if (observationStatus2) {
3010
- const outcomes = extractOutcomesFromTraceEvents(events);
3011
- const matched = outcomes.filter((outcome) => outcome.status === observationStatus2);
3012
- for (const outcome of matched) {
3013
- results.push({
3014
- runId: m.runId,
3015
- runName: m.name,
3016
- runStatus: m.status,
3017
- stepName: outcome.name,
3018
- timestamp: outcome.observedAt,
3019
- matchReason: `observation status=${outcome.status}`,
3020
- matchedFields: ["outcome.status", "outcome.name"],
3021
- filePath: m.filePath
2768
+ const usedCompletes = /* @__PURE__ */ new Set();
2769
+ for (const start of starts) {
2770
+ const stepId = stepIdOf(start);
2771
+ let match;
2772
+ if (legacyEvent(start) === "run_started") {
2773
+ const candidates = completes.filter(
2774
+ (c) => !usedCompletes.has(c.eventId) && legacyEvent(c) === "run_completed"
2775
+ );
2776
+ if (candidates.length > 1) {
2777
+ diagnostics.push({
2778
+ code: "AI_LOGICAL_PAIR_AMBIGUOUS",
2779
+ message: `Multiple run_completed rows for run ${runId}; using first by eventId.`,
2780
+ eventIds: candidates.map((c) => c.eventId)
2781
+ });
2782
+ candidates.sort((a, b) => a.eventId.localeCompare(b.eventId));
2783
+ }
2784
+ match = candidates[0];
2785
+ } else if (stepId) {
2786
+ const candidates = completes.filter(
2787
+ (c) => !usedCompletes.has(c.eventId) && legacyEvent(c) === "step_completed" && stepIdOf(c) === stepId
2788
+ );
2789
+ if (candidates.length > 1) {
2790
+ diagnostics.push({
2791
+ code: "AI_LOGICAL_PAIR_AMBIGUOUS",
2792
+ message: `Multiple step_completed rows for stepId ${stepId}; using first by eventId.`,
2793
+ eventIds: candidates.map((c) => c.eventId)
2794
+ });
2795
+ candidates.sort((a, b) => a.eventId.localeCompare(b.eventId));
2796
+ }
2797
+ match = candidates[0];
2798
+ }
2799
+ if (match) {
2800
+ usedCompletes.add(match.eventId);
2801
+ absorbedIds.add(match.eventId);
2802
+ const paired = pairStartComplete(start, match);
2803
+ logical.push(paired);
2804
+ logicalByRawId.set(start.eventId, paired);
2805
+ logicalByRawId.set(match.eventId, paired);
2806
+ if (stepId) stepIdToLogicalId.set(`${runId}:${stepId}`, paired.eventId);
2807
+ } else {
2808
+ diagnostics.push({
2809
+ code: "AI_LOGICAL_PAIR_UNMATCHED_START",
2810
+ message: `No matching complete for start ${start.eventId}.`,
2811
+ eventIds: [start.eventId]
3022
2812
  });
2813
+ const alone = asLogical(start);
2814
+ logical.push(alone);
2815
+ logicalByRawId.set(start.eventId, alone);
2816
+ if (stepId) stepIdToLogicalId.set(`${runId}:${stepId}`, alone.eventId);
2817
+ }
2818
+ }
2819
+ for (const complete of completes) {
2820
+ if (usedCompletes.has(complete.eventId)) continue;
2821
+ diagnostics.push({
2822
+ code: "AI_LOGICAL_PAIR_UNMATCHED_COMPLETE",
2823
+ message: `No matching start for complete ${complete.eventId}.`,
2824
+ eventIds: [complete.eventId]
2825
+ });
2826
+ const alone = asLogical(complete);
2827
+ logical.push(alone);
2828
+ logicalByRawId.set(complete.eventId, alone);
2829
+ const stepId = stepIdOf(complete);
2830
+ if (stepId && !stepIdToLogicalId.has(`${runId}:${stepId}`)) {
2831
+ stepIdToLogicalId.set(`${runId}:${stepId}`, alone.eventId);
2832
+ }
2833
+ }
2834
+ for (const event of others) {
2835
+ const alone = asLogical(event);
2836
+ logical.push(alone);
2837
+ logicalByRawId.set(event.eventId, alone);
2838
+ const stepId = stepIdOf(event);
2839
+ if (stepId && !stepIdToLogicalId.has(`${runId}:${stepId}`)) {
2840
+ stepIdToLogicalId.set(`${runId}:${stepId}`, alone.eventId);
3023
2841
  }
3024
2842
  }
3025
2843
  }
3026
- results.sort((a, b) => {
3027
- const ta = a.timestamp ?? 0;
3028
- const tb = b.timestamp ?? 0;
3029
- if (ta !== tb) return tb - ta;
3030
- const runCmp = a.runId.localeCompare(b.runId);
3031
- if (runCmp !== 0) return runCmp;
3032
- return (a.stepName ?? "").localeCompare(b.stepName ?? "");
3033
- });
3034
- return results.slice(0, limit);
3035
- }
3036
- function matchRunLevel(m, opts) {
3037
- if (opts.stepTypeFilter || opts.toolQuery) return [];
3038
- if (opts.statusFilter && m.status !== opts.statusFilter) return [];
3039
- if (opts.nameQuery && !nameMatches(m.name ?? m.runId, opts.nameQuery)) {
3040
- return [];
3041
- }
3042
- if (opts.durationFilter && !durationMatches(m.durationMs, opts.durationFilter)) {
3043
- return [];
3044
- }
3045
- const fields = [];
3046
- if (opts.statusFilter) fields.push("run.status");
3047
- if (opts.nameQuery) fields.push("run.name");
3048
- if (opts.durationFilter) fields.push("run.durationMs");
3049
- if (fields.length === 0) return [];
3050
- return [
3051
- {
3052
- runId: m.runId,
3053
- runName: m.name,
3054
- runStatus: m.status,
3055
- timestamp: m.startedAt,
3056
- durationMs: m.durationMs,
3057
- matchReason: `run match: ${fields.join(", ")}`,
3058
- matchedFields: fields,
3059
- filePath: m.filePath
2844
+ const logicalById = new Map(logical.map((e) => [e.eventId, e]));
2845
+ const normalized = [];
2846
+ for (const event of logical) {
2847
+ const originalParentId = event.parentId;
2848
+ if (!originalParentId) {
2849
+ normalized.push(event);
2850
+ continue;
3060
2851
  }
3061
- ];
3062
- }
3063
- function matchStepLevel(m, events, opts) {
3064
- const out = [];
3065
- const started = /* @__PURE__ */ new Map();
3066
- for (const e of events) {
3067
- if (e.event === "step_started") {
3068
- started.set(e.stepId, e);
2852
+ let nextParent = originalParentId;
2853
+ let remapped = false;
2854
+ const viaAbsorbed = logicalByRawId.get(originalParentId);
2855
+ if (viaAbsorbed && viaAbsorbed.eventId !== originalParentId) {
2856
+ nextParent = viaAbsorbed.eventId;
2857
+ remapped = true;
2858
+ } else if (!logicalById.has(originalParentId)) {
2859
+ const viaStep = stepIdToLogicalId.get(`${event.runId}:${originalParentId}`);
2860
+ if (viaStep) {
2861
+ nextParent = viaStep;
2862
+ remapped = true;
2863
+ }
3069
2864
  }
3070
- }
3071
- for (const e of events) {
3072
- if (e.event !== "step_completed") continue;
3073
- const c = e;
3074
- const s = started.get(c.stepId);
3075
- if (!s) continue;
3076
- const fields = [];
3077
- const stepType = s.type;
3078
- if (opts.stepTypeFilter && stepType !== opts.stepTypeFilter) {
2865
+ if (!remapped) {
2866
+ if (originalParentId === event.eventId) {
2867
+ diagnostics.push({
2868
+ code: "AI_LOGICAL_SELF_PARENT_REMOVED",
2869
+ message: formatProgrammaticDiagnostic(
2870
+ "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2871
+ `Removed self-parent edge on ${event.eventId}.`
2872
+ ),
2873
+ eventIds: [event.eventId]
2874
+ });
2875
+ const { parentId: _drop, ...rest } = event;
2876
+ normalized.push({
2877
+ ...rest,
2878
+ projection: {
2879
+ ...event.projection,
2880
+ parentNormalized: true,
2881
+ originalParentId
2882
+ }
2883
+ });
2884
+ continue;
2885
+ }
2886
+ if (!logicalById.has(originalParentId) && !logicalByRawId.has(originalParentId)) {
2887
+ const mapping = event.attributes?.parentMapping;
2888
+ const unresolved = mapping === "unresolved" || event.attributes?.parentUnresolved === true || event.attributes?.unresolvedParent === true || // LangGraph/framework scaffolding sentinel labels are not event ids.
2889
+ /^LangGraph$/i.test(originalParentId) || originalParentId.startsWith("unresolved:");
2890
+ if (!unresolved) {
2891
+ diagnostics.push({
2892
+ code: "AI_LOGICAL_PARENT_UNRESOLVED",
2893
+ message: `Parent ${originalParentId} unresolved for ${event.eventId}.`,
2894
+ eventIds: [event.eventId]
2895
+ });
2896
+ }
2897
+ }
2898
+ normalized.push(event);
3079
2899
  continue;
3080
2900
  }
3081
- const hasStepFilters = opts.stepTypeFilter || opts.nameQuery || opts.toolQuery || opts.durationFilter || opts.statusFilter === "error" || opts.statusFilter === "success";
3082
- if (!hasStepFilters) continue;
3083
- if (opts.statusFilter === "error" && c.status === "error") {
3084
- fields.push("step.status");
3085
- } else if (opts.statusFilter === "success" && c.status === "success") {
3086
- fields.push("step.status");
3087
- } else if (opts.statusFilter === "error" || opts.statusFilter === "success") {
2901
+ if (nextParent === event.eventId) {
2902
+ diagnostics.push({
2903
+ code: "AI_LOGICAL_SELF_PARENT_REMOVED",
2904
+ message: formatProgrammaticDiagnostic(
2905
+ "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2906
+ `Removed self-parent edge on ${event.eventId} (was ${originalParentId}).`
2907
+ ),
2908
+ eventIds: [event.eventId]
2909
+ });
2910
+ const { parentId: _drop, ...rest } = event;
2911
+ normalized.push({
2912
+ ...rest,
2913
+ projection: {
2914
+ ...event.projection,
2915
+ parentNormalized: true,
2916
+ originalParentId
2917
+ }
2918
+ });
3088
2919
  continue;
3089
2920
  }
3090
- if (opts.nameQuery) {
3091
- if (!nameMatches(s.name, opts.nameQuery)) continue;
3092
- fields.push("step.name");
3093
- }
3094
- if (opts.toolQuery) {
3095
- const toolName2 = typeof s.metadata?.toolName === "string" ? s.metadata.toolName : s.name;
3096
- if (!nameMatches(toolName2, opts.toolQuery)) continue;
3097
- fields.push("step.tool");
3098
- }
3099
- if (opts.durationFilter) {
3100
- if (!durationMatches(c.durationMs, opts.durationFilter)) continue;
3101
- fields.push("step.durationMs");
3102
- }
3103
- if (opts.stepTypeFilter) {
3104
- fields.push("step.type");
3105
- }
3106
- if (fields.length === 0) continue;
3107
- out.push({
3108
- runId: m.runId,
3109
- runName: m.name,
3110
- runStatus: m.status,
3111
- stepId: c.stepId,
3112
- stepName: s.name,
3113
- stepType,
3114
- timestamp: s.startTime ?? s.timestamp,
3115
- durationMs: c.durationMs,
3116
- matchReason: `step match: ${fields.join(", ")}`,
3117
- matchedFields: fields,
3118
- filePath: m.filePath
2921
+ diagnostics.push({
2922
+ code: "AI_LOGICAL_PARENT_REMAPPED",
2923
+ message: `Remapped parent ${originalParentId} \u2192 ${nextParent} for ${event.eventId}.`,
2924
+ eventIds: [event.eventId]
2925
+ });
2926
+ normalized.push({
2927
+ ...event,
2928
+ parentId: nextParent,
2929
+ projection: {
2930
+ ...event.projection,
2931
+ parentNormalized: true,
2932
+ originalParentId
2933
+ }
3119
2934
  });
3120
2935
  }
3121
- return out;
3122
- }
3123
- async function loadTraceMetadataList(_traceDir, fileNames, getPath) {
3124
- const metas = [];
3125
- for (const fileName of fileNames) {
3126
- try {
3127
- const filePath = getPath(fileName);
3128
- const meta = await extractMetadata(filePath);
3129
- metas.push(meta);
3130
- } catch {
3131
- }
3132
- }
3133
- return metas;
3134
- }
3135
-
3136
- // packages/core/src/sessions/activity.ts
3137
- function statusLine(session) {
3138
- const name = session.workflowId ?? session.correlationId ?? session.sessionId;
3139
- const status = session.status;
3140
- if (session.lastError) {
3141
- return `${name} session ${session.sessionId} failed at ${session.lastError.message}`;
3142
- }
3143
- if (session.observationSummary) {
3144
- return `${name} session ${session.sessionId} ${status} with observation warning`;
3145
- }
3146
- return `${name} session ${session.sessionId} ${status}`;
3147
- }
3148
- function parseSinceMs(since, nowMs) {
3149
- if (!since || since.trim() === "") return nowMs - 7 * 864e5;
3150
- const trimmed = since.trim().toLowerCase();
3151
- const match = /^(\d+)([smhd])$/.exec(trimmed);
3152
- if (!match) return nowMs - 7 * 864e5;
3153
- const amount = Number.parseInt(match[1], 10);
3154
- const unit = match[2];
3155
- const mult = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
3156
- return nowMs - amount * mult;
3157
- }
3158
- function isFailed(status) {
3159
- return status === "error";
3160
- }
3161
- function isStale(status) {
3162
- return status === "stale";
3163
- }
3164
- function guardrailWarnings(session) {
3165
- const summary = session.checkSummary;
3166
- if (!summary) return 0;
3167
- return summary.warn;
3168
- }
3169
- function buildActivitySummary(index, options = {}) {
3170
- const nowMs = options.nowMs ?? Date.now();
3171
- const sinceMs = parseSinceMs(options.since, nowMs);
3172
- const sinceIso = new Date(sinceMs).toISOString();
3173
- const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 20;
3174
- const inWindow = index.sessions.filter((session) => {
3175
- const activityMs2 = Date.parse(session.lastActivity);
3176
- return Number.isFinite(activityMs2) && activityMs2 >= sinceMs;
3177
- });
3178
- const entries = [...inWindow].sort((a, b) => Date.parse(b.lastActivity) - Date.parse(a.lastActivity)).slice(0, limit).map((session) => ({
3179
- sessionId: session.sessionId,
3180
- status: session.status,
3181
- summary: statusLine(session),
3182
- lastActivity: session.lastActivity,
3183
- runCount: session.runIds.length
3184
- }));
3185
- let failed = 0;
3186
- let stale = 0;
3187
- let guardrailWarningTotal = 0;
3188
- for (const session of inWindow) {
3189
- if (isFailed(session.status)) failed += 1;
3190
- if (isStale(session.status)) stale += 1;
3191
- guardrailWarningTotal += guardrailWarnings(session);
3192
- }
3193
- return {
3194
- since: sinceIso,
3195
- sessions: inWindow.length,
3196
- failed,
3197
- stale,
3198
- guardrailWarnings: guardrailWarningTotal,
3199
- entries
2936
+ const rawIndex = new Map(events.map((e, i) => [e.eventId, i]));
2937
+ normalized.sort((a, b) => {
2938
+ const ai = rawIndex.get(a.sourceEventIds[0]) ?? 0;
2939
+ const bi = rawIndex.get(b.sourceEventIds[0]) ?? 0;
2940
+ return ai - bi || a.eventId.localeCompare(b.eventId);
2941
+ });
2942
+ return {
2943
+ logicalEvents: Object.freeze(normalized),
2944
+ diagnostics: Object.freeze(diagnostics)
3200
2945
  };
3201
2946
  }
3202
- function renderActivitySummaryHuman(summary, options) {
3203
- const lines = [];
3204
- const nowMs = options?.nowMs ?? Date.now();
3205
- const todayStart = new Date(nowMs);
3206
- todayStart.setHours(0, 0, 0, 0);
3207
- const todayMs = todayStart.getTime();
3208
- const today = summary.entries.filter(
3209
- (entry) => Date.parse(entry.lastActivity) >= todayMs
3210
- );
3211
- if (today.length > 0) {
3212
- lines.push("Today");
3213
- for (const entry of today) {
3214
- lines.push(` ${entry.summary}`);
3215
- }
3216
- lines.push("");
2947
+ function resolveCanonicalToolName(event) {
2948
+ const attrs = event.attributes;
2949
+ const direct = pickString(attrs, ["toolName", "tool"]);
2950
+ if (direct) return direct;
2951
+ const metadata = attrs?.metadata;
2952
+ if (isRecord6(metadata)) {
2953
+ const nested = pickString(metadata, ["toolName", "tool"]);
2954
+ if (nested) return nested;
3217
2955
  }
3218
- lines.push(`Since ${summary.since}`);
3219
- lines.push(` ${summary.sessions} sessions`);
3220
- lines.push(` ${summary.failed} failed`);
3221
- lines.push(` ${summary.stale} stale`);
3222
- lines.push(` ${summary.guardrailWarnings} guardrail warnings`);
3223
- return lines.join("\n");
2956
+ for (const prefix of ["tool:", "function:", "mcp-tools:"]) {
2957
+ if (event.name.startsWith(prefix)) return event.name.slice(prefix.length);
2958
+ }
2959
+ return event.name;
2960
+ }
2961
+ function pickString(record, keys) {
2962
+ if (!record) return void 0;
2963
+ for (const key of keys) {
2964
+ const value = record[key];
2965
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
2966
+ }
2967
+ return void 0;
3224
2968
  }
3225
2969
 
3226
- // packages/core/src/sessions/load.ts
3227
- async function enrichSessionRunRecord(meta) {
3228
- let metadata;
3229
- try {
3230
- const events = await readTraceEventsFromFile(meta.filePath);
3231
- for (const event of events) {
3232
- if (event.event !== "run_started") continue;
3233
- if (event.metadata && typeof event.metadata === "object") {
3234
- metadata = event.metadata;
3235
- }
3236
- break;
3237
- }
3238
- } catch {
2970
+ // packages/core/src/explain.ts
2971
+ function toDisplayStatus(status) {
2972
+ if (status === "ok") return "success";
2973
+ if (status === "error") return "error";
2974
+ if (status === "running") return "running";
2975
+ return "unknown";
2976
+ }
2977
+ function flatten(nodes, out = []) {
2978
+ for (const node of nodes) {
2979
+ out.push({ node, index: out.length + 1 });
2980
+ flatten(node.children, out);
3239
2981
  }
2982
+ return out;
2983
+ }
2984
+ function redactValue(redactor, key, value) {
2985
+ return redactor.redactValue(key, value);
2986
+ }
2987
+ function fact(id, label, value, redactor) {
3240
2988
  return {
3241
- runId: meta.runId,
3242
- name: meta.name,
3243
- status: meta.status,
3244
- startedAt: meta.startedAt,
3245
- endedAt: meta.endedAt,
3246
- durationMs: meta.durationMs,
3247
- filePath: meta.filePath,
3248
- metadata
2989
+ id,
2990
+ label,
2991
+ value: redactValue(redactor, id.split(".").at(-1) ?? id, value),
2992
+ source: "trace",
2993
+ confidence: "observed"
3249
2994
  };
3250
2995
  }
3251
- async function loadSessionRunRecords(metas) {
3252
- const out = [];
3253
- for (const meta of metas) {
3254
- out.push(await enrichSessionRunRecord(meta));
3255
- }
3256
- return out;
2996
+ function topKinds(run) {
2997
+ return Object.entries(run.metadata.kinds).filter(([, count]) => count > 0).sort((a, b) => {
2998
+ if (b[1] !== a[1]) return b[1] - a[1];
2999
+ return a[0].localeCompare(b[0]);
3000
+ }).slice(0, 5).map(([kind, count]) => `${kind}:${count}`);
3257
3001
  }
3258
-
3259
- // packages/core/src/sessions/metadata.ts
3260
- function isNonEmptyString3(value) {
3261
- return typeof value === "string" && value.trim() !== "";
3002
+ function countErrorNodes(nodes) {
3003
+ return nodes.filter((entry) => entry.node.event.status === "error").length;
3262
3004
  }
3263
- function finitePositiveInt(value) {
3264
- if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
3265
- return void 0;
3005
+ function slowestNode(nodes) {
3006
+ return nodes.filter(
3007
+ (entry) => entry.node.event.durationMs !== void 0 && // The RUN boundary spans the whole run, so its duration always dominates
3008
+ // and shadows every real step. Rank actual work nodes only, matching the
3009
+ // stats slowest-step ranking, which never counts the run envelope.
3010
+ entry.node.event.kind !== "RUN"
3011
+ ).sort((a, b) => {
3012
+ const delta = (b.node.event.durationMs ?? 0) - (a.node.event.durationMs ?? 0);
3013
+ return delta !== 0 ? delta : a.index - b.index;
3014
+ })[0];
3015
+ }
3016
+ function attributeFacts(nodes, redactor) {
3017
+ const facts = [];
3018
+ for (const entry of nodes) {
3019
+ const attrs = entry.node.event.attributes;
3020
+ if (attrs === void 0) continue;
3021
+ for (const key of Object.keys(attrs).sort()) {
3022
+ facts.push({
3023
+ id: `node.${entry.index}.attributes.${key}`,
3024
+ label: `${entry.node.event.name} attribute ${key}`,
3025
+ value: redactValue(redactor, key, attrs[key]),
3026
+ source: "trace",
3027
+ confidence: "observed"
3028
+ });
3029
+ if (facts.length >= 8) return facts;
3030
+ }
3266
3031
  }
3267
- return Math.trunc(value);
3032
+ return facts;
3268
3033
  }
3269
- function extractSessionWorkflowMetadata(record) {
3270
- if (!record) return void 0;
3271
- const out = {};
3272
- let found = false;
3273
- const assignString = (key, value) => {
3274
- if (isNonEmptyString3(value)) {
3275
- out[key] = value.trim();
3276
- found = true;
3034
+ function buildFacts(run, redactor, events) {
3035
+ const nodes = flatten(run.children);
3036
+ const facts = [
3037
+ fact("run.id", "Run id", run.runId, redactor),
3038
+ fact("run.name", "Run name", run.name ?? run.runId, redactor),
3039
+ fact("run.status", "Run status", toDisplayStatus(run.status), redactor),
3040
+ fact("run.totalEvents", "Total events", run.metadata.totalEvents, redactor),
3041
+ fact("run.stepCount", "Top-level step count", run.children.length, redactor),
3042
+ fact("run.nodeCount", "Total node count", nodes.length, redactor),
3043
+ fact("run.errorNodeCount", "Error node count", countErrorNodes(nodes), redactor),
3044
+ fact("run.kinds", "Observed kind mix", topKinds(run), redactor)
3045
+ ];
3046
+ if (run.durationMs !== void 0) {
3047
+ facts.push(fact("run.durationMs", "Run duration milliseconds", run.durationMs, redactor));
3048
+ }
3049
+ const slowest = slowestNode(nodes);
3050
+ if (slowest !== void 0) {
3051
+ facts.push(
3052
+ fact("run.slowestNode", "Slowest observed node", {
3053
+ name: slowest.node.event.name,
3054
+ kind: slowest.node.event.kind,
3055
+ durationMs: slowest.node.event.durationMs
3056
+ }, redactor)
3057
+ );
3058
+ }
3059
+ if (events !== void 0) {
3060
+ const scoped = events.filter((event) => event.runId === run.runId);
3061
+ const { logicalEvents: logical } = projectLogicalEvents(scoped);
3062
+ const logicalSteps = logical.filter((event) => event.kind !== "RUN");
3063
+ facts.push(
3064
+ fact("run.rawEventCount", "Raw persisted event count", scoped.length, redactor),
3065
+ fact(
3066
+ "run.logicalStepCount",
3067
+ "Logical lifecycle step count",
3068
+ logicalSteps.length,
3069
+ redactor
3070
+ ),
3071
+ fact(
3072
+ "run.logicalEventCount",
3073
+ "Logical lifecycle event count (including RUN)",
3074
+ logical.length,
3075
+ redactor
3076
+ )
3077
+ );
3078
+ const slowestLogical = [...logicalSteps].filter((event) => typeof event.durationMs === "number").sort((a, b) => {
3079
+ const delta = (b.durationMs ?? 0) - (a.durationMs ?? 0);
3080
+ return delta !== 0 ? delta : a.eventId.localeCompare(b.eventId);
3081
+ })[0];
3082
+ if (slowestLogical !== void 0) {
3083
+ facts.push(
3084
+ fact("run.slowestLogicalStep", "Slowest logical lifecycle step", {
3085
+ name: slowestLogical.name,
3086
+ kind: slowestLogical.kind,
3087
+ durationMs: slowestLogical.durationMs
3088
+ }, redactor)
3089
+ );
3277
3090
  }
3278
- };
3279
- assignString("sessionId", record.sessionId);
3280
- assignString("conversationId", record.conversationId);
3281
- assignString("groupId", record.groupId);
3282
- assignString("parentGroupId", record.parentGroupId);
3283
- assignString("retryOf", record.retryOf);
3284
- assignString("retryReason", record.retryReason);
3285
- assignString("handoffFrom", record.handoffFrom);
3286
- assignString("handoffTo", record.handoffTo);
3287
- assignString("subAgentId", record.subAgentId);
3288
- assignString("subAgentName", record.subAgentName);
3289
- assignString("jobId", record.jobId);
3290
- assignString("queueName", record.queueName);
3291
- assignString("workflowName", record.workflowName);
3292
- assignString("workflowStep", record.workflowStep);
3293
- assignString("toolCallId", record.toolCallId);
3294
- assignString("mcpToolCallId", record.mcpToolCallId);
3295
- assignString("linkedStepId", record.linkedStepId);
3296
- assignString("operationId", record.operationId);
3297
- assignString("attemptId", record.attemptId);
3298
- assignString("fallbackOf", record.fallbackOf);
3299
- assignString("idempotencyKey", record.idempotencyKey);
3300
- assignString("correlationId", record.correlationId);
3301
- assignString("requestId", record.requestId);
3302
- assignString("decisionId", record.decisionId);
3303
- const attempt = finitePositiveInt(record.attempt);
3304
- if (attempt !== void 0) {
3305
- out.attempt = attempt;
3306
- found = true;
3307
3091
  }
3308
- const attemptNumber = finitePositiveInt(record.attemptNumber);
3309
- if (attemptNumber !== void 0) {
3310
- out.attemptNumber = attemptNumber;
3311
- found = true;
3092
+ facts.push(...attributeFacts(nodes, redactor));
3093
+ return facts;
3094
+ }
3095
+ function buildInferences(run, facts) {
3096
+ const inferences = [];
3097
+ const errorFact = facts.find((item) => item.id === "run.errorNodeCount");
3098
+ const kindFact = facts.find((item) => item.id === "run.kinds");
3099
+ const durationFact = facts.find((item) => item.id === "run.durationMs");
3100
+ const errorNodeCount = typeof errorFact?.value === "number" ? errorFact.value : 0;
3101
+ if (run.status === "error" || errorNodeCount > 0) {
3102
+ inferences.push({
3103
+ id: "outcome.error",
3104
+ label: "Outcome",
3105
+ text: "The run recorded an error status or at least one error node.",
3106
+ evidence: ["run.status", "run.errorNodeCount"],
3107
+ confidence: "deterministic"
3108
+ });
3109
+ } else if (run.status === "ok") {
3110
+ inferences.push({
3111
+ id: "outcome.success",
3112
+ label: "Outcome",
3113
+ text: "The run completed without observed error nodes.",
3114
+ evidence: ["run.status", "run.errorNodeCount"],
3115
+ confidence: "deterministic"
3116
+ });
3312
3117
  }
3313
- return found ? out : void 0;
3118
+ if (kindFact !== void 0) {
3119
+ inferences.push({
3120
+ id: "shape.kind-mix",
3121
+ label: "Trace shape",
3122
+ text: "The explanation is based on the observed event kind mix, not generated content.",
3123
+ evidence: [kindFact.id],
3124
+ confidence: "deterministic"
3125
+ });
3126
+ }
3127
+ if (durationFact !== void 0) {
3128
+ inferences.push({
3129
+ id: "timing.duration",
3130
+ label: "Timing",
3131
+ text: "Timing claims are limited to persisted duration fields in the trace.",
3132
+ evidence: [durationFact.id],
3133
+ confidence: "deterministic"
3134
+ });
3135
+ }
3136
+ return inferences;
3137
+ }
3138
+ function buildLocalExplanation(run, options = {}) {
3139
+ const redactionProfile = options.redactionProfile ?? "local";
3140
+ const resolved = resolveRedactionProfile(redactionProfile);
3141
+ const redactor = new Redactor({ extraKeys: resolved.extraKeys });
3142
+ const mode = options.mode ?? "local";
3143
+ const facts = buildFacts(run, redactor, options.events);
3144
+ return {
3145
+ mode,
3146
+ runId: String(redactValue(redactor, "runId", run.runId)),
3147
+ ...run.name !== void 0 ? { name: String(redactValue(redactor, "name", run.name)) } : {},
3148
+ ...run.status !== void 0 ? { status: toDisplayStatus(run.status) } : {},
3149
+ redactionProfile,
3150
+ facts,
3151
+ inferences: mode === "dry-run" ? [] : buildInferences(run, facts),
3152
+ notes: [
3153
+ "Generated locally without provider or network calls.",
3154
+ "Facts are observed from normalized trace data; inferences are deterministic labels."
3155
+ ]
3156
+ };
3314
3157
  }
3315
- function sessionKeyForRun(meta, options) {
3316
- if (meta?.sessionId) return meta.sessionId;
3317
- if (options?.correlateByGroupId && meta?.groupId) {
3318
- return `group:${meta.groupId}`;
3158
+
3159
+ // packages/core/src/outcomes/types.ts
3160
+ var OBSERVED_OUTCOME_STATUSES = [
3161
+ "passed",
3162
+ "failed",
3163
+ "unknown",
3164
+ "skipped"
3165
+ ];
3166
+ var OBSERVED_OUTCOME_METHODS = [
3167
+ "dom",
3168
+ "accessibility",
3169
+ "snapshot",
3170
+ "network",
3171
+ "storage",
3172
+ "filesystem",
3173
+ "database",
3174
+ "queue",
3175
+ "custom"
3176
+ ];
3177
+ var OUTCOME_ATTRIBUTE_STATUS_KEY = "outcomeStatus";
3178
+ var OUTCOME_ATTRIBUTE_EXPECTATION_KEY = "expectation";
3179
+ var OUTCOME_ATTRIBUTE_METHOD_KEY = "method";
3180
+ var OUTCOME_ATTRIBUTE_OBSERVED_AT_KEY = "observedAt";
3181
+ var OUTCOME_LEGACY_EVENT = "outcome_observed";
3182
+
3183
+ // packages/core/src/outcomes/validate.ts
3184
+ function parseObservedOutcomeStatus(value) {
3185
+ const trimmed = value.trim().toLowerCase();
3186
+ if (OBSERVED_OUTCOME_STATUSES.includes(trimmed)) {
3187
+ return trimmed;
3319
3188
  }
3320
- return void 0;
3189
+ throw new Error(
3190
+ `Unsupported observation status "${value}". Use passed, failed, unknown, or skipped.`
3191
+ );
3321
3192
  }
3322
3193
 
3323
- // packages/core/src/sessions/status.ts
3324
- var DEFAULT_STALE_THRESHOLD_MS = 864e5;
3325
- var EXPLICIT_STATUS_PRIORITY = {
3326
- error: 5,
3327
- waiting_input: 4,
3328
- idle: 3,
3329
- stale: 2,
3330
- completed: 1
3331
- };
3332
- var EXPLICIT_SESSION_STATUSES = /* @__PURE__ */ new Set([
3333
- "running",
3334
- "waiting_input",
3335
- "idle",
3336
- "completed",
3337
- "error",
3338
- "stale",
3339
- "unknown"
3340
- ]);
3341
- function isExplicitSessionStatus(value) {
3342
- return typeof value === "string" && EXPLICIT_SESSION_STATUSES.has(value);
3194
+ // packages/core/src/outcomes/extract.ts
3195
+ function isOutcomeStatus(value) {
3196
+ return value === "passed" || value === "failed" || value === "unknown" || value === "skipped";
3343
3197
  }
3344
- function activityMs(run) {
3345
- return run.endedAt ?? run.startedAt ?? 0;
3198
+ function parseMethod(value) {
3199
+ if (typeof value !== "string" || value.trim() === "") return void 0;
3200
+ return value;
3346
3201
  }
3347
- function latestActivityMs(runs) {
3348
- let latest = 0;
3349
- for (const run of runs) {
3350
- const ms = activityMs(run);
3351
- if (ms > latest) latest = ms;
3352
- }
3353
- return latest;
3202
+ function fromOutcomeObservedEvent(event) {
3203
+ return {
3204
+ outcomeId: event.outcomeId,
3205
+ runId: event.runId,
3206
+ ...event.parentId !== void 0 ? { parentId: event.parentId } : {},
3207
+ name: event.name,
3208
+ expectation: event.expectation,
3209
+ status: event.status,
3210
+ ...event.method !== void 0 ? { method: event.method } : {},
3211
+ ...event.actual !== void 0 ? { actual: event.actual } : {},
3212
+ ...event.evidence !== void 0 ? { evidence: event.evidence } : {},
3213
+ observedAt: event.observedAt
3214
+ };
3354
3215
  }
3355
- function earliestStart(runs) {
3356
- let earliest;
3357
- for (const run of runs) {
3358
- if (run.startedAt === void 0) continue;
3359
- if (earliest === void 0 || run.startedAt < earliest) {
3360
- earliest = run.startedAt;
3216
+ function fromPersistedOutcome(event) {
3217
+ if (event.kind !== "OUTCOME") return void 0;
3218
+ const attrs = event.attributes ?? {};
3219
+ const statusRaw = attrs[OUTCOME_ATTRIBUTE_STATUS_KEY];
3220
+ if (!isOutcomeStatus(statusRaw)) return void 0;
3221
+ const expectation = typeof attrs[OUTCOME_ATTRIBUTE_EXPECTATION_KEY] === "string" ? attrs[OUTCOME_ATTRIBUTE_EXPECTATION_KEY] : event.name;
3222
+ const observedAtRaw = attrs[OUTCOME_ATTRIBUTE_OBSERVED_AT_KEY];
3223
+ const observedAt = typeof observedAtRaw === "string" ? Date.parse(observedAtRaw) : typeof observedAtRaw === "number" && Number.isFinite(observedAtRaw) ? observedAtRaw : Date.parse(event.timestamp);
3224
+ return {
3225
+ outcomeId: event.eventId,
3226
+ runId: event.runId,
3227
+ ...event.parentId !== void 0 ? { parentId: event.parentId } : {},
3228
+ name: event.name,
3229
+ expectation,
3230
+ status: statusRaw,
3231
+ ...parseMethod(attrs[OUTCOME_ATTRIBUTE_METHOD_KEY]) !== void 0 ? { method: parseMethod(attrs[OUTCOME_ATTRIBUTE_METHOD_KEY]) } : {},
3232
+ ...event.outputSummary !== void 0 ? { actual: event.outputSummary } : {},
3233
+ ...attrs.evidence !== void 0 ? { evidence: attrs.evidence } : {},
3234
+ observedAt: Number.isFinite(observedAt) ? observedAt : Date.parse(event.timestamp)
3235
+ };
3236
+ }
3237
+ function extractOutcomesFromTraceEvents(events) {
3238
+ const out = [];
3239
+ for (const event of events) {
3240
+ if (event.event === OUTCOME_LEGACY_EVENT) {
3241
+ out.push(fromOutcomeObservedEvent(event));
3361
3242
  }
3362
3243
  }
3363
- return earliest;
3244
+ return out.sort((a, b) => a.observedAt - b.observedAt || a.name.localeCompare(b.name));
3364
3245
  }
3365
- function latestEndWhenAllEnded(runs) {
3366
- if (runs.length === 0) return void 0;
3367
- let latest;
3368
- for (const run of runs) {
3369
- if (run.endedAt === void 0) return void 0;
3370
- if (latest === void 0 || run.endedAt > latest) latest = run.endedAt;
3246
+ function extractOutcomesFromPersistedEvents(events) {
3247
+ const out = [];
3248
+ for (const event of events) {
3249
+ const outcome = fromPersistedOutcome(event);
3250
+ if (outcome) out.push(outcome);
3371
3251
  }
3372
- return latest;
3252
+ return out.sort((a, b) => a.observedAt - b.observedAt || a.name.localeCompare(b.name));
3373
3253
  }
3374
- function pickExplicitStatus(runs) {
3375
- let best;
3376
- let bestPriority = 0;
3377
- for (const run of runs) {
3378
- const raw = run.metadata?.sessionStatus;
3379
- if (!isExplicitSessionStatus(raw)) continue;
3380
- const priority = EXPLICIT_STATUS_PRIORITY[raw] ?? 0;
3381
- if (priority > bestPriority) {
3382
- bestPriority = priority;
3383
- best = raw;
3384
- }
3254
+ function summarizeObservedOutcomes(outcomes) {
3255
+ const summary = {
3256
+ total: outcomes.length,
3257
+ passed: 0,
3258
+ failed: 0,
3259
+ unknown: 0,
3260
+ skipped: 0,
3261
+ outcomes: [...outcomes]
3262
+ };
3263
+ for (const outcome of outcomes) {
3264
+ summary[outcome.status] += 1;
3385
3265
  }
3386
- return best;
3266
+ return summary;
3387
3267
  }
3388
- function deriveLastError(runs) {
3389
- const errorRuns = runs.filter((run) => run.status === "error").sort((a, b) => activityMs(b) - activityMs(a));
3390
- const latest = errorRuns[0];
3391
- if (!latest) return void 0;
3392
- const meta = latest.metadata ?? {};
3393
- const message = typeof meta.errorMessage === "string" && meta.errorMessage.trim() !== "" ? meta.errorMessage.trim() : latest.name ?? latest.runId;
3394
- const code = typeof meta.errorCode === "string" && meta.errorCode.trim() !== "" ? meta.errorCode.trim() : void 0;
3395
- return { runId: latest.runId, message, code };
3268
+ function outcomesMatchingStatus(outcomes, statuses) {
3269
+ const set = new Set(statuses);
3270
+ return outcomes.filter((outcome) => set.has(outcome.status));
3396
3271
  }
3397
- function deriveCheckSummary(runs) {
3398
- let pass = 0;
3399
- let fail4 = 0;
3400
- let warn2 = 0;
3401
- let found = false;
3402
- for (const run of runs) {
3403
- const summary = run.metadata?.checkSummary;
3404
- if (!summary || typeof summary !== "object") continue;
3405
- const record = summary;
3406
- if (typeof record.pass === "number") {
3407
- pass += record.pass;
3408
- found = true;
3409
- }
3410
- if (typeof record.fail === "number") {
3411
- fail4 += record.fail;
3412
- found = true;
3413
- }
3414
- if (typeof record.warn === "number") {
3415
- warn2 += record.warn;
3416
- found = true;
3417
- }
3418
- }
3419
- return found ? { pass, fail: fail4, warn: warn2 } : void 0;
3272
+ function parseObservationFilter(value) {
3273
+ if (value === void 0 || value.trim() === "") return void 0;
3274
+ return parseObservedOutcomeStatus(value);
3420
3275
  }
3421
- function deriveObservationSummary(runs) {
3422
- for (const run of [...runs].sort((a, b) => activityMs(b) - activityMs(a))) {
3423
- const value = run.metadata?.observationSummary;
3424
- if (typeof value === "string" && value.trim() !== "") {
3425
- return value.trim();
3426
- }
3276
+
3277
+ // packages/core/src/outcomes/render.ts
3278
+ function renderObservedOutcomesMarkdown(summary) {
3279
+ if (summary.total === 0) {
3280
+ return "No observed outcomes recorded for this run.";
3427
3281
  }
3428
- return void 0;
3429
- }
3430
- function deriveSessionStatus(runs, options = {}) {
3431
- if (runs.length === 0) return "unknown";
3432
- if (runs.some((run) => run.status === "running")) return "running";
3433
- const explicit = pickExplicitStatus(runs);
3434
- if (explicit && explicit !== "running") return explicit;
3435
- if (runs.some((run) => run.status === "error")) return "error";
3436
- if (runs.every((run) => run.status === "success")) return "completed";
3437
- const nowMs = options.nowMs ?? Date.now();
3438
- const staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
3439
- const lastMs = latestActivityMs(runs);
3440
- if (lastMs > 0 && nowMs - lastMs > staleThresholdMs) return "stale";
3441
- return "unknown";
3442
- }
3443
- function enrichSessionSummary(summary, runs, options = {}) {
3444
- const sessionRuns = runs.filter((run) => summary.runIds.includes(run.runId)).sort((a, b) => a.runId.localeCompare(b.runId));
3445
- const startedAt = earliestStart(sessionRuns);
3446
- const endedAt = latestEndWhenAllEnded(sessionRuns);
3447
- const durationMs = startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0;
3448
- let correlationId;
3449
- let jobId;
3450
- let workflowId;
3451
- for (const run of sessionRuns) {
3452
- const meta = extractSessionWorkflowMetadata(run.metadata);
3453
- if (!correlationId && meta?.correlationId) correlationId = meta.correlationId;
3454
- if (!jobId && meta?.jobId) jobId = meta.jobId;
3455
- if (!workflowId && meta?.workflowName) workflowId = meta.workflowName;
3456
- else if (!workflowId && meta?.workflowStep) workflowId = meta.workflowStep;
3282
+ const lines = [
3283
+ `Total: ${summary.total} (passed ${summary.passed}, failed ${summary.failed}, unknown ${summary.unknown}, skipped ${summary.skipped})`,
3284
+ "",
3285
+ "| Name | Status | Expectation | Method |",
3286
+ "| --- | --- | --- | --- |"
3287
+ ];
3288
+ for (const outcome of summary.outcomes) {
3289
+ lines.push(
3290
+ `| ${outcome.name} | ${outcome.status} | ${outcome.expectation.replace(/\|/g, "\\|")} | ${outcome.method ?? "-"} |`
3291
+ );
3457
3292
  }
3458
- const lastMs = latestActivityMs(sessionRuns);
3459
- const lastActivity = lastMs > 0 ? new Date(lastMs).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
3460
- const retryCount2 = summary.retries.filter(
3461
- (retry) => retry.retryOf !== void 0 || (retry.attempt ?? 0) > 1
3462
- ).length;
3463
- return {
3464
- ...summary,
3465
- status: deriveSessionStatus(sessionRuns, options),
3466
- startedAt,
3467
- endedAt,
3468
- durationMs,
3469
- correlationId,
3470
- jobId,
3471
- workflowId,
3472
- lastError: deriveLastError(sessionRuns),
3473
- lastActivity,
3474
- retryCount: retryCount2,
3475
- observationSummary: deriveObservationSummary(sessionRuns),
3476
- checkSummary: deriveCheckSummary(sessionRuns)
3477
- };
3293
+ return lines.join("\n");
3294
+ }
3295
+ function renderObservedOutcomesHtml(summary) {
3296
+ if (summary.total === 0) {
3297
+ return "<p>No observed outcomes recorded for this run.</p>";
3298
+ }
3299
+ const rows = summary.outcomes.map((outcome) => {
3300
+ const esc = (v) => v.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3301
+ return `<tr><td>${esc(outcome.name)}</td><td>${esc(outcome.status)}</td><td>${esc(outcome.expectation)}</td><td>${esc(outcome.method ?? "-")}</td></tr>`;
3302
+ }).join("");
3303
+ return `<p>Total: ${summary.total} (passed ${summary.passed}, failed ${summary.failed}, unknown ${summary.unknown}, skipped ${summary.skipped})</p><table><thead><tr><th>Name</th><th>Status</th><th>Expectation</th><th>Method</th></tr></thead><tbody>${rows}</tbody></table>`;
3478
3304
  }
3479
3305
 
3480
- // packages/core/src/sessions/checks.ts
3481
- function emptySummary() {
3482
- return { passed: 0, failed: 0, warnings: 0, errors: 0, rulesEvaluated: 0 };
3306
+ // packages/core/src/search.ts
3307
+ function parseDurationFilter(expr) {
3308
+ const raw = expr.trim();
3309
+ const m = raw.match(/^(>=|<=|>|<)\s*(.+)$/);
3310
+ if (!m) {
3311
+ throw new Error(
3312
+ `Invalid --duration "${expr}". Use forms like >5s, >=500ms, <2m.`
3313
+ );
3314
+ }
3315
+ const op = m[1];
3316
+ const ms = parseDuration(m[2].trim());
3317
+ return { op, ms };
3483
3318
  }
3484
- function mergeSummary(target, source) {
3485
- return {
3486
- passed: target.passed + source.passed,
3487
- failed: target.failed + source.failed,
3488
- warnings: target.warnings + source.warnings,
3489
- errors: target.errors + source.errors,
3490
- rulesEvaluated: target.rulesEvaluated + source.rulesEvaluated
3491
- };
3319
+ function durationMatches(valueMs, filter) {
3320
+ if (valueMs === void 0 || !Number.isFinite(valueMs)) return false;
3321
+ switch (filter.op) {
3322
+ case ">":
3323
+ return valueMs > filter.ms;
3324
+ case ">=":
3325
+ return valueMs >= filter.ms;
3326
+ case "<":
3327
+ return valueMs < filter.ms;
3328
+ case "<=":
3329
+ return valueMs <= filter.ms;
3330
+ default:
3331
+ return false;
3332
+ }
3492
3333
  }
3493
- function sessionDiagnostic(code, message) {
3494
- return { code, message, severity: "error" };
3334
+ function normalizeStepTypeFilter(kind, type) {
3335
+ const v = (kind ?? type)?.trim().toLowerCase();
3336
+ return v && v !== "" ? v : void 0;
3495
3337
  }
3496
- function aggregateSessionCheckResults(perRun, scope) {
3497
- if (scope.notFound) {
3498
- return {
3499
- ok: false,
3500
- status: "error",
3501
- format: perRun[0]?.format ?? "agent-inspect-jsonl",
3502
- scopeKind: scope.scopeKind,
3503
- scopeLabel: scope.scopeLabel,
3504
- runIds: [],
3505
- runResults: [],
3506
- summary: { ...emptySummary(), errors: 1 },
3507
- findings: [],
3508
- diagnostics: [
3509
- sessionDiagnostic(
3510
- "AI_CHECK_INVALID_ARGUMENTS",
3511
- `${scope.scopeKind} not found: ${scope.scopeLabel}`
3512
- )
3513
- ],
3514
- ruleExecutions: [],
3515
- ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3516
- };
3338
+ function nameMatches(hay, needle) {
3339
+ return hay.toLowerCase().includes(needle.toLowerCase());
3340
+ }
3341
+ async function searchTraces(metas, options) {
3342
+ let filtered = filterTraces(metas, { since: options.since });
3343
+ const stepTypeFilter = normalizeStepTypeFilter(options.kind, options.type);
3344
+ const nameQuery = options.name?.trim();
3345
+ const toolQuery = options.tool?.trim();
3346
+ let durationFilter;
3347
+ if (options.duration) {
3348
+ durationFilter = parseDurationFilter(options.duration);
3517
3349
  }
3518
- if (scope.empty || perRun.length === 0) {
3519
- return {
3520
- ok: false,
3521
- status: "error",
3522
- format: perRun[0]?.format ?? "agent-inspect-jsonl",
3523
- scopeKind: scope.scopeKind,
3524
- scopeLabel: scope.scopeLabel,
3525
- runIds: scope.runIds,
3526
- runResults: [],
3527
- summary: { ...emptySummary(), errors: 1 },
3528
- findings: [],
3529
- diagnostics: [
3530
- sessionDiagnostic(
3531
- "AI_CHECK_TRACE_UNREADABLE",
3532
- `No readable traces in ${scope.scopeKind}: ${scope.scopeLabel}`
3533
- )
3534
- ],
3535
- ruleExecutions: [],
3536
- ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3537
- };
3350
+ const limit = options.limit ?? 50;
3351
+ const sessionId = options.session?.trim();
3352
+ const observationStatus2 = parseObservationFilter(options.observation);
3353
+ const hasContentFilter = Boolean(
3354
+ options.status || stepTypeFilter || nameQuery || toolQuery || durationFilter || observationStatus2
3355
+ );
3356
+ const results = [];
3357
+ const sessionLabel = sessionId && sessionId !== "" ? sessionId : void 0;
3358
+ if (!hasContentFilter) {
3359
+ for (const m of filtered) {
3360
+ results.push({
3361
+ runId: m.runId,
3362
+ runName: m.name,
3363
+ runStatus: m.status,
3364
+ timestamp: m.startedAt,
3365
+ durationMs: m.durationMs,
3366
+ matchReason: sessionLabel ? `trace in session ${sessionLabel}` : "trace in directory",
3367
+ matchedFields: sessionLabel ? ["run", "session"] : ["run"],
3368
+ filePath: m.filePath,
3369
+ ...sessionLabel ? { sessionId: sessionLabel } : {}
3370
+ });
3371
+ }
3372
+ results.sort((a, b) => {
3373
+ const ta = a.timestamp ?? 0;
3374
+ const tb = b.timestamp ?? 0;
3375
+ if (ta !== tb) return tb - ta;
3376
+ return a.runId.localeCompare(b.runId);
3377
+ });
3378
+ return results.slice(0, limit);
3538
3379
  }
3539
- let summary = emptySummary();
3540
- const findings = [];
3541
- const diagnostics = [];
3542
- const ruleExecutions = [];
3543
- const runResults = [];
3544
- for (const result of perRun) {
3545
- summary = mergeSummary(summary, result.summary);
3546
- findings.push(...result.findings);
3547
- diagnostics.push(...result.diagnostics);
3548
- ruleExecutions.push(...result.ruleExecutions ?? []);
3549
- if (result.runId) {
3550
- runResults.push({ runId: result.runId, status: result.status });
3380
+ for (const m of filtered) {
3381
+ if (options.status && m.status !== options.status) continue;
3382
+ let events = [];
3383
+ try {
3384
+ events = await readTraceEventsFromFile(m.filePath);
3385
+ } catch {
3386
+ continue;
3387
+ }
3388
+ if (events.length === 0) continue;
3389
+ const runMatches = matchRunLevel(m, {
3390
+ stepTypeFilter,
3391
+ nameQuery,
3392
+ toolQuery,
3393
+ durationFilter,
3394
+ statusFilter: options.status
3395
+ });
3396
+ results.push(...runMatches);
3397
+ const stepMatches = matchStepLevel(m, events, {
3398
+ stepTypeFilter,
3399
+ nameQuery,
3400
+ toolQuery,
3401
+ durationFilter,
3402
+ statusFilter: options.status
3403
+ });
3404
+ results.push(...stepMatches);
3405
+ if (observationStatus2) {
3406
+ const outcomes = extractOutcomesFromTraceEvents(events);
3407
+ const matched = outcomes.filter((outcome) => outcome.status === observationStatus2);
3408
+ for (const outcome of matched) {
3409
+ results.push({
3410
+ runId: m.runId,
3411
+ runName: m.name,
3412
+ runStatus: m.status,
3413
+ stepName: outcome.name,
3414
+ timestamp: outcome.observedAt,
3415
+ matchReason: `observation status=${outcome.status}`,
3416
+ matchedFields: ["outcome.status", "outcome.name"],
3417
+ filePath: m.filePath
3418
+ });
3419
+ }
3551
3420
  }
3552
3421
  }
3553
- runResults.sort((a, b) => a.runId.localeCompare(b.runId));
3554
- findings.sort((a, b) => {
3555
- const runCmp = (a.evidence[0]?.runId ?? "").localeCompare(
3556
- b.evidence[0]?.runId ?? ""
3557
- );
3558
- if (runCmp !== 0) return runCmp;
3559
- return a.ruleId.localeCompare(b.ruleId);
3560
- });
3561
- ruleExecutions.sort((a, b) => {
3562
- const runCmp = (a.runId ?? "").localeCompare(b.runId ?? "");
3422
+ results.sort((a, b) => {
3423
+ const ta = a.timestamp ?? 0;
3424
+ const tb = b.timestamp ?? 0;
3425
+ if (ta !== tb) return tb - ta;
3426
+ const runCmp = a.runId.localeCompare(b.runId);
3563
3427
  if (runCmp !== 0) return runCmp;
3564
- return a.ruleId.localeCompare(b.ruleId);
3428
+ return (a.stepName ?? "").localeCompare(b.stepName ?? "");
3565
3429
  });
3566
- const hasErrors = diagnostics.some((item) => item.severity === "error");
3567
- const status = hasErrors ? "error" : summary.failed > 0 ? "fail" : "pass";
3568
- return {
3569
- ok: status === "pass",
3570
- status,
3571
- format: perRun[0]?.format ?? "agent-inspect-jsonl",
3572
- scopeKind: scope.scopeKind,
3573
- scopeLabel: scope.scopeLabel,
3574
- runIds: scope.runIds,
3575
- runResults,
3576
- summary,
3577
- findings,
3578
- diagnostics,
3579
- ruleExecutions,
3580
- ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3581
- };
3430
+ return results.slice(0, limit);
3582
3431
  }
3583
-
3584
- // packages/core/src/sessions/index.ts
3585
- function compareRuns(a, b) {
3586
- const aStart = a.startedAt ?? 0;
3587
- const bStart = b.startedAt ?? 0;
3588
- if (aStart !== bStart) return aStart - bStart;
3589
- return a.runId.localeCompare(b.runId);
3432
+ function matchRunLevel(m, opts) {
3433
+ if (opts.stepTypeFilter || opts.toolQuery) return [];
3434
+ if (opts.statusFilter && m.status !== opts.statusFilter) return [];
3435
+ if (opts.nameQuery && !nameMatches(m.name ?? m.runId, opts.nameQuery)) {
3436
+ return [];
3437
+ }
3438
+ if (opts.durationFilter && !durationMatches(m.durationMs, opts.durationFilter)) {
3439
+ return [];
3440
+ }
3441
+ const fields = [];
3442
+ if (opts.statusFilter) fields.push("run.status");
3443
+ if (opts.nameQuery) fields.push("run.name");
3444
+ if (opts.durationFilter) fields.push("run.durationMs");
3445
+ if (fields.length === 0) return [];
3446
+ return [
3447
+ {
3448
+ runId: m.runId,
3449
+ runName: m.name,
3450
+ runStatus: m.status,
3451
+ timestamp: m.startedAt,
3452
+ durationMs: m.durationMs,
3453
+ matchReason: `run match: ${fields.join(", ")}`,
3454
+ matchedFields: fields,
3455
+ filePath: m.filePath
3456
+ }
3457
+ ];
3590
3458
  }
3591
- function buildGroups(runIds, metaByRunId) {
3592
- const byGroup = /* @__PURE__ */ new Map();
3593
- for (const runId of runIds) {
3594
- const groupId = metaByRunId.get(runId)?.groupId;
3595
- if (!groupId) continue;
3596
- const existing = byGroup.get(groupId);
3597
- if (existing) {
3598
- existing.runIds.push(runId);
3599
- } else {
3600
- byGroup.set(groupId, {
3601
- groupId,
3602
- parentGroupId: metaByRunId.get(runId)?.parentGroupId,
3603
- runIds: [runId]
3604
- });
3459
+ function matchStepLevel(m, events, opts) {
3460
+ const out = [];
3461
+ const started = /* @__PURE__ */ new Map();
3462
+ for (const e of events) {
3463
+ if (e.event === "step_started") {
3464
+ started.set(e.stepId, e);
3605
3465
  }
3606
3466
  }
3607
- return [...byGroup.values()].map((group) => ({
3608
- ...group,
3609
- runIds: [...group.runIds].sort((a, b) => a.localeCompare(b))
3610
- }));
3611
- }
3612
- function buildHandoffs(runIds, metaByRunId, warnings, sessionId) {
3613
- const edges = [];
3614
- const seen = /* @__PURE__ */ new Set();
3615
- const pushEdge = (edge) => {
3616
- const key = `${edge.from}->${edge.to}:${edge.confidence}`;
3617
- if (seen.has(key)) return;
3618
- seen.add(key);
3619
- edges.push(edge);
3620
- };
3621
- for (const runId of runIds) {
3622
- const meta = metaByRunId.get(runId);
3623
- if (!meta) continue;
3624
- if (meta.handoffFrom && meta.handoffTo) {
3625
- pushEdge({
3626
- from: meta.handoffFrom,
3627
- to: meta.handoffTo,
3628
- source: "manual",
3629
- confidence: "explicit"
3630
- });
3467
+ for (const e of events) {
3468
+ if (e.event !== "step_completed") continue;
3469
+ const c = e;
3470
+ const s = started.get(c.stepId);
3471
+ if (!s) continue;
3472
+ const fields = [];
3473
+ const stepType = s.type;
3474
+ if (opts.stepTypeFilter && stepType !== opts.stepTypeFilter) {
3631
3475
  continue;
3632
3476
  }
3633
- if (meta.handoffFrom) {
3634
- pushEdge({
3635
- from: meta.handoffFrom,
3636
- to: runId,
3637
- source: "manual",
3638
- confidence: "explicit"
3639
- });
3477
+ const hasStepFilters = opts.stepTypeFilter || opts.nameQuery || opts.toolQuery || opts.durationFilter || opts.statusFilter === "error" || opts.statusFilter === "success";
3478
+ if (!hasStepFilters) continue;
3479
+ if (opts.statusFilter === "error" && c.status === "error") {
3480
+ fields.push("step.status");
3481
+ } else if (opts.statusFilter === "success" && c.status === "success") {
3482
+ fields.push("step.status");
3483
+ } else if (opts.statusFilter === "error" || opts.statusFilter === "success") {
3484
+ continue;
3640
3485
  }
3641
- if (meta.handoffTo) {
3642
- pushEdge({
3643
- from: runId,
3644
- to: meta.handoffTo,
3645
- source: "manual",
3646
- confidence: "explicit"
3647
- });
3486
+ if (opts.nameQuery) {
3487
+ if (!nameMatches(s.name, opts.nameQuery)) continue;
3488
+ fields.push("step.name");
3648
3489
  }
3649
- if (meta.subAgentId && meta.parentGroupId && !meta.handoffFrom && !meta.handoffTo) {
3650
- pushEdge({
3651
- from: meta.parentGroupId,
3652
- to: meta.subAgentId,
3653
- source: "inferred",
3654
- confidence: "correlated"
3655
- });
3656
- warnings.push({
3657
- code: "ambiguous-handoff-endpoints",
3658
- message: "Handoff inferred from parentGroupId and subAgentId without explicit handoffFrom/handoffTo.",
3659
- runId,
3660
- sessionId
3661
- });
3490
+ if (opts.toolQuery) {
3491
+ const toolName2 = typeof s.metadata?.toolName === "string" ? s.metadata.toolName : s.name;
3492
+ if (!nameMatches(toolName2, opts.toolQuery)) continue;
3493
+ fields.push("step.tool");
3494
+ }
3495
+ if (opts.durationFilter) {
3496
+ if (!durationMatches(c.durationMs, opts.durationFilter)) continue;
3497
+ fields.push("step.durationMs");
3498
+ }
3499
+ if (opts.stepTypeFilter) {
3500
+ fields.push("step.type");
3662
3501
  }
3502
+ if (fields.length === 0) continue;
3503
+ out.push({
3504
+ runId: m.runId,
3505
+ runName: m.name,
3506
+ runStatus: m.status,
3507
+ stepId: c.stepId,
3508
+ stepName: s.name,
3509
+ stepType,
3510
+ timestamp: s.startTime ?? s.timestamp,
3511
+ durationMs: c.durationMs,
3512
+ matchReason: `step match: ${fields.join(", ")}`,
3513
+ matchedFields: fields,
3514
+ filePath: m.filePath
3515
+ });
3663
3516
  }
3664
- return edges.sort((a, b) => {
3665
- const from = a.from.localeCompare(b.from);
3666
- if (from !== 0) return from;
3667
- return a.to.localeCompare(b.to);
3668
- });
3517
+ return out;
3669
3518
  }
3670
- function buildRetries(runIds, metaByRunId, warnings, sessionId) {
3671
- const retries = [];
3672
- for (const runId of runIds) {
3673
- const meta = metaByRunId.get(runId);
3674
- if (!meta) continue;
3675
- if (meta.retryOf) {
3676
- retries.push({
3677
- runId,
3678
- retryOf: meta.retryOf,
3679
- attempt: meta.attempt,
3680
- source: "manual",
3681
- confidence: "explicit"
3682
- });
3683
- continue;
3684
- }
3685
- if (meta.attempt !== void 0 && meta.attempt > 1) {
3686
- retries.push({
3687
- runId,
3688
- attempt: meta.attempt,
3689
- source: "inferred",
3690
- confidence: "correlated"
3691
- });
3692
- warnings.push({
3693
- code: "ambiguous-retry-link",
3694
- message: "attempt > 1 without retryOf; retry link is correlated only.",
3695
- runId,
3696
- sessionId
3697
- });
3519
+ async function loadTraceMetadataList(_traceDir, fileNames, getPath) {
3520
+ const metas = [];
3521
+ for (const fileName of fileNames) {
3522
+ try {
3523
+ const filePath = getPath(fileName);
3524
+ const meta = await extractMetadata(filePath);
3525
+ metas.push(meta);
3526
+ } catch {
3698
3527
  }
3699
3528
  }
3700
- return retries.sort((a, b) => a.runId.localeCompare(b.runId));
3529
+ return metas;
3701
3530
  }
3702
- function buildCriticalPath(runs, handoffs) {
3703
- const runById = new Map(runs.map((run) => [run.runId, run]));
3704
- const explicitTargets = new Set(
3705
- handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.to)
3706
- );
3707
- const explicitSources = new Set(
3708
- handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.from)
3709
- );
3710
- const ordered = [...runs].sort(compareRuns);
3711
- const path14 = [];
3712
- const visited = /* @__PURE__ */ new Set();
3713
- const pushRun = (run, confidence, source) => {
3714
- if (visited.has(run.runId)) return;
3715
- visited.add(run.runId);
3716
- path14.push({
3717
- runId: run.runId,
3718
- name: run.name,
3719
- startedAt: run.startedAt,
3720
- durationMs: run.durationMs,
3721
- confidence,
3722
- source
3723
- });
3724
- };
3725
- for (const edge of handoffs) {
3726
- if (edge.confidence !== "explicit") continue;
3727
- const fromRun = [...runById.values()].find(
3728
- (run) => run.runId === edge.from || metaRunIdMatches(run, edge.from, runById)
3729
- );
3730
- const toRun = [...runById.values()].find(
3731
- (run) => run.runId === edge.to || metaRunIdMatches(run, edge.to, runById)
3732
- );
3733
- if (fromRun) pushRun(fromRun, "explicit", "manual");
3734
- if (toRun) pushRun(toRun, "explicit", "manual");
3531
+
3532
+ // packages/core/src/sessions/activity.ts
3533
+ function statusLine(session) {
3534
+ const name = session.workflowId ?? session.correlationId ?? session.sessionId;
3535
+ const status = session.status;
3536
+ if (session.lastError) {
3537
+ return `${name} session ${session.sessionId} failed at ${session.lastError.message}`;
3735
3538
  }
3736
- for (const run of ordered) {
3737
- if (visited.has(run.runId)) continue;
3738
- const confidence = explicitTargets.has(run.runId) || explicitSources.has(run.runId) ? "explicit" : "correlated";
3739
- pushRun(run, confidence, confidence === "explicit" ? "manual" : "inferred");
3539
+ if (session.observationSummary) {
3540
+ return `${name} session ${session.sessionId} ${status} with observation warning`;
3541
+ }
3542
+ return `${name} session ${session.sessionId} ${status}`;
3543
+ }
3544
+ function parseSinceMs(since, nowMs) {
3545
+ if (!since || since.trim() === "") return nowMs - 7 * 864e5;
3546
+ const trimmed = since.trim().toLowerCase();
3547
+ const match = /^(\d+)([smhd])$/.exec(trimmed);
3548
+ if (!match) return nowMs - 7 * 864e5;
3549
+ const amount = Number.parseInt(match[1], 10);
3550
+ const unit = match[2];
3551
+ const mult = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
3552
+ return nowMs - amount * mult;
3553
+ }
3554
+ function isFailed(status) {
3555
+ return status === "error";
3556
+ }
3557
+ function isStale(status) {
3558
+ return status === "stale";
3559
+ }
3560
+ function guardrailWarnings(session) {
3561
+ const summary = session.checkSummary;
3562
+ if (!summary) return 0;
3563
+ return summary.warn;
3564
+ }
3565
+ function buildActivitySummary(index, options = {}) {
3566
+ const nowMs = options.nowMs ?? Date.now();
3567
+ const sinceMs = parseSinceMs(options.since, nowMs);
3568
+ const sinceIso = new Date(sinceMs).toISOString();
3569
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 20;
3570
+ const inWindow = index.sessions.filter((session) => {
3571
+ const activityMs2 = Date.parse(session.lastActivity);
3572
+ return Number.isFinite(activityMs2) && activityMs2 >= sinceMs;
3573
+ });
3574
+ const entries = [...inWindow].sort((a, b) => Date.parse(b.lastActivity) - Date.parse(a.lastActivity)).slice(0, limit).map((session) => ({
3575
+ sessionId: session.sessionId,
3576
+ status: session.status,
3577
+ summary: statusLine(session),
3578
+ lastActivity: session.lastActivity,
3579
+ runCount: session.runIds.length
3580
+ }));
3581
+ let failed = 0;
3582
+ let stale = 0;
3583
+ let guardrailWarningTotal = 0;
3584
+ for (const session of inWindow) {
3585
+ if (isFailed(session.status)) failed += 1;
3586
+ if (isStale(session.status)) stale += 1;
3587
+ guardrailWarningTotal += guardrailWarnings(session);
3740
3588
  }
3741
- return path14;
3742
- }
3743
- function metaRunIdMatches(run, token, runById) {
3744
- const meta = extractSessionWorkflowMetadata(run.metadata);
3745
- return meta?.subAgentId === token || meta?.groupId === token || runById.has(token);
3589
+ return {
3590
+ since: sinceIso,
3591
+ sessions: inWindow.length,
3592
+ failed,
3593
+ stale,
3594
+ guardrailWarnings: guardrailWarningTotal,
3595
+ entries
3596
+ };
3746
3597
  }
3747
- function buildSessionIndex(inputRuns, options = {}) {
3748
- const warnings = [];
3749
- const runs = [...inputRuns].sort(compareRuns);
3750
- const metaByRunId = /* @__PURE__ */ new Map();
3751
- for (const run of runs) {
3752
- metaByRunId.set(run.runId, extractSessionWorkflowMetadata(run.metadata));
3753
- }
3754
- const sessionsByKey = /* @__PURE__ */ new Map();
3755
- const unscopedRunIds = [];
3756
- for (const run of runs) {
3757
- const meta = metaByRunId.get(run.runId);
3758
- const key = sessionKeyForRun(meta, {
3759
- correlateByGroupId: options.correlateByGroupId === true
3760
- });
3761
- if (!key) {
3762
- unscopedRunIds.push(run.runId);
3763
- continue;
3598
+ function renderActivitySummaryHuman(summary, options) {
3599
+ const lines = [];
3600
+ const nowMs = options?.nowMs ?? Date.now();
3601
+ const todayStart = new Date(nowMs);
3602
+ todayStart.setHours(0, 0, 0, 0);
3603
+ const todayMs = todayStart.getTime();
3604
+ const today = summary.entries.filter(
3605
+ (entry) => Date.parse(entry.lastActivity) >= todayMs
3606
+ );
3607
+ if (today.length > 0) {
3608
+ lines.push("Today");
3609
+ for (const entry of today) {
3610
+ lines.push(` ${entry.summary}`);
3764
3611
  }
3765
- const bucket = sessionsByKey.get(key) ?? [];
3766
- bucket.push(run);
3767
- sessionsByKey.set(key, bucket);
3612
+ lines.push("");
3768
3613
  }
3769
- const sessions = [...sessionsByKey.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([sessionId, sessionRuns]) => {
3770
- const runIds = sessionRuns.map((run) => run.runId).sort();
3771
- const handoffs = buildHandoffs(runIds, metaByRunId, warnings, sessionId);
3772
- const retries = buildRetries(runIds, metaByRunId, warnings, sessionId);
3773
- const groups = buildGroups(runIds, metaByRunId);
3774
- const criticalPath = buildCriticalPath(sessionRuns, handoffs);
3775
- const confidences = new Set(handoffs.map((edge) => edge.confidence));
3776
- if (confidences.has("explicit") && confidences.has("correlated")) {
3777
- warnings.push({
3778
- code: "mixed-confidence-group",
3779
- message: "Session aggregates explicit and correlated handoff edges.",
3780
- sessionId
3781
- });
3782
- }
3783
- return enrichSessionSummary(
3784
- {
3785
- sessionId,
3786
- runIds,
3787
- groups,
3788
- handoffs,
3789
- retries,
3790
- criticalPath
3791
- },
3792
- runs,
3793
- {
3794
- nowMs: options.nowMs,
3795
- staleThresholdMs: options.staleThresholdMs
3614
+ lines.push(`Since ${summary.since}`);
3615
+ lines.push(` ${summary.sessions} sessions`);
3616
+ lines.push(` ${summary.failed} failed`);
3617
+ lines.push(` ${summary.stale} stale`);
3618
+ lines.push(` ${summary.guardrailWarnings} guardrail warnings`);
3619
+ return lines.join("\n");
3620
+ }
3621
+
3622
+ // packages/core/src/sessions/load.ts
3623
+ async function enrichSessionRunRecord(meta) {
3624
+ let metadata;
3625
+ try {
3626
+ const events = await readTraceEventsFromFile(meta.filePath);
3627
+ for (const event of events) {
3628
+ if (event.event !== "run_started") continue;
3629
+ if (event.metadata && typeof event.metadata === "object") {
3630
+ metadata = event.metadata;
3796
3631
  }
3797
- );
3798
- });
3799
- if (sessions.length === 0 && runs.length > 0) {
3800
- warnings.push({
3801
- code: "missing-session-id",
3802
- message: "No sessionId (or correlated groupId) found on input runs."
3803
- });
3632
+ break;
3633
+ }
3634
+ } catch {
3804
3635
  }
3805
- warnings.sort((a, b) => {
3806
- const code = a.code.localeCompare(b.code);
3807
- if (code !== 0) return code;
3808
- return (a.runId ?? "").localeCompare(b.runId ?? "");
3809
- });
3810
3636
  return {
3811
- runs,
3812
- sessions,
3813
- unscopedRunIds: unscopedRunIds.sort(),
3814
- warnings
3637
+ runId: meta.runId,
3638
+ name: meta.name,
3639
+ status: meta.status,
3640
+ startedAt: meta.startedAt,
3641
+ endedAt: meta.endedAt,
3642
+ durationMs: meta.durationMs,
3643
+ filePath: meta.filePath,
3644
+ metadata
3815
3645
  };
3816
3646
  }
3647
+ async function loadSessionRunRecords(metas) {
3648
+ const out = [];
3649
+ for (const meta of metas) {
3650
+ out.push(await enrichSessionRunRecord(meta));
3651
+ }
3652
+ return out;
3653
+ }
3817
3654
 
3818
- // packages/core/src/sessions/scope.ts
3819
- function filterMetasBySessionScope(metas, records, options) {
3820
- const sessionId = options.sessionId?.trim();
3821
- const groupId = options.groupId?.trim();
3822
- const warnings = [];
3823
- if (sessionId) {
3824
- const index = buildSessionIndex(records, {
3825
- correlateByGroupId: options.correlateByGroupId === true
3826
- });
3827
- warnings.push(...index.warnings);
3828
- const session = index.sessions.find((item) => item.sessionId === sessionId);
3829
- if (!session) {
3830
- return {
3831
- metas: [],
3832
- scopeLabel: sessionId,
3833
- scopeKind: "session",
3834
- runIds: [],
3835
- warnings,
3836
- notFound: true
3837
- };
3838
- }
3839
- const runIdSet = new Set(session.runIds);
3840
- const filtered = metas.filter((meta) => runIdSet.has(meta.runId));
3841
- return {
3842
- metas: filtered,
3843
- scopeLabel: sessionId,
3844
- scopeKind: "session",
3845
- runIds: session.runIds,
3846
- warnings,
3847
- notFound: false
3848
- };
3655
+ // packages/core/src/sessions/metadata.ts
3656
+ function isNonEmptyString3(value) {
3657
+ return typeof value === "string" && value.trim() !== "";
3658
+ }
3659
+ function finitePositiveInt(value) {
3660
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
3661
+ return void 0;
3849
3662
  }
3850
- if (groupId) {
3851
- const runIds = records.filter((run) => extractSessionWorkflowMetadata(run.metadata)?.groupId === groupId).map((run) => run.runId).sort();
3852
- if (runIds.length === 0) {
3853
- return {
3854
- metas: [],
3855
- scopeLabel: groupId,
3856
- scopeKind: "group",
3857
- runIds: [],
3858
- warnings,
3859
- notFound: true
3860
- };
3663
+ return Math.trunc(value);
3664
+ }
3665
+ function extractSessionWorkflowMetadata(record) {
3666
+ if (!record) return void 0;
3667
+ const out = {};
3668
+ let found = false;
3669
+ const assignString = (key, value) => {
3670
+ if (isNonEmptyString3(value)) {
3671
+ out[key] = value.trim();
3672
+ found = true;
3861
3673
  }
3862
- const runIdSet = new Set(runIds);
3863
- return {
3864
- metas: metas.filter((meta) => runIdSet.has(meta.runId)),
3865
- scopeLabel: groupId,
3866
- scopeKind: "group",
3867
- runIds,
3868
- warnings,
3869
- notFound: false
3870
- };
3871
- }
3872
- return {
3873
- metas: [...metas],
3874
- scopeLabel: "",
3875
- scopeKind: "session",
3876
- runIds: [],
3877
- warnings,
3878
- notFound: false
3879
3674
  };
3675
+ assignString("sessionId", record.sessionId);
3676
+ assignString("conversationId", record.conversationId);
3677
+ assignString("groupId", record.groupId);
3678
+ assignString("parentGroupId", record.parentGroupId);
3679
+ assignString("retryOf", record.retryOf);
3680
+ assignString("retryReason", record.retryReason);
3681
+ assignString("handoffFrom", record.handoffFrom);
3682
+ assignString("handoffTo", record.handoffTo);
3683
+ assignString("subAgentId", record.subAgentId);
3684
+ assignString("subAgentName", record.subAgentName);
3685
+ assignString("jobId", record.jobId);
3686
+ assignString("queueName", record.queueName);
3687
+ assignString("workflowName", record.workflowName);
3688
+ assignString("workflowStep", record.workflowStep);
3689
+ assignString("toolCallId", record.toolCallId);
3690
+ assignString("mcpToolCallId", record.mcpToolCallId);
3691
+ assignString("linkedStepId", record.linkedStepId);
3692
+ assignString("operationId", record.operationId);
3693
+ assignString("attemptId", record.attemptId);
3694
+ assignString("fallbackOf", record.fallbackOf);
3695
+ assignString("idempotencyKey", record.idempotencyKey);
3696
+ assignString("correlationId", record.correlationId);
3697
+ assignString("requestId", record.requestId);
3698
+ assignString("decisionId", record.decisionId);
3699
+ const attempt = finitePositiveInt(record.attempt);
3700
+ if (attempt !== void 0) {
3701
+ out.attempt = attempt;
3702
+ found = true;
3703
+ }
3704
+ const attemptNumber = finitePositiveInt(record.attemptNumber);
3705
+ if (attemptNumber !== void 0) {
3706
+ out.attemptNumber = attemptNumber;
3707
+ found = true;
3708
+ }
3709
+ return found ? out : void 0;
3880
3710
  }
3881
- var KNOWN_EVENTS = /* @__PURE__ */ new Set([
3882
- "run_started",
3883
- "run_completed",
3884
- "step_started",
3885
- "step_completed"
3711
+ function sessionKeyForRun(meta, options) {
3712
+ if (meta?.sessionId) return meta.sessionId;
3713
+ if (options?.correlateByGroupId && meta?.groupId) {
3714
+ return `group:${meta.groupId}`;
3715
+ }
3716
+ return void 0;
3717
+ }
3718
+
3719
+ // packages/core/src/sessions/status.ts
3720
+ var DEFAULT_STALE_THRESHOLD_MS = 864e5;
3721
+ var EXPLICIT_STATUS_PRIORITY = {
3722
+ error: 5,
3723
+ waiting_input: 4,
3724
+ idle: 3,
3725
+ stale: 2,
3726
+ completed: 1
3727
+ };
3728
+ var EXPLICIT_SESSION_STATUSES = /* @__PURE__ */ new Set([
3729
+ "running",
3730
+ "waiting_input",
3731
+ "idle",
3732
+ "completed",
3733
+ "error",
3734
+ "stale",
3735
+ "unknown"
3886
3736
  ]);
3887
- function isRecord6(value) {
3888
- return typeof value === "object" && value !== null && !Array.isArray(value);
3737
+ function isExplicitSessionStatus(value) {
3738
+ return typeof value === "string" && EXPLICIT_SESSION_STATUSES.has(value);
3889
3739
  }
3890
- function safeParse(line) {
3891
- try {
3892
- return JSON.parse(line);
3893
- } catch {
3894
- return void 0;
3740
+ function activityMs(run) {
3741
+ return run.endedAt ?? run.startedAt ?? 0;
3742
+ }
3743
+ function latestActivityMs(runs) {
3744
+ let latest = 0;
3745
+ for (const run of runs) {
3746
+ const ms = activityMs(run);
3747
+ if (ms > latest) latest = ms;
3895
3748
  }
3749
+ return latest;
3896
3750
  }
3897
- async function isAgentInspectTrace(filePath) {
3898
- try {
3899
- const rl = createInterface({
3900
- input: createReadStream(filePath, { encoding: "utf8" }),
3901
- crlfDelay: Infinity
3902
- });
3903
- let checked = 0;
3904
- for await (const line of rl) {
3905
- const trimmed = line.trim();
3906
- if (trimmed === "") continue;
3907
- const parsed = safeParse(trimmed);
3908
- if (!parsed) continue;
3909
- if (!isRecord6(parsed)) continue;
3910
- checked += 1;
3911
- if (isTraceEvent(parsed)) return true;
3912
- const ev = parsed.event;
3913
- const runId = parsed.runId;
3914
- if (typeof ev === "string" && KNOWN_EVENTS.has(ev) && typeof runId === "string") {
3915
- return true;
3916
- }
3917
- if (checked >= 20) break;
3751
+ function earliestStart(runs) {
3752
+ let earliest;
3753
+ for (const run of runs) {
3754
+ if (run.startedAt === void 0) continue;
3755
+ if (earliest === void 0 || run.startedAt < earliest) {
3756
+ earliest = run.startedAt;
3918
3757
  }
3919
- return false;
3920
- } catch {
3921
- return false;
3922
3758
  }
3759
+ return earliest;
3923
3760
  }
3924
-
3925
- // packages/core/src/bundle/resolve.ts
3926
- function parseSinceCutoff(since) {
3927
- const trimmed = since.trim();
3928
- if (trimmed === "") {
3929
- throw new Error("--since requires a non-empty duration (e.g. 24h, 7d).");
3761
+ function latestEndWhenAllEnded(runs) {
3762
+ if (runs.length === 0) return void 0;
3763
+ let latest;
3764
+ for (const run of runs) {
3765
+ if (run.endedAt === void 0) return void 0;
3766
+ if (latest === void 0 || run.endedAt > latest) latest = run.endedAt;
3930
3767
  }
3931
- return Date.now() - parseDuration(trimmed);
3932
- }
3933
- function runActivityMs(run) {
3934
- if (run.startedAt !== void 0 && Number.isFinite(run.startedAt)) return run.startedAt;
3935
- if (run.endedAt !== void 0 && Number.isFinite(run.endedAt)) return run.endedAt;
3936
- return void 0;
3768
+ return latest;
3937
3769
  }
3938
- function runsInSinceWindow(runs, since) {
3939
- const cutoff = parseSinceCutoff(since);
3940
- const ids = [];
3770
+ function pickExplicitStatus(runs) {
3771
+ let best;
3772
+ let bestPriority = 0;
3941
3773
  for (const run of runs) {
3942
- const activity = runActivityMs(run);
3943
- if (activity !== void 0 && activity >= cutoff) {
3944
- ids.push(run.runId);
3774
+ const raw = run.metadata?.sessionStatus;
3775
+ if (!isExplicitSessionStatus(raw)) continue;
3776
+ const priority = EXPLICIT_STATUS_PRIORITY[raw] ?? 0;
3777
+ if (priority > bestPriority) {
3778
+ bestPriority = priority;
3779
+ best = raw;
3945
3780
  }
3946
3781
  }
3947
- return ids.sort((a, b) => a.localeCompare(b));
3782
+ return best;
3948
3783
  }
3949
- function findSession(index, sessionId) {
3950
- return index.sessions.find((session) => session.sessionId === sessionId);
3784
+ function deriveLastError(runs) {
3785
+ const errorRuns = runs.filter((run) => run.status === "error").sort((a, b) => activityMs(b) - activityMs(a));
3786
+ const latest = errorRuns[0];
3787
+ if (!latest) return void 0;
3788
+ const meta = latest.metadata ?? {};
3789
+ const message = typeof meta.errorMessage === "string" && meta.errorMessage.trim() !== "" ? meta.errorMessage.trim() : latest.name ?? latest.runId;
3790
+ const code = typeof meta.errorCode === "string" && meta.errorCode.trim() !== "" ? meta.errorCode.trim() : void 0;
3791
+ return { runId: latest.runId, message, code };
3951
3792
  }
3952
- function resolveBundleRunIds(index, runs, options) {
3953
- const runId = options.runId?.trim();
3954
- const sessionId = options.sessionId?.trim();
3955
- const since = options.since?.trim();
3956
- const modes = [runId ? 1 : 0, sessionId ? 1 : 0, since ? 1 : 0].reduce((a, b) => a + b, 0);
3957
- if (modes === 0) {
3958
- throw new Error(
3959
- "bundle requires a run id, --session <sessionId>, or --since <duration>."
3960
- );
3961
- }
3962
- if (modes > 1) {
3963
- throw new Error(
3964
- "bundle accepts only one target: a run id, --session, or --since (not combined)."
3965
- );
3793
+ function deriveCheckSummary(runs) {
3794
+ let pass = 0;
3795
+ let fail4 = 0;
3796
+ let warn2 = 0;
3797
+ let found = false;
3798
+ for (const run of runs) {
3799
+ const summary = run.metadata?.checkSummary;
3800
+ if (!summary || typeof summary !== "object") continue;
3801
+ const record = summary;
3802
+ if (typeof record.pass === "number") {
3803
+ pass += record.pass;
3804
+ found = true;
3805
+ }
3806
+ if (typeof record.fail === "number") {
3807
+ fail4 += record.fail;
3808
+ found = true;
3809
+ }
3810
+ if (typeof record.warn === "number") {
3811
+ warn2 += record.warn;
3812
+ found = true;
3813
+ }
3966
3814
  }
3967
- if (runId) {
3968
- const known = runs.some((run) => run.runId === runId);
3969
- if (!known) {
3970
- throw new Error(`Run "${runId}" was not found in the trace directory.`);
3815
+ return found ? { pass, fail: fail4, warn: warn2 } : void 0;
3816
+ }
3817
+ function deriveObservationSummary(runs) {
3818
+ for (const run of [...runs].sort((a, b) => activityMs(b) - activityMs(a))) {
3819
+ const value = run.metadata?.observationSummary;
3820
+ if (typeof value === "string" && value.trim() !== "") {
3821
+ return value.trim();
3971
3822
  }
3972
- return { runIds: [runId] };
3973
3823
  }
3974
- if (sessionId) {
3975
- const session = findSession(index, sessionId);
3976
- if (!session) {
3977
- throw new Error(`Session "${sessionId}" was not found.`);
3978
- }
3979
- if (session.runIds.length === 0) {
3980
- throw new Error(`Session "${sessionId}" has no runs to bundle.`);
3981
- }
3824
+ return void 0;
3825
+ }
3826
+ function deriveSessionStatus(runs, options = {}) {
3827
+ if (runs.length === 0) return "unknown";
3828
+ if (runs.some((run) => run.status === "running")) return "running";
3829
+ const explicit = pickExplicitStatus(runs);
3830
+ if (explicit && explicit !== "running") return explicit;
3831
+ if (runs.some((run) => run.status === "error")) return "error";
3832
+ if (runs.every((run) => run.status === "success")) return "completed";
3833
+ const nowMs = options.nowMs ?? Date.now();
3834
+ const staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
3835
+ const lastMs = latestActivityMs(runs);
3836
+ if (lastMs > 0 && nowMs - lastMs > staleThresholdMs) return "stale";
3837
+ return "unknown";
3838
+ }
3839
+ function enrichSessionSummary(summary, runs, options = {}) {
3840
+ const sessionRuns = runs.filter((run) => summary.runIds.includes(run.runId)).sort((a, b) => a.runId.localeCompare(b.runId));
3841
+ const startedAt = earliestStart(sessionRuns);
3842
+ const endedAt = latestEndWhenAllEnded(sessionRuns);
3843
+ const durationMs = startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0;
3844
+ let correlationId;
3845
+ let jobId;
3846
+ let workflowId;
3847
+ for (const run of sessionRuns) {
3848
+ const meta = extractSessionWorkflowMetadata(run.metadata);
3849
+ if (!correlationId && meta?.correlationId) correlationId = meta.correlationId;
3850
+ if (!jobId && meta?.jobId) jobId = meta.jobId;
3851
+ if (!workflowId && meta?.workflowName) workflowId = meta.workflowName;
3852
+ else if (!workflowId && meta?.workflowStep) workflowId = meta.workflowStep;
3853
+ }
3854
+ const lastMs = latestActivityMs(sessionRuns);
3855
+ const lastActivity = lastMs > 0 ? new Date(lastMs).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
3856
+ const retryCount2 = summary.retries.filter(
3857
+ (retry) => retry.retryOf !== void 0 || (retry.attempt ?? 0) > 1
3858
+ ).length;
3859
+ return {
3860
+ ...summary,
3861
+ status: deriveSessionStatus(sessionRuns, options),
3862
+ startedAt,
3863
+ endedAt,
3864
+ durationMs,
3865
+ correlationId,
3866
+ jobId,
3867
+ workflowId,
3868
+ lastError: deriveLastError(sessionRuns),
3869
+ lastActivity,
3870
+ retryCount: retryCount2,
3871
+ observationSummary: deriveObservationSummary(sessionRuns),
3872
+ checkSummary: deriveCheckSummary(sessionRuns)
3873
+ };
3874
+ }
3875
+
3876
+ // packages/core/src/sessions/checks.ts
3877
+ function emptySummary() {
3878
+ return {
3879
+ passed: 0,
3880
+ failed: 0,
3881
+ warnings: 0,
3882
+ errors: 0,
3883
+ rulesEvaluated: 0,
3884
+ rulesPassed: 0,
3885
+ rulesWarning: 0,
3886
+ rulesFailed: 0,
3887
+ rulesError: 0
3888
+ };
3889
+ }
3890
+ function mergeSummary(target, source) {
3891
+ return {
3892
+ passed: target.passed + source.passed,
3893
+ failed: target.failed + source.failed,
3894
+ warnings: target.warnings + source.warnings,
3895
+ errors: target.errors + source.errors,
3896
+ rulesEvaluated: target.rulesEvaluated + source.rulesEvaluated,
3897
+ rulesPassed: target.rulesPassed + source.rulesPassed,
3898
+ rulesWarning: target.rulesWarning + source.rulesWarning,
3899
+ rulesFailed: target.rulesFailed + source.rulesFailed,
3900
+ rulesError: target.rulesError + source.rulesError
3901
+ };
3902
+ }
3903
+ function sessionDiagnostic(code, message) {
3904
+ return { code, message, severity: "error" };
3905
+ }
3906
+ function aggregateSessionCheckResults(perRun, scope) {
3907
+ if (scope.notFound) {
3908
+ return {
3909
+ ok: false,
3910
+ status: "error",
3911
+ format: perRun[0]?.format ?? "agent-inspect-jsonl",
3912
+ scopeKind: scope.scopeKind,
3913
+ scopeLabel: scope.scopeLabel,
3914
+ runIds: [],
3915
+ runResults: [],
3916
+ summary: { ...emptySummary(), errors: 1 },
3917
+ findings: [],
3918
+ diagnostics: [
3919
+ sessionDiagnostic(
3920
+ "AI_CHECK_INVALID_ARGUMENTS",
3921
+ `${scope.scopeKind} not found: ${scope.scopeLabel}`
3922
+ )
3923
+ ],
3924
+ ruleExecutions: [],
3925
+ ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3926
+ };
3927
+ }
3928
+ if (scope.empty || perRun.length === 0) {
3982
3929
  return {
3983
- runIds: [...session.runIds].sort((a, b) => a.localeCompare(b)),
3984
- sessionId
3930
+ ok: false,
3931
+ status: "error",
3932
+ format: perRun[0]?.format ?? "agent-inspect-jsonl",
3933
+ scopeKind: scope.scopeKind,
3934
+ scopeLabel: scope.scopeLabel,
3935
+ runIds: scope.runIds,
3936
+ runResults: [],
3937
+ summary: { ...emptySummary(), errors: 1 },
3938
+ findings: [],
3939
+ diagnostics: [
3940
+ sessionDiagnostic(
3941
+ "AI_CHECK_TRACE_UNREADABLE",
3942
+ `No readable traces in ${scope.scopeKind}: ${scope.scopeLabel}`
3943
+ )
3944
+ ],
3945
+ ruleExecutions: [],
3946
+ ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
3985
3947
  };
3986
3948
  }
3987
- const runIds = runsInSinceWindow(runs, since);
3988
- if (runIds.length === 0) {
3989
- throw new Error(`No runs matched --since ${since}.`);
3949
+ let summary = emptySummary();
3950
+ const findings = [];
3951
+ const diagnostics = [];
3952
+ const ruleExecutions = [];
3953
+ const runResults = [];
3954
+ for (const result of perRun) {
3955
+ summary = mergeSummary(summary, result.summary);
3956
+ findings.push(...result.findings);
3957
+ diagnostics.push(...result.diagnostics);
3958
+ ruleExecutions.push(...result.ruleExecutions ?? []);
3959
+ if (result.runId) {
3960
+ runResults.push({ runId: result.runId, status: result.status });
3961
+ }
3990
3962
  }
3991
- return { runIds, since };
3992
- }
3993
-
3994
- // packages/core/src/bundle/safety-status.ts
3995
- function aggregateBundleSafeStatus(statuses) {
3996
- if (statuses.length === 0) return "UNKNOWN";
3997
- if (statuses.some((status) => status === "UNSAFE")) return "UNSAFE";
3998
- if (statuses.some((status) => status === "UNKNOWN")) return "UNKNOWN";
3999
- if (statuses.some((status) => status === "SAFE WITH WARNINGS")) return "SAFE WITH WARNINGS";
4000
- return "SAFE";
4001
- }
4002
- function toMetadataSafeStatus(status) {
4003
- if (status === "SAFE WITH WARNINGS") return "SAFE_WITH_WARNINGS";
4004
- return status;
4005
- }
4006
- function bundleFailsOnSafety(status, allowUnsafe) {
4007
- if (allowUnsafe) return false;
4008
- return status === "UNSAFE" || status === "UNKNOWN";
4009
- }
4010
-
4011
- // packages/core/src/bundle/manifest.ts
4012
- var BUNDLE_NOTE = "Generated locally by AgentInspect. Bundles are derived copies for review \u2014 not compliance or security certification. Review before sharing.";
4013
- var PLACEHOLDER_NOTE = "No eval or performance artifacts were requested for this bundle.";
4014
- function buildBundleMetadata(parts) {
4015
- const aggregate = aggregateBundleSafeStatus(
4016
- parts.checks.runs.map((run) => run.status)
4017
- );
4018
- return {
4019
- createdAt: parts.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4020
- agentInspectVersion: parts.agentInspectVersion,
4021
- redactionProfile: parts.profile,
4022
- sourceTraceCount: parts.resolve.runIds.length,
4023
- runIds: [...parts.resolve.runIds],
4024
- safeStatus: toMetadataSafeStatus(aggregate),
4025
- files: [...parts.files].sort((a, b) => a.localeCompare(b)),
4026
- note: BUNDLE_NOTE,
4027
- ...parts.resolve.sessionId !== void 0 ? { sessionId: parts.resolve.sessionId } : {},
4028
- ...parts.resolve.since !== void 0 ? { since: parts.resolve.since } : {}
4029
- };
4030
- }
4031
- function buildPlaceholderArtifact() {
3963
+ runResults.sort((a, b) => a.runId.localeCompare(b.runId));
3964
+ findings.sort((a, b) => {
3965
+ const runCmp = (a.evidence[0]?.runId ?? "").localeCompare(
3966
+ b.evidence[0]?.runId ?? ""
3967
+ );
3968
+ if (runCmp !== 0) return runCmp;
3969
+ return a.ruleId.localeCompare(b.ruleId);
3970
+ });
3971
+ ruleExecutions.sort((a, b) => {
3972
+ const runCmp = (a.runId ?? "").localeCompare(b.runId ?? "");
3973
+ if (runCmp !== 0) return runCmp;
3974
+ return a.ruleId.localeCompare(b.ruleId);
3975
+ });
3976
+ const hasErrors = diagnostics.some((item) => item.severity === "error");
3977
+ const status = hasErrors ? "error" : summary.failed > 0 ? "fail" : "pass";
4032
3978
  return {
4033
- status: "not_requested",
4034
- note: PLACEHOLDER_NOTE
3979
+ ok: status === "pass",
3980
+ status,
3981
+ format: perRun[0]?.format ?? "agent-inspect-jsonl",
3982
+ scopeKind: scope.scopeKind,
3983
+ scopeLabel: scope.scopeLabel,
3984
+ runIds: scope.runIds,
3985
+ runResults,
3986
+ summary,
3987
+ findings,
3988
+ diagnostics,
3989
+ ruleExecutions,
3990
+ ...scope.sessionWarnings?.length ? { sessionWarnings: [...scope.sessionWarnings] } : {}
4035
3991
  };
4036
3992
  }
4037
3993
 
4038
- // packages/core/src/bundle/summary.ts
4039
- function markdownTable(rows) {
4040
- const lines = ["| Field | Value |", "| --- | --- |"];
4041
- for (const [key, value] of rows) {
4042
- lines.push(`| ${key} | ${value ?? "unknown"} |`);
3994
+ // packages/core/src/sessions/index.ts
3995
+ function compareRuns(a, b) {
3996
+ const aStart = a.startedAt ?? 0;
3997
+ const bStart = b.startedAt ?? 0;
3998
+ if (aStart !== bStart) return aStart - bStart;
3999
+ return a.runId.localeCompare(b.runId);
4000
+ }
4001
+ function buildGroups(runIds, metaByRunId) {
4002
+ const byGroup = /* @__PURE__ */ new Map();
4003
+ for (const runId of runIds) {
4004
+ const groupId = metaByRunId.get(runId)?.groupId;
4005
+ if (!groupId) continue;
4006
+ const existing = byGroup.get(groupId);
4007
+ if (existing) {
4008
+ existing.runIds.push(runId);
4009
+ } else {
4010
+ byGroup.set(groupId, {
4011
+ groupId,
4012
+ parentGroupId: metaByRunId.get(runId)?.parentGroupId,
4013
+ runIds: [runId]
4014
+ });
4015
+ }
4043
4016
  }
4044
- return lines.join("\n");
4017
+ return [...byGroup.values()].map((group) => ({
4018
+ ...group,
4019
+ runIds: [...group.runIds].sort((a, b) => a.localeCompare(b))
4020
+ }));
4045
4021
  }
4046
- function buildBundleSummaryMarkdown(parts) {
4047
- const { metadata, checks, redaction } = parts;
4048
- const lines = [
4049
- "# AgentInspect trace bundle",
4050
- "",
4051
- metadata.note,
4052
- "",
4053
- "## Overview",
4054
- "",
4055
- markdownTable([
4056
- ["Created", metadata.createdAt],
4057
- ["AgentInspect", metadata.agentInspectVersion],
4058
- ["Redaction profile", metadata.redactionProfile],
4059
- ["Safe status", metadata.safeStatus],
4060
- ["Source traces", metadata.sourceTraceCount],
4061
- ["Runs", metadata.runIds.join(", ")],
4062
- ...metadata.sessionId ? [["Session", metadata.sessionId]] : [],
4063
- ...metadata.since ? [["Since", metadata.since]] : []
4064
- ]),
4065
- "",
4066
- "## Safety checks",
4067
- "",
4068
- `Aggregate: **${checks.aggregateStatus}**`,
4069
- ""
4070
- ];
4071
- for (const run of checks.runs) {
4072
- const source = run.sourceStatus !== void 0 && run.sourceStatus !== run.status ? `; source ${run.sourceStatus}` : "";
4073
- lines.push(
4074
- `- \`${run.runId}\`: artifact ${run.status}${source} (${run.findings} finding(s), ${run.errors} error(s), ${run.warnings} warning(s))`
4075
- );
4022
+ function buildHandoffs(runIds, metaByRunId, warnings, sessionId) {
4023
+ const edges = [];
4024
+ const seen = /* @__PURE__ */ new Set();
4025
+ const pushEdge = (edge) => {
4026
+ const key = `${edge.from}->${edge.to}:${edge.confidence}`;
4027
+ if (seen.has(key)) return;
4028
+ seen.add(key);
4029
+ edges.push(edge);
4030
+ };
4031
+ for (const runId of runIds) {
4032
+ const meta = metaByRunId.get(runId);
4033
+ if (!meta) continue;
4034
+ if (meta.handoffFrom && meta.handoffTo) {
4035
+ pushEdge({
4036
+ from: meta.handoffFrom,
4037
+ to: meta.handoffTo,
4038
+ source: "manual",
4039
+ confidence: "explicit"
4040
+ });
4041
+ continue;
4042
+ }
4043
+ if (meta.handoffFrom) {
4044
+ pushEdge({
4045
+ from: meta.handoffFrom,
4046
+ to: runId,
4047
+ source: "manual",
4048
+ confidence: "explicit"
4049
+ });
4050
+ }
4051
+ if (meta.handoffTo) {
4052
+ pushEdge({
4053
+ from: runId,
4054
+ to: meta.handoffTo,
4055
+ source: "manual",
4056
+ confidence: "explicit"
4057
+ });
4058
+ }
4059
+ if (meta.subAgentId && meta.parentGroupId && !meta.handoffFrom && !meta.handoffTo) {
4060
+ pushEdge({
4061
+ from: meta.parentGroupId,
4062
+ to: meta.subAgentId,
4063
+ source: "inferred",
4064
+ confidence: "correlated"
4065
+ });
4066
+ warnings.push({
4067
+ code: "ambiguous-handoff-endpoints",
4068
+ message: "Handoff inferred from parentGroupId and subAgentId without explicit handoffFrom/handoffTo.",
4069
+ runId,
4070
+ sessionId
4071
+ });
4072
+ }
4076
4073
  }
4077
- lines.push("", "## Redaction", "", `Total findings: ${redaction.totalFindings}`, "");
4078
- for (const run of redaction.runs) {
4079
- const detectors = run.detectors.length > 0 ? run.detectors.join(", ") : "none";
4080
- lines.push(`- \`${run.runId}\`: ${run.findings} finding(s); detectors: ${detectors}`);
4074
+ return edges.sort((a, b) => {
4075
+ const from = a.from.localeCompare(b.from);
4076
+ if (from !== 0) return from;
4077
+ return a.to.localeCompare(b.to);
4078
+ });
4079
+ }
4080
+ function buildRetries(runIds, metaByRunId, warnings, sessionId) {
4081
+ const retries = [];
4082
+ for (const runId of runIds) {
4083
+ const meta = metaByRunId.get(runId);
4084
+ if (!meta) continue;
4085
+ if (meta.retryOf) {
4086
+ retries.push({
4087
+ runId,
4088
+ retryOf: meta.retryOf,
4089
+ attempt: meta.attempt,
4090
+ source: "manual",
4091
+ confidence: "explicit"
4092
+ });
4093
+ continue;
4094
+ }
4095
+ if (meta.attempt !== void 0 && meta.attempt > 1) {
4096
+ retries.push({
4097
+ runId,
4098
+ attempt: meta.attempt,
4099
+ source: "inferred",
4100
+ confidence: "correlated"
4101
+ });
4102
+ warnings.push({
4103
+ code: "ambiguous-retry-link",
4104
+ message: "attempt > 1 without retryOf; retry link is correlated only.",
4105
+ runId,
4106
+ sessionId
4107
+ });
4108
+ }
4081
4109
  }
4082
- lines.push(
4083
- "",
4084
- "## Files",
4085
- "",
4086
- ...metadata.files.map((file) => `- \`${file}\``),
4087
- "",
4088
- "_Review every generated artifact before sharing outside your team._",
4089
- ""
4090
- );
4091
- return lines.join("\n");
4110
+ return retries.sort((a, b) => a.runId.localeCompare(b.runId));
4092
4111
  }
4093
- function sanitizeBundleRunId(runId) {
4094
- let safe = typeof runId === "string" && runId.trim() !== "" ? runId.trim() : "run_unknown";
4095
- safe = path7.basename(safe);
4096
- safe = safe.replace(/[^a-zA-Z0-9._-]+/g, "_");
4097
- if (safe === "" || safe === "." || safe === "..") {
4098
- safe = "run_unknown";
4112
+ function buildCriticalPath(runs, handoffs) {
4113
+ const runById = new Map(runs.map((run) => [run.runId, run]));
4114
+ const explicitTargets = new Set(
4115
+ handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.to)
4116
+ );
4117
+ const explicitSources = new Set(
4118
+ handoffs.filter((edge) => edge.confidence === "explicit").map((edge) => edge.from)
4119
+ );
4120
+ const ordered = [...runs].sort(compareRuns);
4121
+ const path14 = [];
4122
+ const visited = /* @__PURE__ */ new Set();
4123
+ const pushRun = (run, confidence, source) => {
4124
+ if (visited.has(run.runId)) return;
4125
+ visited.add(run.runId);
4126
+ path14.push({
4127
+ runId: run.runId,
4128
+ name: run.name,
4129
+ startedAt: run.startedAt,
4130
+ durationMs: run.durationMs,
4131
+ confidence,
4132
+ source
4133
+ });
4134
+ };
4135
+ for (const edge of handoffs) {
4136
+ if (edge.confidence !== "explicit") continue;
4137
+ const fromRun = [...runById.values()].find(
4138
+ (run) => run.runId === edge.from || metaRunIdMatches(run, edge.from, runById)
4139
+ );
4140
+ const toRun = [...runById.values()].find(
4141
+ (run) => run.runId === edge.to || metaRunIdMatches(run, edge.to, runById)
4142
+ );
4143
+ if (fromRun) pushRun(fromRun, "explicit", "manual");
4144
+ if (toRun) pushRun(toRun, "explicit", "manual");
4099
4145
  }
4100
- return safe;
4146
+ for (const run of ordered) {
4147
+ if (visited.has(run.runId)) continue;
4148
+ const confidence = explicitTargets.has(run.runId) || explicitSources.has(run.runId) ? "explicit" : "correlated";
4149
+ pushRun(run, confidence, confidence === "explicit" ? "manual" : "inferred");
4150
+ }
4151
+ return path14;
4101
4152
  }
4102
- function bundleRunAssetRelativePath(runId, extension) {
4103
- const safe = sanitizeBundleRunId(runId);
4104
- const ext = extension.startsWith(".") ? extension : `.${extension}`;
4105
- return path7.posix.join("assets", "runs", `${safe}${ext}`);
4153
+ function metaRunIdMatches(run, token, runById) {
4154
+ const meta = extractSessionWorkflowMetadata(run.metadata);
4155
+ return meta?.subAgentId === token || meta?.groupId === token || runById.has(token);
4106
4156
  }
4107
- function assertBundlePathContained(outputDir, relativePath) {
4108
- const base = path7.resolve(outputDir);
4109
- const resolved = path7.resolve(base, relativePath);
4110
- if (resolved !== base && !resolved.startsWith(base + path7.sep)) {
4111
- throw new Error(`Bundle path escapes output directory: ${relativePath}`);
4157
+ function buildSessionIndex(inputRuns, options = {}) {
4158
+ const warnings = [];
4159
+ const runs = [...inputRuns].sort(compareRuns);
4160
+ const metaByRunId = /* @__PURE__ */ new Map();
4161
+ for (const run of runs) {
4162
+ metaByRunId.set(run.runId, extractSessionWorkflowMetadata(run.metadata));
4112
4163
  }
4113
- return resolved;
4114
- }
4115
- function normalizeBundleOutputPath(out, options) {
4116
- const trimmed = out.trim();
4117
- if (trimmed === "") {
4118
- throw new Error("--out requires a non-empty path.");
4164
+ const sessionsByKey = /* @__PURE__ */ new Map();
4165
+ const unscopedRunIds = [];
4166
+ for (const run of runs) {
4167
+ const meta = metaByRunId.get(run.runId);
4168
+ const key = sessionKeyForRun(meta, {
4169
+ correlateByGroupId: options.correlateByGroupId === true
4170
+ });
4171
+ if (!key) {
4172
+ unscopedRunIds.push(run.runId);
4173
+ continue;
4174
+ }
4175
+ const bucket = sessionsByKey.get(key) ?? [];
4176
+ bucket.push(run);
4177
+ sessionsByKey.set(key, bucket);
4119
4178
  }
4120
- const resolved = path7.resolve(trimmed);
4121
- if (options?.preserveZipExtension !== true && resolved.toLowerCase().endsWith(".zip")) {
4122
- return resolved.slice(0, -4);
4179
+ const sessions = [...sessionsByKey.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([sessionId, sessionRuns]) => {
4180
+ const runIds = sessionRuns.map((run) => run.runId).sort();
4181
+ const handoffs = buildHandoffs(runIds, metaByRunId, warnings, sessionId);
4182
+ const retries = buildRetries(runIds, metaByRunId, warnings, sessionId);
4183
+ const groups = buildGroups(runIds, metaByRunId);
4184
+ const criticalPath = buildCriticalPath(sessionRuns, handoffs);
4185
+ const confidences = new Set(handoffs.map((edge) => edge.confidence));
4186
+ if (confidences.has("explicit") && confidences.has("correlated")) {
4187
+ warnings.push({
4188
+ code: "mixed-confidence-group",
4189
+ message: "Session aggregates explicit and correlated handoff edges.",
4190
+ sessionId
4191
+ });
4192
+ }
4193
+ return enrichSessionSummary(
4194
+ {
4195
+ sessionId,
4196
+ runIds,
4197
+ groups,
4198
+ handoffs,
4199
+ retries,
4200
+ criticalPath
4201
+ },
4202
+ runs,
4203
+ {
4204
+ nowMs: options.nowMs,
4205
+ staleThresholdMs: options.staleThresholdMs
4206
+ }
4207
+ );
4208
+ });
4209
+ if (sessions.length === 0 && runs.length > 0) {
4210
+ warnings.push({
4211
+ code: "missing-session-id",
4212
+ message: "No sessionId (or correlated groupId) found on input runs."
4213
+ });
4123
4214
  }
4124
- return resolved;
4125
- }
4126
- function defaultBundleOutputPath(runIds) {
4127
- const label = runIds.length === 1 ? sanitizeBundleRunId(runIds[0]) : `multi-${runIds.length}`;
4128
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4129
- return path7.resolve(`agent-inspect-bundle-${label}-${stamp}`);
4215
+ warnings.sort((a, b) => {
4216
+ const code = a.code.localeCompare(b.code);
4217
+ if (code !== 0) return code;
4218
+ return (a.runId ?? "").localeCompare(b.runId ?? "");
4219
+ });
4220
+ return {
4221
+ runs,
4222
+ sessions,
4223
+ unscopedRunIds: unscopedRunIds.sort(),
4224
+ warnings
4225
+ };
4130
4226
  }
4131
4227
 
4132
- // packages/core/src/evidence/types.ts
4133
- var EVIDENCE_FORMAT_VERSION = "1.0";
4134
- var EVIDENCE_ASSESSMENT_NOTE = "Best-effort local safety verification only; not a compliance certification.";
4135
- var EVIDENCE_MANIFEST_FILENAME = "evidence.json";
4136
- var EVIDENCE_RESOLVED_CONTRACT_FILENAME = "contract.resolved.json";
4137
- var SHA256_RE = /^[a-f0-9]{64}$/i;
4138
- function sha256Hex(data) {
4139
- const hash = createHash("sha256");
4140
- if (typeof data === "string") {
4141
- hash.update(data, "utf8");
4142
- } else {
4143
- hash.update(data);
4144
- }
4145
- return hash.digest("hex");
4146
- }
4147
- function isSha256Hex(value) {
4148
- return SHA256_RE.test(value);
4149
- }
4150
- function sha256Equals(expected, actual) {
4151
- if (!isSha256Hex(expected) || !isSha256Hex(actual)) {
4152
- return false;
4228
+ // packages/core/src/sessions/scope.ts
4229
+ function filterMetasBySessionScope(metas, records, options) {
4230
+ const sessionId = options.sessionId?.trim();
4231
+ const groupId = options.groupId?.trim();
4232
+ const warnings = [];
4233
+ if (sessionId) {
4234
+ const index = buildSessionIndex(records, {
4235
+ correlateByGroupId: options.correlateByGroupId === true
4236
+ });
4237
+ warnings.push(...index.warnings);
4238
+ const session = index.sessions.find((item) => item.sessionId === sessionId);
4239
+ if (!session) {
4240
+ return {
4241
+ metas: [],
4242
+ scopeLabel: sessionId,
4243
+ scopeKind: "session",
4244
+ runIds: [],
4245
+ warnings,
4246
+ notFound: true
4247
+ };
4248
+ }
4249
+ const runIdSet = new Set(session.runIds);
4250
+ const filtered = metas.filter((meta) => runIdSet.has(meta.runId));
4251
+ return {
4252
+ metas: filtered,
4253
+ scopeLabel: sessionId,
4254
+ scopeKind: "session",
4255
+ runIds: session.runIds,
4256
+ warnings,
4257
+ notFound: false
4258
+ };
4153
4259
  }
4154
- const a = expected.toLowerCase();
4155
- const b = actual.toLowerCase();
4156
- if (a.length !== b.length) return false;
4157
- let mismatch = 0;
4158
- for (let i = 0; i < a.length; i += 1) {
4159
- mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
4260
+ if (groupId) {
4261
+ const runIds = records.filter((run) => extractSessionWorkflowMetadata(run.metadata)?.groupId === groupId).map((run) => run.runId).sort();
4262
+ if (runIds.length === 0) {
4263
+ return {
4264
+ metas: [],
4265
+ scopeLabel: groupId,
4266
+ scopeKind: "group",
4267
+ runIds: [],
4268
+ warnings,
4269
+ notFound: true
4270
+ };
4271
+ }
4272
+ const runIdSet = new Set(runIds);
4273
+ return {
4274
+ metas: metas.filter((meta) => runIdSet.has(meta.runId)),
4275
+ scopeLabel: groupId,
4276
+ scopeKind: "group",
4277
+ runIds,
4278
+ warnings,
4279
+ notFound: false
4280
+ };
4160
4281
  }
4161
- return mismatch === 0;
4282
+ return {
4283
+ metas: [...metas],
4284
+ scopeLabel: "",
4285
+ scopeKind: "session",
4286
+ runIds: [],
4287
+ warnings,
4288
+ notFound: false
4289
+ };
4290
+ }
4291
+ var KNOWN_EVENTS = /* @__PURE__ */ new Set([
4292
+ "run_started",
4293
+ "run_completed",
4294
+ "step_started",
4295
+ "step_completed"
4296
+ ]);
4297
+ function isRecord7(value) {
4298
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4162
4299
  }
4163
- function assertEvidenceRelativePath(relativePath) {
4164
- if (typeof relativePath !== "string" || relativePath.trim() === "") {
4165
- throw new Error("Evidence file path must be a non-empty relative path.");
4166
- }
4167
- const trimmed = relativePath.trim().replaceAll("\\", "/");
4168
- if (path7.isAbsolute(trimmed) || trimmed.startsWith("/")) {
4169
- throw new Error(`Evidence file path must be relative: ${relativePath}`);
4170
- }
4171
- const parts = trimmed.split("/").filter((part) => part !== "");
4172
- if (parts.length === 0) {
4173
- throw new Error(`Evidence file path must be relative: ${relativePath}`);
4300
+ function safeParse(line) {
4301
+ try {
4302
+ return JSON.parse(line);
4303
+ } catch {
4304
+ return void 0;
4174
4305
  }
4175
- for (const part of parts) {
4176
- if (part === "." || part === "..") {
4177
- throw new Error(`Evidence file path must not contain "." or "..": ${relativePath}`);
4306
+ }
4307
+ async function isAgentInspectTrace(filePath) {
4308
+ try {
4309
+ const rl = createInterface({
4310
+ input: createReadStream(filePath, { encoding: "utf8" }),
4311
+ crlfDelay: Infinity
4312
+ });
4313
+ let checked = 0;
4314
+ for await (const line of rl) {
4315
+ const trimmed = line.trim();
4316
+ if (trimmed === "") continue;
4317
+ const parsed = safeParse(trimmed);
4318
+ if (!parsed) continue;
4319
+ if (!isRecord7(parsed)) continue;
4320
+ checked += 1;
4321
+ if (isTraceEvent(parsed)) return true;
4322
+ const ev = parsed.event;
4323
+ const runId = parsed.runId;
4324
+ if (typeof ev === "string" && KNOWN_EVENTS.has(ev) && typeof runId === "string") {
4325
+ return true;
4326
+ }
4327
+ if (checked >= 20) break;
4178
4328
  }
4329
+ return false;
4330
+ } catch {
4331
+ return false;
4179
4332
  }
4180
- return parts.join("/");
4181
4333
  }
4182
4334
 
4183
- // packages/core/src/evidence/manifest.ts
4184
- function stable(value) {
4185
- if (Array.isArray(value)) return value.map(stable);
4186
- if (value === null || typeof value !== "object") return value;
4187
- const record = value;
4188
- return Object.fromEntries(
4189
- Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable(record[key])])
4190
- );
4335
+ // packages/core/src/bundle/resolve.ts
4336
+ function parseSinceCutoff(since) {
4337
+ const trimmed = since.trim();
4338
+ if (trimmed === "") {
4339
+ throw new Error("--since requires a non-empty duration (e.g. 24h, 7d).");
4340
+ }
4341
+ return Date.now() - parseDuration(trimmed);
4191
4342
  }
4192
- function serializeEvidenceManifest(manifest) {
4193
- return `${JSON.stringify(stable(manifest), null, 2)}
4194
- `;
4343
+ function runActivityMs(run) {
4344
+ if (run.startedAt !== void 0 && Number.isFinite(run.startedAt)) return run.startedAt;
4345
+ if (run.endedAt !== void 0 && Number.isFinite(run.endedAt)) return run.endedAt;
4346
+ return void 0;
4195
4347
  }
4196
- function inferEvidenceFileRole(relativePath) {
4197
- const normalized = assertEvidenceRelativePath(relativePath);
4198
- const base = normalized.includes("/") ? normalized.slice(normalized.lastIndexOf("/") + 1) : normalized;
4199
- if (base === "evidence.html" || base === "trace.html" || base.endsWith(".html")) {
4200
- return "report";
4201
- }
4202
- if (base === "trace.jsonl" || base.endsWith(".jsonl")) {
4203
- return "redacted-trace";
4204
- }
4205
- if (base === "check-results.json") {
4206
- return "checks";
4207
- }
4208
- if (base === "contract.resolved.json") {
4209
- return "contract";
4348
+ function runsInSinceWindow(runs, since) {
4349
+ const cutoff = parseSinceCutoff(since);
4350
+ const ids = [];
4351
+ for (const run of runs) {
4352
+ const activity = runActivityMs(run);
4353
+ if (activity !== void 0 && activity >= cutoff) {
4354
+ ids.push(run.runId);
4355
+ }
4210
4356
  }
4211
- if (base === "redaction-report.json") {
4212
- return "redaction-report";
4357
+ return ids.sort((a, b) => a.localeCompare(b));
4358
+ }
4359
+ function findSession(index, sessionId) {
4360
+ return index.sessions.find((session) => session.sessionId === sessionId);
4361
+ }
4362
+ function resolveBundleRunIds(index, runs, options) {
4363
+ const runId = options.runId?.trim();
4364
+ const sessionId = options.sessionId?.trim();
4365
+ const since = options.since?.trim();
4366
+ const modes = [runId ? 1 : 0, sessionId ? 1 : 0, since ? 1 : 0].reduce((a, b) => a + b, 0);
4367
+ if (modes === 0) {
4368
+ throw new Error(
4369
+ "bundle requires a run id, --session <sessionId>, or --since <duration>."
4370
+ );
4213
4371
  }
4214
- if (base === "summary.md") {
4215
- return "summary";
4372
+ if (modes > 1) {
4373
+ throw new Error(
4374
+ "bundle accepts only one target: a run id, --session, or --since (not combined)."
4375
+ );
4216
4376
  }
4217
- return "other";
4218
- }
4219
- function buildEvidenceFileEntries(files) {
4220
- const byPath = /* @__PURE__ */ new Map();
4221
- for (const file of files) {
4222
- const relativePath = assertEvidenceRelativePath(file.path);
4223
- if (relativePath === EVIDENCE_MANIFEST_FILENAME) {
4224
- throw new Error(
4225
- `Do not include ${EVIDENCE_MANIFEST_FILENAME} in packaged file hashes (self-hash is undefined).`
4226
- );
4227
- }
4228
- if (byPath.has(relativePath)) {
4229
- throw new Error(`Duplicate evidence file path: ${relativePath}`);
4377
+ if (runId) {
4378
+ const known = runs.some((run) => run.runId === runId);
4379
+ if (!known) {
4380
+ throw new Error(`Run "${runId}" was not found in the trace directory.`);
4230
4381
  }
4231
- byPath.set(relativePath, {
4232
- path: relativePath,
4233
- sha256: sha256Hex(file.content),
4234
- role: file.role ?? inferEvidenceFileRole(relativePath)
4235
- });
4236
- }
4237
- return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
4238
- }
4239
- function buildEvidenceManifest(parts) {
4240
- const runIds = [...parts.runIds];
4241
- if (runIds.length === 0) {
4242
- throw new Error("Evidence manifest requires at least one run id.");
4382
+ return { runIds: [runId] };
4243
4383
  }
4244
- for (const item of parts.sourceHashes) {
4245
- if (!runIds.includes(item.runId)) {
4246
- throw new Error(`sourceHashes runId "${item.runId}" is not listed in source.runIds.`);
4384
+ if (sessionId) {
4385
+ const session = findSession(index, sessionId);
4386
+ if (!session) {
4387
+ throw new Error(`Session "${sessionId}" was not found.`);
4247
4388
  }
4248
- if (item.algorithm !== "sha256") {
4249
- throw new Error(`Unsupported source hash algorithm: ${item.algorithm}`);
4389
+ if (session.runIds.length === 0) {
4390
+ throw new Error(`Session "${sessionId}" has no runs to bundle.`);
4250
4391
  }
4392
+ return {
4393
+ runIds: [...session.runIds].sort((a, b) => a.localeCompare(b)),
4394
+ sessionId
4395
+ };
4251
4396
  }
4252
- const assessment = {
4253
- status: parts.assessmentStatus,
4254
- note: parts.note ?? EVIDENCE_ASSESSMENT_NOTE
4255
- };
4256
- if (parts.sourceStatus !== void 0) {
4257
- assessment.sourceStatus = parts.sourceStatus;
4397
+ const runIds = runsInSinceWindow(runs, since);
4398
+ if (runIds.length === 0) {
4399
+ throw new Error(`No runs matched --since ${since}.`);
4258
4400
  }
4401
+ return { runIds, since };
4402
+ }
4403
+
4404
+ // packages/core/src/bundle/safety-status.ts
4405
+ function aggregateBundleSafeStatus(statuses) {
4406
+ if (statuses.length === 0) return "UNKNOWN";
4407
+ if (statuses.some((status) => status === "UNSAFE")) return "UNSAFE";
4408
+ if (statuses.some((status) => status === "UNKNOWN")) return "UNKNOWN";
4409
+ if (statuses.some((status) => status === "SAFE WITH WARNINGS")) return "SAFE WITH WARNINGS";
4410
+ return "SAFE";
4411
+ }
4412
+ function toMetadataSafeStatus(status) {
4413
+ if (status === "SAFE WITH WARNINGS") return "SAFE_WITH_WARNINGS";
4414
+ return status;
4415
+ }
4416
+ function bundleFailsOnSafety(status, allowUnsafe) {
4417
+ if (allowUnsafe) return false;
4418
+ return status === "UNSAFE" || status === "UNKNOWN";
4419
+ }
4420
+
4421
+ // packages/core/src/bundle/manifest.ts
4422
+ var BUNDLE_NOTE = "Generated locally by AgentInspect. Bundles are derived copies for review \u2014 not compliance or security certification. Review before sharing.";
4423
+ var PLACEHOLDER_NOTE = "No eval or performance artifacts were requested for this bundle.";
4424
+ function buildBundleMetadata(parts) {
4425
+ const aggregate = aggregateBundleSafeStatus(
4426
+ parts.checks.runs.map((run) => run.status)
4427
+ );
4259
4428
  return {
4260
- evidenceFormatVersion: EVIDENCE_FORMAT_VERSION,
4261
- generator: {
4262
- name: parts.generatorName ?? "agent-inspect",
4263
- version: parts.generatorVersion
4264
- },
4265
4429
  createdAt: parts.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4266
- source: {
4267
- runIds,
4268
- traceSchemaVersions: [...parts.traceSchemaVersions].sort((a, b) => a.localeCompare(b)),
4269
- sourceHashes: [...parts.sourceHashes].sort((a, b) => a.runId.localeCompare(b.runId))
4270
- },
4271
- policy: {
4272
- redactionProfile: parts.redactionProfile,
4273
- verificationPolicy: parts.verificationPolicy ?? parts.redactionProfile
4274
- },
4275
- assessment,
4276
- ...parts.semantics !== void 0 ? { semantics: { ...parts.semantics } } : {},
4277
- ...parts.contract !== void 0 ? { contract: { ...parts.contract } } : {},
4278
- files: buildEvidenceFileEntries(parts.files)
4430
+ agentInspectVersion: parts.agentInspectVersion,
4431
+ redactionProfile: parts.profile,
4432
+ sourceTraceCount: parts.resolve.runIds.length,
4433
+ runIds: [...parts.resolve.runIds],
4434
+ safeStatus: toMetadataSafeStatus(aggregate),
4435
+ files: [...parts.files].sort((a, b) => a.localeCompare(b)),
4436
+ note: BUNDLE_NOTE,
4437
+ ...parts.resolve.sessionId !== void 0 ? { sessionId: parts.resolve.sessionId } : {},
4438
+ ...parts.resolve.since !== void 0 ? { since: parts.resolve.since } : {}
4279
4439
  };
4280
4440
  }
4281
- function validateEvidenceManifest(value) {
4282
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
4283
- throw new Error("Evidence manifest must be a JSON object.");
4284
- }
4285
- const record = value;
4286
- if (record.evidenceFormatVersion !== EVIDENCE_FORMAT_VERSION) {
4287
- throw new Error(
4288
- `Unsupported evidenceFormatVersion: ${String(record.evidenceFormatVersion)}`
4289
- );
4290
- }
4291
- const generator = record.generator;
4292
- if (generator === null || typeof generator !== "object" || Array.isArray(generator) || typeof generator.name !== "string" || typeof generator.version !== "string") {
4293
- throw new Error("Evidence manifest requires generator.name and generator.version.");
4294
- }
4295
- const source = record.source;
4296
- if (source === null || typeof source !== "object" || Array.isArray(source)) {
4297
- throw new Error("Evidence manifest requires source.");
4298
- }
4299
- const sourceRecord = source;
4300
- if (!Array.isArray(sourceRecord.runIds) || sourceRecord.runIds.length === 0) {
4301
- throw new Error("Evidence manifest source.runIds must be a non-empty array.");
4302
- }
4303
- if (!Array.isArray(sourceRecord.traceSchemaVersions)) {
4304
- throw new Error("Evidence manifest source.traceSchemaVersions must be an array.");
4305
- }
4306
- if (!Array.isArray(sourceRecord.sourceHashes)) {
4307
- throw new Error("Evidence manifest source.sourceHashes must be an array.");
4308
- }
4309
- const policy = record.policy;
4310
- if (policy === null || typeof policy !== "object" || Array.isArray(policy)) {
4311
- throw new Error("Evidence manifest requires policy.");
4312
- }
4313
- const assessment = record.assessment;
4314
- if (assessment === null || typeof assessment !== "object" || Array.isArray(assessment) || typeof assessment.status !== "string") {
4315
- throw new Error("Evidence manifest requires assessment.status.");
4441
+ function buildPlaceholderArtifact() {
4442
+ return {
4443
+ status: "not_requested",
4444
+ note: PLACEHOLDER_NOTE
4445
+ };
4446
+ }
4447
+
4448
+ // packages/core/src/bundle/summary.ts
4449
+ function markdownTable(rows) {
4450
+ const lines = ["| Field | Value |", "| --- | --- |"];
4451
+ for (const [key, value] of rows) {
4452
+ lines.push(`| ${key} | ${value ?? "unknown"} |`);
4316
4453
  }
4317
- if (!Array.isArray(record.files) || record.files.length === 0) {
4318
- throw new Error("Evidence manifest files must be a non-empty array.");
4454
+ return lines.join("\n");
4455
+ }
4456
+ function buildBundleSummaryMarkdown(parts) {
4457
+ const { metadata, checks, redaction } = parts;
4458
+ const lines = [
4459
+ "# AgentInspect trace bundle",
4460
+ "",
4461
+ metadata.note,
4462
+ "",
4463
+ "## Overview",
4464
+ "",
4465
+ markdownTable([
4466
+ ["Created", metadata.createdAt],
4467
+ ["AgentInspect", metadata.agentInspectVersion],
4468
+ ["Redaction profile", metadata.redactionProfile],
4469
+ ["Safe status", metadata.safeStatus],
4470
+ ["Source traces", metadata.sourceTraceCount],
4471
+ ["Runs", metadata.runIds.join(", ")],
4472
+ ...metadata.sessionId ? [["Session", metadata.sessionId]] : [],
4473
+ ...metadata.since ? [["Since", metadata.since]] : []
4474
+ ]),
4475
+ "",
4476
+ "## Safety checks",
4477
+ "",
4478
+ `Aggregate: **${checks.aggregateStatus}**`,
4479
+ ""
4480
+ ];
4481
+ for (const run of checks.runs) {
4482
+ const source = run.sourceStatus !== void 0 && run.sourceStatus !== run.status ? `; source ${run.sourceStatus}` : "";
4483
+ lines.push(
4484
+ `- \`${run.runId}\`: artifact ${run.status}${source} (${run.findings} finding(s), ${run.errors} error(s), ${run.warnings} warning(s))`
4485
+ );
4319
4486
  }
4320
- for (const file of record.files) {
4321
- if (file === null || typeof file !== "object" || Array.isArray(file)) {
4322
- throw new Error("Evidence manifest file entries must be objects.");
4323
- }
4324
- const entry = file;
4325
- if (typeof entry.path !== "string") {
4326
- throw new Error("Evidence file entry requires path.");
4327
- }
4328
- assertEvidenceRelativePath(entry.path);
4329
- if (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(entry.sha256)) {
4330
- throw new Error(`Evidence file entry requires sha256 hex for ${entry.path}.`);
4331
- }
4487
+ lines.push("", "## Redaction", "", `Total findings: ${redaction.totalFindings}`, "");
4488
+ for (const run of redaction.runs) {
4489
+ const detectors = run.detectors.length > 0 ? run.detectors.join(", ") : "none";
4490
+ lines.push(`- \`${run.runId}\`: ${run.findings} finding(s); detectors: ${detectors}`);
4332
4491
  }
4333
- return value;
4492
+ lines.push(
4493
+ "",
4494
+ "## Files",
4495
+ "",
4496
+ ...metadata.files.map((file) => `- \`${file}\``),
4497
+ "",
4498
+ "_Review every generated artifact before sharing outside your team._",
4499
+ ""
4500
+ );
4501
+ return lines.join("\n");
4334
4502
  }
4335
- function parseEvidenceManifestJson(text) {
4336
- let parsed;
4337
- try {
4338
- parsed = JSON.parse(text);
4339
- } catch (error) {
4340
- const message = error instanceof Error ? error.message : String(error);
4341
- throw new Error(`Evidence manifest is not valid JSON: ${message}`);
4503
+ function sanitizeBundleRunId(runId) {
4504
+ let safe = typeof runId === "string" && runId.trim() !== "" ? runId.trim() : "run_unknown";
4505
+ safe = path7.basename(safe);
4506
+ safe = safe.replace(/[^a-zA-Z0-9._-]+/g, "_");
4507
+ if (safe === "" || safe === "." || safe === "..") {
4508
+ safe = "run_unknown";
4342
4509
  }
4343
- return validateEvidenceManifest(parsed);
4510
+ return safe;
4344
4511
  }
4345
- function collectTraceSchemaVersions(jsonl) {
4346
- const versions = /* @__PURE__ */ new Set();
4347
- for (const line of jsonl.split(/\r?\n/)) {
4348
- const trimmed = line.trim();
4349
- if (trimmed === "") continue;
4350
- try {
4351
- const row = JSON.parse(trimmed);
4352
- if (typeof row.schemaVersion === "string" && row.schemaVersion.trim() !== "") {
4353
- versions.add(row.schemaVersion.trim());
4354
- }
4355
- } catch {
4356
- }
4357
- }
4358
- return [...versions].sort((a, b) => a.localeCompare(b));
4512
+ function bundleRunAssetRelativePath(runId, extension) {
4513
+ const safe = sanitizeBundleRunId(runId);
4514
+ const ext = extension.startsWith(".") ? extension : `.${extension}`;
4515
+ return path7.posix.join("assets", "runs", `${safe}${ext}`);
4359
4516
  }
4360
-
4361
- // packages/core/src/diagnostics/programmatic.ts
4362
- var PROGRAMMATIC_DIAGNOSTIC_SPECS = Object.freeze({
4363
- AI_TRACE_INPUT_INVALID: {
4364
- code: "AI_TRACE_INPUT_INVALID",
4365
- summary: 'Expected { type: "file", path }, { type: "directory", path }, { type: "string", content }, { type: "buffer", content }, or { type: "stdin" }.',
4366
- remediation: "For a file path, use openTraceFile(path).",
4367
- relatedCodes: ["invalid_input"]
4368
- },
4369
- AI_TRACE_FORMAT_UNSUPPORTED: {
4370
- code: "AI_TRACE_FORMAT_UNSUPPORTED",
4371
- summary: "No trace reader could detect the input format.",
4372
- remediation: "Pass an AgentInspect JSONL file via openTraceFile, or set options.format to a registered reader.",
4373
- relatedCodes: ["unsupported_format"]
4374
- },
4375
- AI_TRACE_FORMAT_AMBIGUOUS: {
4376
- code: "AI_TRACE_FORMAT_AMBIGUOUS",
4377
- summary: "Multiple trace readers matched the input with equal confidence.",
4378
- remediation: "Set options.format explicitly to disambiguate the reader.",
4379
- relatedCodes: ["ambiguous_format"]
4380
- },
4381
- AI_TRACE_FACTS_INPUT_NOT_NORMALIZED: {
4382
- code: "AI_TRACE_FACTS_INPUT_NOT_NORMALIZED",
4383
- summary: "TraceFacts requires TraceReadResult or PersistedInspectEvent[].",
4384
- remediation: "Use openTraceFile() to normalize a JSONL trace first."
4385
- },
4386
- AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED: {
4387
- code: "AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED",
4388
- summary: "Multiple runs are available; select a run before executing checks.",
4389
- remediation: "Pass options.runId or TraceCheckInput.selectedRun.",
4390
- relatedCodes: ["AI_CHECK_RUN_SELECTION_REQUIRED"]
4391
- },
4392
- AI_TRACE_RELATIONSHIP_SELF_PARENT: {
4393
- code: "AI_TRACE_RELATIONSHIP_SELF_PARENT",
4394
- summary: "A parentId equals its own event/step id.",
4395
- remediation: "Reject at capture (AI_LANGGRAPH_SELF_PARENT_REJECTED) or drop via logical projection; do not invent replacement parents.",
4396
- relatedCodes: [
4397
- "AI_LANGGRAPH_SELF_PARENT_REJECTED",
4398
- "AI_LOGICAL_SELF_PARENT_REMOVED"
4399
- ]
4400
- },
4401
- AI_TRACE_RELATIONSHIP_CYCLE: {
4402
- code: "AI_TRACE_RELATIONSHIP_CYCLE",
4403
- summary: "Trace contains a parentId cycle.",
4404
- remediation: "Use visibility-first tree linking for legacy fixtures; prefer acyclic capture for new adapter output.",
4405
- relatedCodes: ["structure.cycle"]
4517
+ function assertBundlePathContained(outputDir, relativePath) {
4518
+ const base = path7.resolve(outputDir);
4519
+ const resolved = path7.resolve(base, relativePath);
4520
+ if (resolved !== base && !resolved.startsWith(base + path7.sep)) {
4521
+ throw new Error(`Bundle path escapes output directory: ${relativePath}`);
4406
4522
  }
4407
- });
4408
- function formatProgrammaticDiagnostic(code, detail) {
4409
- const spec = PROGRAMMATIC_DIAGNOSTIC_SPECS[code];
4410
- const summary = detail?.trim() ? detail.trim() : spec.summary;
4411
- return `${code}: ${summary} Remediation: ${spec.remediation}`;
4523
+ return resolved;
4412
4524
  }
4413
-
4414
- // packages/core/src/checks/logical-events.ts
4415
- function isRecord7(value) {
4416
- return typeof value === "object" && value !== null && !Array.isArray(value);
4525
+ function normalizeBundleOutputPath(out, options) {
4526
+ const trimmed = out.trim();
4527
+ if (trimmed === "") {
4528
+ throw new Error("--out requires a non-empty path.");
4529
+ }
4530
+ const resolved = path7.resolve(trimmed);
4531
+ if (options?.preserveZipExtension !== true && resolved.toLowerCase().endsWith(".zip")) {
4532
+ return resolved.slice(0, -4);
4533
+ }
4534
+ return resolved;
4417
4535
  }
4418
- function legacyEvent(event) {
4419
- const value = event.attributes?.legacyEvent;
4420
- return typeof value === "string" ? value : void 0;
4536
+ function defaultBundleOutputPath(runIds) {
4537
+ const label = runIds.length === 1 ? sanitizeBundleRunId(runIds[0]) : `multi-${runIds.length}`;
4538
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4539
+ return path7.resolve(`agent-inspect-bundle-${label}-${stamp}`);
4421
4540
  }
4422
- function stepIdOf(event) {
4423
- const value = event.attributes?.stepId;
4424
- if (typeof value === "string" && value.trim() !== "") return value;
4425
- return void 0;
4541
+
4542
+ // packages/core/src/evidence/types.ts
4543
+ var EVIDENCE_FORMAT_VERSION = "1.0";
4544
+ var EVIDENCE_ASSESSMENT_NOTE = "Best-effort local safety verification only; not a compliance certification.";
4545
+ var EVIDENCE_MANIFEST_FILENAME = "evidence.json";
4546
+ var EVIDENCE_RESOLVED_CONTRACT_FILENAME = "contract.resolved.json";
4547
+ var SHA256_RE = /^[a-f0-9]{64}$/i;
4548
+ function sha256Hex(data) {
4549
+ const hash = createHash("sha256");
4550
+ if (typeof data === "string") {
4551
+ hash.update(data, "utf8");
4552
+ } else {
4553
+ hash.update(data);
4554
+ }
4555
+ return hash.digest("hex");
4426
4556
  }
4427
- function cloneEvent(event) {
4428
- return {
4429
- ...event,
4430
- ...event.attributes !== void 0 ? { attributes: { ...event.attributes } } : {},
4431
- ...event.error !== void 0 ? { error: { ...event.error } } : {},
4432
- ...event.tokenUsage !== void 0 ? { tokenUsage: { ...event.tokenUsage } } : {},
4433
- ...event.source !== void 0 ? { source: { ...event.source } } : {}
4434
- };
4557
+ function isSha256Hex(value) {
4558
+ return SHA256_RE.test(value);
4435
4559
  }
4436
- function mergeAttributes(start, complete) {
4437
- const merged = {
4438
- ...isRecord7(complete.attributes) ? complete.attributes : {},
4439
- ...isRecord7(start.attributes) ? start.attributes : {}
4440
- };
4441
- merged.legacyEvent = start.attributes?.legacyEvent ?? complete.attributes?.legacyEvent;
4442
- merged.legacyCompleteEvent = complete.attributes?.legacyEvent;
4443
- if (complete.attributes?.errorStack !== void 0) {
4444
- merged.errorStack = complete.attributes.errorStack;
4560
+ function sha256Equals(expected, actual) {
4561
+ if (!isSha256Hex(expected) || !isSha256Hex(actual)) {
4562
+ return false;
4445
4563
  }
4446
- return Object.keys(merged).length > 0 ? merged : void 0;
4564
+ const a = expected.toLowerCase();
4565
+ const b = actual.toLowerCase();
4566
+ if (a.length !== b.length) return false;
4567
+ let mismatch = 0;
4568
+ for (let i = 0; i < a.length; i += 1) {
4569
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
4570
+ }
4571
+ return mismatch === 0;
4447
4572
  }
4448
- function pairStartComplete(start, complete) {
4449
- const attributes = mergeAttributes(start, complete);
4450
- const paired = {
4451
- ...cloneEvent(start),
4452
- status: complete.status,
4453
- timestamp: complete.timestamp ?? start.timestamp,
4454
- ...complete.endedAt !== void 0 ? { endedAt: complete.endedAt } : {},
4455
- ...complete.durationMs !== void 0 ? { durationMs: complete.durationMs } : {},
4456
- ...complete.error !== void 0 ? { error: { ...complete.error } } : {},
4457
- ...complete.tokenUsage !== void 0 && start.tokenUsage === void 0 ? { tokenUsage: { ...complete.tokenUsage } } : {},
4458
- ...attributes !== void 0 ? { attributes } : {}
4459
- };
4460
- return {
4461
- ...paired,
4462
- sourceEventIds: Object.freeze([start.eventId, complete.eventId]),
4463
- projection: {
4464
- paired: true,
4465
- absorbedEventIds: Object.freeze([complete.eventId]),
4466
- parentNormalized: false,
4467
- ...start.parentId !== void 0 ? { originalParentId: start.parentId } : {}
4573
+ function assertEvidenceRelativePath(relativePath) {
4574
+ if (typeof relativePath !== "string" || relativePath.trim() === "") {
4575
+ throw new Error("Evidence file path must be a non-empty relative path.");
4576
+ }
4577
+ const trimmed = relativePath.trim().replaceAll("\\", "/");
4578
+ if (path7.isAbsolute(trimmed) || trimmed.startsWith("/")) {
4579
+ throw new Error(`Evidence file path must be relative: ${relativePath}`);
4580
+ }
4581
+ const parts = trimmed.split("/").filter((part) => part !== "");
4582
+ if (parts.length === 0) {
4583
+ throw new Error(`Evidence file path must be relative: ${relativePath}`);
4584
+ }
4585
+ for (const part of parts) {
4586
+ if (part === "." || part === "..") {
4587
+ throw new Error(`Evidence file path must not contain "." or "..": ${relativePath}`);
4468
4588
  }
4469
- };
4589
+ }
4590
+ return parts.join("/");
4470
4591
  }
4471
- function asLogical(event, extras) {
4472
- return {
4473
- ...cloneEvent(event),
4474
- sourceEventIds: Object.freeze([event.eventId]),
4475
- projection: {
4476
- paired: false,
4477
- absorbedEventIds: Object.freeze([]),
4478
- parentNormalized: extras?.parentNormalized === true,
4479
- ...{}
4480
- }
4481
- };
4592
+
4593
+ // packages/core/src/evidence/manifest.ts
4594
+ function stable(value) {
4595
+ if (Array.isArray(value)) return value.map(stable);
4596
+ if (value === null || typeof value !== "object") return value;
4597
+ const record = value;
4598
+ return Object.fromEntries(
4599
+ Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, stable(record[key])])
4600
+ );
4482
4601
  }
4483
- function projectLogicalEvents(events) {
4484
- const diagnostics = [];
4485
- const byRun = /* @__PURE__ */ new Map();
4486
- for (const event of events) {
4487
- const list = byRun.get(event.runId) ?? [];
4488
- list.push(event);
4489
- byRun.set(event.runId, list);
4602
+ function serializeEvidenceManifest(manifest) {
4603
+ return `${JSON.stringify(stable(manifest), null, 2)}
4604
+ `;
4605
+ }
4606
+ function inferEvidenceFileRole(relativePath) {
4607
+ const normalized = assertEvidenceRelativePath(relativePath);
4608
+ const base = normalized.includes("/") ? normalized.slice(normalized.lastIndexOf("/") + 1) : normalized;
4609
+ if (base === "evidence.html" || base === "trace.html" || base.endsWith(".html")) {
4610
+ return "report";
4490
4611
  }
4491
- const absorbedIds = /* @__PURE__ */ new Set();
4492
- const logicalByRawId = /* @__PURE__ */ new Map();
4493
- const stepIdToLogicalId = /* @__PURE__ */ new Map();
4494
- const logical = [];
4495
- const orderedRuns = [...byRun.keys()].sort((a, b) => a.localeCompare(b));
4496
- for (const runId of orderedRuns) {
4497
- const runEvents = byRun.get(runId) ?? [];
4498
- const starts = [];
4499
- const completes = [];
4500
- const others = [];
4501
- for (const event of runEvents) {
4502
- const legacy = legacyEvent(event);
4503
- if (legacy === "step_started" || legacy === "run_started" && event.status === "running") {
4504
- starts.push(event);
4505
- } else if (legacy === "step_completed" || legacy === "run_completed") {
4506
- completes.push(event);
4507
- } else {
4508
- others.push(event);
4509
- }
4510
- }
4511
- const usedCompletes = /* @__PURE__ */ new Set();
4512
- for (const start of starts) {
4513
- const stepId = stepIdOf(start);
4514
- let match;
4515
- if (legacyEvent(start) === "run_started") {
4516
- const candidates = completes.filter(
4517
- (c) => !usedCompletes.has(c.eventId) && legacyEvent(c) === "run_completed"
4518
- );
4519
- if (candidates.length > 1) {
4520
- diagnostics.push({
4521
- code: "AI_LOGICAL_PAIR_AMBIGUOUS",
4522
- message: `Multiple run_completed rows for run ${runId}; using first by eventId.`,
4523
- eventIds: candidates.map((c) => c.eventId)
4524
- });
4525
- candidates.sort((a, b) => a.eventId.localeCompare(b.eventId));
4526
- }
4527
- match = candidates[0];
4528
- } else if (stepId) {
4529
- const candidates = completes.filter(
4530
- (c) => !usedCompletes.has(c.eventId) && legacyEvent(c) === "step_completed" && stepIdOf(c) === stepId
4531
- );
4532
- if (candidates.length > 1) {
4533
- diagnostics.push({
4534
- code: "AI_LOGICAL_PAIR_AMBIGUOUS",
4535
- message: `Multiple step_completed rows for stepId ${stepId}; using first by eventId.`,
4536
- eventIds: candidates.map((c) => c.eventId)
4537
- });
4538
- candidates.sort((a, b) => a.eventId.localeCompare(b.eventId));
4539
- }
4540
- match = candidates[0];
4541
- }
4542
- if (match) {
4543
- usedCompletes.add(match.eventId);
4544
- absorbedIds.add(match.eventId);
4545
- const paired = pairStartComplete(start, match);
4546
- logical.push(paired);
4547
- logicalByRawId.set(start.eventId, paired);
4548
- logicalByRawId.set(match.eventId, paired);
4549
- if (stepId) stepIdToLogicalId.set(`${runId}:${stepId}`, paired.eventId);
4550
- } else {
4551
- diagnostics.push({
4552
- code: "AI_LOGICAL_PAIR_UNMATCHED_START",
4553
- message: `No matching complete for start ${start.eventId}.`,
4554
- eventIds: [start.eventId]
4555
- });
4556
- const alone = asLogical(start);
4557
- logical.push(alone);
4558
- logicalByRawId.set(start.eventId, alone);
4559
- if (stepId) stepIdToLogicalId.set(`${runId}:${stepId}`, alone.eventId);
4560
- }
4561
- }
4562
- for (const complete of completes) {
4563
- if (usedCompletes.has(complete.eventId)) continue;
4564
- diagnostics.push({
4565
- code: "AI_LOGICAL_PAIR_UNMATCHED_COMPLETE",
4566
- message: `No matching start for complete ${complete.eventId}.`,
4567
- eventIds: [complete.eventId]
4568
- });
4569
- const alone = asLogical(complete);
4570
- logical.push(alone);
4571
- logicalByRawId.set(complete.eventId, alone);
4572
- const stepId = stepIdOf(complete);
4573
- if (stepId && !stepIdToLogicalId.has(`${runId}:${stepId}`)) {
4574
- stepIdToLogicalId.set(`${runId}:${stepId}`, alone.eventId);
4575
- }
4576
- }
4577
- for (const event of others) {
4578
- const alone = asLogical(event);
4579
- logical.push(alone);
4580
- logicalByRawId.set(event.eventId, alone);
4581
- const stepId = stepIdOf(event);
4582
- if (stepId && !stepIdToLogicalId.has(`${runId}:${stepId}`)) {
4583
- stepIdToLogicalId.set(`${runId}:${stepId}`, alone.eventId);
4584
- }
4585
- }
4612
+ if (base === "trace.jsonl" || base.endsWith(".jsonl")) {
4613
+ return "redacted-trace";
4586
4614
  }
4587
- const logicalById = new Map(logical.map((e) => [e.eventId, e]));
4588
- const normalized = [];
4589
- for (const event of logical) {
4590
- const originalParentId = event.parentId;
4591
- if (!originalParentId) {
4592
- normalized.push(event);
4593
- continue;
4594
- }
4595
- let nextParent = originalParentId;
4596
- let remapped = false;
4597
- const viaAbsorbed = logicalByRawId.get(originalParentId);
4598
- if (viaAbsorbed && viaAbsorbed.eventId !== originalParentId) {
4599
- nextParent = viaAbsorbed.eventId;
4600
- remapped = true;
4601
- } else if (!logicalById.has(originalParentId)) {
4602
- const viaStep = stepIdToLogicalId.get(`${event.runId}:${originalParentId}`);
4603
- if (viaStep) {
4604
- nextParent = viaStep;
4605
- remapped = true;
4606
- }
4607
- }
4608
- if (!remapped) {
4609
- if (originalParentId === event.eventId) {
4610
- diagnostics.push({
4611
- code: "AI_LOGICAL_SELF_PARENT_REMOVED",
4612
- message: formatProgrammaticDiagnostic(
4613
- "AI_TRACE_RELATIONSHIP_SELF_PARENT",
4614
- `Removed self-parent edge on ${event.eventId}.`
4615
- ),
4616
- eventIds: [event.eventId]
4617
- });
4618
- const { parentId: _drop, ...rest } = event;
4619
- normalized.push({
4620
- ...rest,
4621
- projection: {
4622
- ...event.projection,
4623
- parentNormalized: true,
4624
- originalParentId
4625
- }
4626
- });
4627
- continue;
4628
- }
4629
- if (!logicalById.has(originalParentId) && !logicalByRawId.has(originalParentId)) {
4630
- const mapping = event.attributes?.parentMapping;
4631
- const unresolved = mapping === "unresolved" || event.attributes?.parentUnresolved === true || event.attributes?.unresolvedParent === true || // LangGraph/framework scaffolding sentinel labels are not event ids.
4632
- /^LangGraph$/i.test(originalParentId) || originalParentId.startsWith("unresolved:");
4633
- if (!unresolved) {
4634
- diagnostics.push({
4635
- code: "AI_LOGICAL_PARENT_UNRESOLVED",
4636
- message: `Parent ${originalParentId} unresolved for ${event.eventId}.`,
4637
- eventIds: [event.eventId]
4638
- });
4639
- }
4640
- }
4641
- normalized.push(event);
4642
- continue;
4643
- }
4644
- if (nextParent === event.eventId) {
4645
- diagnostics.push({
4646
- code: "AI_LOGICAL_SELF_PARENT_REMOVED",
4647
- message: formatProgrammaticDiagnostic(
4648
- "AI_TRACE_RELATIONSHIP_SELF_PARENT",
4649
- `Removed self-parent edge on ${event.eventId} (was ${originalParentId}).`
4650
- ),
4651
- eventIds: [event.eventId]
4652
- });
4653
- const { parentId: _drop, ...rest } = event;
4654
- normalized.push({
4655
- ...rest,
4656
- projection: {
4657
- ...event.projection,
4658
- parentNormalized: true,
4659
- originalParentId
4660
- }
4661
- });
4662
- continue;
4663
- }
4664
- diagnostics.push({
4665
- code: "AI_LOGICAL_PARENT_REMAPPED",
4666
- message: `Remapped parent ${originalParentId} \u2192 ${nextParent} for ${event.eventId}.`,
4667
- eventIds: [event.eventId]
4668
- });
4669
- normalized.push({
4670
- ...event,
4671
- parentId: nextParent,
4672
- projection: {
4673
- ...event.projection,
4674
- parentNormalized: true,
4675
- originalParentId
4676
- }
4615
+ if (base === "check-results.json") {
4616
+ return "checks";
4617
+ }
4618
+ if (base === "contract.resolved.json") {
4619
+ return "contract";
4620
+ }
4621
+ if (base === "redaction-report.json") {
4622
+ return "redaction-report";
4623
+ }
4624
+ if (base === "summary.md") {
4625
+ return "summary";
4626
+ }
4627
+ return "other";
4628
+ }
4629
+ function buildEvidenceFileEntries(files) {
4630
+ const byPath = /* @__PURE__ */ new Map();
4631
+ for (const file of files) {
4632
+ const relativePath = assertEvidenceRelativePath(file.path);
4633
+ if (relativePath === EVIDENCE_MANIFEST_FILENAME) {
4634
+ throw new Error(
4635
+ `Do not include ${EVIDENCE_MANIFEST_FILENAME} in packaged file hashes (self-hash is undefined).`
4636
+ );
4637
+ }
4638
+ if (byPath.has(relativePath)) {
4639
+ throw new Error(`Duplicate evidence file path: ${relativePath}`);
4640
+ }
4641
+ byPath.set(relativePath, {
4642
+ path: relativePath,
4643
+ sha256: sha256Hex(file.content),
4644
+ role: file.role ?? inferEvidenceFileRole(relativePath)
4677
4645
  });
4678
4646
  }
4679
- const rawIndex = new Map(events.map((e, i) => [e.eventId, i]));
4680
- normalized.sort((a, b) => {
4681
- const ai = rawIndex.get(a.sourceEventIds[0]) ?? 0;
4682
- const bi = rawIndex.get(b.sourceEventIds[0]) ?? 0;
4683
- return ai - bi || a.eventId.localeCompare(b.eventId);
4684
- });
4647
+ return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
4648
+ }
4649
+ function buildEvidenceManifest(parts) {
4650
+ const runIds = [...parts.runIds];
4651
+ if (runIds.length === 0) {
4652
+ throw new Error("Evidence manifest requires at least one run id.");
4653
+ }
4654
+ for (const item of parts.sourceHashes) {
4655
+ if (!runIds.includes(item.runId)) {
4656
+ throw new Error(`sourceHashes runId "${item.runId}" is not listed in source.runIds.`);
4657
+ }
4658
+ if (item.algorithm !== "sha256") {
4659
+ throw new Error(`Unsupported source hash algorithm: ${item.algorithm}`);
4660
+ }
4661
+ }
4662
+ const assessment = {
4663
+ status: parts.assessmentStatus,
4664
+ note: parts.note ?? EVIDENCE_ASSESSMENT_NOTE
4665
+ };
4666
+ if (parts.sourceStatus !== void 0) {
4667
+ assessment.sourceStatus = parts.sourceStatus;
4668
+ }
4685
4669
  return {
4686
- logicalEvents: Object.freeze(normalized),
4687
- diagnostics: Object.freeze(diagnostics)
4670
+ evidenceFormatVersion: EVIDENCE_FORMAT_VERSION,
4671
+ generator: {
4672
+ name: parts.generatorName ?? "agent-inspect",
4673
+ version: parts.generatorVersion
4674
+ },
4675
+ createdAt: parts.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4676
+ source: {
4677
+ runIds,
4678
+ traceSchemaVersions: [...parts.traceSchemaVersions].sort((a, b) => a.localeCompare(b)),
4679
+ sourceHashes: [...parts.sourceHashes].sort((a, b) => a.runId.localeCompare(b.runId))
4680
+ },
4681
+ policy: {
4682
+ redactionProfile: parts.redactionProfile,
4683
+ verificationPolicy: parts.verificationPolicy ?? parts.redactionProfile
4684
+ },
4685
+ assessment,
4686
+ ...parts.semantics !== void 0 ? { semantics: { ...parts.semantics } } : {},
4687
+ ...parts.contract !== void 0 ? { contract: { ...parts.contract } } : {},
4688
+ ...parts.inputs !== void 0 ? { inputs: { ...parts.inputs } } : {},
4689
+ files: buildEvidenceFileEntries(parts.files)
4688
4690
  };
4689
4691
  }
4690
- function resolveCanonicalToolName(event) {
4691
- const attrs = event.attributes;
4692
- const direct = pickString(attrs, ["toolName", "tool"]);
4693
- if (direct) return direct;
4694
- const metadata = attrs?.metadata;
4695
- if (isRecord7(metadata)) {
4696
- const nested = pickString(metadata, ["toolName", "tool"]);
4697
- if (nested) return nested;
4692
+ function validateEvidenceManifest(value) {
4693
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
4694
+ throw new Error("Evidence manifest must be a JSON object.");
4698
4695
  }
4699
- for (const prefix of ["tool:", "function:", "mcp-tools:"]) {
4700
- if (event.name.startsWith(prefix)) return event.name.slice(prefix.length);
4696
+ const record = value;
4697
+ if (record.evidenceFormatVersion !== EVIDENCE_FORMAT_VERSION) {
4698
+ throw new Error(
4699
+ `Unsupported evidenceFormatVersion: ${String(record.evidenceFormatVersion)}`
4700
+ );
4701
4701
  }
4702
- return event.name;
4702
+ const generator = record.generator;
4703
+ if (generator === null || typeof generator !== "object" || Array.isArray(generator) || typeof generator.name !== "string" || typeof generator.version !== "string") {
4704
+ throw new Error("Evidence manifest requires generator.name and generator.version.");
4705
+ }
4706
+ const source = record.source;
4707
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
4708
+ throw new Error("Evidence manifest requires source.");
4709
+ }
4710
+ const sourceRecord = source;
4711
+ if (!Array.isArray(sourceRecord.runIds) || sourceRecord.runIds.length === 0) {
4712
+ throw new Error("Evidence manifest source.runIds must be a non-empty array.");
4713
+ }
4714
+ if (!Array.isArray(sourceRecord.traceSchemaVersions)) {
4715
+ throw new Error("Evidence manifest source.traceSchemaVersions must be an array.");
4716
+ }
4717
+ if (!Array.isArray(sourceRecord.sourceHashes)) {
4718
+ throw new Error("Evidence manifest source.sourceHashes must be an array.");
4719
+ }
4720
+ const policy = record.policy;
4721
+ if (policy === null || typeof policy !== "object" || Array.isArray(policy)) {
4722
+ throw new Error("Evidence manifest requires policy.");
4723
+ }
4724
+ const assessment = record.assessment;
4725
+ if (assessment === null || typeof assessment !== "object" || Array.isArray(assessment) || typeof assessment.status !== "string") {
4726
+ throw new Error("Evidence manifest requires assessment.status.");
4727
+ }
4728
+ if (!Array.isArray(record.files) || record.files.length === 0) {
4729
+ throw new Error("Evidence manifest files must be a non-empty array.");
4730
+ }
4731
+ for (const file of record.files) {
4732
+ if (file === null || typeof file !== "object" || Array.isArray(file)) {
4733
+ throw new Error("Evidence manifest file entries must be objects.");
4734
+ }
4735
+ const entry = file;
4736
+ if (typeof entry.path !== "string") {
4737
+ throw new Error("Evidence file entry requires path.");
4738
+ }
4739
+ assertEvidenceRelativePath(entry.path);
4740
+ if (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(entry.sha256)) {
4741
+ throw new Error(`Evidence file entry requires sha256 hex for ${entry.path}.`);
4742
+ }
4743
+ }
4744
+ return value;
4703
4745
  }
4704
- function pickString(record, keys) {
4705
- if (!record) return void 0;
4706
- for (const key of keys) {
4707
- const value = record[key];
4708
- if (typeof value === "string" && value.trim() !== "") return value.trim();
4746
+ function parseEvidenceManifestJson(text) {
4747
+ let parsed;
4748
+ try {
4749
+ parsed = JSON.parse(text);
4750
+ } catch (error) {
4751
+ const message = error instanceof Error ? error.message : String(error);
4752
+ throw new Error(`Evidence manifest is not valid JSON: ${message}`);
4709
4753
  }
4710
- return void 0;
4754
+ return validateEvidenceManifest(parsed);
4755
+ }
4756
+ function collectTraceSchemaVersions(jsonl) {
4757
+ const versions = /* @__PURE__ */ new Set();
4758
+ for (const line of jsonl.split(/\r?\n/)) {
4759
+ const trimmed = line.trim();
4760
+ if (trimmed === "") continue;
4761
+ try {
4762
+ const row = JSON.parse(trimmed);
4763
+ if (typeof row.schemaVersion === "string" && row.schemaVersion.trim() !== "") {
4764
+ versions.add(row.schemaVersion.trim());
4765
+ }
4766
+ } catch {
4767
+ }
4768
+ }
4769
+ return [...versions].sort((a, b) => a.localeCompare(b));
4711
4770
  }
4712
4771
 
4713
4772
  // packages/core/src/checks/derived-failure.ts
@@ -7058,7 +7117,17 @@ function emptyPassResult(input, runId) {
7058
7117
  status: "pass",
7059
7118
  format: input.read.format,
7060
7119
  ...runId !== void 0 ? { runId } : {},
7061
- summary: { passed: 0, failed: 0, warnings: 0, errors: 0, rulesEvaluated: 0 },
7120
+ summary: {
7121
+ passed: 0,
7122
+ failed: 0,
7123
+ warnings: 0,
7124
+ errors: 0,
7125
+ rulesEvaluated: 0,
7126
+ rulesPassed: 0,
7127
+ rulesWarning: 0,
7128
+ rulesFailed: 0,
7129
+ rulesError: 0
7130
+ },
7062
7131
  findings: [],
7063
7132
  diagnostics: [],
7064
7133
  ruleExecutions: []
@@ -7086,7 +7155,11 @@ function mergeContractResults(input, parts, extraFindings = []) {
7086
7155
  (finding) => finding.status === "warning" || finding.severity === "warning"
7087
7156
  ).length,
7088
7157
  errors,
7089
- rulesEvaluated: ruleExecutions.length
7158
+ rulesEvaluated: ruleExecutions.length,
7159
+ rulesPassed: ruleExecutions.filter((item) => item.status === "pass").length,
7160
+ rulesWarning: ruleExecutions.filter((item) => item.status === "warning").length,
7161
+ rulesFailed: ruleExecutions.filter((item) => item.status === "fail").length,
7162
+ rulesError: ruleExecutions.filter((item) => item.status === "error").length
7090
7163
  },
7091
7164
  findings,
7092
7165
  diagnostics,
@@ -7134,7 +7207,76 @@ function defineTraceContract(input) {
7134
7207
  ...alternatives ? { alternatives } : {}
7135
7208
  };
7136
7209
  }
7210
+ function isTraceReadLike(value) {
7211
+ if (typeof value !== "object" || value === null) return false;
7212
+ const candidate = value;
7213
+ return typeof candidate.format === "string" && Array.isArray(candidate.runs) && Array.isArray(candidate.events);
7214
+ }
7215
+ function isTraceCheckInputLike(value) {
7216
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
7217
+ return false;
7218
+ }
7219
+ return isTraceReadLike(value.read);
7220
+ }
7221
+ function looksLikeTraceContract(value) {
7222
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
7223
+ return false;
7224
+ }
7225
+ if (isTraceReadLike(value) || isTraceCheckInputLike(value)) {
7226
+ return false;
7227
+ }
7228
+ return true;
7229
+ }
7230
+ function invalidContractInputResult() {
7231
+ return {
7232
+ ok: false,
7233
+ status: "error",
7234
+ format: "unknown",
7235
+ summary: {
7236
+ passed: 0,
7237
+ failed: 0,
7238
+ warnings: 0,
7239
+ errors: 1,
7240
+ rulesEvaluated: 0,
7241
+ rulesPassed: 0,
7242
+ rulesWarning: 0,
7243
+ rulesFailed: 0,
7244
+ rulesError: 0
7245
+ },
7246
+ findings: [],
7247
+ diagnostics: [
7248
+ {
7249
+ code: "AI_CHECK_INVALID_ARGUMENTS",
7250
+ message: formatProgrammaticDiagnostic("AI_TRACE_CONTRACT_INPUT_INVALID"),
7251
+ severity: "error"
7252
+ }
7253
+ ],
7254
+ ruleExecutions: []
7255
+ };
7256
+ }
7257
+ function validateEvaluateTraceContractArgs(input, contract) {
7258
+ if (isTraceCheckInputLike(input) && looksLikeTraceContract(contract)) {
7259
+ return void 0;
7260
+ }
7261
+ if (looksLikeTraceContract(input) && (isTraceCheckInputLike(contract) || isTraceReadLike(contract))) {
7262
+ return invalidContractInputResult();
7263
+ }
7264
+ if (isTraceReadLike(input)) {
7265
+ return invalidContractInputResult();
7266
+ }
7267
+ if (!isTraceCheckInputLike(input)) {
7268
+ return invalidContractInputResult();
7269
+ }
7270
+ if (!looksLikeTraceContract(contract)) {
7271
+ return invalidContractInputResult();
7272
+ }
7273
+ return void 0;
7274
+ }
7137
7275
  function evaluateTraceContract(input, contract, options = {}) {
7276
+ const inputGuard = validateEvaluateTraceContractArgs(input, contract);
7277
+ if (inputGuard !== void 0) {
7278
+ return inputGuard;
7279
+ }
7138
7280
  const shapeErrors = [
7139
7281
  ...validateAllowedStatusesShape(contract.run?.allowedStatuses),
7140
7282
  ...validateAlternativesShape(contract.alternatives),
@@ -7152,7 +7294,11 @@ function evaluateTraceContract(input, contract, options = {}) {
7152
7294
  failed: 0,
7153
7295
  warnings: 0,
7154
7296
  errors: shapeErrors.length,
7155
- rulesEvaluated: 0
7297
+ rulesEvaluated: 0,
7298
+ rulesPassed: 0,
7299
+ rulesWarning: 0,
7300
+ rulesFailed: 0,
7301
+ rulesError: 0
7156
7302
  },
7157
7303
  findings: [],
7158
7304
  diagnostics: shapeErrors.map((item) => ({
@@ -7177,7 +7323,11 @@ function evaluateTraceContract(input, contract, options = {}) {
7177
7323
  failed: 0,
7178
7324
  warnings: 0,
7179
7325
  errors: scoped.diagnostics.length,
7180
- rulesEvaluated: 0
7326
+ rulesEvaluated: 0,
7327
+ rulesPassed: 0,
7328
+ rulesWarning: 0,
7329
+ rulesFailed: 0,
7330
+ rulesError: 0
7181
7331
  },
7182
7332
  findings: [],
7183
7333
  diagnostics: scoped.diagnostics,
@@ -7375,7 +7525,11 @@ function emptySummary2() {
7375
7525
  failed: 0,
7376
7526
  warnings: 0,
7377
7527
  errors: 0,
7378
- rulesEvaluated: 0
7528
+ rulesEvaluated: 0,
7529
+ rulesPassed: 0,
7530
+ rulesWarning: 0,
7531
+ rulesFailed: 0,
7532
+ rulesError: 0
7379
7533
  };
7380
7534
  }
7381
7535
  function errorResult(input, diagnostics, selectedRun, ruleExecutions = []) {
@@ -7387,7 +7541,11 @@ function errorResult(input, diagnostics, selectedRun, ruleExecutions = []) {
7387
7541
  summary: {
7388
7542
  ...emptySummary2(),
7389
7543
  errors: diagnostics.filter((item) => item.severity === "error").length,
7390
- rulesEvaluated: ruleExecutions.length
7544
+ rulesEvaluated: ruleExecutions.length,
7545
+ rulesPassed: ruleExecutions.filter((item) => item.status === "pass").length,
7546
+ rulesWarning: ruleExecutions.filter((item) => item.status === "warning").length,
7547
+ rulesFailed: ruleExecutions.filter((item) => item.status === "fail").length,
7548
+ rulesError: ruleExecutions.filter((item) => item.status === "error").length
7391
7549
  },
7392
7550
  findings: [],
7393
7551
  diagnostics: [...diagnostics],
@@ -7547,7 +7705,7 @@ function normalizeFinding(rule, finding) {
7547
7705
  ...finding.action !== void 0 ? { action: finding.action } : {}
7548
7706
  };
7549
7707
  }
7550
- function summarize(findings, diagnostics, rulesEvaluated) {
7708
+ function summarize(findings, diagnostics, ruleExecutions) {
7551
7709
  return {
7552
7710
  passed: findings.filter((finding) => finding.status === "pass").length,
7553
7711
  failed: findings.filter(
@@ -7557,7 +7715,11 @@ function summarize(findings, diagnostics, rulesEvaluated) {
7557
7715
  (finding) => finding.status === "warning" || finding.severity === "warning"
7558
7716
  ).length,
7559
7717
  errors: diagnostics.filter((item) => item.severity === "error").length,
7560
- rulesEvaluated
7718
+ rulesEvaluated: ruleExecutions.length,
7719
+ rulesPassed: ruleExecutions.filter((item) => item.status === "pass").length,
7720
+ rulesWarning: ruleExecutions.filter((item) => item.status === "warning").length,
7721
+ rulesFailed: ruleExecutions.filter((item) => item.status === "fail").length,
7722
+ rulesError: ruleExecutions.filter((item) => item.status === "error").length
7561
7723
  };
7562
7724
  }
7563
7725
  function classifyRuleExecution(findings, threw) {
@@ -9223,7 +9385,7 @@ function runTraceChecks(input, options = {}) {
9223
9385
  }
9224
9386
  const eventById = new Map(input.read.events.map((event) => [event.eventId, event]));
9225
9387
  const sortedFindings = findings.sort(compareFindings(eventById));
9226
- const summary = summarize(sortedFindings, diagnostics, ruleExecutions.length);
9388
+ const summary = summarize(sortedFindings, diagnostics, ruleExecutions);
9227
9389
  const status = summary.failed > 0 ? "fail" : "pass";
9228
9390
  return {
9229
9391
  ok: status === "pass",
@@ -11166,6 +11328,28 @@ function asPositiveNumber(value, label) {
11166
11328
  }
11167
11329
  return value;
11168
11330
  }
11331
+ function asExpectConfig(value, label) {
11332
+ if (value === void 0) return void 0;
11333
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
11334
+ throw new Error(`${label} must be an object.`);
11335
+ }
11336
+ const raw = value;
11337
+ const checkStatus = raw.checkStatus;
11338
+ if (checkStatus !== "pass" && checkStatus !== "fail" && checkStatus !== "error") {
11339
+ throw new Error(`${label}.checkStatus must be "pass", "fail", or "error".`);
11340
+ }
11341
+ const findingRuleIds = asStringArray(raw.findingRuleIds, `${label}.findingRuleIds`);
11342
+ const minAssertions = asPositiveNumber(raw.minAssertions, `${label}.minAssertions`);
11343
+ const rejectUnexpectedFindings = raw.rejectUnexpectedFindings === void 0 ? void 0 : typeof raw.rejectUnexpectedFindings === "boolean" ? raw.rejectUnexpectedFindings : (() => {
11344
+ throw new Error(`${label}.rejectUnexpectedFindings must be a boolean.`);
11345
+ })();
11346
+ return {
11347
+ checkStatus,
11348
+ ...findingRuleIds !== void 0 ? { findingRuleIds } : {},
11349
+ ...rejectUnexpectedFindings !== void 0 ? { rejectUnexpectedFindings } : {},
11350
+ ...minAssertions !== void 0 ? { minAssertions } : {}
11351
+ };
11352
+ }
11169
11353
  function validateCaseConfig(value, index) {
11170
11354
  const diagnostics = [];
11171
11355
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -11208,7 +11392,8 @@ function validateCaseConfig(value, index) {
11208
11392
  raw.expectedObservations,
11209
11393
  `cases[${index}].expectedObservations`
11210
11394
  )
11211
- } : {}
11395
+ } : {},
11396
+ ...asExpectConfig(raw.expect, `cases[${index}].expect`) !== void 0 ? { expect: asExpectConfig(raw.expect, `cases[${index}].expect`) } : {}
11212
11397
  },
11213
11398
  diagnostics
11214
11399
  };
@@ -13843,6 +14028,78 @@ function validateExpectedObservations(suiteCase, read) {
13843
14028
  }
13844
14029
  return { ok: diagnostics.length === 0, diagnostics };
13845
14030
  }
14031
+ function evaluateCaseExpect(suiteCase, checkResult, observationCount) {
14032
+ const expectation = suiteCase.expect;
14033
+ if (expectation === void 0) {
14034
+ return { ok: true, diagnostics: [] };
14035
+ }
14036
+ const diagnostics = [];
14037
+ if (checkResult.status !== expectation.checkStatus) {
14038
+ diagnostics.push(
14039
+ diagnostic5(
14040
+ "AI_SUITE_EXPECT_MISMATCH",
14041
+ `Expected check status "${expectation.checkStatus}", got "${checkResult.status}".`,
14042
+ "error",
14043
+ suiteCase.id
14044
+ )
14045
+ );
14046
+ }
14047
+ const failedFindings = checkResult.findings.filter(
14048
+ (finding) => finding.status === "fail" && finding.severity === "error"
14049
+ );
14050
+ const requiredIds = expectation.findingRuleIds ?? [];
14051
+ for (const ruleId of requiredIds) {
14052
+ if (!failedFindings.some((finding) => finding.ruleId === ruleId)) {
14053
+ diagnostics.push(
14054
+ diagnostic5(
14055
+ "AI_SUITE_EXPECT_MISMATCH",
14056
+ `Expected failing finding for rule "${ruleId}" was not present.`,
14057
+ "error",
14058
+ suiteCase.id
14059
+ )
14060
+ );
14061
+ }
14062
+ }
14063
+ const rejectUnexpected = expectation.rejectUnexpectedFindings !== false;
14064
+ if (rejectUnexpected && requiredIds.length > 0) {
14065
+ for (const finding of failedFindings) {
14066
+ if (!requiredIds.includes(finding.ruleId)) {
14067
+ diagnostics.push(
14068
+ diagnostic5(
14069
+ "AI_SUITE_EXPECT_MISMATCH",
14070
+ `Unexpected failing finding for rule "${finding.ruleId}".`,
14071
+ "error",
14072
+ suiteCase.id
14073
+ )
14074
+ );
14075
+ }
14076
+ }
14077
+ }
14078
+ if (expectation.minAssertions !== void 0) {
14079
+ const assertionCount = checkResult.ruleExecutions.length + observationCount;
14080
+ if (assertionCount < expectation.minAssertions) {
14081
+ diagnostics.push(
14082
+ diagnostic5(
14083
+ "AI_SUITE_EXPECT_MISMATCH",
14084
+ `Expected at least ${expectation.minAssertions} assertion(s), got ${assertionCount}.`,
14085
+ "error",
14086
+ suiteCase.id
14087
+ )
14088
+ );
14089
+ }
14090
+ }
14091
+ if (expectation.checkStatus === "fail" && checkResult.diagnostics.some((item) => item.severity === "error") && checkResult.status === "error") {
14092
+ diagnostics.push(
14093
+ diagnostic5(
14094
+ "AI_SUITE_EXPECT_MISMATCH",
14095
+ 'Check ended in evaluator/config error; semantic failure expectation requires status "fail".',
14096
+ "error",
14097
+ suiteCase.id
14098
+ )
14099
+ );
14100
+ }
14101
+ return { ok: diagnostics.length === 0, diagnostics };
14102
+ }
13846
14103
  async function runSuiteCase(suiteCase, config, options) {
13847
14104
  const resolved = await resolveSuiteCaseTrace(suiteCase, options);
13848
14105
  if (resolved.missing || resolved.tracePath === void 0) {
@@ -13895,9 +14152,21 @@ async function runSuiteCase(suiteCase, config, options) {
13895
14152
  status: "pass",
13896
14153
  format: read.format,
13897
14154
  findings: [],
13898
- diagnostics: []};
14155
+ diagnostics: [],
14156
+ ruleExecutions: []
14157
+ };
13899
14158
  const observationResult = validateExpectedObservations(suiteCase, read);
13900
- const diagnostics = [
14159
+ const checkOk = checkResult.ok;
14160
+ const observationsOk = observationResult.ok;
14161
+ const evalOk = config.eval?.requireSuccess === true ? checkResult.findings.every(
14162
+ (finding) => finding.ruleId !== "run.status" || finding.status !== "fail"
14163
+ ) && checkResult.diagnostics.every((item) => item.severity !== "error") : void 0;
14164
+ const expectResult = evaluateCaseExpect(
14165
+ suiteCase,
14166
+ checkResult,
14167
+ compiled.observationCount
14168
+ );
14169
+ const checkFailureDiagnostics = [
13901
14170
  ...checkResult.diagnostics.map(
13902
14171
  (item) => diagnostic5(
13903
14172
  "AI_SUITE_CASE_CHECK_FAILED",
@@ -13913,15 +14182,24 @@ async function runSuiteCase(suiteCase, config, options) {
13913
14182
  finding.severity,
13914
14183
  suiteCase.id
13915
14184
  )
13916
- ),
13917
- ...observationResult.diagnostics
14185
+ )
13918
14186
  ];
13919
- const checkOk = checkResult.ok;
13920
- const observationsOk = observationResult.ok;
13921
- const evalOk = config.eval?.requireSuccess === true ? checkResult.findings.every(
13922
- (finding) => finding.ruleId !== "run.status" || finding.status !== "fail"
13923
- ) && checkResult.diagnostics.every((item) => item.severity !== "error") : void 0;
13924
- const ok = checkOk && observationsOk;
14187
+ const expectMatched = suiteCase.expect !== void 0 && expectResult.ok && observationsOk;
14188
+ const diagnostics = expectMatched ? [
14189
+ ...observationResult.diagnostics,
14190
+ ...expectResult.diagnostics,
14191
+ diagnostic5(
14192
+ "AI_SUITE_CASE_CHECK_FAILED",
14193
+ `Expected semantic failure matched (check.status=${checkResult.status}).`,
14194
+ "info",
14195
+ suiteCase.id
14196
+ )
14197
+ ] : [
14198
+ ...checkFailureDiagnostics,
14199
+ ...observationResult.diagnostics,
14200
+ ...expectResult.diagnostics
14201
+ ];
14202
+ const ok = suiteCase.expect !== void 0 ? expectResult.ok && observationsOk : checkOk && observationsOk;
13925
14203
  const status = ok ? "pass" : checkResult.status === "error" ? "error" : "fail";
13926
14204
  return {
13927
14205
  id: suiteCase.id,
@@ -15097,5 +15375,5 @@ function renderGateReport(result, options = {}) {
15097
15375
  }
15098
15376
 
15099
15377
  export { ATTRIBUTION_CONFIDENCES, COHORT_METRIC_IDS, DEFAULT_REDACT_KEYS, DEFAULT_SUITE_ARTIFACTS_DIR, EVIDENCE_FORMAT_VERSION, EVIDENCE_HTML_FILENAME, EVIDENCE_MANIFEST_FILENAME, Redactor, TraceDirectory, TraceReadError, TreeBuilder, aggregateBundleSafeStatus, aggregateSessionCheckResults, analyzeCohort, applyProfileMetadataCaps, assertBundlePathContained, assertEvidenceRelativePath, buildActivitySummary, buildBundleMetadata, buildBundleSummaryMarkdown, buildEvidenceCausalFailureViewHtml, buildEvidenceCiPackage, buildEvidenceCircuitViewHtml, buildEvidenceContractsViewHtml, buildEvidenceDiffViewHtml, buildEvidenceHtmlShell, buildEvidenceManifest, buildEvidenceOutcomesViewHtml, buildEvidenceProvenanceViewHtml, buildEvidenceSafetyViewHtml, buildEvidenceTimelineViewHtml, buildEvidenceToolsLlmViewHtml, buildEvidenceTreeViewHtml, buildLocalExplanation, buildPlaceholderArtifact, buildRunSummary, buildRunTimeline, buildRunWhatSummary, buildSessionIndex, buildTraceStats, buildZipArchive, bundleFailsOnSafety, bundleRunAssetRelativePath, collectTraceSchemaVersions, compactAttributes, createBaselineRegressionRule, createLlmUsageRule, createMaxStepDurationRule, createObservedOutcomeRule, createRequireCompletedRule, createRunDepthRule, createRunDurationRule, createRunStatusRule, createSafetyOversizedAttributeRule, createSafetyRawContentRule, createSafetyRedactionRule, createSafetySecretPatternRule, createStallDetectionRule, createStructureCycleRule, createStructureOrphanRule, createStructureParallelWidthRule, createStructureRelationshipRule, createToolUsageRule, defaultBundleOutputPath, defaultSuiteConfigTemplate, defineTraceContract, diffRuns, diffTraceEvents, enrichSessionRunRecord, escapeHtml, escapeMarkdown, evaluateTraceContract, extractMetadata, extractOutcomesFromTraceEvents, filterMetasBySessionScope, filterTraces, flattenTree, formatDuration2 as formatDuration, formatStepLabel, formatTimestamp, gateHasThresholds, getIndent, getTraceFilePath, inferEvidenceFileRole, isAgentInspectTrace, isAttributionConfidence, isCredentialSensitiveKey, isPersistedInspectEvent, loadSessionRunRecords, loadSuiteConfig, loadTraceMetadataList, manualTraceEventsToComparableRun, nanoid, normalizeBundleOutputPath, openTrace, parseCohortMetricList, parseDuration, parseDurationFilter, parseGateList, parseTraceJsonl, persistedInspectEventsToRunTrees, persistedInspectEventsToTraceEvents, projectLogicalEvents, renderActivitySummaryHuman, renderCohortReport, renderErrorLine, renderGateReport, renderObservedOutcomesHtml, renderObservedOutcomesMarkdown, renderRunDiff, renderRunWhat, renderStepLine, renderSuiteReport, renderTimeline, renderTraceStats, resolveBundleRunIds, resolveRedactionProfile, resolveSuiteTemplate, resolveTraceDir, runGate, runSuite, runTraceChecks, safeString, sanitizeBundleRunId, searchTraces, serializeEvidenceManifest, sha256Hex, stableJson, stringContainsHighConfidenceCredential, summarizeObservedOutcomes, summarizeSemanticParity, traceEventToPersistedInspectEvent, truncateName, truncateStringForProfile, validateEvent, validateSuiteConfig, verifyEvidenceDirectory, zeroKinds };
15100
- //# sourceMappingURL=chunk-SWUZRRDT.mjs.map
15101
- //# sourceMappingURL=chunk-SWUZRRDT.mjs.map
15378
+ //# sourceMappingURL=chunk-MQYHJL3W.mjs.map
15379
+ //# sourceMappingURL=chunk-MQYHJL3W.mjs.map