@opensearch-project/agent-health 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/cli/dist/index.js +722 -330
  2. package/dist/assets/index-D-Np_l_T.js +246 -0
  3. package/dist/assets/index-vZt9QZKf.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/docs/CLI.md +138 -1
  6. package/docs/CONNECTORS.md +1 -1
  7. package/docs/SDK.md +126 -2
  8. package/docs/skills/add-connector/SKILL.md +5 -1
  9. package/lib/dist/lib/agentTrends.d.ts +210 -0
  10. package/lib/dist/lib/agentTrends.d.ts.map +1 -0
  11. package/lib/dist/lib/agentTrends.js +360 -0
  12. package/lib/dist/lib/agentTrends.js.map +1 -0
  13. package/lib/dist/lib/benchmarkCaseReview.d.ts +114 -0
  14. package/lib/dist/lib/benchmarkCaseReview.d.ts.map +1 -0
  15. package/lib/dist/lib/benchmarkCaseReview.js +177 -0
  16. package/lib/dist/lib/benchmarkCaseReview.js.map +1 -0
  17. package/lib/dist/lib/benchmarkRunsTable.d.ts +109 -0
  18. package/lib/dist/lib/benchmarkRunsTable.d.ts.map +1 -0
  19. package/lib/dist/lib/benchmarkRunsTable.js +212 -0
  20. package/lib/dist/lib/benchmarkRunsTable.js.map +1 -0
  21. package/lib/dist/lib/comparisonInsights.d.ts +49 -2
  22. package/lib/dist/lib/comparisonInsights.d.ts.map +1 -1
  23. package/lib/dist/lib/comparisonInsights.js +65 -7
  24. package/lib/dist/lib/comparisonInsights.js.map +1 -1
  25. package/lib/dist/lib/config/loader.d.ts.map +1 -1
  26. package/lib/dist/lib/config/loader.js +11 -1
  27. package/lib/dist/lib/config/loader.js.map +1 -1
  28. package/lib/dist/lib/dashboardMetrics.d.ts +11 -2
  29. package/lib/dist/lib/dashboardMetrics.d.ts.map +1 -1
  30. package/lib/dist/lib/dashboardMetrics.js +38 -3
  31. package/lib/dist/lib/dashboardMetrics.js.map +1 -1
  32. package/lib/dist/lib/evaluationRerun.d.ts +39 -0
  33. package/lib/dist/lib/evaluationRerun.d.ts.map +1 -1
  34. package/lib/dist/lib/evaluationRerun.js +49 -0
  35. package/lib/dist/lib/evaluationRerun.js.map +1 -1
  36. package/lib/dist/lib/judgeFailureSummary.d.ts +66 -0
  37. package/lib/dist/lib/judgeFailureSummary.d.ts.map +1 -0
  38. package/lib/dist/lib/judgeFailureSummary.js +68 -0
  39. package/lib/dist/lib/judgeFailureSummary.js.map +1 -0
  40. package/lib/dist/lib/judgeStrategies.d.ts +108 -0
  41. package/lib/dist/lib/judgeStrategies.d.ts.map +1 -0
  42. package/lib/dist/lib/judgeStrategies.js +135 -0
  43. package/lib/dist/lib/judgeStrategies.js.map +1 -0
  44. package/lib/dist/lib/matchers/expect.d.ts +21 -1
  45. package/lib/dist/lib/matchers/expect.d.ts.map +1 -1
  46. package/lib/dist/lib/matchers/expect.js +51 -0
  47. package/lib/dist/lib/matchers/expect.js.map +1 -1
  48. package/lib/dist/lib/matchers/judgeAccessor.d.ts +4 -0
  49. package/lib/dist/lib/matchers/judgeAccessor.d.ts.map +1 -1
  50. package/lib/dist/lib/matchers/judgeAccessor.js +13 -2
  51. package/lib/dist/lib/matchers/judgeAccessor.js.map +1 -1
  52. package/lib/dist/lib/matchers/judgeReasoningParse.d.ts +49 -0
  53. package/lib/dist/lib/matchers/judgeReasoningParse.d.ts.map +1 -0
  54. package/lib/dist/lib/matchers/judgeReasoningParse.js +128 -0
  55. package/lib/dist/lib/matchers/judgeReasoningParse.js.map +1 -0
  56. package/lib/dist/lib/matchers/types.d.ts +25 -0
  57. package/lib/dist/lib/matchers/types.d.ts.map +1 -1
  58. package/lib/dist/lib/resolveCanonicalRun.d.ts +23 -0
  59. package/lib/dist/lib/resolveCanonicalRun.d.ts.map +1 -0
  60. package/lib/dist/lib/resolveCanonicalRun.js +26 -0
  61. package/lib/dist/lib/resolveCanonicalRun.js.map +1 -0
  62. package/lib/dist/lib/runActions.d.ts +120 -0
  63. package/lib/dist/lib/runActions.d.ts.map +1 -0
  64. package/lib/dist/lib/runActions.js +130 -0
  65. package/lib/dist/lib/runActions.js.map +1 -0
  66. package/lib/dist/lib/runInsights.d.ts +86 -0
  67. package/lib/dist/lib/runInsights.d.ts.map +1 -0
  68. package/lib/dist/lib/runInsights.js +185 -0
  69. package/lib/dist/lib/runInsights.js.map +1 -0
  70. package/lib/dist/lib/runName.d.ts +29 -0
  71. package/lib/dist/lib/runName.d.ts.map +1 -0
  72. package/lib/dist/lib/runName.js +38 -0
  73. package/lib/dist/lib/runName.js.map +1 -0
  74. package/lib/dist/lib/runReportPath.d.ts +16 -0
  75. package/lib/dist/lib/runReportPath.d.ts.map +1 -0
  76. package/lib/dist/lib/runReportPath.js +22 -0
  77. package/lib/dist/lib/runReportPath.js.map +1 -0
  78. package/lib/dist/lib/runSort.d.ts +27 -0
  79. package/lib/dist/lib/runSort.d.ts.map +1 -0
  80. package/lib/dist/lib/runSort.js +31 -0
  81. package/lib/dist/lib/runSort.js.map +1 -0
  82. package/lib/dist/lib/runStats.d.ts +86 -6
  83. package/lib/dist/lib/runStats.d.ts.map +1 -1
  84. package/lib/dist/lib/runStats.js +170 -19
  85. package/lib/dist/lib/runStats.js.map +1 -1
  86. package/lib/dist/lib/testCases/define.d.ts.map +1 -1
  87. package/lib/dist/lib/testCases/define.js +93 -46
  88. package/lib/dist/lib/testCases/define.js.map +1 -1
  89. package/lib/dist/lib/testCases/judge.d.ts.map +1 -1
  90. package/lib/dist/lib/testCases/judge.js +22 -4
  91. package/lib/dist/lib/testCases/judge.js.map +1 -1
  92. package/lib/dist/lib/testCases/loader.d.ts +24 -0
  93. package/lib/dist/lib/testCases/loader.d.ts.map +1 -1
  94. package/lib/dist/lib/testCases/loader.js +253 -36
  95. package/lib/dist/lib/testCases/loader.js.map +1 -1
  96. package/lib/dist/lib/trajectoryStepDisplay.d.ts +25 -0
  97. package/lib/dist/lib/trajectoryStepDisplay.d.ts.map +1 -0
  98. package/lib/dist/lib/trajectoryStepDisplay.js +42 -0
  99. package/lib/dist/lib/trajectoryStepDisplay.js.map +1 -0
  100. package/lib/dist/lib/utils.d.ts +19 -0
  101. package/lib/dist/lib/utils.d.ts.map +1 -1
  102. package/lib/dist/lib/utils.js +27 -0
  103. package/lib/dist/lib/utils.js.map +1 -1
  104. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.d.ts +68 -32
  105. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.d.ts.map +1 -1
  106. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.js +148 -125
  107. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.js.map +1 -1
  108. package/lib/dist/services/connectors/kiro/KiroConnector.d.ts +21 -13
  109. package/lib/dist/services/connectors/kiro/KiroConnector.d.ts.map +1 -1
  110. package/lib/dist/services/connectors/kiro/KiroConnector.js +22 -25
  111. package/lib/dist/services/connectors/kiro/KiroConnector.js.map +1 -1
  112. package/lib/dist/services/connectors/pi/PiConnector.d.ts +49 -10
  113. package/lib/dist/services/connectors/pi/PiConnector.d.ts.map +1 -1
  114. package/lib/dist/services/connectors/pi/PiConnector.js +102 -86
  115. package/lib/dist/services/connectors/pi/PiConnector.js.map +1 -1
  116. package/lib/dist/services/connectors/subprocess/SubprocessConnector.d.ts +59 -14
  117. package/lib/dist/services/connectors/subprocess/SubprocessConnector.d.ts.map +1 -1
  118. package/lib/dist/services/connectors/subprocess/SubprocessConnector.js +87 -61
  119. package/lib/dist/services/connectors/subprocess/SubprocessConnector.js.map +1 -1
  120. package/lib/dist/services/evaluation/bedrockJudge.d.ts +11 -0
  121. package/lib/dist/services/evaluation/bedrockJudge.d.ts.map +1 -1
  122. package/lib/dist/services/evaluation/bedrockJudge.js +2 -0
  123. package/lib/dist/services/evaluation/bedrockJudge.js.map +1 -1
  124. package/lib/dist/services/evaluation/index.d.ts +18 -1
  125. package/lib/dist/services/evaluation/index.d.ts.map +1 -1
  126. package/lib/dist/services/evaluation/index.js +156 -19
  127. package/lib/dist/services/evaluation/index.js.map +1 -1
  128. package/lib/dist/services/metrics.d.ts +55 -0
  129. package/lib/dist/services/metrics.d.ts.map +1 -0
  130. package/lib/dist/services/metrics.js +89 -0
  131. package/lib/dist/services/metrics.js.map +1 -0
  132. package/lib/dist/services/storage/asyncBenchmarkStorage.d.ts.map +1 -1
  133. package/lib/dist/services/storage/asyncBenchmarkStorage.js +16 -0
  134. package/lib/dist/services/storage/asyncBenchmarkStorage.js.map +1 -1
  135. package/lib/dist/services/storage/asyncRunStorage.d.ts +11 -0
  136. package/lib/dist/services/storage/asyncRunStorage.d.ts.map +1 -1
  137. package/lib/dist/services/storage/asyncRunStorage.js +49 -2
  138. package/lib/dist/services/storage/asyncRunStorage.js.map +1 -1
  139. package/lib/dist/services/storage/asyncTestCaseStorage.d.ts +1 -1
  140. package/lib/dist/services/storage/asyncTestCaseStorage.d.ts.map +1 -1
  141. package/lib/dist/services/storage/asyncTestCaseStorage.js +2 -1
  142. package/lib/dist/services/storage/asyncTestCaseStorage.js.map +1 -1
  143. package/lib/dist/services/storage/opensearchClient.d.ts +12 -0
  144. package/lib/dist/services/storage/opensearchClient.d.ts.map +1 -1
  145. package/lib/dist/services/storage/opensearchClient.js.map +1 -1
  146. package/lib/dist/services/traces/browserRecovery.d.ts.map +1 -1
  147. package/lib/dist/services/traces/browserRecovery.js +3 -0
  148. package/lib/dist/services/traces/browserRecovery.js.map +1 -1
  149. package/lib/dist/services/traces/index.d.ts +8 -1
  150. package/lib/dist/services/traces/index.d.ts.map +1 -1
  151. package/lib/dist/services/traces/index.js +33 -12
  152. package/lib/dist/services/traces/index.js.map +1 -1
  153. package/lib/dist/services/traces/judgeAgentsHints.d.ts +98 -3
  154. package/lib/dist/services/traces/judgeAgentsHints.d.ts.map +1 -1
  155. package/lib/dist/services/traces/judgeAgentsHints.js +144 -3
  156. package/lib/dist/services/traces/judgeAgentsHints.js.map +1 -1
  157. package/lib/dist/services/traces/spansToTrajectory.js +4 -4
  158. package/lib/dist/services/traces/spansToTrajectory.js.map +1 -1
  159. package/lib/dist/services/traces/tracePoller.d.ts.map +1 -1
  160. package/lib/dist/services/traces/tracePoller.js +17 -20
  161. package/lib/dist/services/traces/tracePoller.js.map +1 -1
  162. package/lib/dist/services/traces/trajectoryMerge.d.ts +79 -0
  163. package/lib/dist/services/traces/trajectoryMerge.d.ts.map +1 -0
  164. package/lib/dist/services/traces/trajectoryMerge.js +109 -0
  165. package/lib/dist/services/traces/trajectoryMerge.js.map +1 -0
  166. package/lib/dist/types/index.d.ts +195 -2
  167. package/lib/dist/types/index.d.ts.map +1 -1
  168. package/lib/dist/types/index.js +24 -0
  169. package/lib/dist/types/index.js.map +1 -1
  170. package/package.json +6 -5
  171. package/server/dist/app.js +3398 -926
  172. package/server/dist/index.js +3401 -929
  173. package/dist/assets/index-BfxtxmKc.css +0 -1
  174. package/dist/assets/index-CrjAfDHu.js +0 -243
package/cli/dist/index.js CHANGED
@@ -560,7 +560,19 @@ async function loadUserConfig(configPath) {
560
560
  return module2.default ?? module2;
561
561
  } catch (error) {
562
562
  const message = error instanceof Error ? error.message : String(error);
563
- throw new Error(`Failed to load config file ${configPath}: ${message}`);
563
+ let helpText = "";
564
+ if (message.includes("ERR_MODULE_NOT_FOUND") || message.includes("Cannot find")) {
565
+ helpText = `
566
+
567
+ Ensure your cwd package.json has: {"type":"module"}`;
568
+ }
569
+ if (message.includes("tsconfig")) {
570
+ helpText = `
571
+
572
+ Set TSX_TSCONFIG_PATH to your tsconfig.json location, e.g.:
573
+ TSX_TSCONFIG_PATH=/path/to/agent-health/tsconfig.json npx @opensearch-project/agent-health`;
574
+ }
575
+ throw new Error(`Failed to load config file ${configPath}: ${message}${helpText}`);
564
576
  }
