@opensearch-project/agent-health 0.5.2 → 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 (230) hide show
  1. package/README.md +1 -0
  2. package/cli/dist/index.js +2325 -961
  3. package/dist/assets/index-D-Np_l_T.js +246 -0
  4. package/dist/assets/index-vZt9QZKf.css +1 -0
  5. package/dist/index.html +2 -2
  6. package/docs/CLI.md +183 -3
  7. package/docs/CONFIGURATION.md +1 -1
  8. package/docs/CONNECTORS.md +1 -1
  9. package/docs/INSTRUMENT_WITH_OTEL.md +7 -0
  10. package/docs/SDK.md +178 -5
  11. package/docs/STORAGE_INDEX_FIELD_LIMITS.md +216 -0
  12. package/docs/skills/AGENT_HEALTH.md +55 -1
  13. package/docs/skills/add-connector/SKILL.md +5 -1
  14. package/examples/eval-files/demo.eval.js +1 -1
  15. package/examples/eval-files/ops-rca-classification.eval.js +71 -0
  16. package/examples/eval-files/ops-rca-evaluator.json +15 -0
  17. package/examples/eval-files/sdk-demo.eval.js +72 -0
  18. package/examples/eval-files/sdk-describe-demo.eval.js +50 -0
  19. package/examples/eval-files/sdk-hooks-demo.eval.js +1 -1
  20. package/lib/dist/lib/agentTrends.d.ts +210 -0
  21. package/lib/dist/lib/agentTrends.d.ts.map +1 -0
  22. package/lib/dist/lib/agentTrends.js +360 -0
  23. package/lib/dist/lib/agentTrends.js.map +1 -0
  24. package/lib/dist/lib/bedrockCompat.d.ts +27 -0
  25. package/lib/dist/lib/bedrockCompat.d.ts.map +1 -0
  26. package/lib/dist/lib/bedrockCompat.js +83 -0
  27. package/lib/dist/lib/bedrockCompat.js.map +1 -0
  28. package/lib/dist/lib/benchmarkCaseReview.d.ts +114 -0
  29. package/lib/dist/lib/benchmarkCaseReview.d.ts.map +1 -0
  30. package/lib/dist/lib/benchmarkCaseReview.js +177 -0
  31. package/lib/dist/lib/benchmarkCaseReview.js.map +1 -0
  32. package/lib/dist/lib/benchmarkImage.d.ts +52 -0
  33. package/lib/dist/lib/benchmarkImage.d.ts.map +1 -0
  34. package/lib/dist/lib/benchmarkImage.js +113 -0
  35. package/lib/dist/lib/benchmarkImage.js.map +1 -0
  36. package/lib/dist/lib/benchmarkRunsTable.d.ts +109 -0
  37. package/lib/dist/lib/benchmarkRunsTable.d.ts.map +1 -0
  38. package/lib/dist/lib/benchmarkRunsTable.js +212 -0
  39. package/lib/dist/lib/benchmarkRunsTable.js.map +1 -0
  40. package/lib/dist/lib/benchmarkVersionUtils.d.ts +13 -0
  41. package/lib/dist/lib/benchmarkVersionUtils.d.ts.map +1 -1
  42. package/lib/dist/lib/benchmarkVersionUtils.js +21 -0
  43. package/lib/dist/lib/benchmarkVersionUtils.js.map +1 -1
  44. package/lib/dist/lib/chunkedFetch.d.ts +18 -0
  45. package/lib/dist/lib/chunkedFetch.d.ts.map +1 -0
  46. package/lib/dist/lib/chunkedFetch.js +40 -0
  47. package/lib/dist/lib/chunkedFetch.js.map +1 -0
  48. package/lib/dist/lib/comparisonInsights.d.ts +151 -0
  49. package/lib/dist/lib/comparisonInsights.d.ts.map +1 -0
  50. package/lib/dist/lib/comparisonInsights.js +270 -0
  51. package/lib/dist/lib/comparisonInsights.js.map +1 -0
  52. package/lib/dist/lib/config/loader.d.ts.map +1 -1
  53. package/lib/dist/lib/config/loader.js +16 -1
  54. package/lib/dist/lib/config/loader.js.map +1 -1
  55. package/lib/dist/lib/config/types.d.ts +14 -0
  56. package/lib/dist/lib/config/types.d.ts.map +1 -1
  57. package/lib/dist/lib/constants.d.ts +11 -0
  58. package/lib/dist/lib/constants.d.ts.map +1 -1
  59. package/lib/dist/lib/constants.js +10 -1
  60. package/lib/dist/lib/constants.js.map +1 -1
  61. package/lib/dist/lib/contextFormat.d.ts +26 -0
  62. package/lib/dist/lib/contextFormat.d.ts.map +1 -0
  63. package/lib/dist/lib/contextFormat.js +28 -0
  64. package/lib/dist/lib/contextFormat.js.map +1 -0
  65. package/lib/dist/lib/dashboardMetrics.d.ts +11 -2
  66. package/lib/dist/lib/dashboardMetrics.d.ts.map +1 -1
  67. package/lib/dist/lib/dashboardMetrics.js +38 -3
  68. package/lib/dist/lib/dashboardMetrics.js.map +1 -1
  69. package/lib/dist/lib/envCompat.d.ts.map +1 -1
  70. package/lib/dist/lib/envCompat.js +14 -5
  71. package/lib/dist/lib/envCompat.js.map +1 -1
  72. package/lib/dist/lib/evaluationRerun.d.ts +102 -0
  73. package/lib/dist/lib/evaluationRerun.d.ts.map +1 -0
  74. package/lib/dist/lib/evaluationRerun.js +134 -0
  75. package/lib/dist/lib/evaluationRerun.js.map +1 -0
  76. package/lib/dist/lib/judgeFailureSummary.d.ts +66 -0
  77. package/lib/dist/lib/judgeFailureSummary.d.ts.map +1 -0
  78. package/lib/dist/lib/judgeFailureSummary.js +68 -0
  79. package/lib/dist/lib/judgeFailureSummary.js.map +1 -0
  80. package/lib/dist/lib/judgeStrategies.d.ts +108 -0
  81. package/lib/dist/lib/judgeStrategies.d.ts.map +1 -0
  82. package/lib/dist/lib/judgeStrategies.js +135 -0
  83. package/lib/dist/lib/judgeStrategies.js.map +1 -0
  84. package/lib/dist/lib/matchers/expect.d.ts +21 -1
  85. package/lib/dist/lib/matchers/expect.d.ts.map +1 -1
  86. package/lib/dist/lib/matchers/expect.js +51 -0
  87. package/lib/dist/lib/matchers/expect.js.map +1 -1
  88. package/lib/dist/lib/matchers/judgeAccessor.d.ts +4 -0
  89. package/lib/dist/lib/matchers/judgeAccessor.d.ts.map +1 -1
  90. package/lib/dist/lib/matchers/judgeAccessor.js +13 -2
  91. package/lib/dist/lib/matchers/judgeAccessor.js.map +1 -1
  92. package/lib/dist/lib/matchers/judgeReasoningParse.d.ts +49 -0
  93. package/lib/dist/lib/matchers/judgeReasoningParse.d.ts.map +1 -0
  94. package/lib/dist/lib/matchers/judgeReasoningParse.js +128 -0
  95. package/lib/dist/lib/matchers/judgeReasoningParse.js.map +1 -0
  96. package/lib/dist/lib/matchers/traces.d.ts +17 -2
  97. package/lib/dist/lib/matchers/traces.d.ts.map +1 -1
  98. package/lib/dist/lib/matchers/traces.js +136 -16
  99. package/lib/dist/lib/matchers/traces.js.map +1 -1
  100. package/lib/dist/lib/matchers/tracesPricing.d.ts +38 -0
  101. package/lib/dist/lib/matchers/tracesPricing.d.ts.map +1 -0
  102. package/lib/dist/lib/matchers/tracesPricing.js +64 -0
  103. package/lib/dist/lib/matchers/tracesPricing.js.map +1 -0
  104. package/lib/dist/lib/matchers/types.d.ts +25 -0
  105. package/lib/dist/lib/matchers/types.d.ts.map +1 -1
  106. package/lib/dist/lib/resolveCanonicalRun.d.ts +23 -0
  107. package/lib/dist/lib/resolveCanonicalRun.d.ts.map +1 -0
  108. package/lib/dist/lib/resolveCanonicalRun.js +26 -0
  109. package/lib/dist/lib/resolveCanonicalRun.js.map +1 -0
  110. package/lib/dist/lib/runActions.d.ts +120 -0
  111. package/lib/dist/lib/runActions.d.ts.map +1 -0
  112. package/lib/dist/lib/runActions.js +130 -0
  113. package/lib/dist/lib/runActions.js.map +1 -0
  114. package/lib/dist/lib/runInsights.d.ts +86 -0
  115. package/lib/dist/lib/runInsights.d.ts.map +1 -0
  116. package/lib/dist/lib/runInsights.js +185 -0
  117. package/lib/dist/lib/runInsights.js.map +1 -0
  118. package/lib/dist/lib/runName.d.ts +29 -0
  119. package/lib/dist/lib/runName.d.ts.map +1 -0
  120. package/lib/dist/lib/runName.js +38 -0
  121. package/lib/dist/lib/runName.js.map +1 -0
  122. package/lib/dist/lib/runReportPath.d.ts +16 -0
  123. package/lib/dist/lib/runReportPath.d.ts.map +1 -0
  124. package/lib/dist/lib/runReportPath.js +22 -0
  125. package/lib/dist/lib/runReportPath.js.map +1 -0
  126. package/lib/dist/lib/runSort.d.ts +27 -0
  127. package/lib/dist/lib/runSort.d.ts.map +1 -0
  128. package/lib/dist/lib/runSort.js +31 -0
  129. package/lib/dist/lib/runSort.js.map +1 -0
  130. package/lib/dist/lib/runStats.d.ts +109 -5
  131. package/lib/dist/lib/runStats.d.ts.map +1 -1
  132. package/lib/dist/lib/runStats.js +193 -10
  133. package/lib/dist/lib/runStats.js.map +1 -1
  134. package/lib/dist/lib/testCases/define.d.ts.map +1 -1
  135. package/lib/dist/lib/testCases/define.js +93 -46
  136. package/lib/dist/lib/testCases/define.js.map +1 -1
  137. package/lib/dist/lib/testCases/judge.d.ts.map +1 -1
  138. package/lib/dist/lib/testCases/judge.js +22 -4
  139. package/lib/dist/lib/testCases/judge.js.map +1 -1
  140. package/lib/dist/lib/testCases/loader.d.ts +35 -1
  141. package/lib/dist/lib/testCases/loader.d.ts.map +1 -1
  142. package/lib/dist/lib/testCases/loader.js +278 -31
  143. package/lib/dist/lib/testCases/loader.js.map +1 -1
  144. package/lib/dist/lib/trajectoryStepDisplay.d.ts +25 -0
  145. package/lib/dist/lib/trajectoryStepDisplay.d.ts.map +1 -0
  146. package/lib/dist/lib/trajectoryStepDisplay.js +42 -0
  147. package/lib/dist/lib/trajectoryStepDisplay.js.map +1 -0
  148. package/lib/dist/lib/utils.d.ts +34 -0
  149. package/lib/dist/lib/utils.d.ts.map +1 -1
  150. package/lib/dist/lib/utils.js +49 -0
  151. package/lib/dist/lib/utils.js.map +1 -1
  152. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.d.ts +68 -22
  153. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.d.ts.map +1 -1
  154. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.js +148 -111
  155. package/lib/dist/services/connectors/claude-code/ClaudeCodeConnector.js.map +1 -1
  156. package/lib/dist/services/connectors/kiro/KiroConnector.d.ts +21 -13
  157. package/lib/dist/services/connectors/kiro/KiroConnector.d.ts.map +1 -1
  158. package/lib/dist/services/connectors/kiro/KiroConnector.js +22 -25
  159. package/lib/dist/services/connectors/kiro/KiroConnector.js.map +1 -1
  160. package/lib/dist/services/connectors/pi/PiConnector.d.ts +49 -10
  161. package/lib/dist/services/connectors/pi/PiConnector.d.ts.map +1 -1
  162. package/lib/dist/services/connectors/pi/PiConnector.js +102 -86
  163. package/lib/dist/services/connectors/pi/PiConnector.js.map +1 -1
  164. package/lib/dist/services/connectors/subprocess/SubprocessConnector.d.ts +59 -14
  165. package/lib/dist/services/connectors/subprocess/SubprocessConnector.d.ts.map +1 -1
  166. package/lib/dist/services/connectors/subprocess/SubprocessConnector.js +95 -61
  167. package/lib/dist/services/connectors/subprocess/SubprocessConnector.js.map +1 -1
  168. package/lib/dist/services/evaluation/bedrockJudge.d.ts +18 -0
  169. package/lib/dist/services/evaluation/bedrockJudge.d.ts.map +1 -1
  170. package/lib/dist/services/evaluation/bedrockJudge.js +4 -0
  171. package/lib/dist/services/evaluation/bedrockJudge.js.map +1 -1
  172. package/lib/dist/services/evaluation/index.d.ts +18 -1
  173. package/lib/dist/services/evaluation/index.d.ts.map +1 -1
  174. package/lib/dist/services/evaluation/index.js +156 -19
  175. package/lib/dist/services/evaluation/index.js.map +1 -1
  176. package/lib/dist/services/metrics.d.ts +55 -0
  177. package/lib/dist/services/metrics.d.ts.map +1 -0
  178. package/lib/dist/services/metrics.js +89 -0
  179. package/lib/dist/services/metrics.js.map +1 -0
  180. package/lib/dist/services/storage/asyncBenchmarkStorage.d.ts +1 -1
  181. package/lib/dist/services/storage/asyncBenchmarkStorage.d.ts.map +1 -1
  182. package/lib/dist/services/storage/asyncBenchmarkStorage.js +30 -3
  183. package/lib/dist/services/storage/asyncBenchmarkStorage.js.map +1 -1
  184. package/lib/dist/services/storage/asyncRunStorage.d.ts +20 -1
  185. package/lib/dist/services/storage/asyncRunStorage.d.ts.map +1 -1
  186. package/lib/dist/services/storage/asyncRunStorage.js +125 -2
  187. package/lib/dist/services/storage/asyncRunStorage.js.map +1 -1
  188. package/lib/dist/services/storage/asyncTestCaseStorage.d.ts +18 -4
  189. package/lib/dist/services/storage/asyncTestCaseStorage.d.ts.map +1 -1
  190. package/lib/dist/services/storage/asyncTestCaseStorage.js +21 -4
  191. package/lib/dist/services/storage/asyncTestCaseStorage.js.map +1 -1
  192. package/lib/dist/services/storage/opensearchClient.d.ts +47 -12
  193. package/lib/dist/services/storage/opensearchClient.d.ts.map +1 -1
  194. package/lib/dist/services/storage/opensearchClient.js +12 -5
  195. package/lib/dist/services/storage/opensearchClient.js.map +1 -1
  196. package/lib/dist/services/traces/browserRecovery.d.ts +12 -1
  197. package/lib/dist/services/traces/browserRecovery.d.ts.map +1 -1
  198. package/lib/dist/services/traces/browserRecovery.js +35 -5
  199. package/lib/dist/services/traces/browserRecovery.js.map +1 -1
  200. package/lib/dist/services/traces/index.d.ts +8 -1
  201. package/lib/dist/services/traces/index.d.ts.map +1 -1
  202. package/lib/dist/services/traces/index.js +33 -12
  203. package/lib/dist/services/traces/index.js.map +1 -1
  204. package/lib/dist/services/traces/judgeAgentsHints.d.ts +98 -3
  205. package/lib/dist/services/traces/judgeAgentsHints.d.ts.map +1 -1
  206. package/lib/dist/services/traces/judgeAgentsHints.js +144 -3
  207. package/lib/dist/services/traces/judgeAgentsHints.js.map +1 -1
  208. package/lib/dist/services/traces/messageExtraction.d.ts.map +1 -1
  209. package/lib/dist/services/traces/messageExtraction.js +95 -33
  210. package/lib/dist/services/traces/messageExtraction.js.map +1 -1
  211. package/lib/dist/services/traces/spansToTrajectory.d.ts.map +1 -1
  212. package/lib/dist/services/traces/spansToTrajectory.js +51 -13
  213. package/lib/dist/services/traces/spansToTrajectory.js.map +1 -1
  214. package/lib/dist/services/traces/tracePoller.d.ts +27 -3
  215. package/lib/dist/services/traces/tracePoller.d.ts.map +1 -1
  216. package/lib/dist/services/traces/tracePoller.js +196 -33
  217. package/lib/dist/services/traces/tracePoller.js.map +1 -1
  218. package/lib/dist/services/traces/trajectoryMerge.d.ts +79 -0
  219. package/lib/dist/services/traces/trajectoryMerge.d.ts.map +1 -0
  220. package/lib/dist/services/traces/trajectoryMerge.js +109 -0
  221. package/lib/dist/services/traces/trajectoryMerge.js.map +1 -0
  222. package/lib/dist/types/index.d.ts +250 -3
  223. package/lib/dist/types/index.d.ts.map +1 -1
  224. package/lib/dist/types/index.js +24 -0
  225. package/lib/dist/types/index.js.map +1 -1
  226. package/package.json +8 -5
  227. package/server/dist/app.js +5490 -1557
  228. package/server/dist/index.js +5490 -1557
  229. package/dist/assets/index-CCQRDlO0.js +0 -243
  230. package/dist/assets/index-CNHQVbcj.css +0 -1
