@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
@@ -0,0 +1,216 @@
1
+ # Storage: OpenSearch index field-limit growth (`evals_runs`)
2
+
3
+ ## Incident
4
+
5
+ Owner-hit while running code-QA benchmarks: report/run persistence on the
6
+ shared cluster's `evals_runs` index failed with
7
+
8
+ ```
9
+ illegal_argument_exception: Limit of total fields [5000] has been exceeded
10
+ ```
11
+
12
+ Run **execution** succeeded — only the **write** errored out, i.e. data loss
13
+ (the completed report was never persisted).
14
+
15
+ This is the same *class* of bug PR #418 fixed for `evals_experiments`
16
+ (`EvaluationRun.results` / `testCaseSnapshots`): OpenSearch's default dynamic
17
+ mapping mints a new mapped field for every previously-unseen key under a
18
+ free-form object, and the field-count budget (`index.mapping.total_fields.limit`)
19
+ is **shared across every document in the index**, not per-document. #418 did
20
+ not cover `evals_runs` (the report/`TestCaseRun` index used by the code-SDK
21
+ path) — this fix does.
22
+
23
+ ## Root cause: two unprotected growth vectors in `evals_runs`
24
+
25
+ Both are driven by the same source: `EvaluationMetrics`
26
+ (`types/index.ts`) is an open index signature (`[key: string]: number |
27
+ undefined`) by design — custom/system evaluators declare arbitrary metric
28
+ dimension names via `evaluator.scoringConfig.metrics`
29
+ (`server/services/judgeResponseParser.ts`'s `extractMetrics()`,
30
+ `services/storage/asyncRunStorage.ts`'s `storedMetricsToApp()` /
31
+ `toStorageFormat()` — see the comments in both, which explicitly call out
32
+ "preserve every metric the judge emitted, not just the four legacy keys").
33
+ Every *distinct* custom metric name, across every run/matcher ever written,
34
+ used to mint a brand-new mapped field, shared index-wide, forever.
35
+
36
+ | Field (in `evals_runs`) | Shape | Growth vector |
37
+ |---|---|---|
38
+ | `metrics` (report-level) | `Record<string, number>` | One set of dynamic names per run — one custom evaluator with N metric names adds ≤N new fields **the first time it's seen**, but a code-QA benchmark suite iterating on many custom evaluators over time accumulates without bound. |
39
+ | `matcherResults[].judgeMetrics` | `Record<string, number>`, nested inside a `nested`-typed array | Same growth, but **per SDK `judge()` call** — a single code-QA test case with many `expect`/`judge()` claims × many custom judge dimensions multiplies fast. This is the "code-SDK path" referenced in the incident — `matcherResults` is populated exclusively by the code-based test SDK (`docs/SDK.md`), not the legacy UI-driven runner. |
40
+
41
+ Everything else already flagged in the original bug report — matcher
42
+ `actual`/`expected`, `trajectory`, `logs`, `rawEvents`, `improvementStrategies`,
43
+ `spans` (span attributes) — was **already** `{ type: 'object', enabled: false
44
+ }` in `server/constants/indexMappings.ts` before this change (audited, not
45
+ touched). `llmJudgeResponse` (which itself has an open `extraFields`/
46
+ `parsedMetrics` shape) is **never persisted** to `evals_runs` at all
47
+ (`toStorageFormat()` doesn't include it) — confirmed via `git grep
48
+ llmJudgeResponse services/storage server/adapters`, no hits — so it isn't a
49
+ growth vector for this index either.
50
+
51
+ ## Fix (mirrors #418's pattern)
52
+
53
+ `server/constants/indexMappings.ts`, `evals_runs` index:
54
+
55
+ ```diff
56
+ metrics: {
57
+ + dynamic: false,
58
+ properties: {
59
+ accuracy: { type: 'float' },
60
+ faithfulness: { type: 'float' },
61
+ latency_score: { type: 'float' },
62
+ trajectory_alignment_score: { type: 'float' },
63
+ },
64
+ },
65
+ ...
66
+ matcherResults: {
67
+ type: 'nested',
68
+ properties: {
69
+ ...
70
+ judgeMetrics: {
71
+ + dynamic: false,
72
+ properties: {
73
+ accuracy: { type: 'float' },
74
+ faithfulness: { type: 'float' },
75
+ latency_score: { type: 'float' },
76
+ trajectory_alignment_score: { type: 'float' },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ ```
82
+
83
+ Unlike #418's `results`/`testCaseSnapshots` (`enabled: false`, fully opaque),
84
+ this uses `dynamic: false` **with explicit typed sub-properties** for the
85
+ four legacy metric names — they stay real, typed, queryable fields (nothing
86
+ queries them today — see the audit below — but it's free to keep them typed),
87
+ while every *other* metric/dimension name is stored in `_source` (readable,
88
+ unaffected) but never added to the mapping. `_source` is unaffected either
89
+ way — the choice between `enabled:false` and `dynamic:false` only changes
90
+ what OpenSearch can filter/sort/aggregate on, never what's persisted or
91
+ returned.
92
+
93
+ ## Query audit — nothing queried becomes unsearchable
94
+
95
+ Every OpenSearch-level query/filter/sort/aggregation against `evals_runs`
96
+ (`server/adapters/opensearch/StorageModule.ts`'s `OpenSearchRunOperations`)
97
+ was enumerated. None touch `metrics.*` or `matcherResults[].judgeMetrics.*`
98
+ beyond the four legacy names, which stay mapped:
99
+
100
+ | Consumer | Query | Fields used | Affected by this fix? |
101
+ |---|---|---|---|
102
+ | `OpenSearchRunOperations.search()` | `term` filters | `experimentId`, `experimentRunId`, `testCaseId`, `agentId`, `modelId`, `status`, `passFailStatus` | No — untouched, still explicit `keyword` fields |
103
+ | `OpenSearchRunOperations.search()` | `range` filter | `createdAt` | No — untouched, still `date` |
104
+ | `OpenSearchRunOperations.getAll()` / `.search()` | `sort` | `createdAt` | No |
105
+ | `OpenSearchRunOperations.countsByTestCase()` | `terms` agg | `testCaseId` | No |
106
+ | `asyncRunStorage.ts` `SearchQuery.minAccuracy` | **application-level** `Array.filter()`, not an OpenSearch query (`reports.filter(r => r.metrics.accuracy >= ...)`) | `metrics.accuracy` (read from `_source` in JS) | No — reads the value out of `_source`, which is unaffected by `dynamic: false`. If this were ever converted to a server-side `range` query, it would still work: `accuracy` stays an explicitly mapped, queryable field. |
107
+ | UI (`MatcherResultsPanel.tsx`, `JudgeSection.tsx`, `RunDetailsContent.tsx`) | none — reads `matcherResults`/`judgeMetrics` out of the fetched JSON document, never issues its own OpenSearch query | n/a | No |
108
+ | `services/evaluation/index.ts`, `services/benchmarkRunner.ts`, `services/hookOrchestrator.ts` | none — same, in-process consumption of the already-fetched report | n/a | No |
109
+
110
+ Conclusion: **no consumer anywhere issues an OpenSearch-side query against a
111
+ non-legacy `metrics.*` or `judgeMetrics.*` name.** Both are read back via
112
+ `_source` wherever consumed (search, list, comparison, UI). This mirrors
113
+ exactly the trade-off #418 already made and documented for
114
+ `EvaluationRun.results`.
115
+
116
+ ## Migration story — what to run, exactly
117
+
118
+ **Nothing runs automatically against the live cluster from this PR.**
119
+
120
+ ### New / fresh indexes
121
+
122
+ No action needed. `ensureIndexes()` (`server/services/indexInitializer.ts`,
123
+ called on every server boot and on "attach new cluster") creates any missing
124
+ index straight from the updated `INDEX_MAPPINGS` — new deployments and any
125
+ environment that doesn't have `evals_runs` yet get the fix immediately.
126
+
127
+ ### Existing, NOT-YET-poisoned `evals_runs` (most environments)
128
+
129
+ Also no action needed, but not immediate — `ensureIndexes()` also calls
130
+ `client.indices.putMapping()` on every boot for existing indexes, which is
131
+ how the `dynamic: false` fix reaches an already-existing-but-clean index: it
132
+ succeeds silently and the index is protected from the next write onward.
133
+
134
+ ### The shared cluster's `evals_runs`, if already poisoned
135
+
136
+ If any code-QA benchmark run already wrote a custom evaluator metric name to
137
+ the shared cluster before this fix ships, `evals_runs.metrics` (and/or
138
+ `matcherResults.judgeMetrics`) already has real, dynamically-inferred
139
+ sub-properties. OpenSearch's `putMapping` **rejects** an `enabled`/`dynamic`
140
+ change on a field that already has sub-properties
141
+ (`mapper_exception: the [dynamic] parameter can't be updated for the object
142
+ mapping [metrics]`) — `ensureIndexes()` catches this, logs a warning, and
143
+ otherwise no-ops (no crash, no data loss, same as #418's documented
144
+ `mapper_exception` handling for `results`). **The index keeps growing** until
145
+ an explicit reindex is run.
146
+
147
+ **This is not new migration code** — the existing generic reindex mechanism
148
+ (`reindexSingleIndex()`, `server/services/mappingFixer.ts`, already shipped
149
+ and already exposed at `POST /api/storage/reindex`, `server/routes/storage/admin.ts`)
150
+ already recreates any `INDEX_MAPPINGS`-registered index from scratch and
151
+ copies every document across, which sheds a poisoned mapping's dynamically-
152
+ inferred sub-fields while preserving 100% of the underlying `_source` data
153
+ (proven in `tests/integration/services/storage/evalsRunsMappingMigrationRecipe.integration.test.ts`,
154
+ run against a real OpenSearch container with a deliberately-poisoned index).
155
+
156
+ **The owner's exact recipe, when ready to run it against the shared cluster:**
157
+
158
+ ```bash
159
+ # 1. Confirm the target index actually needs it (optional sanity check):
160
+ curl -s -X GET "$OPENSEARCH_STORAGE_ENDPOINT/evals_runs/_mapping" \
161
+ -u "$OPENSEARCH_STORAGE_USERNAME:$OPENSEARCH_STORAGE_PASSWORD" \
162
+ | jq '.evals_runs.mappings.properties.metrics'
163
+ # If this prints a `dynamic` key, it's already fixed. If it prints only
164
+ # `properties` with more than the 4 legacy metric names, it's poisoned.
165
+
166
+ # 2. Run the reindex via the running server's admin API (recreates the
167
+ # index from the current INDEX_MAPPINGS and copies every document
168
+ # across; the same doc-count-validated, recovery-safe path
169
+ # reindexSingleIndex() has always used for keyword-type mismatch fixes):
170
+ curl -s -X POST "http://localhost:4001/api/storage/reindex" \
171
+ -H 'Content-Type: application/json' \
172
+ -d '{"index": "evals_runs"}'
173
+ ```
174
+
175
+ Caveats to read before running this against the shared cluster:
176
+
177
+ - **No write lock during a manual `/api/storage/reindex` call.** The
178
+ auto-fix boot path (`fixIndexMappings()`) acquires a process-local
179
+ migration lock around the reindex; the manual admin route calls
180
+ `reindexSingleIndex()` directly and does **not** (pre-existing gap in
181
+ `server/routes/storage/admin.ts`, not introduced by this PR — flagged here,
182
+ not fixed, since it's out of scope for this change). Run it during a quiet
183
+ window (no in-flight evaluation runs writing reports) to avoid a write
184
+ racing the index delete/recreate step.
185
+ - It touches only `evals_runs`. The already-known-poisoned `evals_experiments`
186
+ (800+ stale `results.*` fields, per the incident notes) uses the identical
187
+ recipe (`{"index": "evals_experiments"}`) — that cleanup is separately
188
+ planned by ops; this PR does not touch or schedule it.
189
+ - Document count is validated before the temporary index is deleted; if the
190
+ copy-back count doesn't match, the error message names the surviving temp
191
+ index (`evals_runs_reindex_temp`) for manual recovery — nothing is deleted
192
+ until the counts are confirmed equal.
193
+
194
+ ## Tests
195
+
196
+ - Unit (`tests/unit/server/constants/indexMappings.test.ts`): mapping-shape
197
+ assertions — `dynamic: false` + typed legacy properties on both `metrics`
198
+ and `matcherResults.judgeMetrics`; pre-existing `enabled:false` fields stay
199
+ disabled; every field `OpenSearchRunOperations.search()` queries stays
200
+ explicitly mapped.
201
+ - Integration, real OpenSearch
202
+ (`tests/integration/services/storage/testCaseRunMetricsMappingGrowth.integration.test.ts`):
203
+ writes one report with 1000+ distinct custom `metrics`/`judgeMetrics` names
204
+ (500 report-level + 125 `judge()` calls × 4 dimensions), asserts it
205
+ round-trips correctly and the index's total mapped-field count does not
206
+ grow; asserts the query-audit fields stay queryable.
207
+ - Integration, real OpenSearch, migration recipe
208
+ (`tests/integration/services/storage/evalsRunsMappingMigrationRecipe.integration.test.ts`):
209
+ deliberately poisons a throwaway index the old way, runs the *existing*
210
+ `reindexSingleIndex()`, asserts the mapping resets to `dynamic: false` and
211
+ all document data survives byte-for-byte.
212
+
213
+ Both integration suites skip gracefully (with a console warning) if no
214
+ OpenSearch cluster is reachable at `TEST_OPENSEARCH_ENDPOINT` (default
215
+ `http://localhost:9200`) — the unit suite covers the mapping-shape assertions
216
+ unconditionally.
@@ -167,7 +167,7 @@ run a JSON file:
167
167
 
168
168
  ```bash
169
169
  # `-f` accepts BOTH JSON test-case files and code SDK (.eval.js / .eval.ts) files
170
- npx @opensearch-project/agent-health benchmark -f ./evals/demo.eval.js -a my-agent
170
+ npx @opensearch-project/agent-health benchmark -f ./examples/eval-files/demo.eval.js -a my-agent
171
171
  ```
172
172
 
173
173
  They produce **per-matcher results** (`matcherResults[]`) instead of a single
@@ -303,6 +303,60 @@ Repeat until all high-priority issues are resolved.
303
303
  | Server / config issues | `npx agent-health doctor` (checks config + connectivity) |
304
304
  | "OpenSearch storage not configured" | Fine for local use — file-based storage is the default. Set `OPENSEARCH_STORAGE_*` only for shared / production persistence. |
305
305
 
306
+ > **Note (benchmark execution vs. reads):** file-based storage covers reads,
307
+ > single `run`s, and sample data, but **executing a multi-case `benchmark`
308
+ > currently requires an OpenSearch storage client** — without one the execute
309
+ > path returns `Cannot execute in sample-only mode`. Point `OPENSEARCH_STORAGE_*`
310
+ > at a cluster (a local security-disabled Docker OpenSearch on plain HTTP with
311
+ > `authType=none` is enough) before running `benchmark`.
312
+
313
+ ---
314
+
315
+ ## Benchmarking a local (subprocess) agent — gotchas
316
+
317
+ When you wrap a local CLI agent (Kiro, Claude Code, Pi, or your own script) as a
318
+ **subprocess** agent and run a `benchmark`, these are the traps that bite first
319
+ — check them before blaming the agent:
320
+
321
+ 1. **Pin a real judge model — don't leave it on `demo`.** The `demo` provider is
322
+ a mock judge that returns high pass rates without calling an LLM, so a run
323
+ can look like "100% pass" while nothing was actually judged. Set the run's
324
+ judge model to a real Bedrock model, e.g.
325
+ `us.anthropic.claude-sonnet-4-5-20250929-v1:0`, and confirm it's invocable in
326
+ your account (`aws bedrock ... ` / `doctor`). If your pass rate looks too
327
+ good, check the judge provider first.
328
+ 2. **Raise the subprocess timeout for slow agents.** The subprocess connector
329
+ defaults to a 5-minute (`300000` ms) timeout. A real ops/RCA agent can run
330
+ ~10 min. Set it in your agent's `connectorConfig`:
331
+ ```ts
332
+ { key: 'my-ops-agent', connectorType: 'subprocess',
333
+ connectorConfig: { timeout: 1200000 /* 20 min */, /* ... */ } }
334
+ ```
335
+ 3. **Fail loud on a wrong agent name.** If your wrapper points at an agent key
336
+ that doesn't exist, the underlying CLI may silently fall back to a default
337
+ agent — so you benchmark the wrong thing. Echo the resolved agent name in
338
+ your wrapper and eyeball the first trajectory.
339
+ 4. **Long runs + CLI SSE disconnects.** On runs longer than a few minutes the
340
+ CLI's streaming connection can drop and report `0/0` / `fetch failed` **while
341
+ the server keeps going**. The results are still persisted — read them back
342
+ from storage (`list runs` / the UI run inspector / `--export`) rather than
343
+ trusting the CLI summary.
344
+ 5. **Watch for a stale server on the port.** If you patch/upgrade the package
345
+ but a previously-started `npx` server is still bound to port 4001, your runs
346
+ are served by the old code. Confirm which process owns the port
347
+ (`lsof -i :4001` / a `/proc` sweep) and restart the one you actually patched
348
+ (the server loads `server/dist/app.js`, not `index.js`).
349
+ 6. **Judge `CredentialsProviderError` after a session rotates.** If your AWS
350
+ sandbox session rotates, `AWS_SHARED_CREDENTIALS_FILE` can point at a stale/
351
+ empty file and the judge fails. Re-vend credentials and restart the server
352
+ with the AWS env explicit (`aws sts get-caller-identity` to confirm first).
353
+ 7. **Local OpenSearch dying (exit 255) = memory pressure.** Give the container
354
+ enough heap and run it with `--restart=unless-stopped`.
355
+ 8. **Case schema is strict.** Test cases need `expectedOutcomes` (a
356
+ **string[]**, not a singular `expectedOutcome` string) and a capitalized
357
+ `difficulty` (`Easy` | `Medium` | `Hard`). Validate your converter output
358
+ against a known-good case before importing 16 of them.
359
+
306
360
  ---
307
361
 
308
362
  ## Server API Reference
@@ -62,7 +62,11 @@ connectorRegistry.register(new CustomConnector());
62
62
  - `propagateHeader: true` → inject a `traceparent` HTTP header into HTTP/SSE agents (`injectTraceparentHeaders()`).
63
63
  - `serviceName: '<otel-service-name>'` → service-name + time-window fallback. Defaults: `claude-code-agent`, `kiro-agent`, `pi-agent`, `observio-sample-agent`. See the "Trace correlation conventions" section in `AGENTS.md`.
64
64
  - **Subprocess connectors** (`SubprocessConnector` subclasses) can override
65
- `parseStderrChunk(chunk)` to turn stderr markers into trajectory steps (how
65
+ `parseStderrChunk(chunk, trajectory, onProgress, state)` to turn stderr markers into trajectory steps (how
66
66
  `kiro` surfaces `[tool] Running:` / `[tool] status:` as `action` +
67
67
  `tool_result` steps). The base class persists `stderr` to `rawOutput` and
68
68
  honors per-request `connectorConfig` overrides (`args` / `inputMode` / `timeout`).
69
+ Keep ALL streaming state (partial-line buffers, pending tool names, captured
70
+ ids) on the per-invocation `state` object (extend `SubprocessExecutionState`
71
+ via `createExecutionState()`), never on `this` — the registry shares one
72
+ connector instance across concurrent runs.
@@ -18,7 +18,7 @@
18
18
  * -H 'Content-Type: application/json' \
19
19
  * -d '{
20
20
  * "name":"SDK Demo",
21
- * "sources":[{"type":"code-import","filenames":["evals/demo.eval.js"],"testCaseIds":[]}],
21
+ * "sources":[{"type":"code-import","filenames":["examples/eval-files/demo.eval.js"],"testCaseIds":[]}],
22
22
  * "agentKey":"observio",
23
23
  * "modelId":"claude-sonnet"
24
24
  * }'
@@ -0,0 +1,71 @@
1
+ /*
2
+ * Copyright OpenSearch Contributors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Worked example — evaluating an ops / RCA agent that triages a ticket.
8
+ *
9
+ * The agent under test reads a ticket and emits a structured classification:
10
+ * - ticketType: "latency" | "fault" (deterministic — exact match)
11
+ * - rootCause: one of a fixed category set (deterministic — exact match)
12
+ * - sop: a recommended runbook (non-deterministic — LLM judge)
13
+ *
14
+ * This shows the two check styles side by side:
15
+ * • deterministic → expect(...).to.equal(...) on the parsed output
16
+ * • non-deterministic → judge(result, '<natural-language claim>')
17
+ *
18
+ * WEIGHTED SCORING (root-cause 60%, SOP, metrics, latency 10%):
19
+ * per-matcher pass/fail lives here, but the *single weighted aggregate score*
20
+ * across criteria is defined once in a custom EVALUATOR, not in the test body.
21
+ * Attach it at run time with `evaluatorId` — see ops-rca-evaluator.json next to
22
+ * this file. Run:
23
+ *
24
+ * npx @opensearch-project/agent-health benchmark \
25
+ * -f ./examples/eval-files/ops-rca-classification.eval.js -a my-ops-agent
26
+ *
27
+ * Docs: ../../docs/SDK.md Instrumentation: ../../docs/INSTRUMENT_WITH_OTEL.md
28
+ */
29
+
30
+ const { test, expect } = require('@opensearch-project/agent-health');
31
+
32
+ // The agent's allowed root-cause categories — deterministic ground truth.
33
+ const ROOT_CAUSE_CATEGORIES = [
34
+ 'dependency_outage',
35
+ 'resource_exhaustion',
36
+ 'config_error',
37
+ 'code_regression',
38
+ 'network',
39
+ ];
40
+
41
+ test('ticket-123-db-outage', {
42
+ prompt: 'Triage ticket TICKET-123 and return your classification table.',
43
+ description: 'DB dependency outage — must classify as fault + dependency_outage',
44
+ context: [
45
+ {
46
+ description: 'Ticket body',
47
+ value:
48
+ 'TICKET-123: payment-service returning 500s since 10:30. ' +
49
+ 'Logs: "Connection refused to database-primary:5432". p99 latency normal until errors began.',
50
+ },
51
+ ],
52
+ labels: ['category:RCA', 'difficulty:Medium', 'agent:ops', 'type:fault'],
53
+ }, async function ({ agent, judge }) {
54
+ const result = await agent.run();
55
+
56
+ // ── Deterministic checks — exact classification, no LLM, $0 ──────────────
57
+ const out = result.parsedOutput() || {}; // agent emits JSON classification
58
+ expect(out.ticketType).to.equal('fault');
59
+ expect(ROOT_CAUSE_CATEGORIES).to.include(out.rootCause);
60
+ expect(out.rootCause).to.equal('dependency_outage');
61
+
62
+ // Prove it actually investigated rather than guessing.
63
+ expect(result.trajectory).to.haveStepsOfType('action');
64
+
65
+ // ── Non-deterministic checks — LLM judge on the free-text SOP ────────────
66
+ await judge(result, 'Recommends a runbook appropriate for a database dependency outage');
67
+ await judge(result, 'Explains that payment-service cannot reach database-primary as the root cause');
68
+
69
+ // ── Budget guard (feeds the "latency 10%" weight via the evaluator) ──────
70
+ expect(result).to.haveCompletedWithin(120_000);
71
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "Ops RCA (weighted)",
3
+ "description": "Weighted scoring for a ticket-triage / RCA ops agent. Root-cause accuracy dominates; latency is a small tie-breaker. Attach to a run via evaluatorId.",
4
+ "systemPrompt": "You are evaluating an ops agent that triages a support ticket. The agent must (1) classify the ticket type (latency vs fault), (2) identify the root cause from a fixed category set, (3) recommend an appropriate SOP/runbook, and (4) check the relevant metrics. Score each metric from 0 to 100.\n\nCRITICAL CRITERIA:\n- root_cause_accuracy is the PRIMARY metric. A wrong root cause is a failed triage regardless of everything else.\n- sop_selection: did the agent recommend a runbook appropriate for the identified root cause?\n- relevant_metrics: did the agent inspect the metrics/logs that actually matter for this failure mode?\n- latency: score higher when the agent reaches a correct answer in fewer steps / less wall-clock time.\n\nReturn pass_fail_status, reasoning, a metrics object with the four numeric scores, and improvement_strategies.",
5
+ "scoringConfig": {
6
+ "metrics": [
7
+ { "name": "root_cause_accuracy", "weight": 0.6, "scale": 100 },
8
+ { "name": "sop_selection", "weight": 0.2, "scale": 100 },
9
+ { "name": "relevant_metrics", "weight": 0.1, "scale": 100 },
10
+ { "name": "latency", "weight": 0.1, "scale": 100 }
11
+ ],
12
+ "passThreshold": 80,
13
+ "scale": 100
14
+ }
15
+ }
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Copyright OpenSearch Contributors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * SDK demo eval — three test cases that show the spectrum of evaluation
8
+ * methods the code-based SDK supports:
9
+ *
10
+ * 1. mock-says-hello (deterministic) — agent invoked, only chai matchers
11
+ * 2. mock-rca-judged (agentic) — agent invoked + LLM judge matcher
12
+ * 3. data-only-no-prompt (deterministic) — no agent call at all
13
+ *
14
+ * Run with:
15
+ * AH_PORT=4002 npx @opensearch-project/agent-health benchmark \
16
+ * -f examples/eval-files/sdk-demo.eval.js -a demo
17
+ */
18
+
19
+ const { test, expect } = require('@opensearch-project/agent-health');
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // 1. Deterministic — agent runs, all assertions are local chai matchers
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ test('mock-says-hello', {
26
+ prompt: 'Say hello in one short sentence.',
27
+ description: 'Mock agent must produce a non-empty response within 30s',
28
+ labels: ['category:Smoke', 'difficulty:Easy', 'method:deterministic'],
29
+ }, async function ({ agent }) {
30
+ const result = await agent.run();
31
+ expect(result.trajectory).to.have.length.greaterThan(0);
32
+ expect(result.agentOutput.trim()).to.have.length.greaterThan(0);
33
+ expect(result).to.haveCompletedWithin(30_000);
34
+ });
35
+
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // 2. Agentic (hybrid) — deterministic preflight + LLM judge for semantic claim
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ test('mock-rca-judged', {
41
+ prompt: 'Diagnose why the payment service is failing and explain the root cause.',
42
+ description: 'Hybrid: structural checks first, then LLM judge for semantic correctness',
43
+ context: [
44
+ {
45
+ description: 'Error log',
46
+ value: 'ERROR 2026-05-20 10:31:22 [payment-service] Connection refused to db-primary:5432',
47
+ },
48
+ ],
49
+ labels: ['category:RCA', 'difficulty:Medium', 'method:agentic'],
50
+ }, async function ({ agent, judge }) {
51
+ const result = await agent.run();
52
+
53
+ // Cheap deterministic preflight — fail fast before spending $ on the judge
54
+ expect(result.trajectory).to.have.length.greaterThan(0);
55
+ expect(result).to.haveCompletedWithin(60_000);
56
+
57
+ // LLM judge — produces a structured matcher verdict with score + reasoning
58
+ await judge(result, 'Mentions the payment service or its database connection failure');
59
+ });
60
+
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // 3. Deterministic, no prompt — agent never invoked, $0 / 0ms agent step
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+
65
+ test('data-only-no-prompt', {
66
+ description: 'Pure data check; agent invocation skipped entirely',
67
+ labels: ['category:Data Quality', 'difficulty:Easy', 'method:deterministic'],
68
+ }, function ({ result }) {
69
+ expect(result.durationMs).to.equal(0);
70
+ expect(result.trajectory).to.have.length(0);
71
+ expect(2 + 2).to.equal(4);
72
+ });
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Copyright OpenSearch Contributors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * SDK demo with describe() grouping. Each describe() block declares a
8
+ * benchmark; CLI auto-creates one benchmark per group and runs them.
9
+ */
10
+
11
+ const { describe, test, expect } = require('@opensearch-project/agent-health');
12
+
13
+ describe('RCA Suite', () => {
14
+ test('payment service is the root cause', {
15
+ prompt: 'Diagnose why the payment service is failing.',
16
+ context: [{ description: 'Error log', value: 'ERROR [payment-service] Connection refused to db-primary:5432' }],
17
+ labels: ['category:RCA', 'difficulty:Medium'],
18
+ }, async function ({ agent, judge }) {
19
+ const result = await agent.run();
20
+ expect(result.trajectory).to.have.length.greaterThan(0);
21
+ expect(result).to.haveCompletedWithin(60_000);
22
+ await judge(result, 'Mentions the payment service or its database connection failure');
23
+ });
24
+
25
+ test('budget cap', {
26
+ prompt: 'Summarize the system in one paragraph.',
27
+ labels: ['category:Budget', 'difficulty:Easy'],
28
+ }, async function ({ agent, traces }) {
29
+ const result = await agent.run();
30
+ expect(result.agentOutput.length).to.be.greaterThan(0);
31
+ expect(traces.totalTokens).to.be.lessThan(50_000);
32
+ });
33
+ });
34
+
35
+ describe('Smoke', () => {
36
+ test('says hello', {
37
+ prompt: 'Say hello in one short sentence.',
38
+ labels: ['category:Smoke', 'difficulty:Easy'],
39
+ }, async function ({ agent }) {
40
+ const result = await agent.run();
41
+ expect(result.trajectory).to.have.length.greaterThan(0);
42
+ expect(result.agentOutput.trim()).to.have.length.greaterThan(0);
43
+ expect(result).to.haveCompletedWithin(30_000);
44
+ });
45
+ });
46
+
47
+ // Tests outside any describe() — go to the file-default benchmark
48
+ test('orphan-deterministic', function () {
49
+ expect(2 + 2).to.equal(4);
50
+ });
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Run with:
16
16
  * AH_PORT=4002 npx @opensearch-project/agent-health benchmark \
17
- * -f evals/sdk-hooks-demo.eval.js -a demo
17
+ * -f examples/eval-files/sdk-hooks-demo.eval.js -a demo
18
18
  */
19
19
 
20
20
  const fs = require('fs');