565
577
  }
566
578
  async function loadConfig(cwd = process.cwd(), force = false) {
@@ -2308,9 +2320,6 @@ var init_SubprocessConnector = __esm({
2308
2320
  * `serviceName` to point at their OpenSearch service.name for Strategy C.
2309
2321
  */
2310
2322
  this.traceContext = { propagateEnv: true };
2311
- /** Buffer of clean stdout lines accumulated during streaming.
2312
- * Used by onBeforeStreamEnd() to emit a consolidated `response` step. */
2313
- this.streamBuffer = [];
2314
2323
  this.config = { ...DEFAULT_SUBPROCESS_CONFIG, ...config };
2315
2324
  }
2316
2325
  /**
@@ -2332,31 +2341,24 @@ Question: ${prompt}`;
2332
2341
  */
2333
2342
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
2334
2343
  this.debug("========== execute() STARTED ==========");
2335
- this.streamBuffer = [];
2336
- const cfgOverride = request.connectorConfig || {};
2337
- if (cfgOverride.command !== void 0) this.config.command = cfgOverride.command;
2338
- if (cfgOverride.args !== void 0) this.config.args = cfgOverride.args;
2339
- if (cfgOverride.env !== void 0) this.config.env = { ...this.config.env || {}, ...cfgOverride.env };
2340
- if (cfgOverride.inputMode !== void 0) this.config.inputMode = cfgOverride.inputMode;
2341
- if (cfgOverride.outputParser !== void 0) this.config.outputParser = cfgOverride.outputParser;
2342
- if (cfgOverride.timeout !== void 0) this.config.timeout = cfgOverride.timeout;
2343
- if (cfgOverride.workingDir !== void 0) this.config.workingDir = cfgOverride.workingDir;
2344
- const command = endpoint || this.config.command;
2345
- const args = this.config.args || [];
2344
+ const state = this.createExecutionState();
2345
+ const config = this.resolveExecutionConfig(request);
2346
+ const command = endpoint || config.command;
2347
+ const args = config.args || [];
2346
2348
  const input = request.payload || this.buildPayload(request);
2347
2349
  const runId = `subprocess-${Date.now()}`;
2348
2350
  this.debug("Command:", command);
2349
2351
  this.debug("Args:", args);
2350
- this.debug("Input mode:", this.config.inputMode);
2351
- this.debug("Output parser:", this.config.outputParser);
2352
- this.debug("Timeout:", this.config.timeout);
2352
+ this.debug("Input mode:", config.inputMode);
2353
+ this.debug("Output parser:", config.outputParser);
2354
+ this.debug("Timeout:", config.timeout);
2353
2355
  this.debug("Input (first 500 chars):", input.substring(0, 500));
2354
- this.debug("Working dir:", this.config.workingDir || process.cwd());
2356
+ this.debug("Working dir:", config.workingDir || process.cwd());
2355
2357
  this.debug("Run ID:", runId);
2356
2358
  const env = {
2357
2359
  ...process.env,
2358
2360
  ...this.buildAuthEnv(auth),
2359
- ...this.config.env,
2361
+ ...config.env,
2360
2362
  // W3C trace context (Strategy A): TRACEPARENT/TRACESTATE from the active
2361
2363
  // eval `test_case` span. Agents whose OTel SDK honors TRACEPARENT (pi —
2362
2364
  // verified) emit their spans under the eval span's traceId, giving the
@@ -2373,12 +2375,12 @@ Question: ${prompt}`;
2373
2375
  let stdout = "";
2374
2376
  let stderr = "";
2375
2377
  let settled = false;
2376
- const finalArgs = this.config.inputMode === "arg" ? [...args, input] : args;
2378
+ const finalArgs = config.inputMode === "arg" ? [...args, input] : args;
2377
2379
  this.debug("Spawning process...");
2378
2380
  this.debug("Full command:", command, finalArgs.join(" "));
2379
2381
  const proc = spawn(command, finalArgs, {
2380
2382
  env,
2381
- cwd: this.config.workingDir,
2383
+ cwd: config.workingDir,
2382
2384
  shell: false
2383
2385
  });
2384
2386
  this.debug("Process spawned, PID:", proc.pid);
@@ -2387,9 +2389,9 @@ Question: ${prompt}`;
2387
2389
  settled = true;
2388
2390
  this.debug("TIMEOUT reached, killing process");
2389
2391
  proc.kill("SIGTERM");
2390
- reject(new Error(`Subprocess timed out after ${this.config.timeout}ms`));
2391
- }, this.config.timeout);
2392
- if (this.config.inputMode === "stdin") {
2392
+ reject(new Error(`Subprocess timed out after ${config.timeout}ms`));
2393
+ }, config.timeout);
2394
+ if (config.inputMode === "stdin") {
2393
2395
  this.debug("Writing input to stdin...");
2394
2396
  proc.stdin.write(input);
2395
2397
  proc.stdin.end();
@@ -2402,8 +2404,8 @@ Question: ${prompt}`;
2402
2404
  stdout += chunk;
2403
2405
  rawOutput.push({ type: "stdout", data: chunk, timestamp: Date.now() });
2404
2406
  onRawEvent?.({ type: "stdout", data: chunk });
2405
- if (this.config.outputParser === "streaming") {
2406
- this.parseStreamingOutput(chunk, trajectory, onProgress);
2407
+ if (config.outputParser === "streaming") {
2408
+ this.parseStreamingOutput(chunk, trajectory, onProgress, state);
2407
2409
  }
2408
2410
  });
2409
2411
  proc.stderr.on("data", (data) => {
@@ -2413,8 +2415,8 @@ Question: ${prompt}`;
2413
2415
  stderr += chunk;
2414
2416
  rawOutput.push({ type: "stderr", data: chunk, timestamp: Date.now() });
2415
2417
  onRawEvent?.({ type: "stderr", data: chunk });
2416
- if (this.config.outputParser === "streaming") {
2417
- this.parseStderrChunk(chunk, trajectory, onProgress);
2418
+ if (config.outputParser === "streaming") {
2419
+ this.parseStderrChunk(chunk, trajectory, onProgress, state);
2418
2420
  }
2419
2421
  });
2420
2422
  proc.on("close", (code2, signal) => {
@@ -2427,8 +2429,8 @@ Question: ${prompt}`;
2427
2429
  this.error(`Process exited with code ${code2}`);
2428
2430
  this.error("stderr:", stderr);
2429
2431
  }
2430
- if (this.config.outputParser === "streaming") {
2431
- this.onBeforeStreamEnd(trajectory, onProgress);
2432
+ if (config.outputParser === "streaming") {
2433
+ this.onBeforeStreamEnd(trajectory, onProgress, state);
2432
2434
  if (code2 !== 0 && trajectory.length === 0) {
2433
2435
  const errorContent = stderr.trim() ? `Error: Process exited with code ${code2}. ${stderr.trim()}` : `Error: Process exited with code ${code2}`;
2434
2436
  const errorStep = this.createStep("tool_result", errorContent, {
@@ -2438,8 +2440,8 @@ Question: ${prompt}`;
2438
2440
  onProgress?.(errorStep);
2439
2441
  }
2440
2442
  }
2441
- const finalTrajectory = this.config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code2 });
2442
- if (this.config.outputParser !== "streaming") {
2443
+ const finalTrajectory = config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code2 }, config);
2444
+ if (config.outputParser !== "streaming") {
2443
2445
  finalTrajectory.forEach((step) => onProgress?.(step));
2444
2446
  }
2445
2447
  this.debug("Resolving with trajectory of", finalTrajectory.length, "steps");
@@ -2452,7 +2454,7 @@ Question: ${prompt}`;
2452
2454
  args: finalArgs,
2453
2455
  exitCode: code2,
2454
2456
  stderr: stderr || void 0,
2455
- ...this.extraResultMetadata()
2457
+ ...this.extraResultMetadata(state)
2456
2458
  }
2457
2459
  });
2458
2460
  });
@@ -2477,22 +2479,55 @@ Question: ${prompt}`;
2477
2479
  });
2478
2480
  this.debug("========== execute() COMPLETED ==========");
2479
2481
  }
2482
+ /**
2483
+ * Create the per-invocation parse state for one `execute()` call.
2484
+ * Subclasses that buffer anything while streaming (partial NDJSON lines,
2485
+ * pending tool names, captured ids) override this to add their fields —
2486
+ * and must NOT keep that data on the instance.
2487
+ */
2488
+ createExecutionState() {
2489
+ return { streamBuffer: [] };
2490
+ }
2491
+ /**
2492
+ * Resolve the effective config for one `execute()` call: constructor
2493
+ * defaults overlaid with the request's `connectorConfig` (so any agent
2494
+ * registered with `connectorType: 'subprocess'` can specify command / args /
2495
+ * inputMode / outputParser / timeout / workingDir / env per agent). Pure —
2496
+ * MUST NOT write to `this.config`. Subclasses override to translate their
2497
+ * own `connectorConfig` shape (Claude Code flags, Pi package/model) into the
2498
+ * base fields, typically by calling `super.resolveExecutionConfig()` on a
2499
+ * request whose `connectorConfig` has been rewritten into base shape.
2500
+ */
2501
+ resolveExecutionConfig(request) {
2502
+ const o = request.connectorConfig || {};
2503
+ return {
2504
+ ...this.config,
2505
+ ...o.command !== void 0 ? { command: o.command } : {},
2506
+ ...o.args !== void 0 ? { args: [...o.args] } : { args: [...this.config.args || []] },
2507
+ env: { ...this.config.env || {}, ...o.env || {} },
2508
+ ...o.inputMode !== void 0 ? { inputMode: o.inputMode } : {},
2509
+ ...o.outputParser !== void 0 ? { outputParser: o.outputParser } : {},
2510
+ ...o.timeout !== void 0 ? { timeout: o.timeout } : {},
2511
+ ...o.workingDir !== void 0 ? { workingDir: o.workingDir } : {}
2512
+ };
2513
+ }
2480
2514
  /**
2481
2515
  * Parse a stderr chunk in streaming mode. Default is a no-op.
2482
2516
  *
2483
2517
  * Override in subclasses for CLIs that carry tool-event markers on stderr.
2484
2518
  * Implementations should buffer partial lines (chunks rarely align with
2485
- * line boundaries) and emit steps via `onProgress` AND push them onto
2486
- * `trajectory` so they appear in the final response.
2519
+ * line boundaries) in `state` and emit steps via `onProgress` AND push them
2520
+ * onto `trajectory` so they appear in the final response.
2487
2521
  */
