@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.cjs CHANGED
@@ -2223,6 +2223,81 @@ async function resolveSuiteCaseTrace(suiteCase, options) {
2223
2223
  };
2224
2224
  }
2225
2225
 
2226
+ // packages/core/src/diagnostics/programmatic.ts
2227
+ var PROGRAMMATIC_DIAGNOSTIC_SPECS = Object.freeze({
2228
+ AI_TRACE_INPUT_INVALID: {
2229
+ code: "AI_TRACE_INPUT_INVALID",
2230
+ summary: 'Expected { type: "file", path }, { type: "directory", path }, { type: "string", content }, { type: "buffer", content }, or { type: "stdin" }.',
2231
+ remediation: "For a file path, use openTraceFile(path).",
2232
+ relatedCodes: ["invalid_input"]
2233
+ },
2234
+ AI_TRACE_FORMAT_UNSUPPORTED: {
2235
+ code: "AI_TRACE_FORMAT_UNSUPPORTED",
2236
+ summary: "No trace reader could detect the input format.",
2237
+ remediation: "Pass an AgentInspect JSONL file via openTraceFile, or set options.format to a registered reader.",
2238
+ relatedCodes: ["unsupported_format"]
2239
+ },
2240
+ AI_TRACE_FORMAT_AMBIGUOUS: {
2241
+ code: "AI_TRACE_FORMAT_AMBIGUOUS",
2242
+ summary: "Multiple trace readers matched the input with equal confidence.",
2243
+ remediation: "Set options.format explicitly to disambiguate the reader.",
2244
+ relatedCodes: ["ambiguous_format"]
2245
+ },
2246
+ AI_TRACE_FACTS_INPUT_NOT_NORMALIZED: {
2247
+ code: "AI_TRACE_FACTS_INPUT_NOT_NORMALIZED",
2248
+ summary: "TraceFacts requires TraceReadResult or PersistedInspectEvent[].",
2249
+ remediation: "Use openTraceFile() to normalize a JSONL trace first."
2250
+ },
2251
+ AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED: {
2252
+ code: "AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED",
2253
+ summary: "Multiple runs are available; select a run before executing checks.",
2254
+ remediation: "Pass options.runId or TraceCheckInput.selectedRun.",
2255
+ relatedCodes: ["AI_CHECK_RUN_SELECTION_REQUIRED"]
2256
+ },
2257
+ AI_TRACE_RELATIONSHIP_SELF_PARENT: {
2258
+ code: "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2259
+ summary: "A parentId equals its own event/step id.",
2260
+ remediation: "Reject at capture (AI_LANGGRAPH_SELF_PARENT_REJECTED) or drop via logical projection; do not invent replacement parents.",
2261
+ relatedCodes: [
2262
+ "AI_LANGGRAPH_SELF_PARENT_REJECTED",
2263
+ "AI_LOGICAL_SELF_PARENT_REMOVED"
2264
+ ]
2265
+ },
2266
+ AI_TRACE_RELATIONSHIP_CYCLE: {
2267
+ code: "AI_TRACE_RELATIONSHIP_CYCLE",
2268
+ summary: "Trace contains a parentId cycle.",
2269
+ remediation: "Use visibility-first tree linking for legacy fixtures; prefer acyclic capture for new adapter output.",
2270
+ relatedCodes: ["structure.cycle"]
2271
+ }
2272
+ });
2273
+ function formatProgrammaticDiagnostic(code, detail) {
2274
+ const spec = PROGRAMMATIC_DIAGNOSTIC_SPECS[code];
2275
+ const summary = detail?.trim() ? detail.trim() : spec.summary;
2276
+ return `${code}: ${summary} Remediation: ${spec.remediation}`;
2277
+ }
2278
+
2279
+ // packages/core/src/safety/sensitive-key.ts
2280
+ function normalizeSensitiveKey(value) {
2281
+ return value.toLowerCase().replace(/[^a-z0-9_]/g, "");
2282
+ }
2283
+ new Set(
2284
+ [
2285
+ "tokens",
2286
+ "max_tokens",
2287
+ "min_tokens",
2288
+ "ls_max_tokens",
2289
+ "token_count",
2290
+ "token_limit",
2291
+ "token_budget",
2292
+ "input_tokens",
2293
+ "output_tokens",
2294
+ "total_tokens",
2295
+ "cached_tokens",
2296
+ "prompt_tokens",
2297
+ "completion_tokens"
2298
+ ].map(normalizeSensitiveKey)
2299
+ );
2300
+
2226
2301
  // packages/core/src/checks/logical-events.ts
