@agent-inspect/viewer 6.14.1 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2216,6 +2216,81 @@ async function resolveSuiteCaseTrace(suiteCase, options) {
2216
2216
  };
2217
2217
  }
2218
2218
 
2219
+ // packages/core/src/diagnostics/programmatic.ts
2220
+ var PROGRAMMATIC_DIAGNOSTIC_SPECS = Object.freeze({
2221
+ AI_TRACE_INPUT_INVALID: {
2222
+ code: "AI_TRACE_INPUT_INVALID",
2223
+ summary: 'Expected { type: "file", path }, { type: "directory", path }, { type: "string", content }, { type: "buffer", content }, or { type: "stdin" }.',
2224
+ remediation: "For a file path, use openTraceFile(path).",
2225
+ relatedCodes: ["invalid_input"]
2226
+ },
2227
+ AI_TRACE_FORMAT_UNSUPPORTED: {
2228
+ code: "AI_TRACE_FORMAT_UNSUPPORTED",
2229
+ summary: "No trace reader could detect the input format.",
2230
+ remediation: "Pass an AgentInspect JSONL file via openTraceFile, or set options.format to a registered reader.",
2231
+ relatedCodes: ["unsupported_format"]
2232
+ },
2233
+ AI_TRACE_FORMAT_AMBIGUOUS: {
2234
+ code: "AI_TRACE_FORMAT_AMBIGUOUS",
2235
+ summary: "Multiple trace readers matched the input with equal confidence.",
2236
+ remediation: "Set options.format explicitly to disambiguate the reader.",
2237
+ relatedCodes: ["ambiguous_format"]
2238
+ },
2239
+ AI_TRACE_FACTS_INPUT_NOT_NORMALIZED: {
2240
+ code: "AI_TRACE_FACTS_INPUT_NOT_NORMALIZED",
2241
+ summary: "TraceFacts requires TraceReadResult or PersistedInspectEvent[].",
2242
+ remediation: "Use openTraceFile() to normalize a JSONL trace first."
2243
+ },
2244
+ AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED: {
2245
+ code: "AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED",
2246
+ summary: "Multiple runs are available; select a run before executing checks.",
2247
+ remediation: "Pass options.runId or TraceCheckInput.selectedRun.",
2248
+ relatedCodes: ["AI_CHECK_RUN_SELECTION_REQUIRED"]
2249
+ },
2250
+ AI_TRACE_RELATIONSHIP_SELF_PARENT: {
2251
+ code: "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2252
+ summary: "A parentId equals its own event/step id.",
2253
+ remediation: "Reject at capture (AI_LANGGRAPH_SELF_PARENT_REJECTED) or drop via logical projection; do not invent replacement parents.",
2254
+ relatedCodes: [
2255
+ "AI_LANGGRAPH_SELF_PARENT_REJECTED",
2256
+ "AI_LOGICAL_SELF_PARENT_REMOVED"
2257
+ ]
2258
+ },
2259
+ AI_TRACE_RELATIONSHIP_CYCLE: {
2260
+ code: "AI_TRACE_RELATIONSHIP_CYCLE",
2261
+ summary: "Trace contains a parentId cycle.",
2262
+ remediation: "Use visibility-first tree linking for legacy fixtures; prefer acyclic capture for new adapter output.",
2263
+ relatedCodes: ["structure.cycle"]
2264
+ }
2265
+ });
2266
+ function formatProgrammaticDiagnostic(code, detail) {
2267
+ const spec = PROGRAMMATIC_DIAGNOSTIC_SPECS[code];
2268
+ const summary = detail?.trim() ? detail.trim() : spec.summary;
2269
+ return `${code}: ${summary} Remediation: ${spec.remediation}`;
2270
+ }
2271
+
2272
+ // packages/core/src/safety/sensitive-key.ts
2273
+ function normalizeSensitiveKey(value) {
2274
+ return value.toLowerCase().replace(/[^a-z0-9_]/g, "");
2275
+ }
2276
+ new Set(
2277
+ [
2278
+ "tokens",
2279
+ "max_tokens",
2280
+ "min_tokens",
2281
+ "ls_max_tokens",
2282
+ "token_count",
2283
+ "token_limit",
2284
+ "token_budget",
2285
+ "input_tokens",
2286
+ "output_tokens",
2287
+ "total_tokens",
2288
+ "cached_tokens",
2289
+ "prompt_tokens",
2290
+ "completion_tokens"
2291
+ ].map(normalizeSensitiveKey)
2292
+ );
2293
+
2219
2294
  // packages/core/src/checks/logical-events.ts