2488
- parseStderrChunk(_chunk, _trajectory, _onProgress) {
2522
+ parseStderrChunk(_chunk, _trajectory, _onProgress, _state) {
2489
2523
  }
2490
2524
  /**
2491
2525
  * Subclass hook: extra protocol-specific fields to merge into the
2492
- * connector result `metadata`. Default: none. Claude Code overrides this to
2493
- * surface the captured `sessionId` (Strategy D trace correlation).
2526
+ * connector result `metadata`, read from the per-invocation `state` (Claude
2527
+ * Code surfaces its captured `sessionId` for Strategy D trace correlation).
2528
+ * Default: none.
2494
2529
  */
2495
- extraResultMetadata() {
2530
+ extraResultMetadata(_state) {
2496
2531
  return {};
2497
2532
  }
2498
2533
  /**
@@ -2506,7 +2541,8 @@ Question: ${prompt}`;
2506
2541
  * `response` step containing the full coherent answer (good for the
2507
2542
  * judge) — without losing the live stream.
2508
2543
  */
2509
- parseStreamingOutput(chunk, trajectory, onProgress) {
2544
+ parseStreamingOutput(chunk, trajectory, onProgress, state) {
2545
+ const buf = state?.streamBuffer ?? [];
2510
2546
  const stripped = chunk.replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").replace(/\x1b[=>NOP\\]/g, "").replace(/\r/g, "\n");
2511
2547
  const lines = stripped.split("\n");
2512
2548
  for (const raw of lines) {
@@ -2514,7 +2550,7 @@ Question: ${prompt}`;
2514
2550
  if (!line) continue;
2515
2551
  if (/^[⠁-⣿\s]+$/.test(line)) continue;
2516
2552
  if (line.length < 2 && !/[A-Za-z0-9]/.test(line)) continue;
2517
- this.streamBuffer.push(line);
2553
+ buf.push(line);
2518
2554
  const step = this.createStep("assistant", line);
2519
2555
  trajectory.push(step);
2520
2556
  onProgress?.(step);
@@ -2525,10 +2561,10 @@ Question: ${prompt}`;
2525
2561
  * clean output. Streaming gave the user real-time visibility; this final
2526
2562
  * step gives the judge a single coherent answer to grade against.
2527
2563
  */
2528
- onBeforeStreamEnd(trajectory, onProgress) {
2529
- if (this.streamBuffer.length > 0) {
2530
- const finalText = this.streamBuffer.join("\n").trim();
2531
- this.streamBuffer = [];
2564
+ onBeforeStreamEnd(trajectory, onProgress, state) {
2565
+ if (state?.streamBuffer?.length) {
2566
+ const finalText = state.streamBuffer.join("\n").trim();
2567
+ state.streamBuffer = [];
2532
2568
  if (finalText) {
2533
2569
  const step = this.createStep("response", finalText);
2534
2570
  trajectory.push(step);
@@ -2539,9 +2575,9 @@ Question: ${prompt}`;
2539
2575
  /**
2540
2576
  * Parse final subprocess output
2541
2577
  */
2542
- parseResponse(data) {
2578
+ parseResponse(data, config = this.config) {
2543
2579
  const steps = [];
2544
- if (this.config.outputParser === "json") {
2580
+ if (config.outputParser === "json") {
2545
2581
  try {
2546
2582
  const parsed = JSON.parse(data.stdout);
2547
2583
  return this.parseJsonOutput(parsed);
@@ -2599,6 +2635,24 @@ Question: ${prompt}`;
2599
2635
  });
2600
2636
 
2601
2637
  // services/connectors/claude-code/ClaudeCodeConnector.ts
2638
+ function toolResultText(block, event) {
2639
+ const c = block?.content;
2640
+ if (typeof c === "string") return c;
2641
+ if (Array.isArray(c)) {
2642
+ return c.map(
2643
+ (part) => part && part.type === "text" && typeof part.text === "string" ? part.text : JSON.stringify(part)
2644
+ ).join("\n");
2645
+ }
2646
+ if (c != null) return JSON.stringify(c);
2647
+ const mirror = event?.tool_use_result;
2648
+ if (mirror === void 0 || mirror === null) return "";
2649
+ return typeof mirror === "string" ? mirror : JSON.stringify(mirror);
2650
+ }
2651
+ function boundToolOutput(text, max = CLAUDE_CODE_MAX_TOOL_OUTPUT_CHARS) {
2652
+ if (text.length <= max) return text;
2653
+ return `${text.slice(0, max)}
2654
+ \u2026 [tool output truncated: showing ${max} of ${text.length} chars]`;
2655
+ }
2602
2656
  function createBedrockClaudeCodeConnector() {
2603
2657
  const env = {
2604
2658
  AWS_PROFILE: process.env.AWS_PROFILE || "Bedrock",
@@ -2626,11 +2680,12 @@ function createBedrockClaudeCodeConnector() {
2626
2680
  }
2627
2681
  return new ClaudeCodeConnector({ env });
2628
2682
  }
2629
- var CLAUDE_CODE_DEFAULT_CONFIG, ClaudeCodeConnector, claudeCodeConnector;
2683
+ var CLAUDE_CODE_MAX_TOOL_OUTPUT_CHARS, CLAUDE_CODE_DEFAULT_CONFIG, ClaudeCodeConnector, claudeCodeConnector;
2630
2684
  var init_ClaudeCodeConnector = __esm({
2631
2685
  "services/connectors/claude-code/ClaudeCodeConnector.ts"() {
2632
2686
  init_types();
2633
2687
  init_SubprocessConnector();
2688
+ CLAUDE_CODE_MAX_TOOL_OUTPUT_CHARS = 32 * 1024;
2634
2689
  CLAUDE_CODE_DEFAULT_CONFIG = {
2635
2690
  command: "claude",
2636
2691
  args: ["--print", "--verbose", "--output-format", "stream-json"],
@@ -2653,10 +2708,6 @@ var init_ClaudeCodeConnector = __esm({
2653
2708
  this.type = "claude-code";
2654
2709
  this.name = "Claude Code CLI";
2655
2710
  this.traceContext = { propagateEnv: true, serviceName: "claude-code-agent" };
2656
- this.outputBuffer = "";
2657
- this.thinkingBuffer = "";
2658
- this.textBuffer = "";
2659
- this.isInThinking = false;
2660
2711
  }
2661
2712
  /**
2662
2713
  * Build prompt for Claude Code
@@ -2676,20 +2727,29 @@ var init_ClaudeCodeConnector = __esm({
2676
2727
  parts.push(request.testCase.initialPrompt);
2677
2728
  return parts.join("\n");
2678
2729
  }
2730
+ createExecutionState() {
2731
+ return {
2732
+ ...super.createExecutionState(),
2733
+ outputBuffer: "",
2734
+ thinkingBuffer: "",
2735
+ textBuffer: "",
2736
+ pendingToolUses: /* @__PURE__ */ new Map()
2737
+ };
2738
+ }
2679
2739
  /**
2680
2740
  * Parse Claude Code streaming output (stream-json format)
2681
2741
  * Each line is a JSON object with type and content
2682
2742
  */
2683
- parseStreamingOutput(chunk, trajectory, onProgress) {
2684
- this.outputBuffer += chunk;
2685
- const lines = this.outputBuffer.split("\n");
2686
- this.outputBuffer = lines.pop() || "";
2743
+ parseStreamingOutput(chunk, trajectory, onProgress, state) {
2744
+ state.outputBuffer += chunk;
2745
+ const lines = state.outputBuffer.split("\n");
2746
+ state.outputBuffer = lines.pop() || "";
2687
2747
  for (const line of lines) {
2688
2748
  const trimmed = line.trim();
2689
2749
  if (!trimmed) continue;
2690
2750
  try {
2691
2751
  const event = JSON.parse(trimmed);
2692
- const steps = this.parseJsonEvent(event);
2752
+ const steps = this.parseJsonEvent(event, state);
2693
2753
  for (const step of steps) {
2694
2754
  trajectory.push(step);
2695
2755
  onProgress?.(step);
@@ -2706,10 +2766,10 @@ var init_ClaudeCodeConnector = __esm({
2706
2766
  /**
2707
2767
  * Parse a single JSON event from stream-json output
2708
2768
  */
2709
- parseJsonEvent(event) {
2769
+ parseJsonEvent(event, state) {
2710
2770
  const steps = [];
2711
2771
  if (typeof event.session_id === "string" && event.session_id) {
2712
- this.sessionId = event.session_id;
2772
+ state.sessionId = event.session_id;
2713
2773
  }
2714
2774
  if (event.type === "assistant" && event.message?.content) {
2715
2775
  for (const block of event.message.content) {
@@ -2718,6 +2778,9 @@ var init_ClaudeCodeConnector = __esm({
2718
2778
  } else if (block.type === "text" && block.text) {
2719
2779
  steps.push(this.createStep("assistant", block.text));
2720
2780
  } else if (block.type === "tool_use") {
2781
+ if (typeof block.id === "string" && block.id) {
2782
+ state.pendingToolUses.set(block.id, { name: block.name, input: block.input });
2783
+ }
2721
2784
  steps.push(this.createStep("action", JSON.stringify(block.input || {}), {
2722
2785
  toolName: block.name,
2723
2786
  toolArgs: block.input
@@ -2727,10 +2790,14 @@ var init_ClaudeCodeConnector = __esm({
2727
2790
  } else if (event.type === "user" && event.message?.content) {
2728
2791
  for (const block of event.message.content) {
2729
2792
  if (block.type === "tool_result") {
2730
- const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
2793
+ const paired = typeof block.tool_use_id === "string" ? state.pendingToolUses.get(block.tool_use_id) : void 0;
2794
+ if (paired && typeof block.tool_use_id === "string") state.pendingToolUses.delete(block.tool_use_id);
2795
+ const output = boundToolOutput(toolResultText(block, event));
2731
2796
  steps.push(
2732
- this.createStep("tool_result", content, {
2733
- status: block.is_error ? "FAILURE" /* FAILURE */ : "SUCCESS" /* SUCCESS */
2797
+ this.createStep("tool_result", output, {
2798
+ status: block.is_error ? "FAILURE" /* FAILURE */ : "SUCCESS" /* SUCCESS */,
2799
+ ...paired?.name ? { toolName: paired.name } : {},
2800
+ ...output ? { toolOutput: output } : {}
2734
2801
  })
2735
2802
  );
2736
2803
  } else if (block.type === "text" && block.text) {
@@ -2739,18 +2806,18 @@ var init_ClaudeCodeConnector = __esm({
2739
2806
  }
2740
2807
  } else if (event.type === "content_block_delta") {
2741
2808
  if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
2742
- this.thinkingBuffer += event.delta.thinking;
2809
+ state.thinkingBuffer += event.delta.thinking;
2743
2810
  } else if (event.delta?.type === "text_delta" && event.delta.text) {
2744
- this.textBuffer += event.delta.text;
2811
+ state.textBuffer += event.delta.text;
2745
2812
  }
2746
2813
  } else if (event.type === "content_block_stop") {
2747
- if (this.thinkingBuffer) {
2748
- steps.push(this.createStep("thinking", this.thinkingBuffer));
2749
- this.thinkingBuffer = "";
2814
+ if (state.thinkingBuffer) {
2815
+ steps.push(this.createStep("thinking", state.thinkingBuffer));
2816
+ state.thinkingBuffer = "";
2750
2817
  }
2751
- if (this.textBuffer) {
2752
- steps.push(this.createStep("assistant", this.textBuffer));
2753
- this.textBuffer = "";
2818
+ if (state.textBuffer) {
2819
+ steps.push(this.createStep("assistant", state.textBuffer));
2820
+ state.textBuffer = "";
2754
2821
  }
2755
2822
  } else if (event.type === "result" && event.result) {
2756
2823
  steps.push(this.createStep(
@@ -2785,45 +2852,36 @@ var init_ClaudeCodeConnector = __esm({
2785
2852
  }
2786
2853
  return steps;
2787
2854
  }
2788
- /**
2789
- * Reset state for new execution
2790
- */
2791
- resetState() {
2792
- this.outputBuffer = "";
2793
- this.thinkingBuffer = "";
2794
- this.textBuffer = "";
2795
- this.isInThinking = false;
2796
- }
2797
2855
  /**
2798
2856
  * Flush remaining buffers when the subprocess stream ends.
2799
2857
  */
2800
- onBeforeStreamEnd(trajectory, onProgress) {
2801
- if (this.outputBuffer.trim()) {
2858
+ onBeforeStreamEnd(trajectory, onProgress, state) {
2859
+ if (state.outputBuffer.trim()) {
2802
2860
  try {
2803
- const event = JSON.parse(this.outputBuffer.trim());
2804
- const steps = this.parseJsonEvent(event);
2861
+ const event = JSON.parse(state.outputBuffer.trim());
2862
+ const steps = this.parseJsonEvent(event, state);
2805
2863
  for (const step of steps) {
2806
2864
  trajectory.push(step);
2807
2865
  onProgress?.(step);
2808
2866
  }
2809
2867
  } catch {
2810
- const step = this.createStep("assistant", this.outputBuffer.trim());
2868
+ const step = this.createStep("assistant", state.outputBuffer.trim());
2811
2869
  trajectory.push(step);
2812
2870
  onProgress?.(step);
2813
2871
  }
2814
- this.outputBuffer = "";
2872
+ state.outputBuffer = "";
2815
2873
  }
2816
- if (this.thinkingBuffer) {
2817
- const step = this.createStep("thinking", this.thinkingBuffer);
2874
+ if (state.thinkingBuffer) {
2875
+ const step = this.createStep("thinking", state.thinkingBuffer);
2818
2876
  trajectory.push(step);
2819
2877
  onProgress?.(step);
2820
- this.thinkingBuffer = "";
2878
+ state.thinkingBuffer = "";
2821
2879
  }
2822
- if (this.textBuffer) {
2823
- const step = this.createStep("response", this.textBuffer);
2880
+ if (state.textBuffer) {
2881
+ const step = this.createStep("response", state.textBuffer);
2824
2882
  trajectory.push(step);
2825
2883
  onProgress?.(step);
2826
- this.textBuffer = "";
2884
+ state.textBuffer = "";
2827
2885
  }
2828
2886
  }
2829
2887
  /**
@@ -2859,61 +2917,52 @@ var init_ClaudeCodeConnector = __esm({
2859
2917
  return args;
2860
2918
  }
2861
2919
  /**
2862
- * Override execute to reset state and apply connectorConfig
2920
+ * Translate `ClaudeCodeConnectorConfig` (the agent's `connectorConfig`) into
2921
+ * the effective per-execution subprocess config. Pure: builds a fresh
2922
+ * snapshot from the constructor defaults — `this.config` is never written,
2923
+ * so concurrent executions on the shared singleton cannot compound each
2924
+ * other's flags (spawns were once observed with --append-system-prompt /
2925
+ * --allowed-tools duplicated up to 5× at concurrency 3) or swap env /
2926
+ * timeout / cwd.
2927
+ */
2928
+ resolveExecutionConfig(request) {
2929
+ const ccConfig = request.connectorConfig || {};
2930
+ const base = super.resolveExecutionConfig({
2931
+ ...request,
2932
+ // Only `env` / `timeout` / `workingDir` are base-shaped on the Claude
2933
+ // Code config; everything else is translated to CLI flags below.
2934
+ connectorConfig: {
2935
+ ...ccConfig.env ? { env: ccConfig.env } : {},
2936
+ ...ccConfig.timeout !== void 0 ? { timeout: ccConfig.timeout } : {},
2937
+ ...ccConfig.workingDir ? { workingDir: ccConfig.workingDir } : {}
2938
+ }
2939
+ });
2940
+ const configArgs = this.buildConfigArgs(ccConfig);
2941
+ if (configArgs.length > 0) this.debug("Config args added:", configArgs);
2942
+ let env = base.env || {};
2943
+ if (env.CLAUDE_CODE_USE_BEDROCK === "1") {
2944
+ env = { ...env, ANTHROPIC_API_KEY: "" };
2945
+ this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
2946
+ }
2947
+ return {
2948
+ ...base,
2949
+ env,
2950
+ args: [...this.config.args || [], ...configArgs],
2951
+ ...ccConfig.usePromptArg ? { inputMode: "arg" } : {}
2952
+ };
2953
+ }
2954
+ /**
2955
+ * Override execute for debug logging around the base implementation.
2863
2956
  */
2864
2957
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
2865
2958
  this.debug("========== execute() STARTED ==========");
2866
2959
  this.debug("Endpoint:", endpoint);
2867
2960
  this.debug("Test case:", request.testCase.name);
2868
- this.debug("Config:", this["config"]);
2869
- this.resetState();
2870
- this.pristineArgs ??= this.config.args ? [...this.config.args] : [];
2871
- const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
2872
- const originalArgs = [...this.pristineArgs];
2873
- const originalInputMode = this.config.inputMode;
2874
- const originalTimeout = this.config.timeout;
2875
- const originalWorkingDir = this.config.workingDir;
2876
- const ccConfig = request.connectorConfig;
2877
- if (ccConfig) {
2878
- this.debug("Applying connectorConfig:", Object.keys(ccConfig));
2879
- if (ccConfig.env) {
2880
- this.config.env = { ...this.config.env, ...ccConfig.env };
2881
- }
2882
- if (ccConfig.usePromptArg) {
2883
- this.config.inputMode = "arg";
2884
- }
2885
- if (ccConfig.timeout !== void 0) {
2886
- this.config.timeout = ccConfig.timeout;
2887
- }
2888
- if (ccConfig.workingDir) {
2889
- this.config.workingDir = ccConfig.workingDir;
2890
- }
2891
- }
2892
- if (this.config.env?.CLAUDE_CODE_USE_BEDROCK === "1") {
2893
- this.config.env = { ...this.config.env, ANTHROPIC_API_KEY: "" };
2894
- this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
2895
- }
2896
- if (ccConfig) {
2897
- const configArgs = this.buildConfigArgs(ccConfig);
2898
- this.config.args = [...this.pristineArgs, ...configArgs];
2899
- if (configArgs.length > 0) {
2900
- this.debug("Config args added:", configArgs);
2901
- }
2902
- }
2903
- try {
2904
- this.debug("State reset, calling super.execute()...");
2905
- this.sessionId = void 0;
2906
- const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
2907
- this.debug("super.execute() returned with", result.trajectory.length, "steps");
2908
- this.debug("========== execute() COMPLETED ==========");
2909
- return result;
2910
- } finally {
2911
- this.config.env = originalEnv;
2912
- this.config.args = originalArgs;
2913
- this.config.inputMode = originalInputMode;
2914
- this.config.timeout = originalTimeout;
2915
- this.config.workingDir = originalWorkingDir;
2916
- }
2961
+ if (request.connectorConfig) this.debug("Applying connectorConfig:", Object.keys(request.connectorConfig));
2962
+ const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
2963
+ this.debug("super.execute() returned with", result.trajectory.length, "steps");
2964
+ this.debug("========== execute() COMPLETED ==========");
2965
+ return result;
2917
2966
  }
2918
2967
  /**
2919
2968
  * Health check - verify claude command exists
@@ -2923,10 +2972,15 @@ var init_ClaudeCodeConnector = __esm({
2923
2972
  }
2924
2973
  /**
2925
2974
  * Surface the captured Claude Code `session_id` so the runner can persist it
2926
- * as `report.sessionId` for Strategy D trace correlation.
2975
+ * as `report.sessionId` for Strategy D trace correlation. Read from the
2976
+ * per-invocation state — never from the instance — so concurrent runs on
2977
+ * the shared singleton can't swap session ids.
2927
2978
  */
2928
- extraResultMetadata() {
2929
- return this.sessionId ? { sessionId: this.sessionId } : {};
2979
+ extraResultMetadata(state) {
2980
+ return {
2981
+ ...super.extraResultMetadata(state),
2982
+ ...state.sessionId ? { sessionId: state.sessionId } : {}
2983
+ };
2930
2984
  }
2931
2985
  };
2932
2986
  claudeCodeConnector = new ClaudeCodeConnector();
@@ -2960,39 +3014,36 @@ var init_KiroConnector = __esm({
2960
3014
  this.type = "kiro";
2961
3015
  this.name = "Kiro CLI";
2962
3016
  this.traceContext = { propagateEnv: true, serviceName: "kiro-agent" };
2963
- /** Carry-over for a partial last line on stderr stream */
2964
- this.stderrLineBuffer = "";
2965
- /** Tool name from the last unmatched `[tool] Running:` line, used to
2966
- * attach the same toolName to the corresponding `[tool] status:` row. */
2967
- this.pendingToolName = null;
3017
+ }
3018
+ createExecutionState() {
3019
+ return { ...super.createExecutionState(), stderrLineBuffer: "", pendingToolName: null };
2968
3020
  }
2969
3021
  /**
2970
3022
  * Convert kiro-cli's stderr `[tool]` markers into trajectory steps.
2971
3023
  *
2972
3024
  * Each chunk may straddle line boundaries, so we accumulate into
2973
- * `stderrLineBuffer` and only consume complete lines (the trailing
3025
+ * `state.stderrLineBuffer` and only consume complete lines (the trailing
2974
3026
  * partial line is held until the next chunk or `onBeforeStreamEnd`).
2975
3027
  */
2976
- parseStderrChunk(chunk, trajectory, onProgress) {
2977
- this.stderrLineBuffer += chunk;
2978
- const lines = this.stderrLineBuffer.split("\n");
2979
- this.stderrLineBuffer = lines.pop() || "";
3028
+ parseStderrChunk(chunk, trajectory, onProgress, state) {
3029
+ state.stderrLineBuffer += chunk;
3030
+ const lines = state.stderrLineBuffer.split("\n");
3031
+ state.stderrLineBuffer = lines.pop() || "";
2980
3032
  for (const raw of lines) {
2981
- this.processStderrLine(raw, trajectory, onProgress);
3033
+ this.processStderrLine(raw, trajectory, onProgress, state);
2982
3034
  }
2983
3035
  }
2984
3036
  /**
2985
- * Drain any held partial line from the stderr buffer and reset
2986
- * per-run state, then defer to the base class to flush the
2987
- * consolidated stdout response step.
3037
+ * Drain any held partial line from the stderr buffer, then defer to the
3038
+ * base class to flush the consolidated stdout response step.
2988
3039
  */
2989
- onBeforeStreamEnd(trajectory, onProgress) {
2990
- if (this.stderrLineBuffer.trim()) {
2991
- this.processStderrLine(this.stderrLineBuffer, trajectory, onProgress);
3040
+ onBeforeStreamEnd(trajectory, onProgress, state) {
3041
+ if (state.stderrLineBuffer.trim()) {
3042
+ this.processStderrLine(state.stderrLineBuffer, trajectory, onProgress, state);
2992
3043
  }
2993
- this.stderrLineBuffer = "";
2994
- this.pendingToolName = null;
2995
- super.onBeforeStreamEnd(trajectory, onProgress);
3044
+ state.stderrLineBuffer = "";
3045
+ state.pendingToolName = null;
3046
+ super.onBeforeStreamEnd(trajectory, onProgress, state);
2996
3047
  }
2997
3048
  /**
2998
3049
  * Parse a single stderr line. Recognised forms:
@@ -3004,7 +3055,7 @@ var init_KiroConnector = __esm({
3004
3055
  *
3005
3056
  * Other stderr lines (AWS SDK warnings, telemetry info, etc.) are ignored.
3006
3057
  */
3007
- processStderrLine(raw, trajectory, onProgress) {
3058
+ processStderrLine(raw, trajectory, onProgress, state) {
3008
3059
  const line = raw.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "").trim();
3009
3060
  if (!line.startsWith("[tool]")) return;
3010
3061
  const m2 = line.match(/^\[tool\]\s+(Running|status):\s*(.*)$/i);
@@ -3014,7 +3065,7 @@ var init_KiroConnector = __esm({
3014
3065
  if (kind === "running") {
3015
3066
  const toolName = payload.split(/\s+/)[0] || "unknown";
3016
3067
  const toolArgs = { command: payload };
3017
- this.pendingToolName = toolName;
3068
+ state.pendingToolName = toolName;
3018
3069
  const step = this.createStep("action", JSON.stringify(toolArgs), {
3019
3070
  toolName,
3020
3071
  toolArgs
@@ -3030,12 +3081,12 @@ var init_KiroConnector = __esm({
3030
3081
  `status: ${payload}`,
3031
3082
  {
3032
3083
  status: success ? "SUCCESS" /* SUCCESS */ : "FAILURE" /* FAILURE */,
3033
- toolName: this.pendingToolName ?? void 0
3084
+ toolName: state.pendingToolName ?? void 0
3034
3085
  }
3035
3086
  );
3036
3087
  trajectory.push(step);
3037
3088
  onProgress?.(step);
3038
- this.pendingToolName = null;
3089
+ state.pendingToolName = null;
3039
3090
  }
3040
3091
  }
3041
3092
  async healthCheck(endpoint, auth) {
@@ -3076,9 +3127,9 @@ var init_PiConnector = __esm({
3076
3127
  this.type = "pi";
3077
3128
  this.name = "Pi (pi.dev)";
3078
3129
  this.traceContext = { propagateEnv: true, serviceName: "pi-agent" };
3079
- this.piOutputBuffer = "";
3080
- this.piThinkingBuffer = "";
3081
- this.piTextBuffer = "";
3130
+ }
3131
+ createExecutionState() {
3132
+ return { ...super.createExecutionState(), piOutputBuffer: "", piThinkingBuffer: "", piTextBuffer: "" };
3082
3133
  }
3083
3134
  /**
3084
3135
  * Build prompt for Pi
@@ -3101,16 +3152,16 @@ var init_PiConnector = __esm({
3101
3152
  * Parse Pi streaming output (JSON format)
3102
3153
  * Pi's --mode json produces NDJSON lines
3103
3154
  */
3104
- parseStreamingOutput(chunk, trajectory, onProgress) {
3105
- this.piOutputBuffer += chunk;
3106
- const lines = this.piOutputBuffer.split("\n");
3107
- this.piOutputBuffer = lines.pop() || "";
3155
+ parseStreamingOutput(chunk, trajectory, onProgress, state) {
3156
+ state.piOutputBuffer += chunk;
3157
+ const lines = state.piOutputBuffer.split("\n");
3158
+ state.piOutputBuffer = lines.pop() || "";
3108
3159
  for (const line of lines) {
3109
3160
  const trimmed = line.trim();
3110
3161
  if (!trimmed) continue;
3111
3162
  try {
3112
3163
  const event = JSON.parse(trimmed);
3113
- const steps = this.parsePiEvent(event);
3164
+ const steps = this.parsePiEvent(event, state);
3114
3165
  for (const step of steps) {
3115
3166
  trajectory.push(step);
3116
3167
  onProgress?.(step);
@@ -3133,7 +3184,7 @@ var init_PiConnector = __esm({
3133
3184
  * - message_start / message_end — full message with content blocks
3134
3185
  * - message_update — streaming deltas with assistantMessageEvent
3135
3186
  */
3136
- parsePiEvent(event) {
3187
+ parsePiEvent(event, state) {
3137
3188
  const steps = [];
3138
3189
  if (event.type === "message_end" && event.message?.role === "assistant") {
3139
3190
  const content = event.message.content;
@@ -3154,9 +3205,9 @@ var init_PiConnector = __esm({
3154
3205
  } else if (event.type === "message_update") {
3155
3206
  const assistantEvent = event.assistantMessageEvent;
3156
3207
  if (assistantEvent?.type === "text_delta" && assistantEvent.delta) {
3157
- this.piTextBuffer += assistantEvent.delta;
3208
+ state.piTextBuffer += assistantEvent.delta;
3158
3209
  } else if (assistantEvent?.type === "thinking_delta" && assistantEvent.delta) {
3159
- this.piThinkingBuffer += assistantEvent.delta;
3210
+ state.piThinkingBuffer += assistantEvent.delta;
3160
3211
  }
3161
3212
  } else if (event.type === "tool_result") {
3162
3213
  const content = event.content || event.output || JSON.stringify(event);
@@ -3166,13 +3217,13 @@ var init_PiConnector = __esm({
3166
3217
  { status: event.is_error ? "FAILURE" /* FAILURE */ : "SUCCESS" /* SUCCESS */ }
3167
3218
  ));
3168
3219
  } else if (event.type === "agent_end") {
3169
- if (this.piThinkingBuffer) {
3170
- steps.push(this.createStep("thinking", this.piThinkingBuffer));
3171
- this.piThinkingBuffer = "";
3220
+ if (state.piThinkingBuffer) {
3221
+ steps.push(this.createStep("thinking", state.piThinkingBuffer));
3222
+ state.piThinkingBuffer = "";
3172
3223
  }
3173
- if (this.piTextBuffer) {
3174
- steps.push(this.createStep("response", this.piTextBuffer));
3175
- this.piTextBuffer = "";
3224
+ if (state.piTextBuffer) {
3225
+ steps.push(this.createStep("response", state.piTextBuffer));
3226
+ state.piTextBuffer = "";
3176
3227
  }
3177
3228
  }
3178
3229
  return steps;
@@ -3196,90 +3247,105 @@ var init_PiConnector = __esm({
3196
3247
  /**
3197
3248
  * Flush remaining buffers when the subprocess stream ends.
3198
3249
  */
3199
- onBeforeStreamEnd(trajectory, onProgress) {
3200
- if (this.piOutputBuffer.trim()) {
3250
+ onBeforeStreamEnd(trajectory, onProgress, state) {
3251
+ if (state.piOutputBuffer.trim()) {
3201
3252
  try {
3202
- const event = JSON.parse(this.piOutputBuffer.trim());
3203
- const steps = this.parsePiEvent(event);
3253
+ const event = JSON.parse(state.piOutputBuffer.trim());
3254
+ const steps = this.parsePiEvent(event, state);
3204
3255
  for (const step of steps) {
3205
3256
  trajectory.push(step);
3206
3257
  onProgress?.(step);
3207
3258
  }
3208
3259
  } catch {
3209
- const step = this.createStep("assistant", this.piOutputBuffer.trim());
3260
+ const step = this.createStep("assistant", state.piOutputBuffer.trim());
3210
3261
  trajectory.push(step);
3211
3262
  onProgress?.(step);
3212
3263
  }
3213
- this.piOutputBuffer = "";
3264
+ state.piOutputBuffer = "";
3214
3265
  }
3215
- if (this.piThinkingBuffer) {
3216
- const step = this.createStep("thinking", this.piThinkingBuffer);
3266
+ if (state.piThinkingBuffer) {
3267
+ const step = this.createStep("thinking", state.piThinkingBuffer);
3217
3268
  trajectory.push(step);
3218
3269
  onProgress?.(step);
3219
- this.piThinkingBuffer = "";
3270
+ state.piThinkingBuffer = "";
3220
3271
  }
3221
- if (this.piTextBuffer) {
3222
- const step = this.createStep("response", this.piTextBuffer);
3272
+ if (state.piTextBuffer) {
3273
+ const step = this.createStep("response", state.piTextBuffer);
3223
3274
  trajectory.push(step);
3224
3275
  onProgress?.(step);
3225
- this.piTextBuffer = "";
3276
+ state.piTextBuffer = "";
3226
3277
  }
3227
3278
  }
3228
3279
  /**
3229
- * Reset state and apply connectorConfig
3280
+ * Translate `PiConnectorConfig` into the effective per-execution subprocess
3281
+ * config. Pure — `this.config` is never written, so concurrent executions
3282
+ * on the shared singleton cannot see each other's args / env / timeout.
3230
3283
  */
3231
- async execute(endpoint, request, auth, onProgress, onRawEvent) {
3232
- this.piOutputBuffer = "";
3233
- this.piThinkingBuffer = "";
3234
- this.piTextBuffer = "";
3235
- const originalArgs = this.config.args ? [...this.config.args] : [];
3236
- const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
3237
- const originalTimeout = this.config.timeout;
3238
- const originalWorkingDir = this.config.workingDir;
3239
- const piConfig = request.connectorConfig;
3240
- if (piConfig) {
3241
- if (piConfig.env) {
3242
- this.config.env = { ...this.config.env, ...piConfig.env };
3243
- }
3244
- if (piConfig.timeout !== void 0) {
3245
- this.config.timeout = piConfig.timeout;
3246
- }
3247
- if (piConfig.workingDir) {
3248
- this.config.workingDir = piConfig.workingDir;
3249
- }
3250
- const extraArgs = [];
3251
- if (piConfig.packagePath) {
3252
- extraArgs.push("--skill", `${piConfig.packagePath}/skills/*`);
3253
- extraArgs.push("--extension", `${piConfig.packagePath}/extensions/agent-health.ts`);
3254
- extraArgs.push("--append-system-prompt", `${piConfig.packagePath}/prompts/agent-health.md`);
3255
- }
3256
- if (piConfig.model) {
3257
- extraArgs.push("--model", piConfig.model);
3258
- }
3259
- if (piConfig.additionalArgs) {
3260
- extraArgs.push(...piConfig.additionalArgs);
3261
- }
3262
- if (extraArgs.length > 0) {
3263
- this.config.args = [...this.config.args || [], ...extraArgs];
3284
+ resolveExecutionConfig(request) {
3285
+ const piConfig = request.connectorConfig || {};
3286
+ const base = super.resolveExecutionConfig({
3287
+ ...request,
3288
+ connectorConfig: {
3289
+ ...piConfig.env ? { env: piConfig.env } : {},
3290
+ ...piConfig.timeout !== void 0 ? { timeout: piConfig.timeout } : {},
3291
+ ...piConfig.workingDir ? { workingDir: piConfig.workingDir } : {}
3264
3292
  }
3293
+ });
3294
+ const extraArgs = [];
3295
+ if (piConfig.packagePath) {
3296
+ extraArgs.push("--skill", `${piConfig.packagePath}/skills/*`);
3297
+ extraArgs.push("--extension", `${piConfig.packagePath}/extensions/agent-health.ts`);
3298
+ extraArgs.push("--append-system-prompt", `${piConfig.packagePath}/prompts/agent-health.md`);
3265
3299
  }
3266
- if (request.modelId) {
3267
- this.config.args = [...this.config.args || [], "--model", request.modelId];
3300
+ if (piConfig.model) {
3301
+ extraArgs.push("--model", piConfig.model);
3268
3302
  }
3269
- if (process.env.AWS_PROFILE) {
3270
- this.config.env = { ...this.config.env, AWS_PROFILE: process.env.AWS_PROFILE };
3303
+ if (piConfig.additionalArgs) {
3304
+ extraArgs.push(...piConfig.additionalArgs);
3271
3305
  }
3272
- if (process.env.AWS_REGION) {
3273
- this.config.env = { ...this.config.env, AWS_REGION: process.env.AWS_REGION };
3306
+ let args = [...this.config.args || [], ...extraArgs];
3307
+ if (request.modelId) {
3308
+ args = [...this.stripModelFlag(args), "--model", request.modelId];
3274
3309
  }
3275
- try {
3276
- return await super.execute(endpoint, request, auth, onProgress, onRawEvent);
3277
- } finally {
3278
- this.config.args = originalArgs;
3279
- this.config.env = originalEnv;
3280
- this.config.timeout = originalTimeout;
3281
- this.config.workingDir = originalWorkingDir;
3310
+ const env = { ...base.env || {} };
3311
+ if (process.env.AWS_PROFILE) env.AWS_PROFILE = process.env.AWS_PROFILE;
3312
+ if (process.env.AWS_REGION) env.AWS_REGION = process.env.AWS_REGION;
3313
+ return { ...base, args, env };
3314
+ }
3315
+ /**
3316
+ * Remove any `--model <value>` (two-token) or `--model=<value>` (single-
3317
+ * token) occurrence from an argv array. Used so `request.modelId` can
3318
+ * override `connectorConfig.model` without leaving a stale second
3319
+ * `--model` flag in the final argv.
3320
+ *
3321
+ * This connector only ever generates the two-token form itself, but
3322
+ * `connectorConfig.additionalArgs` is user-supplied and could plausibly
3323
+ * contain the `--model=value` form, so both are handled (codex_review
3324
+ * finding: a stray `--model=value` from `additionalArgs` would otherwise
3325
+ * survive alongside the newly-appended two-token flag, recreating the
3326
+ * duplicate-flag bug). If a bare `--model` has no following value (or is
3327
+ * the last element), only the bare flag is removed — the next token is
3328
+ * left alone rather than being swallowed as a "value" when it's actually
3329
+ * another flag (codex_review finding: guards against corrupting unrelated
3330
+ * malformed argv, e.g. `['--foo', '--model', '--bar']`).
3331
+ */
3332
+ stripModelFlag(args) {
3333
+ const result = [];
3334
+ for (let i = 0; i < args.length; i++) {
3335
+ const arg = args[i];
3336
+ if (arg === "--model") {
3337
+ const next = args[i + 1];
3338
+ if (next !== void 0 && !next.startsWith("-")) {
3339
+ i++;
3340
+ }
3341
+ continue;
3342
+ }
3343
+ if (arg.startsWith("--model=")) {
3344
+ continue;
3345
+ }
3346
+ result.push(arg);
3282
3347
  }
3348
+ return result;
3283
3349
  }
3284
3350
  /**
3285
3351
  * Health check - verify pi command exists
@@ -3557,21 +3623,38 @@ var init_server = __esm({
3557
3623
  });
3558
3624
 
3559
3625
  // lib/testCases/define.ts
3626
+ function getSharedState() {
3627
+ const g = globalThis;
3628
+ let state = g[REGISTRY_KEY];
3629
+ if (!state) {
3630
+ state = {
3631
+ registries: /* @__PURE__ */ new Map(),
3632
+ hookRegistries: /* @__PURE__ */ new Map(),
3633
+ activeFile: null,
3634
+ describeStack: [],
3635
+ experimentalWarningEmitted: false
3636
+ };
3637
+ g[REGISTRY_KEY] = state;
3638
+ }
3639
+ return state;
3640
+ }
3560
3641
  function emitExperimentalWarningOnce() {
3561
- if (experimentalWarningEmitted) return;
3562
- experimentalWarningEmitted = true;
3642
+ const state = getSharedState();
3643
+ if (state.experimentalWarningEmitted) return;
3644
+ state.experimentalWarningEmitted = true;
3563
3645
  if (readEnv("AH_SUPPRESS_EXPERIMENTAL", "AGENT_HEALTH_SUPPRESS_EXPERIMENTAL") === "1") return;
3564
3646
  console.warn(
3565
3647
  "[agent-health] The code-based test SDK (test()/judge()/expect()) is experimental. The API may change in a minor release without a deprecation cycle. Pin your @opensearch-project/agent-health version, or set AH_SUPPRESS_EXPERIMENTAL=1 to silence this notice."
3566
3648
  );
3567
3649
  }
3568
3650
  function setActiveFile(filePath2) {
3569
- activeFile = filePath2;
3570
- if (!registries.has(filePath2)) {
3571
- registries.set(filePath2, []);
3651
+ const state = getSharedState();
3652
+ state.activeFile = filePath2;
3653
+ if (!state.registries.has(filePath2)) {
3654
+ state.registries.set(filePath2, []);
3572
3655
  }
3573
- if (!hookRegistries.has(filePath2)) {
3574
- hookRegistries.set(filePath2, []);
3656
+ if (!state.hookRegistries.has(filePath2)) {
3657
+ state.hookRegistries.set(filePath2, []);
3575
3658
  }
3576
3659
  }
3577
3660
  function test(name, optionsOrBody, maybeBody) {
@@ -3591,14 +3674,15 @@ function test(name, optionsOrBody, maybeBody) {
3591
3674
  if (typeof evaluate2 !== "function") {
3592
3675
  throw new Error(`test("${name}") requires a body function`);
3593
3676
  }
3594
- const key = activeFile ?? DEFAULT_KEY;
3595
- if (!registries.has(key)) {
3596
- registries.set(key, []);
3677
+ const state = getSharedState();
3678
+ const key = state.activeFile ?? DEFAULT_KEY;
3679
+ if (!state.registries.has(key)) {
3680
+ state.registries.set(key, []);
3597
3681
  }
3598
- const registry2 = registries.get(key);
3599
- const benchmarkPath = describeStack.length > 0 ? describeStack.join(" > ") : void 0;
3682
+ const registry2 = state.registries.get(key);
3683
+ const benchmarkPath = state.describeStack.length > 0 ? state.describeStack.join(" > ") : void 0;
3600
3684
  if (registry2.some((t) => t.name === name && t.benchmarkPath === benchmarkPath)) {
3601
- const fileLabel = activeFile ? ` in ${activeFile}` : "";
3685
+ const fileLabel = state.activeFile ? ` in ${state.activeFile}` : "";
3602
3686
  const groupLabel = benchmarkPath ? ` (in describe "${benchmarkPath}")` : "";
3603
3687
  throw new Error(
3604
3688
  `Duplicate test name "${name}"${groupLabel}${fileLabel}. Test names must be unique within their describe block. Move one of the tests to a different describe() or rename it.`
@@ -3608,7 +3692,7 @@ function test(name, optionsOrBody, maybeBody) {
3608
3692
  name,
3609
3693
  options,
3610
3694
  evaluate: evaluate2,
3611
- sourceFile: activeFile ?? void 0,
3695
+ sourceFile: state.activeFile ?? void 0,
3612
3696
  benchmarkPath
3613
3697
  });
3614
3698
  }
@@ -3620,7 +3704,8 @@ function describe(name, fn) {
3620
3704
  if (typeof fn !== "function") {
3621
3705
  throw new Error(`describe("${name}") requires a body function`);
3622
3706
  }
3623
- describeStack.push(name);
3707
+ const state = getSharedState();
3708
+ state.describeStack.push(name);
3624
3709
  try {
3625
3710
  const result = fn();
3626
3711
  if (result && typeof result.then === "function") {
@@ -3629,41 +3714,45 @@ function describe(name, fn) {
3629
3714
  );
3630
3715
  }
3631
3716
  } finally {
3632
- describeStack.pop();
3717
+ state.describeStack.pop();
3633
3718
  }
3634
3719
  }
3635
3720
  function getRegisteredTests(filePath2) {
3636
- if (filePath2) return [...registries.get(filePath2) ?? []];
3637
- return [...registries.values()].flatMap((r) => [...r]);
3721
+ const state = getSharedState();
3722
+ if (filePath2) return [...state.registries.get(filePath2) ?? []];
3723
+ return [...state.registries.values()].flatMap((r) => [...r]);
3638
3724
  }
3639
3725
  function getRegisteredHooks(filePath2) {
3640
- if (filePath2) return [...hookRegistries.get(filePath2) ?? []];
3641
- return [...hookRegistries.values()].flatMap((r) => [...r]);
3726
+ const state = getSharedState();
3727
+ if (filePath2) return [...state.hookRegistries.get(filePath2) ?? []];
3728
+ return [...state.hookRegistries.values()].flatMap((r) => [...r]);
3642
3729
  }
3643
3730
  function clearRegistry(filePath2) {
3731
+ const state = getSharedState();
3644
3732
  if (filePath2) {
3645
- registries.delete(filePath2);
3646
- hookRegistries.delete(filePath2);
3733
+ state.registries.delete(filePath2);
3734
+ state.hookRegistries.delete(filePath2);
3647
3735
  } else {
3648
- registries.clear();
3649
- hookRegistries.clear();
3736
+ state.registries.clear();
3737
+ state.hookRegistries.clear();
3650
3738
  }
3651
- activeFile = null;
3739
+ state.activeFile = null;
3652
3740
  }
3653
3741
  function registerHook(kind, fn) {
3654
3742
  emitExperimentalWarningOnce();
3655
3743
  if (typeof fn !== "function") {
3656
3744
  throw new Error(`${kind}() requires a function as its first argument`);
3657
3745
  }
3658
- const key = activeFile ?? DEFAULT_KEY;
3659
- if (!hookRegistries.has(key)) {
3660
- hookRegistries.set(key, []);
3746
+ const state = getSharedState();
3747
+ const key = state.activeFile ?? DEFAULT_KEY;
3748
+ if (!state.hookRegistries.has(key)) {
3749
+ state.hookRegistries.set(key, []);
3661
3750
  }
3662
- const describePath = describeStack.length > 0 ? describeStack.join(" > ") : void 0;
3663
- hookRegistries.get(key).push({
3751
+ const describePath = state.describeStack.length > 0 ? state.describeStack.join(" > ") : void 0;
3752
+ state.hookRegistries.get(key).push({
3664
3753
  kind,
3665
3754
  fn,
3666
- sourceFile: activeFile ?? void 0,
3755
+ sourceFile: state.activeFile ?? void 0,
3667
3756
  describePath
3668
3757
  });
3669
3758
  }
@@ -3679,16 +3768,12 @@ function beforeEach(fn) {
3679
3768
  function afterEach(fn) {
3680
3769
  registerHook("afterEach", fn);
3681
3770
  }
3682
- var registries, hookRegistries, activeFile, DEFAULT_KEY, describeStack, experimentalWarningEmitted;
3771
+ var DEFAULT_KEY, REGISTRY_KEY;
3683
3772
  var init_define = __esm({
3684
3773
  "lib/testCases/define.ts"() {
3685
3774
  init_envCompat();
3686
- registries = /* @__PURE__ */ new Map();
3687
- hookRegistries = /* @__PURE__ */ new Map();
3688
- activeFile = null;
3689
3775
  DEFAULT_KEY = "__default__";
3690
- describeStack = [];
3691
- experimentalWarningEmitted = false;
3776
+ REGISTRY_KEY = /* @__PURE__ */ Symbol.for("agent-health.test-registry.v1");
3692
3777
  ((test2) => {
3693
3778
  test2.beforeAll = (fn) => registerHook("beforeAll", fn);
3694
3779
  test2.afterAll = (fn) => registerHook("afterAll", fn);
@@ -3790,10 +3875,10 @@ async function runJudge(resultOrTrajectory, claimOrClaims, options, role) {
3790
3875
  if (runId) requestBody.runId = runId;
3791
3876
  if (Array.isArray(judgeAgents) && judgeAgents.length > 0) requestBody.agents = judgeAgents;
3792
3877
  const finalize = (raw2, durationMs) => {
3793
- const accuracy = raw2.metrics?.accuracy ?? 0;
3878
+ const headline = typeof raw2.metrics?.accuracy === "number" ? raw2.metrics.accuracy : typeof raw2.overallScore === "number" ? raw2.overallScore : void 0;
3794
3879
  const verdict = makeVerdict({
3795
3880
  passFailStatus: raw2.passFailStatus ?? "failed",
3796
- accuracy,
3881
+ accuracy: headline ?? 0,
3797
3882
  reasoning: raw2.llmJudgeReasoning ?? "",
3798
3883
  role,
3799
3884
  skipped: false,
@@ -3805,15 +3890,22 @@ async function runJudge(resultOrTrajectory, claimOrClaims, options, role) {
3805
3890
  method: "llm-judge",
3806
3891
  role,
3807
3892
  durationMs,
3808
- score: verdict.score,
3893
+ // Only record a score when the judge actually produced a headline
3894
+ // number — a fabricated 0 renders as a misleading "score 0%".
3895
+ ...headline !== void 0 ? { score: headline / 100 } : {},
3809
3896
  reasoning: verdict.reasoning,
3810
3897
  model: options?.model,
3811
- errorMessage: verdict.passFailStatus === "failed" ? verdict.reasoning : void 0,
3898
+ // NOTE: no `errorMessage` mirror. It used to copy `reasoning` verbatim
3899
+ // on failure, which persisted the same multi-KB string twice and made
3900
+ // the UI render it twice (once red as "error", once as "reasoning").
3901
+ // `reasoning` is the single source of truth; consumers that need a
3902
+ // failure detail fall back to it (see expect.toPass()).
3812
3903
  // Preserve the rest of the judge payload — these were silently
3813
3904
  // dropped before, which made SDK `judge()` calls strictly less
3814
3905
  // informative than the legacy auto-judge path. See MatcherResult.
3815
3906
  ...Array.isArray(raw2.improvementStrategies) && raw2.improvementStrategies.length > 0 ? { improvementStrategies: raw2.improvementStrategies } : {},
3816
- ...raw2.metrics && typeof raw2.metrics === "object" ? { judgeMetrics: { ...raw2.metrics } } : {}
3907
+ ...raw2.metrics && typeof raw2.metrics === "object" ? { judgeMetrics: { ...raw2.metrics } } : {},
3908
+ ...raw2.extraFields && typeof raw2.extraFields === "object" && Object.keys(raw2.extraFields).length > 0 ? { judgeExtraFields: raw2.extraFields } : {}
3817
3909
  });
3818
3910
  return verdict;
3819
3911
  };
@@ -4076,6 +4168,9 @@ function ensurePluginsInstalled() {
4076
4168
  errorMessage: err?.message || String(err)
4077
4169
  });
4078
4170
  }
4171
+ if (utils.flag(this, "ahSoft")) {
4172
+ return;
4173
+ }
4079
4174
  throw err;
4080
4175
  }
4081
4176
  };
@@ -4147,6 +4242,11 @@ function ensurePluginsInstalled() {
4147
4242
  );
4148
4243
  });
4149
4244
  }
4245
+ function softExpect(val, msg) {
4246
+ const assertion = chai.expect(val, msg);
4247
+ chai.util.flag(assertion, "ahSoft", true);
4248
+ return assertion;
4249
+ }
4150
4250
  function describeAssertion(ctx, _expr, msg, negateMsg, expected, actual, utils) {
4151
4251
  const rawMsg = typeof msg === "function" ? msg() : msg;
4152
4252
  const rawNeg = typeof negateMsg === "function" ? negateMsg() : negateMsg;
@@ -4202,6 +4302,7 @@ var init_expect = __esm({
4202
4302
  pluginsInstalled = false;
4203
4303
  ensurePluginsInstalled();
4204
4304
  expect2 = chai.expect;
4305
+ expect2.soft = softExpect;
4205
4306
  }
4206
4307
  });
4207
4308
 
@@ -4627,8 +4728,8 @@ function claudeNativeTrajectory(spans) {
4627
4728
  const redacted = !prompt || prompt === "<REDACTED>";
4628
4729
  steps.push({
4629
4730
  ...base,
4630
- type: "thinking",
4631
- content: redacted ? `User: [prompt redacted \u2014 set OTEL_LOG_USER_PROMPTS=1 to capture] (${a["user_prompt_length"] ?? "?"} chars)` : `User: ${prompt}`
4731
+ type: "user",
4732
+ content: redacted ? `[prompt redacted \u2014 set OTEL_LOG_USER_PROMPTS=1 to capture] (${a["user_prompt_length"] ?? "?"} chars)` : String(prompt)
4632
4733
  });
4633
4734
  } else if (t === "llm_request") {
4634
4735
  const model = a["model"] || a["gen_ai.request.model"] || "";
@@ -4839,7 +4940,7 @@ function genericTrajectory(spans, serviceName) {
4839
4940
  break;
4840
4941
  }
4841
4942
  case "user":
4842
- steps.push({ ...base, type: "thinking", content: `User: ${m2.content}` });
4943
+ steps.push({ ...base, type: "user", content: m2.content });
4843
4944
  break;
4844
4945
  default:
4845
4946
  steps.push({ ...base, type: "thinking", content: m2.content });
@@ -5260,6 +5361,8 @@ var init_utils = __esm({
5260
5361
  // lib/testCases/loader.ts
5261
5362
  var loader_exports = {};
5262
5363
  __export(loader_exports, {
5364
+ DEFINITION_BODY_SOURCE_MAX_CHARS: () => DEFINITION_BODY_SOURCE_MAX_CHARS,
5365
+ captureTestDefinition: () => captureTestDefinition,
5263
5366
  computeTestCaseHash: () => computeTestCaseHash,
5264
5367
  detectSourceLanguage: () => detectSourceLanguage,
5265
5368
  isCodeFile: () => isCodeFile,
@@ -5300,58 +5403,144 @@ function computeTestCaseHash(tc, fileSource2) {
5300
5403
  });
5301
5404
  return createHash("sha256").update(content).digest("hex");
5302
5405
  }
5406
+ function captureTestDefinition(tc) {
5407
+ let options = {};
5408
+ try {
5409
+ const parsed = JSON.parse(JSON.stringify(tc.options ?? {}));
5410
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) options = parsed;
5411
+ } catch {
5412
+ }
5413
+ let bodySource = "";
5414
+ try {
5415
+ bodySource = typeof tc.evaluate === "function" ? Function.prototype.toString.call(tc.evaluate) : "";
5416
+ } catch {
5417
+ bodySource = "";
5418
+ }
5419
+ let bodyTruncated = false;
5420
+ if (bodySource.length > DEFINITION_BODY_SOURCE_MAX_CHARS) {
5421
+ bodySource = bodySource.slice(0, DEFINITION_BODY_SOURCE_MAX_CHARS) + BODY_TRUNCATION_MARKER;
5422
+ bodyTruncated = true;
5423
+ }
5424
+ return {
5425
+ registeredAs: "sdk",
5426
+ options,
5427
+ bodySource,
5428
+ ...bodyTruncated ? { bodyTruncated: true } : {}
5429
+ };
5430
+ }
5431
+ function getEsbuildTransformSync() {
5432
+ if (!esbuildTransformSyncPromise) {
5433
+ esbuildTransformSyncPromise = import("esbuild").then((m2) => m2.transformSync);
5434
+ }
5435
+ return esbuildTransformSyncPromise;
5436
+ }
5303
5437
  async function loadTestCasesFromModule(filePath) {
5304
5438
  const absPath = resolve4(filePath);
5305
5439
  const fileSource = readFileSync2(absPath, "utf-8");
5306
5440
  clearRegistry(absPath);
5307
5441
  setActiveFile(absPath);
5442
+ const transformSync = absPath.endsWith(".mjs") ? void 0 : await getEsbuildTransformSync();
5443
+ const tsModuleCache = /* @__PURE__ */ new Map();
5308
5444
  let module;
5309
- if (absPath.endsWith(".js")) {
5310
- const code = fileSource;
5311
- const fileDir = dirname3(absPath);
5445
+ const runAsSyntheticCjs = (code, modulePath) => {
5446
+ const fileDir = dirname3(modulePath);
5312
5447
  const Module = NodeModule;
5313
- const m = new Module(absPath);
5314
- m.filename = absPath;
5448
+ const m = new Module(modulePath);
5449
+ m.filename = modulePath;
5315
5450
  m.paths = Module._nodeModulePaths(fileDir);
5316
- const fileRequire = createRequire(absPath);
5451
+ const fileRequire = createRequire(modulePath);
5317
5452
  const isDefineId = (id) => {
5318
5453
  const normalized = id.replace(/\\/g, "/").replace(/\.js$/, "");
5319
5454
  return normalized === "lib/testCases/define" || normalized.endsWith("/lib/testCases/define");
5320
5455
  };
5321
5456
  const isPackageName = (id) => id === "@opensearch-project/agent-health" || id === "@opensearch/agent-health" || id === "agent-health";
5322
5457
  const sdkExports = getAuthoringSurface();
5458
+ const requireLocalTsModule = (resolvedTsPath) => {
5459
+ const cached = tsModuleCache.get(resolvedTsPath);
5460
+ if (cached !== void 0) return cached;
5461
+ if (!transformSync) {
5462
+ throw new Error(
5463
+ `Cannot import TypeScript file: ${resolvedTsPath}
5464
+ esbuild (required to load .ts files) was not resolved for this load.`
5465
+ );
5466
+ }
5467
+ const helperSource = readFileSync2(resolvedTsPath, "utf-8");
5468
+ const helperCjs = transformSync(helperSource, {
5469
+ loader: "ts",
5470
+ format: "cjs",
5471
+ target: "node18",
5472
+ sourcefile: resolvedTsPath
5473
+ }).code;
5474
+ const helperExports = runAsSyntheticCjs(helperCjs, resolvedTsPath);
5475
+ tsModuleCache.set(resolvedTsPath, helperExports);
5476
+ return helperExports;
5477
+ };
5323
5478
  const wrappedRequire = (id) => {
5324
5479
  if (isDefineId(id) || isPackageName(id)) {
5325
5480
  return sdkExports;
5326
5481
  }
5482
+ let resolved;
5327
5483
  try {
5328
- const resolved = fileRequire.resolve(id);
5329
- const normalized = resolved.replace(/\\/g, "/");
5330
- if (normalized.endsWith("/lib/testCases/define.js") || normalized.endsWith("/lib/testCases/define")) {
5484
+ resolved = fileRequire.resolve(id);
5485
+ } catch {
5486
+ }
5487
+ if (resolved) {
5488
+ const normalizedResolved = resolved.replace(/\\/g, "/");
5489
+ if (normalizedResolved.endsWith("/lib/testCases/define.js") || normalizedResolved.endsWith("/lib/testCases/define")) {
5331
5490
  return sdkExports;
5332
5491
  }
5333
- } catch {
5492
+ if (normalizedResolved.endsWith(".ts")) {
5493
+ return requireLocalTsModule(resolved);
5494
+ }
5495
+ }
5496
+ try {
5497
+ return fileRequire(id);
5498
+ } catch (err) {
5499
+ if (err.code === "ERR_REQUIRE_ESM") {
5500
+ throw new Error(
5501
+ `.eval.ts fixtures cannot import ESM-only packages via require() (attempted: '${id}').
5502
+ '${id}' has no CommonJS entry point, and .eval.ts files execute as synthetic CJS.
5503
+ Use .eval.mjs (real ESM -- can import ESM-only packages) or pre-compile to .eval.js instead.`
5504
+ );
5505
+ }
5506
+ throw err;
5334
5507
  }
5335
- return fileRequire(id);
5336
5508
  };
5337
5509
  wrappedRequire.resolve = fileRequire.resolve;
5338
5510
  const wrapper = `(function(exports, require, module, __filename, __dirname) { ${code}
5339
5511
  });`;
5340
5512
  const compiledFn = eval(wrapper);
5341
- compiledFn(m.exports, wrappedRequire, m, absPath, fileDir);
5342
- module = m.exports;
5513
+ compiledFn(m.exports, wrappedRequire, m, modulePath, fileDir);
5514
+ return m.exports;
5515
+ };
5516
+ if (absPath.endsWith(".js")) {
5517
+ module = runAsSyntheticCjs(fileSource, absPath);
5518
+ } else if (absPath.endsWith(".ts")) {
5519
+ if (!transformSync) {
5520
+ throw new Error(`Cannot import TypeScript file: ${filePath}
5521
+ esbuild was not resolved.`);
5522
+ }
5523
+ let cjsSource;
5524
+ try {
5525
+ cjsSource = transformSync(fileSource, {
5526
+ loader: "ts",
5527
+ format: "cjs",
5528
+ target: "node18",
5529
+ sourcefile: absPath
5530
+ }).code;
5531
+ } catch (esbuildErr) {
5532
+ throw new Error(
5533
+ `Cannot import TypeScript file: ${filePath}
5534
+ esbuild (required to load .eval.ts files) failed to transpile it: ${esbuildErr.message}
5535
+ Or pre-compile .eval.ts to .eval.js before running.`
5536
+ );
5537
+ }
5538
+ module = runAsSyntheticCjs(cjsSource, absPath);
5343
5539
  } else {
5344
5540
  try {
5345
- const fileUrl = pathToFileURL2(absPath).href;
5541
+ const fileUrl = `${pathToFileURL2(absPath).href}?ah-reload=${Date.now()}-${Math.random().toString(36).slice(2)}`;
5346
5542
  module = await import(fileUrl);
5347
5543
  } catch (err) {
5348
- if (err.code === "ERR_UNKNOWN_FILE_EXTENSION" && absPath.endsWith(".ts")) {
5349
- throw new Error(
5350
- `Cannot import TypeScript file: ${filePath}
5351
- Install tsx as a dependency: npm install tsx
5352
- Or pre-compile .eval.ts to .eval.js before running.`
5353
- );
5354
- }
5355
5544
  throw new Error(`Failed to import module: ${filePath}
5356
5545
  ${err.message}`);
5357
5546
  }
@@ -5364,7 +5553,8 @@ ${err.message}`);
5364
5553
  }
5365
5554
  const loaded = testCases.map((tc) => ({
5366
5555
  ...tc,
5367
- hash: computeTestCaseHash(tc, fileSource)
5556
+ hash: computeTestCaseHash(tc, fileSource),
5557
+ definition: captureTestDefinition(tc)
5368
5558
  }));
5369
5559
  const benchmarks = /* @__PURE__ */ new Map();
5370
5560
  for (const tc of loaded) {
@@ -5377,13 +5567,15 @@ ${err.message}`);
5377
5567
  const hooks = getRegisteredHooks(absPath);
5378
5568
  return { testCases: loaded, filePath: absPath, benchmarks, hooks, fileSource };
5379
5569
  }
5380
- var CODE_EXTENSIONS;
5570
+ var CODE_EXTENSIONS, DEFINITION_BODY_SOURCE_MAX_CHARS, BODY_TRUNCATION_MARKER, esbuildTransformSyncPromise;
5381
5571
  var init_loader2 = __esm({
5382
5572
  "lib/testCases/loader.ts"() {
5383
5573
  init_define();
5384
5574
  init_authoringSurface();
5385
5575
  init_utils();
5386
5576
  CODE_EXTENSIONS = [".ts", ".js", ".mjs"];
5577
+ DEFINITION_BODY_SOURCE_MAX_CHARS = 32 * 1024;
5578
+ BODY_TRUNCATION_MARKER = "\n/* \u2026 truncated \u2014 see whole file \u2026 */";
5387
5579
  }
5388
5580
  });
5389
5581
 
@@ -5619,6 +5811,10 @@ async function startServer(options) {
5619
5811
  if (err.code === "EADDRINUSE" && port <= options.port + MAX_PORT_ATTEMPTS) {
5620
5812
  console.log(` Port ${port} is in use, trying ${port + 1}...`);
5621
5813
  resolve10(tryListen(port + 1));
5814
+ } else if (err.code === "EADDRINUSE") {
5815
+ reject(new Error(
5816
+ `Port ${port} is in use and all fallback ports (${options.port}-${options.port + MAX_PORT_ATTEMPTS}) are occupied. Set AH_PORT=<available-port> to use a different port: \`AH_PORT=8001 npx @opensearch-project/agent-health\``
5817
+ ));
5622
5818
  } else {
5623
5819
  reject(err);
5624
5820
  }
@@ -6579,6 +6775,25 @@ var ApiClient = class {
6579
6775
  }
6580
6776
  return res.json();
6581
6777
  }
6778
+ /**
6779
+ * Union test case ids into a benchmark's top-level testCaseIds AND its
6780
+ * current version's testCaseIds, in place (no version bump). Server-side
6781
+ * counterpart of services/benchmarkPromotion.ts:linkTestCaseIdsToBenchmark
6782
+ * — used by `benchmark repair-links --apply` to backfill benchmarks whose
6783
+ * current version's testCaseIds fell behind the top level.
6784
+ */
6785
+ async linkBenchmarkTestCaseIds(id, testCaseIds) {
6786
+ const res = await fetch(`${this.baseUrl}/api/storage/benchmarks/${id}/link-test-case-ids`, {
6787
+ method: "POST",
6788
+ headers: { "Content-Type": "application/json" },
6789
+ body: JSON.stringify({ testCaseIds })
6790
+ });
6791
+ if (!res.ok) {
6792
+ const errorBody = await res.text();
6793
+ throw new Error(`Failed to link test case ids to benchmark: ${errorBody}`);
6794
+ }
6795
+ return res.json();
6796
+ }
6582
6797
  /**
6583
6798
  * Fetch traces from OpenSearch with optional filters
6584
6799
  */
@@ -7510,6 +7725,7 @@ function calculateRunStats(run, reports) {
7510
7725
  let failed = 0;
7511
7726
  let pending = 0;
7512
7727
  let errored = 0;
7728
+ let notRun = 0;
7513
7729
  let total = 0;
7514
7730
  Object.entries(run.results || {}).forEach(([testCaseId, result]) => {
7515
7731
  total++;
@@ -7517,7 +7733,11 @@ function calculateRunStats(run, reports) {
7517
7733
  pending++;
7518
7734
  return;
7519
7735
  }
7520
- if (result.status === "failed" || result.status === "cancelled") {
7736
+ if (result.status === "cancelled") {
7737
+ notRun++;
7738
+ return;
7739
+ }
7740
+ if (result.status === "failed") {
7521
7741
  failed++;
7522
7742
  return;
7523
7743
  }
@@ -7544,13 +7764,14 @@ function calculateRunStats(run, reports) {
7544
7764
  pending++;
7545
7765
  }
7546
7766
  });
7547
- const evaluable = Math.max(0, total - errored);
7767
+ const evaluable = Math.max(0, total - errored - notRun);
7548
7768
  const passRate = evaluable > 0 ? Math.round(passed / evaluable * 100) : 0;
7549
7769
  return {
7550
7770
  passed,
7551
7771
  failed,
7552
7772
  pending,
7553
7773
  errored,
7774
+ notRun,
7554
7775
  total,
7555
7776
  passRate
7556
7777
  };
@@ -7734,7 +7955,8 @@ async function applyDoctorPlan(api, plan) {
7734
7955
  return result;
7735
7956
  }
7736
7957
  async function migrateBenchmarksToImages(api, baseUrl, opts = {}) {
7737
- const result = { migrated: [], skipped: [], errors: [] };
7958
+ const dryRun = opts.dryRun !== false;
7959
+ const result = { dryRun, migrated: [], skipped: [], errors: [] };
7738
7960
  let benchmarks2 = await api.listBenchmarks();
7739
7961
  if (opts.benchmarkIds) {
7740
7962
  const allow = new Set(opts.benchmarkIds);
@@ -7753,7 +7975,7 @@ async function migrateBenchmarksToImages(api, baseUrl, opts = {}) {
7753
7975
  const res = await fetch(`${baseUrl}/api/storage/images`, {
7754
7976
  method: "POST",
7755
7977
  headers: { "Content-Type": "application/json" },
7756
- body: JSON.stringify({ testCaseIds: b.testCaseIds, tags: [b.name] })
7978
+ body: JSON.stringify({ testCaseIds: b.testCaseIds, tags: [b.name], dryRun })
7757
7979
  });
7758
7980
  if (!res.ok) {
7759
7981
  result.errors.push(`${b.name}: ${await res.text()}`);
@@ -7765,11 +7987,13 @@ async function migrateBenchmarksToImages(api, baseUrl, opts = {}) {
7765
7987
  benchmarkId: b.id,
7766
7988
  name: b.name,
7767
7989
  digest: body.image.digest,
7990
+ ...dryRun ? { alreadyExists: body.alreadyExists === true } : {},
7991
+ ...dryRun && Array.isArray(body.wouldAddTags) && body.wouldAddTags.length > 0 ? { wouldAddTags: body.wouldAddTags } : {},
7768
7992
  ...missingTestCaseIds && missingTestCaseIds.length > 0 ? { missingTestCaseIds } : {}
7769
7993
  });
7770
7994
  if (missingTestCaseIds && missingTestCaseIds.length > 0) {
7771
7995
  result.errors.push(
7772
- `${b.name}: migrated from a PARTIAL test-case set \u2014 missing ${missingTestCaseIds.length} id(s): ${missingTestCaseIds.join(", ")}`
7996
+ `${b.name}: ${dryRun ? "would migrate" : "migrated"} from a PARTIAL test-case set \u2014 missing ${missingTestCaseIds.length} id(s): ${missingTestCaseIds.join(", ")}`
7773
7997
  );
7774
7998
  }
7775
7999
  } catch (e) {
@@ -7816,14 +8040,14 @@ function printPlan(plan) {
7816
8040
  console.log(chalk4.gray(" Runs and reports are never deleted.\n"));
7817
8041
  }
7818
8042
  function createBenchmarkDoctorCommand() {
7819
- return new Command3("doctor").description("Detect and clean up duplicated / debris benchmarks (dry-run by default)").option("--dry-run", "Preview only \u2014 this is already the default; use --apply to execute").option("--apply", "Execute the plan (default: dry-run report only)").option("--migrate-images", "Also convert remaining benchmarks into tagged benchmark images").option("--json", "Output as JSON instead of the human-readable report", false).addHelpText("after", "\n Dry-run by default. Nothing is changed without --apply.\n").action(async (options) => {
8043
+ return new Command3("doctor").description("Detect and clean up duplicated / debris benchmarks (dry-run by default)").option("--dry-run", "Preview only \u2014 this is already the default; use --apply to execute").option("--apply", "Execute the plan (default: dry-run report only)").option("--migrate-images", "Preview (or, with --apply, execute) converting remaining benchmarks into tagged benchmark images").option("--json", "Output as JSON instead of the human-readable report", false).addHelpText("after", "\n Dry-run by default. Nothing is changed without --apply.\n").action(async (options) => {
7820
8044
  if (options.dryRun && options.apply) {
7821
8045
  console.error(chalk4.red("\n Error: --dry-run and --apply are mutually exclusive."));
7822
8046
  console.error(chalk4.gray(" Dry-run is the default. Use --apply to execute changes.\n"));
7823
8047
  process.exit(1);
7824
8048
  }
7825
8049
  const config = await loadConfig();
7826
- const isReadOnly = !options.apply && !options.migrateImages;
8050
+ const isReadOnly = !options.apply;
7827
8051
  config.server.readOnly = isReadOnly;
7828
8052
  const serverResult = await ensureServer(config.server);
7829
8053
  const cleanup = createServerCleanup(serverResult, false);
@@ -7854,18 +8078,23 @@ function createBenchmarkDoctorCommand() {
7854
8078
  console.log(chalk4.gray(" Dry-run only. Re-run with --apply to execute.\n"));
7855
8079
  }
7856
8080
  if (options.migrateImages) {
7857
- const migration = await migrateBenchmarksToImages(api, serverResult.baseUrl);
8081
+ const migration = await migrateBenchmarksToImages(api, serverResult.baseUrl, { dryRun: !options.apply });
7858
8082
  if (isJson) {
7859
8083
  jsonOutput.migration = migration;
7860
8084
  } else {
7861
- console.log(chalk4.bold(" Image migration:"));
8085
+ console.log(chalk4.bold(migration.dryRun ? " Image migration plan (dry-run):" : " Image migration:"));
7862
8086
  for (const m2 of migration.migrated) {
7863
- console.log(chalk4.green(` \u2713 ${m2.name} \u2192 ${m2.digest.slice(0, 12)}`));
8087
+ const verb = migration.dryRun ? m2.alreadyExists ? m2.wouldAddTags?.length ? "= already an image, would add tag(s)" : "= already an image, no change" : "+ would create" : "\u2713";
8088
+ const tagSuffix = migration.dryRun && m2.wouldAddTags?.length ? ` [${m2.wouldAddTags.join(", ")}]` : "";
8089
+ console.log(chalk4.green(` ${verb} ${m2.name} \u2192 ${m2.digest.slice(0, 12)}${tagSuffix}`));
7864
8090
  }
7865
8091
  for (const s of migration.skipped) {
7866
8092
  console.log(chalk4.gray(` - ${s.name}: skipped (${s.reason})`));
7867
8093
  }
7868
8094
  for (const err of migration.errors) console.log(chalk4.red(` ! ${err}`));
8095
+ if (migration.dryRun && migration.migrated.length > 0) {
8096
+ console.log(chalk4.gray(" Dry-run only. Re-run with --migrate-images --apply to execute."));
8097
+ }
7869
8098
  console.log();
7870
8099
  }
7871
8100
  }
@@ -7882,6 +8111,60 @@ function createBenchmarkDoctorCommand() {
7882
8111
  });
7883
8112
  }
7884
8113
 
8114
+ // cli/utils/benchmarkDoctor.ts
8115
+ function computeBenchmarkRepairPlan(benchmark, evaluationRuns) {
8116
+ const existing = new Set(benchmark.testCaseIds || []);
8117
+ const missing = /* @__PURE__ */ new Set();
8118
+ const affectedRunIds = [];
8119
+ for (const run of evaluationRuns) {
8120
+ if (run.benchmarkId !== benchmark.id) continue;
8121
+ let runHasMissing = false;
8122
+ for (const snapshot of run.testCaseSnapshots || []) {
8123
+ if (snapshot?.id && !existing.has(snapshot.id)) {
8124
+ missing.add(snapshot.id);
8125
+ runHasMissing = true;
8126
+ }
8127
+ }
8128
+ if (runHasMissing) affectedRunIds.push(run.id);
8129
+ }
8130
+ if (missing.size === 0) return null;
8131
+ return {
8132
+ benchmarkId: benchmark.id,
8133
+ benchmarkName: benchmark.name,
8134
+ missingTestCaseIds: Array.from(missing),
8135
+ affectedRunIds
8136
+ };
8137
+ }
8138
+ function applyRepairPlan(existingTestCaseIds, plan) {
8139
+ const existing = new Set(existingTestCaseIds || []);
8140
+ const result = [...existingTestCaseIds];
8141
+ for (const id of plan.missingTestCaseIds) {
8142
+ if (!existing.has(id)) {
8143
+ existing.add(id);
8144
+ result.push(id);
8145
+ }
8146
+ }
8147
+ return result;
8148
+ }
8149
+ function computeVersionLinkRepairPlan(benchmark) {
8150
+ const topLevelIds = benchmark.testCaseIds || [];
8151
+ if (topLevelIds.length === 0) return null;
8152
+ const versions = benchmark.versions || [];
8153
+ if (versions.length === 0) return null;
8154
+ const currentVersion = benchmark.currentVersion ?? 1;
8155
+ const currentEntry = versions.find((v) => v.version === currentVersion) || versions[versions.length - 1];
8156
+ const existing = new Set(currentEntry?.testCaseIds || []);
8157
+ const missing = topLevelIds.filter((id) => !existing.has(id));
8158
+ if (missing.length === 0) return null;
8159
+ return {
8160
+ benchmarkId: benchmark.id,
8161
+ benchmarkName: benchmark.name,
8162
+ currentVersion: currentEntry?.version ?? currentVersion,
8163
+ missingTestCaseIds: missing,
8164
+ needsManualReview: versions.length > 1
8165
+ };
8166
+ }
8167
+
7885
8168
  // cli/commands/benchmark.ts
7886
8169
  function findAgent2(identifier, config) {
7887
8170
  return config.agents.find(
@@ -7891,6 +8174,16 @@ function findAgent2(identifier, config) {
7891
8174
  function getDefaultModel2(config) {
7892
8175
  return Object.keys(config.models)[0] || "claude-sonnet";
7893
8176
  }
8177
+ function resolveDefaultAgentKey(config) {
8178
+ const enabledAgent = config.agents.find((a) => a.enabled !== false);
8179
+ if (!enabledAgent) {
8180
+ console.error(chalk5.red(" Error: No enabled agents found."));
8181
+ process.exit(1);
8182
+ throw new Error("No enabled agents found (process.exit(1) did not terminate the process).");
8183
+ }
8184
+ console.log(chalk5.gray(` Agent: ${enabledAgent.name} (default)`));
8185
+ return enabledAgent.key;
8186
+ }
7894
8187
  function isFilePath(value) {
7895
8188
  return value.toLowerCase().endsWith(".json") || isCodeFile(value);
7896
8189
  }
@@ -8215,13 +8508,7 @@ async function runUnifiedMode(options, config, serverConfig, isCI, fileArray) {
8215
8508
  const api = new ApiClient(serverResult.baseUrl);
8216
8509
  let agentKey;
8217
8510
  if (options.agent.length === 0) {
8218
- const enabledAgent = config.agents.find((a) => a.enabled !== false);
8219
- if (!enabledAgent) {
8220
- console.error(chalk5.red(" Error: No enabled agents found."));
8221
- process.exit(1);
8222
- }
8223
- agentKey = enabledAgent.key;
8224
- console.log(chalk5.gray(` Agent: ${enabledAgent.name} (default)`));
8511
+ agentKey = resolveDefaultAgentKey(config);
8225
8512
  } else {
8226
8513
  agentKey = options.agent[0];
8227
8514
  console.log(chalk5.gray(` Agent: ${agentKey}`));
@@ -8488,6 +8775,10 @@ function createBenchmarkCommand() {
8488
8775
  sourceCode: loaded2.fileSource,
8489
8776
  sourceFileName,
8490
8777
  sourceLanguage,
8778
+ // Per-test slice of the above: THIS test's resolved
8779
+ // options + evaluate body. Lets the run report show one
8780
+ // test's definition instead of the whole file.
8781
+ definition: tc.definition,
8491
8782
  description: tc.options.description,
8492
8783
  // Forward expectedOutcomes / expectedTrajectory — see
8493
8784
  // services/sourceResolver.ts for rationale. Without
@@ -8755,8 +9046,106 @@ function createBenchmarkCommand() {
8755
9046
  cleanup();
8756
9047
  }
8757
9048
  });
9049
+ command.addCommand(createBenchmarkLinkRepairCommand());
8758
9050
  return command;
8759
9051
  }
9052
+ function createBenchmarkLinkRepairCommand() {
9053
+ return new Command4("repair-links").description("Detect benchmarks whose testCaseIds are out of sync (run-referenced ids missing from testCaseIds, or testCaseIds missing from the current version) \u2014 use --apply to fix").option("--apply", "Apply the repair (default: dry-run, report only)").option("--stop-server", "Stop the server after the check completes (default: keep running)").action(async (options) => {
9054
+ console.log(chalk5.bold("\nAgent Health - Benchmark Link Repair\n"));
9055
+ const config = await loadConfig();
9056
+ const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
9057
+ const isCI = !!process.env.CI;
9058
+ const connectSpinner = ora2("Connecting to server...").start();
9059
+ let serverResult;
9060
+ let cleanup;
9061
+ try {
9062
+ serverResult = await ensureServer(serverConfig);
9063
+ cleanup = createServerCleanup(serverResult, isCI || options.stopServer);
9064
+ connectSpinner.succeed(serverResult.wasStarted ? `Started server on port ${serverConfig.port}` : `Connected to existing server on port ${serverConfig.port}`);
9065
+ } catch (error) {
9066
+ connectSpinner.fail(`Failed to connect: ${error instanceof Error ? error.message : error}`);
9067
+ process.exit(1);
9068
+ }
9069
+ const api = new ApiClient(serverResult.baseUrl);
9070
+ try {
9071
+ const benchmarks2 = await api.listBenchmarks();
9072
+ let shellFlagged = 0;
9073
+ let shellRepaired = 0;
9074
+ let versionFlagged = 0;
9075
+ let versionRepaired = 0;
9076
+ let versionNeedsReview = 0;
9077
+ for (let bm of benchmarks2) {
9078
+ let evaluationRuns = [];
9079
+ try {
9080
+ const res = await fetch(
9081
+ `${serverResult.baseUrl}/api/storage/evaluation-runs?benchmarkId=${encodeURIComponent(bm.id)}&size=500`
9082
+ );
9083
+ if (res.ok) {
9084
+ const data = await res.json();
9085
+ evaluationRuns = data.evaluationRuns || [];
9086
+ }
9087
+ } catch {
9088
+ }
9089
+ if (evaluationRuns.length > 0) {
9090
+ const shellPlan = computeBenchmarkRepairPlan(bm, evaluationRuns);
9091
+ if (shellPlan) {
9092
+ shellFlagged++;
9093
+ console.log("");
9094
+ console.log(chalk5.yellow(` Stale shell: ${shellPlan.benchmarkName} (${shellPlan.benchmarkId})`));
9095
+ console.log(chalk5.gray(` ${shellPlan.missingTestCaseIds.length} test case id(s) missing from testCaseIds, referenced by ${shellPlan.affectedRunIds.length} run(s)`));
9096
+ console.log(chalk5.gray(` Missing: ${shellPlan.missingTestCaseIds.join(", ")}`));
9097
+ if (options.apply) {
9098
+ const newTestCaseIds = applyRepairPlan(bm.testCaseIds || [], shellPlan);
9099
+ await api.updateBenchmark(bm.id, { testCaseIds: newTestCaseIds });
9100
+ shellRepaired++;
9101
+ console.log(chalk5.green(` \u2713 Repaired: testCaseIds now has ${newTestCaseIds.length} id(s)`));
9102
+ const refreshed = await api.getBenchmark(bm.id);
9103
+ if (refreshed) bm = refreshed;
9104
+ }
9105
+ }
9106
+ }
9107
+ const versionPlan = computeVersionLinkRepairPlan(bm);
9108
+ if (versionPlan) {
9109
+ versionFlagged++;
9110
+ console.log("");
9111
+ console.log(chalk5.yellow(` Stale version: ${versionPlan.benchmarkName} (${versionPlan.benchmarkId})`));
9112
+ console.log(chalk5.gray(` v${versionPlan.currentVersion}'s testCaseIds is missing ${versionPlan.missingTestCaseIds.length} id(s) present at the top level`));
9113
+ console.log(chalk5.gray(` Missing: ${versionPlan.missingTestCaseIds.join(", ")}`));
9114
+ if (versionPlan.needsManualReview) {
9115
+ versionNeedsReview++;
9116
+ console.log(chalk5.red(" \u26A0 Skipped: this benchmark has more than one version. The top-level"));
9117
+ console.log(chalk5.red(" testCaseIds can contain ids from an OLDER version's runs (see"));
9118
+ console.log(chalk5.red(" cli/utils/benchmarkDoctor.ts) -- copying them into the CURRENT"));
9119
+ console.log(chalk5.red(" version could mix in test cases that do not belong to it. Review"));
9120
+ console.log(chalk5.red(` manually: POST /api/storage/benchmarks/${versionPlan.benchmarkId}/link-test-case-ids`));
9121
+ console.log(chalk5.red(" with the exact ids you intend for the current version, or edit the"));
9122
+ console.log(chalk5.red(" benchmark in the UI."));
9123
+ } else if (options.apply) {
9124
+ const { benchmark: repaired } = await api.linkBenchmarkTestCaseIds(bm.id, bm.testCaseIds || []);
9125
+ versionRepaired++;
9126
+ const fixedVersion = (repaired.versions || []).find((v) => v.version === (repaired.currentVersion ?? versionPlan.currentVersion));
9127
+ console.log(chalk5.green(` \u2713 Repaired: v${versionPlan.currentVersion}'s testCaseIds now has ${fixedVersion?.testCaseIds?.length ?? "?"} id(s)`));
9128
+ }
9129
+ }
9130
+ }
9131
+ const flagged = shellFlagged + versionFlagged;
9132
+ console.log("");
9133
+ if (flagged === 0) {
9134
+ console.log(chalk5.green(" All benchmarks healthy \u2014 no missing test case links found."));
9135
+ } else if (options.apply) {
9136
+ console.log(chalk5.green(` Repaired ${shellRepaired}/${shellFlagged} stale shell benchmark(s), ${versionRepaired}/${versionFlagged - versionNeedsReview} stale version benchmark(s).`));
9137
+ if (versionNeedsReview > 0) {
9138
+ console.log(chalk5.red(` ${versionNeedsReview} multi-version benchmark(s) need manual review (not auto-repaired) -- see above.`));
9139
+ }
9140
+ } else {
9141
+ console.log(chalk5.yellow(` Found ${shellFlagged} stale shell benchmark(s), ${versionFlagged} stale version benchmark(s) (${versionNeedsReview} of which need manual review, not auto-fixable).`));
9142
+ console.log(chalk5.gray(" Re-run with --apply to fix the auto-fixable ones."));
9143
+ }
9144
+ } finally {
9145
+ cleanup();
9146
+ }
9147
+ });
9148
+ }
8760
9149
 
8761
9150
  // cli/commands/export.ts
8762
9151
  init_config2();
@@ -8834,6 +9223,7 @@ function createReportCommand() {
8834
9223
  `Failed to connect to server: ${error instanceof Error ? error.message : error}`
8835
9224
  );
8836
9225
  process.exit(1);
9226
+ return;
8837
9227
  }
8838
9228
  const api = new ApiClient(serverResult.baseUrl);
8839
9229
  try {
@@ -8846,6 +9236,7 @@ function createReportCommand() {
8846
9236
  console.log(chalk7.gray(" npx agent-health list benchmarks"));
8847
9237
  console.log("");
8848
9238
  process.exit(1);
9239
+ return;
8849
9240
  }
8850
9241
  spinner.succeed(`Found benchmark: ${benchmark.name} (${benchmark.id})`);
8851
9242
  const params = new URLSearchParams({ format: options.format });
@@ -8859,6 +9250,7 @@ function createReportCommand() {
8859
9250
  const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
8860
9251
  reportSpinner.fail(`Report generation failed: ${errorBody.error}`);
8861
9252
  process.exit(1);
9253
+ return;
8862
9254
  }
8863
9255
  const contentDisposition = response.headers.get("content-disposition") || "";
8864
9256
  const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);