2227
2302
  function isRecord5(value) {
2228
2303
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2418,6 +2493,26 @@ function projectLogicalEvents(events) {
2418
2493
  }
2419
2494
  }
2420
2495
  if (!remapped) {
2496
+ if (originalParentId === event.eventId) {
2497
+ diagnostics.push({
2498
+ code: "AI_LOGICAL_SELF_PARENT_REMOVED",
2499
+ message: formatProgrammaticDiagnostic(
2500
+ "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2501
+ `Removed self-parent edge on ${event.eventId}.`
2502
+ ),
2503
+ eventIds: [event.eventId]
2504
+ });
2505
+ const { parentId: _drop, ...rest } = event;
2506
+ normalized.push({
2507
+ ...rest,
2508
+ projection: {
2509
+ ...event.projection,
2510
+ parentNormalized: true,
2511
+ originalParentId
2512
+ }
2513
+ });
2514
+ continue;
2515
+ }
2421
2516
  if (!logicalById.has(originalParentId) && !logicalByRawId.has(originalParentId)) {
2422
2517
  const mapping = event.attributes?.parentMapping;
2423
2518
  const unresolved = mapping === "unresolved" || event.attributes?.parentUnresolved === true || event.attributes?.unresolvedParent === true || // LangGraph/framework scaffolding sentinel labels are not event ids.
@@ -2433,6 +2528,26 @@ function projectLogicalEvents(events) {
2433
2528
  normalized.push(event);
2434
2529
  continue;
2435
2530
  }
2531
+ if (nextParent === event.eventId) {
2532
+ diagnostics.push({
2533
+ code: "AI_LOGICAL_SELF_PARENT_REMOVED",
2534
+ message: formatProgrammaticDiagnostic(
2535
+ "AI_TRACE_RELATIONSHIP_SELF_PARENT",
2536
+ `Removed self-parent edge on ${event.eventId} (was ${originalParentId}).`
2537
+ ),
2538
+ eventIds: [event.eventId]
2539
+ });
2540
+ const { parentId: _drop, ...rest } = event;
2541
+ normalized.push({
2542
+ ...rest,
2543
+ projection: {
2544
+ ...event.projection,
2545
+ parentNormalized: true,
2546
+ originalParentId
2547
+ }
2548
+ });
2549
+ continue;
2550
+ }
2436
2551
  diagnostics.push({
2437
2552
  code: "AI_LOGICAL_PARENT_REMAPPED",
2438
2553
  message: `Remapped parent ${originalParentId} \u2192 ${nextParent} for ${event.eventId}.`,
@@ -2482,6 +2597,11 @@ function pickString(record, keys) {
2482
2597
  return void 0;
2483
2598
  }
2484
2599
 
2600
+ // packages/core/src/checks/trace-facts.ts
2601
+ formatProgrammaticDiagnostic(
2602
+ "AI_TRACE_FACTS_INPUT_NOT_NORMALIZED"
2603
+ );
2604
+
2485
2605
  // packages/core/src/checks/index.ts
2486
2606
  var SEVERITY_RANK = {
2487
2607
  error: 0,
@@ -2591,7 +2711,13 @@ function resolveSelectedRun(input, runId) {
2591
2711
  if (input.read.runs.length === 0) {
2592
2712
  return {
2593
2713
  diagnostics: [
2594
- diagnostic3("AI_CHECK_RUN_SELECTION_REQUIRED", "No runs are available for checks.")
2714
+ diagnostic3(
2715
+ "AI_CHECK_RUN_SELECTION_REQUIRED",
2716
+ formatProgrammaticDiagnostic(
2717
+ "AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED",
2718
+ "No runs are available for checks."
2719
+ )
2720
+ )
2595
2721
  ]
2596
2722
  };
2597
2723
  }
@@ -2599,7 +2725,7 @@ function resolveSelectedRun(input, runId) {
2599
2725
  diagnostics: [
2600
2726
  diagnostic3(
2601
2727
  "AI_CHECK_RUN_SELECTION_REQUIRED",
2602
- "Multiple runs are available; select a run before executing checks."
2728
+ formatProgrammaticDiagnostic("AI_TRACE_CONTRACT_RUN_SELECTION_REQUIRED")
2603
2729
  )
2604
2730
  ]
2605
2731
  };
@@ -2826,7 +2952,7 @@ function createToolUsageRule(options) {
2826
2952
  }
2827
2953
  }
2828
2954
  const forbidden = new Set(options.forbidden ?? []);
2829
- const allowed = options.allowed ? new Set(options.allowed) : void 0;
2955
+ const allowed = options.allowed?.length ? new Set(options.allowed) : void 0;
2830
2956
  for (const event of tools) {
2831
2957
  const name = toolName(event);
2832
2958
  if (forbidden.has(name)) {
@@ -2862,9 +2988,9 @@ function createLlmUsageRule(options) {
2862
2988
  evaluate(context) {
2863
2989
  const llms = finishedEvents(context, "LLM");
2864
2990
  const findings = [];
2865
- const allowedModels = options.allowedModels ? new Set(options.allowedModels) : void 0;
2866
- const allowedProviders = options.allowedProviders ? new Set(options.allowedProviders) : void 0;
2867
- const finishReasons = options.finishReasons ? new Set(options.finishReasons) : void 0;
2991
+ const allowedModels = options.allowedModels?.length ? new Set(options.allowedModels) : void 0;
2992
+ const allowedProviders = options.allowedProviders?.length ? new Set(options.allowedProviders) : void 0;
2993
+ const finishReasons = options.finishReasons?.length ? new Set(options.finishReasons) : void 0;
2868
2994
  if (options.maxCalls !== void 0 && llms.length > options.maxCalls) {
2869
2995
  findings.push(
2870
2996
  failFinding(
@@ -3279,6 +3405,20 @@ function traceEventsToPersistedInspectEvents(events, options) {
3279
3405
  function inc(map, key) {
3280
3406
  map[key] = (map[key] ?? 0) + 1;
3281
3407
  }
3408
+ function confidenceRank(confidence) {
3409
+ switch (confidence) {
3410
+ case "unknown":
3411
+ return 0;
3412
+ case "heuristic":
3413
+ return 1;
3414
+ case "correlated":
3415
+ return 2;
3416
+ case "explicit":
3417
+ return 3;
3418
+ default:
3419
+ return 0;
3420
+ }
3421
+ }
3282
3422
  function computeRunStatus(events) {
3283
3423
  let runTerminal;
3284
3424
  let sawRunEvent = false;
@@ -3299,6 +3439,84 @@ function computeRunStatus(events) {
3299
3439
  }
3300
3440
  return hasRunning ? "running" : "ok";
3301
3441
  }
3442
+ function linkNodesVisibilityFirst(nodes) {
3443
+ let selfParentCount = 0;
3444
+ let unresolvedParentCount = 0;
3445
+ let normalizedEdgeCount = 0;
3446
+ let cycleCount = 0;
3447
+ const pending = [];
3448
+ for (const node of nodes.values()) {
3449
+ const parentId = node.event.parentId;
3450
+ if (!parentId) continue;
3451
+ if (parentId === node.event.eventId) {
3452
+ selfParentCount += 1;
3453
+ normalizedEdgeCount += 1;
3454
+ continue;
3455
+ }
3456
+ if (!nodes.has(parentId)) {
3457
+ unresolvedParentCount += 1;
3458
+ continue;
3459
+ }
3460
+ pending.push({
3461
+ childId: node.event.eventId,
3462
+ parentId,
3463
+ childTimestamp: node.event.timestamp,
3464
+ childConfidence: node.event.confidence
3465
+ });
3466
+ }
3467
+ pending.sort((a, b) => {
3468
+ const conf = confidenceRank(b.childConfidence) - confidenceRank(a.childConfidence);
3469
+ if (conf !== 0) return conf;
3470
+ return a.childTimestamp - b.childTimestamp;
3471
+ });
3472
+ const parentOf = /* @__PURE__ */ new Map();
3473
+ for (const edge of pending) {
3474
+ parentOf.set(edge.childId, edge.parentId);
3475
+ let cursor = edge.parentId;
3476
+ const seen = /* @__PURE__ */ new Set([edge.childId]);
3477
+ let cyclic = false;
3478
+ while (cursor) {
3479
+ if (seen.has(cursor)) {
3480
+ cyclic = true;
3481
+ break;
3482
+ }
3483
+ seen.add(cursor);
3484
+ cursor = parentOf.get(cursor);
3485
+ }
3486
+ if (cyclic) {
3487
+ parentOf.delete(edge.childId);
3488
+ cycleCount += 1;
3489
+ normalizedEdgeCount += 1;
3490
+ }
3491
+ }
3492
+ for (const [childId, parentId] of parentOf) {
3493
+ nodes.get(parentId).children.push(nodes.get(childId));
3494
+ }
3495
+ const roots = [];
3496
+ for (const node of nodes.values()) {
3497
+ if (!parentOf.has(node.event.eventId)) {
3498
+ roots.push(node);
3499
+ }
3500
+ }
3501
+ const assignDepth = (n, depth, stack) => {
3502
+ if (stack.has(n.event.eventId)) return;
3503
+ n.depth = depth;
3504
+ stack.add(n.event.eventId);
3505
+ for (const c of n.children) assignDepth(c, depth + 1, stack);
3506
+ stack.delete(n.event.eventId);
3507
+ };
3508
+ for (const r of roots) assignDepth(r, 0, /* @__PURE__ */ new Set());
3509
+ return {
3510
+ roots,
3511
+ summary: {
3512
+ rootCount: roots.length,
3513
+ selfParentCount,
3514
+ cycleCount,
3515
+ unresolvedParentCount,
3516
+ normalizedEdgeCount
3517
+ }
3518
+ };
3519
+ }
3302
3520
  var TreeBuilder = class {
3303
3521
  constructor(options) {
3304
3522
  void options?.config;
@@ -3316,20 +3534,7 @@ var TreeBuilder = class {
3316
3534
  for (const e of sorted) {
3317
3535
  nodes.set(e.eventId, { event: e, children: [], depth: 0 });
3318
3536
  }
3319
- const roots = [];
3320
- for (const node of nodes.values()) {
3321
- const parentId = node.event.parentId;
3322
- if (parentId && nodes.has(parentId)) {
3323
- nodes.get(parentId).children.push(node);
3324
- } else {
3325
- roots.push(node);
3326
- }
3327
- }
3328
- const assignDepth = (n, depth) => {
3329
- n.depth = depth;
3330
- for (const c of n.children) assignDepth(c, depth + 1);
3331
- };
3332
- for (const r of roots) assignDepth(r, 0);
3537
+ const { roots, summary } = linkNodesVisibilityFirst(nodes);
3333
3538
  const confidenceBreakdown = {
3334
3539
  explicit: 0,
3335
3540
  correlated: 0,
@@ -3357,7 +3562,8 @@ var TreeBuilder = class {
3357
3562
  metadata: {
3358
3563
  totalEvents: sorted.length,
3359
3564
  confidenceBreakdown,
3360
- kinds
3565
+ kinds,
3566
+ relationshipSummary: summary
3361
3567
  }
3362
3568
  });
3363
3569
  }
@@ -3586,6 +3792,38 @@ var TraceReadError = class extends Error {
3586
3792
  this.warnings = warnings;
3587
3793
  }
3588
3794
  };
3795
+ var TRACE_INPUT_INVALID_MESSAGE = formatProgrammaticDiagnostic(
3796
+ "AI_TRACE_INPUT_INVALID"
3797
+ );
3798
+ function isTraceInput(input) {
3799
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3800
+ return false;
3801
+ }
3802
+ const record = input;
3803
+ switch (record.type) {
3804
+ case "file":
3805
+ case "directory":
3806
+ return typeof record.path === "string";
3807
+ case "string":
3808
+ return typeof record.content === "string";
3809
+ case "buffer":
3810
+ return Buffer.isBuffer(record.content);
3811
+ case "stdin":
3812
+ return true;
3813
+ default:
3814
+ return false;
3815
+ }
3816
+ }
3817
+ function assertTraceInput(input) {
3818
+ if (isTraceInput(input)) return;
3819
+ throw new TraceReadError("invalid_input", TRACE_INPUT_INVALID_MESSAGE, [
3820
+ {
3821
+ code: "AI_TRACE_INPUT_INVALID",
3822
+ message: TRACE_INPUT_INVALID_MESSAGE,
3823
+ severity: "error"
3824
+ }
3825
+ ]);
3826
+ }
3589
3827
  function normalizeCandidate(reader, candidate) {
3590
3828
  const confidence = Number.isFinite(candidate.confidence) ? Math.max(0, Math.min(1, candidate.confidence)) : 0;
3591
3829
  return {
@@ -4940,6 +5178,7 @@ var DEFAULT_TRACE_READERS = [
4940
5178
  otlpJsonReader
4941
5179
  ];
4942
5180
  async function detectTraceFormat(input, options = {}) {
5181
+ assertTraceInput(input);
4943
5182
  const readers = options.readers ?? DEFAULT_TRACE_READERS;
4944
5183
  if (options.format !== void 0) {
4945
5184
  const reader = findReaderByFormat(options.format, readers);
@@ -5036,19 +5275,20 @@ async function detectTraceFormat(input, options = {}) {
5036
5275
  };
5037
5276
  }
5038
5277
  async function readTrace(input, options = {}) {
5278
+ assertTraceInput(input);
5039
5279
  const readers = options.readers ?? DEFAULT_TRACE_READERS;
5040
5280
  const detection = await detectTraceFormat(input, options);
5041
5281
  if (detection.status === "unsupported" || detection.format === void 0) {
5042
5282
  throw new TraceReadError(
5043
5283
  "unsupported_format",
5044
- "No trace reader could detect the input format.",
5284
+ formatProgrammaticDiagnostic("AI_TRACE_FORMAT_UNSUPPORTED"),
5045
5285
  detection.warnings
5046
5286
  );
5047
5287
  }
5048
5288
  if (detection.status === "ambiguous") {
5049
5289
  throw new TraceReadError(
5050
5290
  "ambiguous_format",
5051
- "Multiple trace readers matched the input with equal confidence.",
5291
+ formatProgrammaticDiagnostic("AI_TRACE_FORMAT_AMBIGUOUS"),
5052
5292
  detection.warnings
5053
5293
  );
5054
5294
  }
@@ -5056,7 +5296,10 @@ async function readTrace(input, options = {}) {
5056
5296
  if (!reader) {
5057
5297
  throw new TraceReadError(
5058
5298
  "unsupported_format",
5059
- `No trace reader is registered for format "${detection.format}".`,
5299
+ formatProgrammaticDiagnostic(
5300
+ "AI_TRACE_FORMAT_UNSUPPORTED",
5301
+ `No trace reader is registered for format "${detection.format}".`
5302
+ ),
5060
5303
  detection.warnings
5061
5304
  );
5062
5305
  }