2220
2295
  function isRecord5(value) {
2221
2296
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2411,6 +2486,26 @@ function projectLogicalEvents(events) {
2411
2486
  }
2412
2487
  }
2413
2488
  if (!remapped) {
2489
+ if (originalParentId === event.eventId) {
2490
+ diagnostics.push({
2491
+ code: "AI_LOGICAL_SELF_PARENT_REMOVED",
2492
+ message: formatProgrammaticDiagnostic(
2493
+ "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2494
+ `Removed self-parent edge on ${event.eventId}.`
2495
+ ),
2496
+ eventIds: [event.eventId]
2497
+ });
2498
+ const { parentId: _drop, ...rest } = event;
2499
+ normalized.push({
2500
+ ...rest,
2501
+ projection: {
2502
+ ...event.projection,
2503
+ parentNormalized: true,
2504
+ originalParentId
2505
+ }
2506
+ });
2507
+ continue;
2508
+ }
2414
2509
  if (!logicalById.has(originalParentId) && !logicalByRawId.has(originalParentId)) {
2415
2510
  const mapping = event.attributes?.parentMapping;
2416
2511
  const unresolved = mapping === "unresolved" || event.attributes?.parentUnresolved === true || event.attributes?.unresolvedParent === true || // LangGraph/framework scaffolding sentinel labels are not event ids.
@@ -2426,6 +2521,26 @@ function projectLogicalEvents(events) {
2426
2521
  normalized.push(event);
2427
2522
  continue;
2428
2523
  }
2524
+ if (nextParent === event.eventId) {
2525
+ diagnostics.push({
2526
+ code: "AI_LOGICAL_SELF_PARENT_REMOVED",
2527
+ message: formatProgrammaticDiagnostic(
2528
+ "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2529
+ `Removed self-parent edge on ${event.eventId} (was ${originalParentId}).`
2530
+ ),
2531
+ eventIds: [event.eventId]
2532
+ });
2533
+ const { parentId: _drop, ...rest } = event;
2534
+ normalized.push({
2535
+ ...rest,
2536
+ projection: {
2537
+ ...event.projection,
2538
+ parentNormalized: true,
2539
+ originalParentId
2540
+ }
2541
+ });
2542
+ continue;
2543
+ }
2429
2544
  diagnostics.push({
2430
2545
  code: "AI_LOGICAL_PARENT_REMAPPED",
2431
2546
  message: `Remapped parent ${originalParentId} \u2192 ${nextParent} for ${event.eventId}.`,
@@ -2475,6 +2590,11 @@ function pickString(record, keys) {
2475
2590
  return void 0;
2476
2591
  }
2477
2592
 
2593
+ // packages/core/src/checks/trace-facts.ts
2594
+ formatProgrammaticDiagnostic(
2595
+ "AI_TRACE_FACTS_INPUT_NOT_NORMALIZED"
2596
+ );
2597
+
2478
2598
  // packages/core/src/checks/index.ts
2479
2599
  var SEVERITY_RANK = {
2480
2600
  error: 0,
@@ -2584,7 +2704,13 @@ function resolveSelectedRun(input, runId) {
2584
2704
  if (input.read.runs.length === 0) {
2585
2705
  return {
2586
2706
  diagnostics: [
2587
- diagnostic3("AI_CHECK_RUN_SELECTION_REQUIRED", "No runs are available for checks.")
2707
+ diagnostic3(
2708
+ "AI_CHECK_RUN_SELECTION_REQUIRED",
2709
+ formatProgrammaticDiagnostic(
2710
+ "AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED",
2711
+ "No runs are available for checks."
2712
+ )
2713
+ )
2588
2714
  ]
2589
2715
  };
2590
2716
  }
@@ -2592,7 +2718,7 @@ function resolveSelectedRun(input, runId) {
2592
2718
  diagnostics: [
2593
2719
  diagnostic3(
2594
2720
  "AI_CHECK_RUN_SELECTION_REQUIRED",
2595
- "Multiple runs are available; select a run before executing checks."
2721
+ formatProgrammaticDiagnostic("AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED")
2596
2722
  )
2597
2723
  ]
2598
2724
  };
@@ -2819,7 +2945,7 @@ function createToolUsageRule(options) {
2819
2945
  }
2820
2946
  }
2821
2947
  const forbidden = new Set(options.forbidden ?? []);
2822
- const allowed = options.allowed ? new Set(options.allowed) : void 0;
2948
+ const allowed = options.allowed?.length ? new Set(options.allowed) : void 0;
2823
2949
  for (const event of tools) {
2824
2950
  const name = toolName(event);
2825
2951
  if (forbidden.has(name)) {
@@ -2855,9 +2981,9 @@ function createLlmUsageRule(options) {
2855
2981
  evaluate(context) {
2856
2982
  const llms = finishedEvents(context, "LLM");
2857
2983
  const findings = [];
2858
- const allowedModels = options.allowedModels ? new Set(options.allowedModels) : void 0;
2859
- const allowedProviders = options.allowedProviders ? new Set(options.allowedProviders) : void 0;
2860
- const finishReasons = options.finishReasons ? new Set(options.finishReasons) : void 0;
2984
+ const allowedModels = options.allowedModels?.length ? new Set(options.allowedModels) : void 0;
2985
+ const allowedProviders = options.allowedProviders?.length ? new Set(options.allowedProviders) : void 0;
2986
+ const finishReasons = options.finishReasons?.length ? new Set(options.finishReasons) : void 0;
2861
2987
  if (options.maxCalls !== void 0 && llms.length > options.maxCalls) {
2862
2988
  findings.push(
2863
2989
  failFinding(
@@ -3272,6 +3398,20 @@ function traceEventsToPersistedInspectEvents(events, options) {
3272
3398
  function inc(map, key) {
3273
3399
  map[key] = (map[key] ?? 0) + 1;
3274
3400
  }
3401
+ function confidenceRank(confidence) {
3402
+ switch (confidence) {
3403
+ case "unknown":
3404
+ return 0;
3405
+ case "heuristic":
3406
+ return 1;
3407
+ case "correlated":
3408
+ return 2;
3409
+ case "explicit":
3410
+ return 3;
3411
+ default:
3412
+ return 0;
3413
+ }
3414
+ }
3275
3415
  function computeRunStatus(events) {
3276
3416
  let runTerminal;
3277
3417
  let sawRunEvent = false;
@@ -3292,6 +3432,84 @@ function computeRunStatus(events) {
3292
3432
  }
3293
3433
  return hasRunning ? "running" : "ok";
3294
3434
  }
3435
+ function linkNodesVisibilityFirst(nodes) {
3436
+ let selfParentCount = 0;
3437
+ let unresolvedParentCount = 0;
3438
+ let normalizedEdgeCount = 0;
3439
+ let cycleCount = 0;
3440
+ const pending = [];
3441
+ for (const node of nodes.values()) {
3442
+ const parentId = node.event.parentId;
3443
+ if (!parentId) continue;
3444
+ if (parentId === node.event.eventId) {
3445
+ selfParentCount += 1;
3446
+ normalizedEdgeCount += 1;
3447
+ continue;
3448
+ }
3449
+ if (!nodes.has(parentId)) {
3450
+ unresolvedParentCount += 1;
3451
+ continue;
3452
+ }
3453
+ pending.push({
3454
+ childId: node.event.eventId,
3455
+ parentId,
3456
+ childTimestamp: node.event.timestamp,
3457
+ childConfidence: node.event.confidence
3458
+ });
3459
+ }
3460
+ pending.sort((a, b) => {
3461
+ const conf = confidenceRank(b.childConfidence) - confidenceRank(a.childConfidence);
3462
+ if (conf !== 0) return conf;
3463
+ return a.childTimestamp - b.childTimestamp;
3464
+ });
3465
+ const parentOf = /* @__PURE__ */ new Map();
3466
+ for (const edge of pending) {
3467
+ parentOf.set(edge.childId, edge.parentId);
3468
+ let cursor = edge.parentId;
3469
+ const seen = /* @__PURE__ */ new Set([edge.childId]);
3470
+ let cyclic = false;
3471
+ while (cursor) {
3472
+ if (seen.has(cursor)) {
3473
+ cyclic = true;
3474
+ break;
3475
+ }
3476
+ seen.add(cursor);
3477
+ cursor = parentOf.get(cursor);
3478
+ }
3479
+ if (cyclic) {
3480
+ parentOf.delete(edge.childId);
3481
+ cycleCount += 1;
3482
+ normalizedEdgeCount += 1;
3483
+ }
3484
+ }
3485
+ for (const [childId, parentId] of parentOf) {
3486
+ nodes.get(parentId).children.push(nodes.get(childId));
3487
+ }
3488
+ const roots = [];
3489
+ for (const node of nodes.values()) {
3490
+ if (!parentOf.has(node.event.eventId)) {
3491
+ roots.push(node);
3492
+ }
3493
+ }
3494
+ const assignDepth = (n, depth, stack) => {
3495
+ if (stack.has(n.event.eventId)) return;
3496
+ n.depth = depth;
3497
+ stack.add(n.event.eventId);
3498
+ for (const c of n.children) assignDepth(c, depth + 1, stack);
3499
+ stack.delete(n.event.eventId);
3500
+ };
3501
+ for (const r of roots) assignDepth(r, 0, /* @__PURE__ */ new Set());
3502
+ return {
3503
+ roots,
3504
+ summary: {
3505
+ rootCount: roots.length,
3506
+ selfParentCount,
3507
+ cycleCount,
3508
+ unresolvedParentCount,
3509
+ normalizedEdgeCount
3510
+ }
3511
+ };
3512
+ }
3295
3513
  var TreeBuilder = class {
3296
3514
  constructor(options) {
3297
3515
  void options?.config;
@@ -3309,20 +3527,7 @@ var TreeBuilder = class {
3309
3527
  for (const e of sorted) {
3310
3528
  nodes.set(e.eventId, { event: e, children: [], depth: 0 });
3311
3529
  }
3312
- const roots = [];
3313
- for (const node of nodes.values()) {
3314
- const parentId = node.event.parentId;
3315
- if (parentId && nodes.has(parentId)) {
3316
- nodes.get(parentId).children.push(node);
3317
- } else {
3318
- roots.push(node);
3319
- }
3320
- }
3321
- const assignDepth = (n, depth) => {
3322
- n.depth = depth;
3323
- for (const c of n.children) assignDepth(c, depth + 1);
3324
- };
3325
- for (const r of roots) assignDepth(r, 0);
3530
+ const { roots, summary } = linkNodesVisibilityFirst(nodes);
3326
3531
  const confidenceBreakdown = {
3327
3532
  explicit: 0,
3328
3533
  correlated: 0,
@@ -3350,7 +3555,8 @@ var TreeBuilder = class {
3350
3555
  metadata: {
3351
3556
  totalEvents: sorted.length,
3352
3557
  confidenceBreakdown,
3353
- kinds
3558
+ kinds,
3559
+ relationshipSummary: summary
3354
3560
  }
3355
3561
  });
3356
3562
  }
@@ -3579,6 +3785,38 @@ var TraceReadError = class extends Error {
3579
3785
  this.warnings = warnings;
3580
3786
  }
3581
3787
  };
3788
+ var TRACE_INPUT_INVALID_MESSAGE = formatProgrammaticDiagnostic(
3789
+ "AI_TRACE_INPUT_INVALID"
3790
+ );
3791
+ function isTraceInput(input) {
3792
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3793
+ return false;
3794
+ }
3795
+ const record = input;
3796
+ switch (record.type) {
3797
+ case "file":
3798
+ case "directory":
3799
+ return typeof record.path === "string";
3800
+ case "string":
3801
+ return typeof record.content === "string";
3802
+ case "buffer":
3803
+ return Buffer.isBuffer(record.content);
3804
+ case "stdin":
3805
+ return true;
3806
+ default:
3807
+ return false;
3808
+ }
3809
+ }
3810
+ function assertTraceInput(input) {
3811
+ if (isTraceInput(input)) return;
3812
+ throw new TraceReadError("invalid_input", TRACE_INPUT_INVALID_MESSAGE, [
3813
+ {
3814
+ code: "AI_TRACE_INPUT_INVALID",
3815
+ message: TRACE_INPUT_INVALID_MESSAGE,
3816
+ severity: "error"
3817
+ }
3818
+ ]);
3819
+ }
3582
3820
  function normalizeCandidate(reader, candidate) {
3583
3821
  const confidence = Number.isFinite(candidate.confidence) ? Math.max(0, Math.min(1, candidate.confidence)) : 0;
3584
3822
  return {
@@ -4933,6 +5171,7 @@ var DEFAULT_TRACE_READERS = [
4933
5171
  otlpJsonReader
4934
5172
  ];
4935
5173
  async function detectTraceFormat(input, options = {}) {
5174
+ assertTraceInput(input);
4936
5175
  const readers = options.readers ?? DEFAULT_TRACE_READERS;
4937
5176
  if (options.format !== void 0) {
4938
5177
  const reader = findReaderByFormat(options.format, readers);
@@ -5029,19 +5268,20 @@ async function detectTraceFormat(input, options = {}) {
5029
5268
  };
5030
5269
  }
5031
5270
  async function readTrace(input, options = {}) {
5271
+ assertTraceInput(input);
5032
5272
  const readers = options.readers ?? DEFAULT_TRACE_READERS;
5033
5273
  const detection = await detectTraceFormat(input, options);
5034
5274
  if (detection.status === "unsupported" || detection.format === void 0) {
5035
5275
  throw new TraceReadError(
5036
5276
  "unsupported_format",
5037
- "No trace reader could detect the input format.",
5277
+ formatProgrammaticDiagnostic("AI_TRACE_FORMAT_UNSUPPORTED"),
5038
5278
  detection.warnings
5039
5279
  );
5040
5280
  }
5041
5281
  if (detection.status === "ambiguous") {
5042
5282
  throw new TraceReadError(
5043
5283
  "ambiguous_format",
5044
- "Multiple trace readers matched the input with equal confidence.",
5284
+ formatProgrammaticDiagnostic("AI_TRACE_FORMAT_AMBIGUOUS"),
5045
5285
  detection.warnings
5046
5286
  );
5047
5287
  }
@@ -5049,7 +5289,10 @@ async function readTrace(input, options = {}) {
5049
5289
  if (!reader) {
5050
5290
  throw new TraceReadError(
5051
5291
  "unsupported_format",
5052
- `No trace reader is registered for format "${detection.format}".`,
5292
+ formatProgrammaticDiagnostic(
5293
+ "AI_TRACE_FORMAT_UNSUPPORTED",
5294
+ `No trace reader is registered for format "${detection.format}".`
5295
+ ),
5053
5296
  detection.warnings
5054
5297
  );
5055
5298
  }