package/docs/CLI.md CHANGED
@@ -61,11 +61,13 @@ agent-health list <resource> [-o table|json]
61
61
  | `models` | | Available models |
62
62
  | `test-cases` | `testcases`, `tc` | Stored test cases |
63
63
  | `benchmarks` | `bench` | Stored benchmarks |
64
+ | `images` | `img` | Benchmark images (content-addressed evaluation-condition snapshots; runs sharing a digest are directly comparable) |
64
65
 
65
66
  ```bash
66
67
  agent-health list agents
67
68
  agent-health list tc -o json
68
69
  agent-health list bench
70
+ agent-health list images
69
71
  ```
70
72
 
71
73
  ---
@@ -122,20 +124,61 @@ agent-health benchmark [options]
122
124
  | `--stop-server` | Stop the server after benchmark completes | Keep running |
123
125
 
124
126
  **Modes:**
125
- - **Quick mode** (no `-n`, no `-f`): Auto-creates a benchmark from all stored test cases
127
+ - **Quick mode** (no `-n`, no `-f`): Runs all stored test cases as an **ad-hoc evaluation run** (no benchmark entity is created)
126
128
  - **Named mode** (`-n <name>`): Runs a specific existing benchmark
127
- - **File mode** (`-f <path>`): Imports test cases from a JSON file **or runs a code SDK file** (`.eval.js` / `.eval.ts` — see [SDK.md](./SDK.md)), creates a benchmark, and runs it
129
+ - **File mode** (`-f <path>`): Imports test cases from a JSON file **or runs a code SDK file** (`.eval.js` / `.eval.ts` — see [SDK.md](./SDK.md)), creates a benchmark, and runs it. `.eval.ts` is executed as synthetic CJS (like `.eval.js`) and works from anywhere on disk; only `.eval.mjs` resolves `@opensearch-project/agent-health` through normal Node module resolution, so an `.eval.mjs` file needs the package reachable as a real dependency from its location (see the note in [SDK.md](./SDK.md#migrating-v1--v2))
130
+
131
+ Every evaluation run is stamped with an **image digest** — a content hash of
132
+ its test-case contents + eval conditions (evaluator, judge model). Runs with
133
+ the same digest ran under identical conditions and are directly comparable;
134
+ re-running the same command converges on the same image instead of creating
135
+ new entities. See `agent-health list images`.
128
136
 
129
137
  ```bash
130
138
  agent-health benchmark # quick mode
131
139
  agent-health benchmark -n "Baseline" -a ml-commons # named mode
132
140
  agent-health benchmark -f ./test-cases.json -a pulsar -v # file mode (JSON)
133
- agent-health benchmark -f ./evals/demo.eval.js -a observio # file mode (code SDK)
141
+ agent-health benchmark -f ./examples/eval-files/demo.eval.js -a observio # file mode (code SDK)
134
142
  agent-health benchmark -f ./test-cases.json -n "My Run" -a pulsar --export results.json
135
143
  agent-health benchmark -n "Baseline" -e system-tool-usage -c 4 # custom evaluator, 4 in parallel
136
144
  agent-health benchmark -n "Baseline" --export report.html --format html
137
145
  ```
138
146
 
147
+ #### benchmark doctor
148
+
149
+ Detect and clean up duplicated / debris benchmarks (dry-run by default).
150
+
151
+ ```
152
+ agent-health benchmark doctor [--dry-run] [--apply] [--migrate-images] [--json]
153
+ ```
154
+
155
+ | Option | Description |
156
+ |--------|-------------|
157
+ | `--dry-run` | Preview only — this is already the default; use --apply to execute |
158
+ | `--apply` | Execute the plan (default: dry-run report only) |
159
+ | `--migrate-images` | Convert remaining benchmarks into tagged benchmark images |
160
+ | `--json` | Output as JSON instead of the human-readable report |
161
+
162
+ What it detects:
163
+ - **Timestamped debris** — `quick-<ts>` / `*-<epoch-ms>` benchmarks with no
164
+ runs anywhere and older than 24h (deleted).
165
+ - **Content duplicates** — benchmarks with identical test-case sets. Embedded
166
+ runs are merged into the canonical (most runs › most references › oldest),
167
+ evaluation runs are re-pointed, duplicate shells deleted.
168
+
169
+ Runs and reports are **never** deleted. Sample data (`demo-*`) is never touched.
170
+
171
+ **Read-only mode**: When running without `--apply` or `--migrate-images` (pure dry-run),
172
+ the command safely reuses existing foreign servers in read-only mode. This allows
173
+ the diagnostic to run even when another agent-health instance is operating on a
174
+ different worktree/port, with a clear notice that no writes will be issued.
175
+
176
+ ```bash
177
+ agent-health benchmark doctor # report what would change (read-only)
178
+ agent-health benchmark doctor --apply # clean up (strict server guard)
179
+ agent-health benchmark doctor --apply --migrate-images
180
+ ```
181
+
139
182
  ---
140
183
 
141
184
  ### export
@@ -429,3 +472,140 @@ Most commands support `-o, --output`:
429
472
  |--------|----------|
430
473
  | `table` | Human-readable (default) |
431
474
  | `json` | Machine-readable, scripting |
475
+
476
+ ---
477
+
478
+ ## Running from Outside the Repo
479
+
480
+ When running `npx @opensearch-project/agent-health` from a directory *outside* the agent-health repository (e.g., from a customer or partner project), you may encounter configuration and environment issues. This section covers the most common friction points and their solutions.
481
+
482
+ ### Issue 1: Port Already In Use
483
+
484
+ **Error message:**
485
+ ```
486
+ Port 4001 is in use, trying 4002...
487
+ ```
488
+
489
+ **Root cause:** The default ports (4001 for server, 4000 for frontend) are busy, and all fallback ports (4001–4010) are occupied.
490
+
491
+ **Solution:** Set `AH_PORT` to use a different port:
492
+
493
+ ```bash
494
+ AH_PORT=8001 npx @opensearch-project/agent-health
495
+ ```
496
+
497
+ The server will listen on port 8001. If your frontend dev port (4000) is also in use, set `AH_DEV_PORT` as well:
498
+
499
+ ```bash
500
+ AH_PORT=8001 AH_DEV_PORT=8000 npx @opensearch-project/agent-health
501
+ ```
502
+
503
+ ### Issue 2: Config File Loading Fails (`package.json` Type)
504
+
505
+ **Error message:**
506
+ ```
507
+ Failed to load config file agent-health.config.ts: ERR_MODULE_NOT_FOUND
508
+ ```
509
+
510
+ **Root cause:** Your cwd's `package.json` doesn't declare `"type": "module"`, and Node.js cannot load the TypeScript config as an ES module.
511
+
512
+ **Solution:** Add `"type": "module"` to your local `package.json`:
513
+
514
+ ```json
515
+ {
516
+ "type": "module",
517
+ "name": "my-project",
518
+ "version": "1.0.0"
519
+ }
520
+ ```
521
+
522
+ Alternatively, create a minimal `package.json` in the directory where you're running the command:
523
+
524
+ ```bash
525
+ echo '{"type":"module"}' > package.json
526
+ npx @opensearch-project/agent-health
527
+ ```
528
+
529
+ ### Issue 3: TypeScript Config Parsing Fails (`TSX_TSCONFIG_PATH`)
530
+
531
+ **Error message:**
532
+ ```
533
+ Failed to load config file agent-health.config.ts: [TypeScript parsing error]
534
+ ```
535
+
536
+ **Root cause:** The TypeScript loader cannot find your `tsconfig.json` (e.g., if you placed your config in a subdirectory).
537
+
538
+ **Solution:** Set `TSX_TSCONFIG_PATH` to point to the agent-health repo's `tsconfig.json`:
539
+
540
+ ```bash
541
+ TSX_TSCONFIG_PATH=/path/to/agent-health/tsconfig.json npx @opensearch-project/agent-health
542
+ ```
543
+
544
+ Or, if you have your own `tsconfig.json` that extends agent-health's:
545
+
546
+ ```bash
547
+ TSX_TSCONFIG_PATH=./tsconfig.json npx @opensearch-project/agent-health
548
+ ```
549
+
550
+ ### Issue 4: Claude Code Binary Not Found
551
+
552
+ **Error message:**
553
+ ```
554
+ Subprocess timed out or failed: ENOENT: no such file or directory, spawn 'claude'
555
+ ```
556
+
557
+ **Root cause:** The `claude` CLI is not installed or not in your `PATH`.
558
+
559
+ **Solution:** Ensure Claude Code CLI is installed and accessible:
560
+
561
+ ```bash
562
+ which claude # Verify CLI is in PATH
563
+ claude --version # Check installation
564
+ ```
565
+
566
+ If the CLI is installed but not in `PATH`, set `CLAUDE_CODE_BIN` to the full path:
567
+
568
+ ```bash
569
+ CLAUDE_CODE_BIN=/usr/local/bin/claude npx @opensearch-project/agent-health
570
+ ```
571
+
572
+ Or install Claude Code globally:
573
+
574
+ ```bash
575
+ npm install -g @anthropic-ai/claude-code
576
+ ```
577
+
578
+ ### Complete External Invocation Example
579
+
580
+ Here's a full example running from outside the repo with all common overrides:
581
+
582
+ ```bash
583
+ # From ~/my-project/ directory (outside agent-health repo):
584
+ export AH_PORT=8001
585
+ export AH_DEV_PORT=8000
586
+ export TSX_TSCONFIG_PATH=/opt/agent-health/tsconfig.json
587
+ export CLAUDE_CODE_BIN=/usr/local/bin/claude
588
+ export AWS_PROFILE=my-profile
589
+
590
+ # Create minimal package.json if needed
591
+ echo '{"type":"module"}' > package.json
592
+
593
+ # Run the CLI
594
+ npx @opensearch-project/agent-health --no-browser
595
+ ```
596
+
597
+ Then navigate to http://localhost:8001 in your browser.
598
+
599
+ ### Checklist
600
+
601
+ - [ ] `AH_PORT` set if default ports are busy
602
+ - [ ] `package.json` has `"type": "module"`
603
+ - [ ] `TSX_TSCONFIG_PATH` set if config parsing fails
604
+ - [ ] Claude Code CLI installed (if using claude-code connector)
605
+ - [ ] AWS credentials configured (if using Bedrock judge)
606
+
607
+ If issues persist, run with `DEBUG=true` for verbose output:
608
+
609
+ ```bash
610
+ DEBUG=true npx @opensearch-project/agent-health
611
+ ```
@@ -113,7 +113,7 @@ Rules of thumb:
113
113
  `OPENSEARCH_STORAGE_*` or `storage` in your config to use a cluster instead).
114
114
  - **`evals/`** is **your test source** — `.eval.js` / `.eval.ts` files you write
115
115
  with the [code SDK](./SDK.md) and run with
116
- `agent-health benchmark -f ./evals/demo.eval.js`. They are **not** stored under
116
+ `agent-health benchmark -f ./examples/eval-files/demo.eval.js`. They are **not** stored under
117
117
  `.agent-health/data/`; *running* them produces run records that land there (or
118
118
  in OpenSearch).
119
119
 
@@ -529,7 +529,7 @@ See `services/connectors/claude-code/ClaudeCodeConnector.ts` for a complete exam
529
529
  ### Kiro Connector
530
530
 
531
531
  See `services/connectors/kiro/KiroConnector.ts` for a `SubprocessConnector`
532
- subclass that overrides `parseStderrChunk()` to convert Kiro's stderr-borne
532
+ subclass that overrides `parseStderrChunk(chunk, trajectory, onProgress, state)` to convert Kiro's stderr-borne
533
533
  `[tool] Running:` / `[tool] status:` markers into structured `action` +
534
534
  `tool_result` steps. The base `SubprocessConnector` also persists `stderr` to
535
535
  `rawOutput` and honors per-request `connectorConfig` overrides (`args` /
@@ -385,6 +385,13 @@ If you run an MCP server, expose this document as a resource at `agent-health://
385
385
 
386
386
  ## Reference
387
387
 
388
+ ### New to OpenTelemetry?
389
+
390
+ - [What is OpenTelemetry?](https://opentelemetry.io/docs/what-is-opentelemetry/) — start here for the concepts (traces, spans, exporters, collectors).
391
+ - [OpenTelemetry GenAI semantic conventions (repo)](https://github.com/open-telemetry/semantic-conventions-genai) — the source-of-truth for the `gen_ai.*` attributes this guide uses.
392
+
393
+ ### Specs used by this guide
394
+
388
395
  - [OTel GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
389
396
  - [OTel GenAI Agent Spans](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/)
390
397
  - [Agent Health Telemetry Setup](./CLAUDE_CODE_TELEMETRY.md)
package/docs/SDK.md CHANGED
@@ -64,7 +64,7 @@ async function ({ agent, judge, evaluate, expect, testInfo, provisioned }) { ...
64
64
  | `agent` | `AgentFixture` | `await agent.run(prompt?, options?)` — invoke the agent **once** (control inversion). Returns an `EvalResult`. |
65
65
  | `judge` | `JudgeFn` | Non-throwing LLM judge. `judge(...)` gates; `judge.observe(...)` is observational. Returns a `Verdict`. |
66
66
  | `evaluate` | `EvaluateFn` | Run a custom programmatic evaluator registered with `defineEvaluator()`. |
67
- | `expect` | chai's `expect` with our recording plugin | Synchronous matcher entry-point |
67
+ | `expect` | chai's `expect` with our recording plugin | Synchronous matcher entry-point. `expect.soft(...)` records a failing assertion instead of throwing — see [Always-record guarantee](#5-matchers-record-structured-verdicts). |
68
68
  | `testInfo` | `TestInfo` (read-only) | `{ name, benchmarkPath, sourceFile, testCaseId }` |
69
69
  | `provisioned` | `Readonly<Record<string, unknown>>` | Values set by `beforeEach` via `provide()` |
70
70
  | `result` | `EvalResult` *(legacy eager path)* | Pre-populated agent result — present when the runner invokes before the body. Prefer `await agent.run()`. |
@@ -166,7 +166,7 @@ Key points:
166
166
  Hooks are a no-op when no test in the run uses them — the orchestrator
167
167
  is short-circuited to a noop variant and existing tests pay zero cost.
168
168
 
169
- See the demo at [`evals/sdk-hooks-demo.eval.js`](../evals/sdk-hooks-demo.eval.js).
169
+ See the demo at [`examples/eval-files/sdk-hooks-demo.eval.js`](../examples/eval-files/sdk-hooks-demo.eval.js).
170
170
 
171
171
  ### 5. Matchers record structured verdicts
172
172
 
@@ -187,6 +187,79 @@ Matchers (4/5 passed)
187
187
  This is the major upgrade over throw-and-fail: every assertion gets its own
188
188
  row, status, and detail block.
189
189
 
190
+ #### Always-record guarantee
191
+
192
+ chai's `expect()` is fail-fast — the first failing assertion throws, and the
193
+ rest of the test body (any later `expect()`/`judge()`/`evaluate()` calls)
194
+ never executes. For a pass/fail verdict that's the intended Playwright-style
195
+ contract, but a report consumed by an optimizer (or by you, comparing runs)
196
+ needs the objective actuals — duration, token usage, USD cost — from every
197
+ axis, even when one gate fails partway through. A token-budget gate failing
198
+ shouldn't erase the cost figure that would otherwise have been on the report.
199
+
200
+ So regardless of where (or whether) the body throws, the runner **always**
201
+ stamps `performanceMetrics.durationMs` / `.agentDurationMs` /
202
+ `.totalTokens` / `.totalCostUsd` onto the report the moment `agent.run()`
203
+ resolves — reading them straight from the same OTel-derived data
204
+ `result.traces`/the `traces` fixture exposes, independent of whether the
205
+ body's own code ever reached a matcher that asserted on them. (`totalTokens`/
206
+ `totalCostUsd` are `undefined`, not `0`, when `useTraces: true` but spans
207
+ never arrived — see the loud-failure accessor in "Traces fixture" below;
208
+ they're real `0`s when `useTraces: false`.)
209
+
210
+ This guarantee has one hard limit: a `judge()`/`evaluate()` call itself is
211
+ already non-throwing and records its score/verdict the instant it's called —
212
+ so a REACHED judge call is always on the report today, throw-or-not. But a
213
+ judge call placed textually *after* a failing `expect()` never executes at
214
+ all — that's a pure source-order problem the runner cannot retroactively fix
215
+ (it can't record a call that never ran). Use `expect.soft(...)` (below) when
216
+ you need later judge/evaluate calls to run even after an earlier assertion
217
+ fails.
218
+
219
+ The matcher panel also gets a distinct **"not reached"** row appended
220
+ whenever the body threw — so "this axis never ran" (grey, not-reached) reads
221
+ differently from "this axis ran and failed" (red). It's excluded from the
222
+ passed/failed counts and gets its own tally: `(3/4 passed, 1 not reached)`.
223
+
224
+ #### `expect.soft(...)` — keep going instead of bailing on the first failure
225
+
226
+ `expect.soft(value)` is the same chai assertion surface as `expect(value)`
227
+ — every built-in BDD matcher plus the custom ones below — except a failing
228
+ assertion **records** the MatcherResult and returns instead of throwing:
229
+
230
+ ```javascript
231
+ test('budget-aware RCA', { prompt: '...' }, async ({ agent, expect, judge }) => {
232
+ const result = await agent.run();
233
+ expect.soft(result.traces.totalTokens).to.be.lessThan(10_000); // fails — recorded, body continues
234
+ expect.soft(result.traces.totalCost).to.be.lessThan(0.05); // still runs
235
+ await judge(result, 'identifies the root cause'); // still runs — the whole point
236
+ expect(result.agentOutput).to.contain('root cause'); // a HARD expect still bails on ITS OWN failure
237
+ });
238
+ ```
239
+
240
+ The runner's overall verdict is unaffected by soft vs. hard: a test already
241
+ fails when ANY recorded gate matcher has `pass: false` (that's exactly how
242
+ non-throwing `judge()`/`evaluate()` gates have always worked) — `expect.soft`
243
+ just extends that same non-throwing contract to chai assertions. Mix soft and
244
+ hard freely in one body; a hard `expect()` after a soft failure still bails
245
+ at that point (source order still matters for the calls *after* a HARD
246
+ failure). Reach for `.soft` on measurement/budget-style axis checks where you
247
+ want every number on the report regardless of which gates failed; keep hard
248
+ `expect()` for a precondition that makes the rest of the body meaningless if
249
+ it's false (e.g. "the agent produced any output at all").
250
+
251
+ > **Known limitation: multi-step property chains can record a derivative
252
+ > second failure.** `expect.soft(obj).to.have.property('x').that.equals(5)`
253
+ > is really two chai assertions run back-to-back on the same chain (`property`
254
+ > checks existence, `equals` checks the value) — with a HARD `expect()`, a
255
+ > missing property throws immediately and `.that.equals(5)` never runs. In
256
+ > soft mode nothing throws, so `.that.equals(5)` *does* run against
257
+ > `undefined` and records its own "expected undefined to equal 5" failure —
258
+ > real, but a symptom of the first failure, not independent information.
259
+ > Prefer single-step matchers on primitive/simple values with `.soft` (as in
260
+ > every example above); for a value that might not exist, use a hard
261
+ > `expect()` to check existence first, then `.soft` on the value itself.
262
+
190
263
  ---
191
264
 
192
265
  ## Test options
@@ -483,8 +556,57 @@ enforced regardless of configuration.
483
556
 
484
557
  ---
485
558
 
559
+ ## Weighted scoring across criteria
560
+
561
+ Each `expect(...)`, `judge(...)`, and `evaluate(...)` call records **one
562
+ per-matcher pass/fail** — they are not weighted against each other. When you
563
+ need a **single aggregate score** that weights criteria differently (e.g. root
564
+ cause 60%, SOP selection 20%, relevant-metrics check 10%, latency 10%), the
565
+ weighting lives in a **custom evaluator's `scoringConfig`**, not in the test
566
+ body. Define the weighted metrics once and attach the evaluator to the run via
567
+ `evaluatorId`:
568
+
569
+ ```json
570
+ {
571
+ "name": "Ops RCA (weighted)",
572
+ "systemPrompt": "You evaluate an ops agent that triages a ticket. Score each metric 0-100. CRITICAL: root_cause_accuracy is the primary metric.",
573
+ "scoringConfig": {
574
+ "metrics": [
575
+ { "name": "root_cause_accuracy", "weight": 0.6, "scale": 100 },
576
+ { "name": "sop_selection", "weight": 0.2, "scale": 100 },
577
+ { "name": "relevant_metrics", "weight": 0.1, "scale": 100 },
578
+ { "name": "latency", "weight": 0.1, "scale": 100 }
579
+ ],
580
+ "passThreshold": 80,
581
+ "scale": 100
582
+ }
583
+ }
584
+ ```
585
+
586
+ The run's overall score is the weighted mean of the metrics the evaluator
587
+ emits, so you get one comparable number per run for progress tracking. Use the
588
+ deterministic `expect(...)` matchers for the hard checks (exact classification,
589
+ tool was called, budget cap) and the judge/evaluator + weighted `scoringConfig`
590
+ for the aggregate score. See the ops-RCA worked example at
591
+ [`examples/eval-files/ops-rca-classification.eval.js`](../examples/eval-files/ops-rca-classification.eval.js)
592
+ and its companion [`examples/eval-files/ops-rca-evaluator.json`](../examples/eval-files/ops-rca-evaluator.json),
593
+ and the evaluator reference in [docs/skills/AGENT_HEALTH.md](./skills/AGENT_HEALTH.md#custom-evaluators).
594
+
595
+ ---
596
+
486
597
  ## Running the tests
487
598
 
599
+ > **Security note:** the ENTIRE contents of an imported `.eval.js`/`.eval.ts`
600
+ > file are persisted verbatim on the resulting test case (`sourceCode`) and
601
+ > rendered on the Test Case detail page as an IDE-style code view, so anyone
602
+ > who can view test cases in this deployment can read the full file --
603
+ > including any comments, hardcoded values, or internal URLs it contains.
604
+ > Treat eval files like any other source file that lands in your repo and
605
+ > gets deployed alongside the app: don't hardcode secrets, tokens, or
606
+ > customer data in them -- pull those from environment variables /
607
+ > `agent-health.config.ts` instead, same as you already would for the app's
608
+ > own credentials.
609
+
488
610
  ### Via the UI
489
611
 
490
612
  `/evaluations/runs/new` → pick "Code import" → select your `.eval.js` files.
@@ -492,7 +614,7 @@ enforced regardless of configuration.
492
614
  ### Via the CLI
493
615
 
494
616
  ```bash
495
- npx @opensearch-project/agent-health benchmark -f ./evals/demo.eval.js -a observio
617
+ npx @opensearch-project/agent-health benchmark -f ./examples/eval-files/demo.eval.js -a observio
496
618
  ```
497
619
 
498
620
  ### Via the HTTP API
@@ -504,7 +626,7 @@ curl -sN -X POST http://localhost:4001/api/storage/evaluation-runs \
504
626
  "name": "Demo",
505
627
  "sources": [{
506
628
  "type": "code-import",
507
- "filenames": ["evals/demo.eval.js"],
629
+ "filenames": ["examples/eval-files/demo.eval.js"],
508
630
  "testCaseIds": []
509
631
  }],
510
632
  "agentKey": "observio",
@@ -526,6 +648,57 @@ The `.eval.js`, `.eval.ts`, and `.eval.mjs` loaders all run through a single
526
648
  code-import execution path, so `benchmark -f <file>` executes the SDK body
527
649
  directly.
528
650
 
651
+ > **`.eval.js` and `.eval.ts` are both executed as synthetic CJS — only
652
+ > `.eval.mjs` needs the package to be really resolvable.** `.eval.js` and
653
+ > `.eval.ts` files are both executed in a synthetic context where the
654
+ > `require(...)` call for `@opensearch-project/agent-health` is intercepted
655
+ > directly, so they work from anywhere on disk regardless of whether the
656
+ > package is actually installed at that location. (`.eval.ts` is
657
+ > transpiled to CommonJS with `esbuild` — a required runtime dependency of
658
+ > this package — before running through that exact same synthetic-CJS
659
+ > path; it deliberately does NOT use a native `import()`, both to avoid two
660
+ > different Node versions giving the same file two different
661
+ > module-execution semantics, and because `import()` caches by URL, which
662
+ > would make a `.eval.ts` file unloadable a second time in one process
663
+ > without a restart. One consequence: an `.eval.ts` fixture can't use
664
+ > `import.meta` or a top-level `await` — real-ESM-only features with no
665
+ > CommonJS equivalent.) `.eval.mjs` files use a plain native `import()`, so
666
+ > `import { test } from '@opensearch-project/agent-health'` in an `.mjs`
667
+ > file must resolve through Node's normal module resolution — the package
668
+ > needs to be a real dependency reachable from the file's location
669
+ > (installed in `node_modules`, a workspace link, or a `node_modules`
670
+ > symlink to a local checkout). Test registrations are shared process-wide
671
+ > (keyed off `globalThis`) regardless of loader, so it doesn't matter
672
+ > *which* physical copy of the package a given import resolves to — dist
673
+ > vs source, symlinked vs installed all register into the same registry
674
+ > the loader reads from.
675
+
676
+ > **`.eval.ts` can `import` sibling `.ts` helper files, but not ESM-only
677
+ > packages.** A relative/absolute `import` inside an `.eval.ts` file that
678
+ > resolves to another `.ts` file (e.g. `import { helper } from
679
+ > './helper.ts'`) is transpiled and executed through the same synthetic-CJS
680
+ > mechanism recursively — multi-file `.eval.ts` fixtures work, and a helper
681
+ > required more than once within one load executes exactly once (cached
682
+ > for the duration of that load only, never across separate loads/reloads).
683
+ > An `import` of a package that ships **no CommonJS entry point** (ESM-only,
684
+ > e.g. modern `chalk`) fails with an actionable error — `.eval.ts` executes
685
+ > as synthetic CJS, so under require()'s own rules that package can't load
686
+ > there — unless the running Node version has native `require(esm)`
687
+ > interop (stable since Node 22.12; check `process.features.require_module`),
688
+ > in which case it works transparently via Node's own mechanism. If you hit
689
+ > the error on an older Node, switch the fixture to `.eval.mjs` (real ESM,
690
+ > can import ESM-only packages directly) or pre-compile to `.eval.js`.
691
+
692
+ > **`.eval.mjs` reloads are cache-busted, not cached.** Every `import()` of
693
+ > an `.eval.mjs` file is given a unique query string
694
+ > (`?ah-reload=<timestamp>-<random>`) so re-loading the same file a second
695
+ > time in one process (e.g. re-running the CLI against an edited fixture)
696
+ > re-executes its top-level code instead of silently returning Node's
697
+ > already-cached module for that URL. Accepted cost: each reload leaves one
698
+ > module instance cached under a never-reused query string for the
699
+ > lifetime of the process — negligible for CLI/server process lifetimes
700
+ > (a handful to a few hundred loads, not an unbounded long-running loop).
701
+
529
702
  ---
530
703
 
531
704
  ## Dev tips
@@ -574,4 +747,4 @@ keeping it user-supplied lets you opt in without breaking anyone else.
574
747
  - [x] Non-throwing run-scoped `judge` with `gate` / `observe` roles + `skip` + `orThrow()`
575
748
  - [x] `defineEvaluator()` / `evaluate()` for mechanical / external verification ([#244](https://github.com/opensearch-project/agent-health/issues/244))
576
749
  - [x] Single code-import execution path (`benchmark -f *.eval.js` runs the SDK body) + unified `.js` / `.ts` / `.mjs` loaders + `agent-health migrate sdk-v2` codemod
577
- - [ ] `expect.soft()` to collect-all-failures instead of bail-on-first
750
+ - [x] `expect.soft()` to collect-all-failures instead of bail-on-first