@plumloom/cli 0.1.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 (308) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +71 -0
  4. package/dist/actions/configured-runs.d.ts +25 -0
  5. package/dist/actions/configured-runs.d.ts.map +1 -0
  6. package/dist/actions/configured-runs.js +35 -0
  7. package/dist/actions/configured-runs.js.map +1 -0
  8. package/dist/actions/context.d.ts +5 -0
  9. package/dist/actions/context.d.ts.map +1 -0
  10. package/dist/actions/context.js +2 -0
  11. package/dist/actions/context.js.map +1 -0
  12. package/dist/actions/doctor.d.ts +46 -0
  13. package/dist/actions/doctor.d.ts.map +1 -0
  14. package/dist/actions/doctor.js +130 -0
  15. package/dist/actions/doctor.js.map +1 -0
  16. package/dist/actions/evaluations.d.ts +87 -0
  17. package/dist/actions/evaluations.d.ts.map +1 -0
  18. package/dist/actions/evaluations.js +307 -0
  19. package/dist/actions/evaluations.js.map +1 -0
  20. package/dist/actions/gate.d.ts +63 -0
  21. package/dist/actions/gate.d.ts.map +1 -0
  22. package/dist/actions/gate.js +272 -0
  23. package/dist/actions/gate.js.map +1 -0
  24. package/dist/actions/identity.d.ts +4 -0
  25. package/dist/actions/identity.d.ts.map +1 -0
  26. package/dist/actions/identity.js +4 -0
  27. package/dist/actions/identity.js.map +1 -0
  28. package/dist/actions/models.d.ts +4 -0
  29. package/dist/actions/models.d.ts.map +1 -0
  30. package/dist/actions/models.js +4 -0
  31. package/dist/actions/models.js.map +1 -0
  32. package/dist/actions/quality-standards.d.ts +30 -0
  33. package/dist/actions/quality-standards.d.ts.map +1 -0
  34. package/dist/actions/quality-standards.js +86 -0
  35. package/dist/actions/quality-standards.js.map +1 -0
  36. package/dist/actions/results.d.ts +12 -0
  37. package/dist/actions/results.d.ts.map +1 -0
  38. package/dist/actions/results.js +65 -0
  39. package/dist/actions/results.js.map +1 -0
  40. package/dist/actions/runs.d.ts +22 -0
  41. package/dist/actions/runs.d.ts.map +1 -0
  42. package/dist/actions/runs.js +59 -0
  43. package/dist/actions/runs.js.map +1 -0
  44. package/dist/actions/suite-gate.d.ts +24 -0
  45. package/dist/actions/suite-gate.d.ts.map +1 -0
  46. package/dist/actions/suite-gate.js +40 -0
  47. package/dist/actions/suite-gate.js.map +1 -0
  48. package/dist/actions/suite.d.ts +49 -0
  49. package/dist/actions/suite.d.ts.map +1 -0
  50. package/dist/actions/suite.js +68 -0
  51. package/dist/actions/suite.js.map +1 -0
  52. package/dist/actions/validation.d.ts +3 -0
  53. package/dist/actions/validation.d.ts.map +1 -0
  54. package/dist/actions/validation.js +26 -0
  55. package/dist/actions/validation.js.map +1 -0
  56. package/dist/actions/workspaces.d.ts +13 -0
  57. package/dist/actions/workspaces.d.ts.map +1 -0
  58. package/dist/actions/workspaces.js +18 -0
  59. package/dist/actions/workspaces.js.map +1 -0
  60. package/dist/api/api.d.ts +237 -0
  61. package/dist/api/api.d.ts.map +1 -0
  62. package/dist/api/api.js +396 -0
  63. package/dist/api/api.js.map +1 -0
  64. package/dist/api/client.d.ts +38 -0
  65. package/dist/api/client.d.ts.map +1 -0
  66. package/dist/api/client.js +295 -0
  67. package/dist/api/client.js.map +1 -0
  68. package/dist/api/endpoints.d.ts +31 -0
  69. package/dist/api/endpoints.d.ts.map +1 -0
  70. package/dist/api/endpoints.js +32 -0
  71. package/dist/api/endpoints.js.map +1 -0
  72. package/dist/api/errors.d.ts +22 -0
  73. package/dist/api/errors.d.ts.map +1 -0
  74. package/dist/api/errors.js +84 -0
  75. package/dist/api/errors.js.map +1 -0
  76. package/dist/api/schemas.d.ts +169 -0
  77. package/dist/api/schemas.d.ts.map +1 -0
  78. package/dist/api/schemas.js +194 -0
  79. package/dist/api/schemas.js.map +1 -0
  80. package/dist/auth/credential-store.d.ts +9 -0
  81. package/dist/auth/credential-store.d.ts.map +1 -0
  82. package/dist/auth/credential-store.js +2 -0
  83. package/dist/auth/credential-store.js.map +1 -0
  84. package/dist/auth/credentials.d.ts +27 -0
  85. package/dist/auth/credentials.d.ts.map +1 -0
  86. package/dist/auth/credentials.js +81 -0
  87. package/dist/auth/credentials.js.map +1 -0
  88. package/dist/auth/keyring-store.d.ts +9 -0
  89. package/dist/auth/keyring-store.d.ts.map +1 -0
  90. package/dist/auth/keyring-store.js +57 -0
  91. package/dist/auth/keyring-store.js.map +1 -0
  92. package/dist/auth/login.d.ts +28 -0
  93. package/dist/auth/login.d.ts.map +1 -0
  94. package/dist/auth/login.js +27 -0
  95. package/dist/auth/login.js.map +1 -0
  96. package/dist/auth/password-prompt.d.ts +5 -0
  97. package/dist/auth/password-prompt.d.ts.map +1 -0
  98. package/dist/auth/password-prompt.js +10 -0
  99. package/dist/auth/password-prompt.js.map +1 -0
  100. package/dist/auth/redact.d.ts +3 -0
  101. package/dist/auth/redact.d.ts.map +1 -0
  102. package/dist/auth/redact.js +69 -0
  103. package/dist/auth/redact.js.map +1 -0
  104. package/dist/cli.d.ts +3 -0
  105. package/dist/cli.d.ts.map +1 -0
  106. package/dist/cli.js +4 -0
  107. package/dist/cli.js.map +1 -0
  108. package/dist/commands/executor.d.ts +36 -0
  109. package/dist/commands/executor.d.ts.map +1 -0
  110. package/dist/commands/executor.js +1218 -0
  111. package/dist/commands/executor.js.map +1 -0
  112. package/dist/commands/program.d.ts +21 -0
  113. package/dist/commands/program.d.ts.map +1 -0
  114. package/dist/commands/program.js +588 -0
  115. package/dist/commands/program.js.map +1 -0
  116. package/dist/commands/types.d.ts +139 -0
  117. package/dist/commands/types.d.ts.map +1 -0
  118. package/dist/commands/types.js +2 -0
  119. package/dist/commands/types.js.map +1 -0
  120. package/dist/config.d.ts +12 -0
  121. package/dist/config.d.ts.map +1 -0
  122. package/dist/config.js +57 -0
  123. package/dist/config.js.map +1 -0
  124. package/dist/configured-run/file-inputs.d.ts +8 -0
  125. package/dist/configured-run/file-inputs.d.ts.map +1 -0
  126. package/dist/configured-run/file-inputs.js +160 -0
  127. package/dist/configured-run/file-inputs.js.map +1 -0
  128. package/dist/configured-run/validation.d.ts +103 -0
  129. package/dist/configured-run/validation.d.ts.map +1 -0
  130. package/dist/configured-run/validation.js +327 -0
  131. package/dist/configured-run/validation.js.map +1 -0
  132. package/dist/domain/common.d.ts +6 -0
  133. package/dist/domain/common.d.ts.map +1 -0
  134. package/dist/domain/common.js +10 -0
  135. package/dist/domain/common.js.map +1 -0
  136. package/dist/domain/types.d.ts +117 -0
  137. package/dist/domain/types.d.ts.map +1 -0
  138. package/dist/domain/types.js +2 -0
  139. package/dist/domain/types.js.map +1 -0
  140. package/dist/errors/autoeval-error.d.ts +13 -0
  141. package/dist/errors/autoeval-error.d.ts.map +1 -0
  142. package/dist/errors/autoeval-error.js +36 -0
  143. package/dist/errors/autoeval-error.js.map +1 -0
  144. package/dist/errors/hints.d.ts +3 -0
  145. package/dist/errors/hints.d.ts.map +1 -0
  146. package/dist/errors/hints.js +43 -0
  147. package/dist/errors/hints.js.map +1 -0
  148. package/dist/gate/decision.d.ts +97 -0
  149. package/dist/gate/decision.d.ts.map +1 -0
  150. package/dist/gate/decision.js +436 -0
  151. package/dist/gate/decision.js.map +1 -0
  152. package/dist/gate/policy.d.ts +31 -0
  153. package/dist/gate/policy.d.ts.map +1 -0
  154. package/dist/gate/policy.js +26 -0
  155. package/dist/gate/policy.js.map +1 -0
  156. package/dist/guide/eval-creation-backend.d.ts +20 -0
  157. package/dist/guide/eval-creation-backend.d.ts.map +1 -0
  158. package/dist/guide/eval-creation-backend.js +16 -0
  159. package/dist/guide/eval-creation-backend.js.map +1 -0
  160. package/dist/guide/eval-guide.d.ts +24 -0
  161. package/dist/guide/eval-guide.d.ts.map +1 -0
  162. package/dist/guide/eval-guide.js +219 -0
  163. package/dist/guide/eval-guide.js.map +1 -0
  164. package/dist/mcp/context.d.ts +21 -0
  165. package/dist/mcp/context.d.ts.map +1 -0
  166. package/dist/mcp/context.js +19 -0
  167. package/dist/mcp/context.js.map +1 -0
  168. package/dist/mcp/schemas.d.ts +206 -0
  169. package/dist/mcp/schemas.d.ts.map +1 -0
  170. package/dist/mcp/schemas.js +80 -0
  171. package/dist/mcp/schemas.js.map +1 -0
  172. package/dist/mcp/server.d.ts +5 -0
  173. package/dist/mcp/server.d.ts.map +1 -0
  174. package/dist/mcp/server.js +10 -0
  175. package/dist/mcp/server.js.map +1 -0
  176. package/dist/mcp/tools.d.ts +42 -0
  177. package/dist/mcp/tools.d.ts.map +1 -0
  178. package/dist/mcp/tools.js +287 -0
  179. package/dist/mcp/tools.js.map +1 -0
  180. package/dist/mcp.d.ts +3 -0
  181. package/dist/mcp.d.ts.map +1 -0
  182. package/dist/mcp.js +32 -0
  183. package/dist/mcp.js.map +1 -0
  184. package/dist/output/big-digits.d.ts +7 -0
  185. package/dist/output/big-digits.d.ts.map +1 -0
  186. package/dist/output/big-digits.js +43 -0
  187. package/dist/output/big-digits.js.map +1 -0
  188. package/dist/output/clipboard.d.ts +29 -0
  189. package/dist/output/clipboard.d.ts.map +1 -0
  190. package/dist/output/clipboard.js +82 -0
  191. package/dist/output/clipboard.js.map +1 -0
  192. package/dist/output/colors.d.ts +21 -0
  193. package/dist/output/colors.d.ts.map +1 -0
  194. package/dist/output/colors.js +21 -0
  195. package/dist/output/colors.js.map +1 -0
  196. package/dist/output/digest.d.ts +165 -0
  197. package/dist/output/digest.d.ts.map +1 -0
  198. package/dist/output/digest.js +771 -0
  199. package/dist/output/digest.js.map +1 -0
  200. package/dist/output/human.d.ts +95 -0
  201. package/dist/output/human.d.ts.map +1 -0
  202. package/dist/output/human.js +643 -0
  203. package/dist/output/human.js.map +1 -0
  204. package/dist/output/json.d.ts +2 -0
  205. package/dist/output/json.d.ts.map +1 -0
  206. package/dist/output/json.js +5 -0
  207. package/dist/output/json.js.map +1 -0
  208. package/dist/output/layout.d.ts +18 -0
  209. package/dist/output/layout.d.ts.map +1 -0
  210. package/dist/output/layout.js +64 -0
  211. package/dist/output/layout.js.map +1 -0
  212. package/dist/output/markdown.d.ts +16 -0
  213. package/dist/output/markdown.d.ts.map +1 -0
  214. package/dist/output/markdown.js +38 -0
  215. package/dist/output/markdown.js.map +1 -0
  216. package/dist/output/progress.d.ts +29 -0
  217. package/dist/output/progress.d.ts.map +1 -0
  218. package/dist/output/progress.js +136 -0
  219. package/dist/output/progress.js.map +1 -0
  220. package/dist/output/safe-text.d.ts +11 -0
  221. package/dist/output/safe-text.d.ts.map +1 -0
  222. package/dist/output/safe-text.js +44 -0
  223. package/dist/output/safe-text.js.map +1 -0
  224. package/dist/output/scorecard.d.ts +23 -0
  225. package/dist/output/scorecard.d.ts.map +1 -0
  226. package/dist/output/scorecard.js +506 -0
  227. package/dist/output/scorecard.js.map +1 -0
  228. package/dist/output/section.d.ts +15 -0
  229. package/dist/output/section.d.ts.map +1 -0
  230. package/dist/output/section.js +44 -0
  231. package/dist/output/section.js.map +1 -0
  232. package/dist/output/spinner.d.ts +24 -0
  233. package/dist/output/spinner.d.ts.map +1 -0
  234. package/dist/output/spinner.js +34 -0
  235. package/dist/output/spinner.js.map +1 -0
  236. package/dist/output/splash.d.ts +37 -0
  237. package/dist/output/splash.d.ts.map +1 -0
  238. package/dist/output/splash.js +169 -0
  239. package/dist/output/splash.js.map +1 -0
  240. package/dist/output/table.d.ts +33 -0
  241. package/dist/output/table.d.ts.map +1 -0
  242. package/dist/output/table.js +105 -0
  243. package/dist/output/table.js.map +1 -0
  244. package/dist/output/trend.d.ts +19 -0
  245. package/dist/output/trend.d.ts.map +1 -0
  246. package/dist/output/trend.js +38 -0
  247. package/dist/output/trend.js.map +1 -0
  248. package/dist/output/writer.d.ts +14 -0
  249. package/dist/output/writer.d.ts.map +1 -0
  250. package/dist/output/writer.js +33 -0
  251. package/dist/output/writer.js.map +1 -0
  252. package/dist/polling/clock.d.ts +9 -0
  253. package/dist/polling/clock.d.ts.map +1 -0
  254. package/dist/polling/clock.js +23 -0
  255. package/dist/polling/clock.js.map +1 -0
  256. package/dist/polling/wait-for-result.d.ts +16 -0
  257. package/dist/polling/wait-for-result.d.ts.map +1 -0
  258. package/dist/polling/wait-for-result.js +29 -0
  259. package/dist/polling/wait-for-result.js.map +1 -0
  260. package/dist/polling/wait-for-run.d.ts +14 -0
  261. package/dist/polling/wait-for-run.d.ts.map +1 -0
  262. package/dist/polling/wait-for-run.js +36 -0
  263. package/dist/polling/wait-for-run.js.map +1 -0
  264. package/dist/quickstart/prompt.d.ts +18 -0
  265. package/dist/quickstart/prompt.d.ts.map +1 -0
  266. package/dist/quickstart/prompt.js +37 -0
  267. package/dist/quickstart/prompt.js.map +1 -0
  268. package/dist/quickstart/sample.d.ts +28 -0
  269. package/dist/quickstart/sample.d.ts.map +1 -0
  270. package/dist/quickstart/sample.js +235 -0
  271. package/dist/quickstart/sample.js.map +1 -0
  272. package/dist/quickstart/selection.d.ts +53 -0
  273. package/dist/quickstart/selection.d.ts.map +1 -0
  274. package/dist/quickstart/selection.js +97 -0
  275. package/dist/quickstart/selection.js.map +1 -0
  276. package/dist/run.d.ts +28 -0
  277. package/dist/run.d.ts.map +1 -0
  278. package/dist/run.js +134 -0
  279. package/dist/run.js.map +1 -0
  280. package/dist/runtime/action-context.d.ts +19 -0
  281. package/dist/runtime/action-context.d.ts.map +1 -0
  282. package/dist/runtime/action-context.js +40 -0
  283. package/dist/runtime/action-context.js.map +1 -0
  284. package/dist/suite/execution.d.ts +46 -0
  285. package/dist/suite/execution.d.ts.map +1 -0
  286. package/dist/suite/execution.js +97 -0
  287. package/dist/suite/execution.js.map +1 -0
  288. package/dist/suite/manifest.d.ts +16 -0
  289. package/dist/suite/manifest.d.ts.map +1 -0
  290. package/dist/suite/manifest.js +92 -0
  291. package/dist/suite/manifest.js.map +1 -0
  292. package/dist/suite/reporting.d.ts +35 -0
  293. package/dist/suite/reporting.d.ts.map +1 -0
  294. package/dist/suite/reporting.js +131 -0
  295. package/dist/suite/reporting.js.map +1 -0
  296. package/dist/trace/deepseek-harness.d.ts +31 -0
  297. package/dist/trace/deepseek-harness.d.ts.map +1 -0
  298. package/dist/trace/deepseek-harness.js +412 -0
  299. package/dist/trace/deepseek-harness.js.map +1 -0
  300. package/dist/trace/import.d.ts +9 -0
  301. package/dist/trace/import.d.ts.map +1 -0
  302. package/dist/trace/import.js +45 -0
  303. package/dist/trace/import.js.map +1 -0
  304. package/dist/version.d.ts +3 -0
  305. package/dist/version.d.ts.map +1 -0
  306. package/dist/version.js +3 -0
  307. package/dist/version.js.map +1 -0
  308. package/package.json +82 -0
@@ -0,0 +1,771 @@
1
+ import { safeTerminalText } from '../output/safe-text.js';
2
+ /**
3
+ * Deterministic, bounded digests over already-validated Autoeval payloads.
4
+ *
5
+ * These helpers only read fields that are present and omit everything else, so
6
+ * a digest can never assert a detail the backend did not return. They perform
7
+ * no network access and no inference.
8
+ */
9
+ export const MAX_DIGEST_CHARS = 4_000;
10
+ const MAX_LISTED_SCENARIOS = 20;
11
+ const MAX_LISTED_MODELS = 12;
12
+ const MAX_LISTED_STEPS = 20;
13
+ const MAX_LISTED_MESSAGES = 20;
14
+ const MAX_LISTED_DOCUMENTS = 10;
15
+ const MAX_TEXT_CHARS = 160;
16
+ const MAX_NAME_CHARS = 120;
17
+ /** The judge rubric drives every score, so it is kept far longer than other prose. */
18
+ const MAX_RUBRIC_CHARS = 1_200;
19
+ const MAX_PROMPT_CHARS = 600;
20
+ function asObject(input) {
21
+ return input !== null && typeof input === 'object' && !Array.isArray(input)
22
+ ? input
23
+ : undefined;
24
+ }
25
+ function asArray(input) {
26
+ return Array.isArray(input) ? input : [];
27
+ }
28
+ function text(input, maxChars = MAX_TEXT_CHARS) {
29
+ if (typeof input !== 'string')
30
+ return undefined;
31
+ const trimmed = input.trim();
32
+ if (trimmed.length === 0)
33
+ return undefined;
34
+ const clipped = trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}…` : trimmed;
35
+ return safeTerminalText(clipped);
36
+ }
37
+ function num(input) {
38
+ return typeof input === 'number' && Number.isFinite(input) ? input : undefined;
39
+ }
40
+ function round(value) {
41
+ return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/u, '');
42
+ }
43
+ function stringList(input, limit = MAX_LISTED_MODELS) {
44
+ const values = asArray(input)
45
+ .map((value) => text(value, MAX_NAME_CHARS))
46
+ .filter((value) => value !== undefined);
47
+ if (values.length === 0)
48
+ return undefined;
49
+ const shown = values.slice(0, limit);
50
+ const suffix = values.length > shown.length ? `, … (${values.length} total)` : '';
51
+ return `${shown.join(', ')}${suffix}`;
52
+ }
53
+ function bound(lines) {
54
+ const rendered = lines.join('\n');
55
+ return rendered.length > MAX_DIGEST_CHARS ? `${rendered.slice(0, MAX_DIGEST_CHARS)}…` : rendered;
56
+ }
57
+ function pushField(lines, label, value) {
58
+ if (value !== undefined)
59
+ lines.push(` ${label}: ${value}`);
60
+ }
61
+ function parsedEvaluatorInstructions(input) {
62
+ const direct = asObject(input);
63
+ if (direct)
64
+ return direct;
65
+ const raw = typeof input === 'string' ? input.trim() : undefined;
66
+ if (!raw)
67
+ return undefined;
68
+ try {
69
+ return asObject(JSON.parse(raw));
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ function qualityStandardName(configuration) {
76
+ return (text(configuration.quality_standard_name, MAX_NAME_CHARS) ??
77
+ text(asObject(configuration.quality_standard)?.name, MAX_NAME_CHARS) ??
78
+ text(configuration.quality_standard_id, MAX_NAME_CHARS) ??
79
+ text(parsedEvaluatorInstructions(configuration.evaluator_instructions)?.name, MAX_NAME_CHARS) ??
80
+ text(parsedEvaluatorInstructions(configuration.evaluator_instructions)?.quality_standard_name, MAX_NAME_CHARS));
81
+ }
82
+ function qualityStandardAnchorCount(configuration) {
83
+ const directAnchors = asArray(asObject(configuration.quality_standard)?.anchors);
84
+ if (directAnchors.length > 0)
85
+ return directAnchors.length;
86
+ const parsedAnchors = asArray(parsedEvaluatorInstructions(configuration.evaluator_instructions)?.anchors);
87
+ return parsedAnchors.length > 0 ? parsedAnchors.length : undefined;
88
+ }
89
+ function judgeModel(configuration) {
90
+ return (text(configuration.judge_model, MAX_NAME_CHARS) ??
91
+ text(configuration.judge_model_id, MAX_NAME_CHARS) ??
92
+ text(configuration.judge_llm, MAX_NAME_CHARS) ??
93
+ text(configuration.judgeModel, MAX_NAME_CHARS) ??
94
+ text(parsedEvaluatorInstructions(configuration.evaluator_instructions)?.judge_model, MAX_NAME_CHARS) ??
95
+ text(parsedEvaluatorInstructions(configuration.evaluator_instructions)?.judge_model_id, MAX_NAME_CHARS));
96
+ }
97
+ /** Extracts the Quality Standard prose from the evaluator instructions field. */
98
+ function qualityStandard(input) {
99
+ const raw = typeof input === 'string' ? input.trim() : undefined;
100
+ if (!raw)
101
+ return undefined;
102
+ if (raw.startsWith('{')) {
103
+ try {
104
+ const parsed = asObject(JSON.parse(raw));
105
+ const instructions = text(parsed?.judge_instructions, MAX_RUBRIC_CHARS);
106
+ if (instructions)
107
+ return instructions;
108
+ }
109
+ catch {
110
+ // Not JSON: fall through and use the raw prose.
111
+ }
112
+ }
113
+ return text(raw, MAX_RUBRIC_CHARS);
114
+ }
115
+ function evaluationName(evaluation) {
116
+ for (const candidate of [evaluation.configuration, evaluation.raw]) {
117
+ for (const key of ['evaluation_name', 'evaluationName', 'eval_name', 'name']) {
118
+ const value = text(candidate[key], MAX_NAME_CHARS);
119
+ if (value)
120
+ return value;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ /** First present string field, so digests survive backend field-name variation. */
126
+ function firstText(source, keys, maxChars = MAX_TEXT_CHARS) {
127
+ for (const key of keys) {
128
+ const value = text(source[key], maxChars);
129
+ if (value)
130
+ return value;
131
+ }
132
+ return undefined;
133
+ }
134
+ function overflowLine(total, shown, noun) {
135
+ return total > shown ? [` … ${total - shown} more ${noun}`] : [];
136
+ }
137
+ function scenarioLines(configuration) {
138
+ const scenarios = asArray(configuration.scenarios);
139
+ if (scenarios.length === 0)
140
+ return [];
141
+ const lines = [` Scenarios (${scenarios.length}):`];
142
+ for (const [index, rawScenario] of scenarios.slice(0, MAX_LISTED_SCENARIOS).entries()) {
143
+ const scenario = asObject(rawScenario);
144
+ if (!scenario)
145
+ continue;
146
+ const name = text(scenario.name, MAX_NAME_CHARS) ?? text(scenario.id, MAX_NAME_CHARS);
147
+ const id = text(scenario.id, MAX_NAME_CHARS);
148
+ const heading = name
149
+ ? `${name}${id && id !== name ? ` (${id})` : ''}`
150
+ : `Scenario ${index + 1}`;
151
+ lines.push(` ${index + 1}. ${heading}`);
152
+ const prompt = text(scenario.prompt);
153
+ if (prompt)
154
+ lines.push(` Prompt: ${prompt}`);
155
+ const expected = text(scenario.expected);
156
+ if (expected)
157
+ lines.push(` Expected: ${expected}`);
158
+ }
159
+ lines.push(...overflowLine(scenarios.length, MAX_LISTED_SCENARIOS, 'scenarios'));
160
+ return lines;
161
+ }
162
+ function referenceDocumentLines(configuration) {
163
+ const documents = asArray(configuration.reference_documents);
164
+ if (documents.length === 0)
165
+ return [];
166
+ const lines = [` Reference documents (${documents.length}):`];
167
+ for (const [index, rawDocument] of documents.slice(0, MAX_LISTED_DOCUMENTS).entries()) {
168
+ const document = asObject(rawDocument);
169
+ const label = document
170
+ ? firstText(document, ['name', 'title', 'filename', 'id'], MAX_NAME_CHARS)
171
+ : text(rawDocument, MAX_NAME_CHARS);
172
+ lines.push(` ${index + 1}. ${label ?? `Document ${index + 1}`}`);
173
+ }
174
+ lines.push(...overflowLine(documents.length, MAX_LISTED_DOCUMENTS, 'documents'));
175
+ return lines;
176
+ }
177
+ /** Agent-trace inputs: the steps the judge scores, with any tool calls. */
178
+ function traceStepLines(configuration) {
179
+ const steps = asArray(configuration.steps);
180
+ if (steps.length === 0)
181
+ return [];
182
+ const lines = [` Trace steps (${steps.length}):`];
183
+ for (const [index, rawStep] of steps.slice(0, MAX_LISTED_STEPS).entries()) {
184
+ const step = asObject(rawStep);
185
+ if (!step)
186
+ continue;
187
+ const role = firstText(step, ['role', 'actor', 'type', 'step_type'], MAX_NAME_CHARS);
188
+ const tool = firstText(step, ['tool', 'tool_name', 'action', 'function'], MAX_NAME_CHARS);
189
+ const content = firstText(step, ['content', 'text', 'message', 'input', 'output', 'thought']);
190
+ const heading = [role ?? `Step ${index + 1}`, tool ? `tool ${tool}` : undefined]
191
+ .filter((part) => part !== undefined)
192
+ .join(' — ');
193
+ lines.push(` ${index + 1}. ${heading}`);
194
+ if (content)
195
+ lines.push(` ${content}`);
196
+ }
197
+ lines.push(...overflowLine(steps.length, MAX_LISTED_STEPS, 'steps'));
198
+ return lines;
199
+ }
200
+ /** Conversation inputs: the turns the judge scores. */
201
+ function conversationLines(configuration) {
202
+ const messages = asArray(configuration.messages);
203
+ if (messages.length === 0)
204
+ return [];
205
+ const lines = [` Conversation turns (${messages.length}):`];
206
+ for (const [index, rawMessage] of messages.slice(0, MAX_LISTED_MESSAGES).entries()) {
207
+ const message = asObject(rawMessage);
208
+ if (!message)
209
+ continue;
210
+ const role = firstText(message, ['role', 'speaker', 'actor'], MAX_NAME_CHARS) ?? 'turn';
211
+ const content = firstText(message, ['content', 'text', 'message']);
212
+ lines.push(` ${index + 1}. ${role}${content ? `: ${content}` : ''}`);
213
+ }
214
+ lines.push(...overflowLine(messages.length, MAX_LISTED_MESSAGES, 'turns'));
215
+ return lines;
216
+ }
217
+ /**
218
+ * Summarises the validated evaluation configuration — the evaluation's inputs —
219
+ * for display and reasoning. The sections shown depend on the context type, so
220
+ * the consumer sees the material the judge actually scored.
221
+ */
222
+ export function summarizeEvaluationConfiguration(evaluation) {
223
+ const configuration = evaluation.configuration;
224
+ const lines = ['Configuration summary:'];
225
+ pushField(lines, 'Name', evaluationName(evaluation));
226
+ pushField(lines, 'Context', safeTerminalText(evaluation.contextType));
227
+ pushField(lines, 'Judge model', judgeModel(configuration));
228
+ pushField(lines, 'Primary model', text(configuration.primary_model, MAX_NAME_CHARS));
229
+ pushField(lines, 'Comparison models', stringList(configuration.comparison_models));
230
+ const temperature = num(configuration.temperature);
231
+ pushField(lines, 'Temperature', temperature === undefined ? undefined : round(temperature));
232
+ pushField(lines, 'Selected metrics', stringList(configuration.selected_metrics, 20));
233
+ pushField(lines, 'Quality standard', qualityStandardName(configuration));
234
+ const anchorCount = qualityStandardAnchorCount(configuration);
235
+ pushField(lines, 'Quality standard anchors', anchorCount === undefined ? undefined : String(anchorCount));
236
+ pushField(lines, 'Judge rubric', qualityStandard(configuration.evaluator_instructions));
237
+ pushField(lines, 'Prompt', text(configuration.prompt_text, MAX_PROMPT_CHARS));
238
+ const turnCount = num(configuration.turn_count);
239
+ pushField(lines, 'Turns', turnCount === undefined ? undefined : String(turnCount));
240
+ pushField(lines, 'Expected outcome', text(configuration.expected_outcome, MAX_PROMPT_CHARS));
241
+ if (evaluation.contextType === 'agent_trace') {
242
+ lines.push(...traceStepLines(configuration));
243
+ }
244
+ else if (evaluation.contextType === 'conversation') {
245
+ lines.push(...conversationLines(configuration));
246
+ }
247
+ else {
248
+ lines.push(...scenarioLines(configuration));
249
+ }
250
+ lines.push(...referenceDocumentLines(configuration));
251
+ if (lines.length === 1)
252
+ lines.push(' No configuration details were returned');
253
+ return bound(lines);
254
+ }
255
+ function modelLabel(model) {
256
+ return text(model.display_name, MAX_NAME_CHARS) ?? text(model.model_key, MAX_NAME_CHARS);
257
+ }
258
+ /** Per-model overall means from the model-performance payload. */
259
+ export function scenarioModelScores(modelPerformance) {
260
+ return asArray(modelPerformance.models).flatMap((rawModel) => {
261
+ const model = asObject(rawModel);
262
+ if (!model)
263
+ return [];
264
+ const overall = asObject(asObject(model.scores)?.overall);
265
+ const mean = num(overall?.mean);
266
+ const name = modelLabel(model);
267
+ if (mean === undefined || !name || overall?.has_data === false)
268
+ return [];
269
+ const sampleSize = num(overall?.sample_size);
270
+ const runs = num(model.n_runs);
271
+ return [
272
+ {
273
+ name,
274
+ mean,
275
+ ...(sampleSize === undefined ? {} : { sampleSize }),
276
+ ...(runs === undefined ? {} : { runs }),
277
+ ...(typeof model.is_primary === 'boolean' ? { isPrimary: model.is_primary } : {}),
278
+ },
279
+ ];
280
+ });
281
+ }
282
+ /**
283
+ * Per-metric means for each scored model. `overall` is an aggregate of the
284
+ * other metrics, so it is excluded here and reported separately as the headline
285
+ * score. Metrics the payload marks without data are dropped, never defaulted.
286
+ */
287
+ export function scenarioMetricScores(modelPerformance) {
288
+ return asArray(modelPerformance.models).flatMap((rawModel) => {
289
+ const model = asObject(rawModel);
290
+ const name = model ? modelLabel(model) : undefined;
291
+ const scores = asObject(model?.scores);
292
+ if (!model || !name || !scores)
293
+ return [];
294
+ const metrics = Object.entries(scores).flatMap(([metricName, rawScore]) => {
295
+ if (metricName === 'overall')
296
+ return [];
297
+ const score = asObject(rawScore);
298
+ const mean = num(score?.mean);
299
+ const label = text(metricName, MAX_NAME_CHARS);
300
+ if (!label || mean === undefined || score?.has_data === false)
301
+ return [];
302
+ const ci95 = num(score?.ci95);
303
+ const ci95Lower = num(score?.ci95_lower);
304
+ const ci95Upper = num(score?.ci95_upper);
305
+ const sampleSize = num(score?.sample_size);
306
+ return [
307
+ {
308
+ metric: label,
309
+ mean,
310
+ ...(ci95 === undefined ? {} : { ci95 }),
311
+ ...(ci95Lower === undefined ? {} : { ci95Lower }),
312
+ ...(ci95Upper === undefined ? {} : { ci95Upper }),
313
+ ...(sampleSize === undefined ? {} : { sampleSize }),
314
+ ...(score?.insufficient_samples === true ? { insufficientSamples: true } : {}),
315
+ },
316
+ ];
317
+ });
318
+ if (metrics.length === 0)
319
+ return [];
320
+ metrics.sort((left, right) => left.metric.localeCompare(right.metric));
321
+ const runs = num(model.n_runs);
322
+ return [
323
+ {
324
+ model: name,
325
+ metrics,
326
+ ...(runs === undefined ? {} : { runs }),
327
+ ...(typeof model.is_primary === 'boolean' ? { isPrimary: model.is_primary } : {}),
328
+ },
329
+ ];
330
+ });
331
+ }
332
+ /**
333
+ * Per-test-case means in the order the backend indexed them, so repeated runs
334
+ * of the same evaluation always list the cases identically.
335
+ */
336
+ export function scenarioTestCaseScores(scenarioComparison) {
337
+ const rows = asArray(scenarioComparison.scenarios).flatMap((rawScenario, position) => {
338
+ const scenario = asObject(rawScenario);
339
+ if (!scenario)
340
+ return [];
341
+ const index = num(scenario.scenario_index) ?? position;
342
+ const name = text(scenario.scenario_name, MAX_NAME_CHARS) ??
343
+ text(scenario.scenario_id, MAX_NAME_CHARS) ??
344
+ `Test case ${index + 1}`;
345
+ const scores = asArray(scenario.model_scores).flatMap((rawScore) => {
346
+ const entry = asObject(rawScore);
347
+ const score = asObject(entry?.score);
348
+ const mean = num(score?.mean);
349
+ const model = entry ? modelLabel(entry) : undefined;
350
+ if (!model || mean === undefined || score?.has_data === false)
351
+ return [];
352
+ const ci95 = num(score?.ci95);
353
+ return [
354
+ {
355
+ model,
356
+ mean,
357
+ ...(ci95 === undefined ? {} : { ci95 }),
358
+ ...(score?.insufficient_samples === true ? { insufficientSamples: true } : {}),
359
+ },
360
+ ];
361
+ });
362
+ return scores.length === 0 ? [] : [{ name, index, scores }];
363
+ });
364
+ return rows.sort((left, right) => left.index - right.index);
365
+ }
366
+ /** Headline numbers for a scenario run, read straight from the payloads. */
367
+ export function scenarioHeadline(modelPerformance, scenarioComparison) {
368
+ const models = asArray(modelPerformance.models)
369
+ .map((rawModel) => asObject(rawModel))
370
+ .filter((model) => model !== undefined);
371
+ const primary = models.find((model) => model.is_primary === true) ?? models[0];
372
+ const overall = asObject(asObject(primary?.scores)?.overall);
373
+ const summary = asObject(scenarioComparison.evaluation_summary);
374
+ const mean = num(overall?.mean) ?? num(summary?.overall_mean);
375
+ const ci95 = num(overall?.ci95) ?? num(summary?.overall_ci);
376
+ const ci95Lower = num(overall?.ci95_lower) ?? num(summary?.overall_ci_lower);
377
+ const ci95Upper = num(overall?.ci95_upper) ?? num(summary?.overall_ci_upper);
378
+ const runs = num(primary?.n_runs) ?? num(overall?.sample_size);
379
+ const testCases = num(summary?.test_case_count) ?? asArray(scenarioComparison.scenarios).length;
380
+ const primaryModel = primary ? modelLabel(primary) : undefined;
381
+ return {
382
+ ...(mean === undefined ? {} : { mean }),
383
+ ...(ci95 === undefined ? {} : { ci95 }),
384
+ ...(ci95Lower === undefined ? {} : { ci95Lower }),
385
+ ...(ci95Upper === undefined ? {} : { ci95Upper }),
386
+ ...(runs === undefined ? {} : { runs }),
387
+ ...(testCases ? { testCases } : {}),
388
+ ...(primaryModel === undefined ? {} : { primaryModel }),
389
+ };
390
+ }
391
+ const CONVERGENCE_KEYS = ['convergence', 'evaluation_status', 'reliability', 'stability'];
392
+ /**
393
+ * The Evaluation Status card's own numbers. Nothing here is derived: when the
394
+ * backend does not report convergence detail the block is omitted entirely,
395
+ * because a variation figure recomputed from a standard deviation would be a
396
+ * different statistic wearing the same label.
397
+ */
398
+ export function scenarioConvergence(...payloads) {
399
+ for (const payload of payloads) {
400
+ if (payload === undefined)
401
+ continue;
402
+ // A status payload may carry the detail nested or at its top level.
403
+ const candidates = [...CONVERGENCE_KEYS.map((key) => asObject(payload[key])), payload];
404
+ for (const source of candidates) {
405
+ if (!source)
406
+ continue;
407
+ const observedVariation = num(source.observed_variation) ?? num(source.observed_cv);
408
+ const autoStopRun = num(source.auto_stop_run) ?? num(source.stopped_after_run);
409
+ const runsPlanned = num(source.runs_planned) ?? num(source.max_runs);
410
+ const judgesPerOutput = num(source.judges_per_output) ?? num(source.judge_count);
411
+ const targetMet = typeof source.target_met === 'boolean'
412
+ ? source.target_met
413
+ : typeof source.is_stable === 'boolean'
414
+ ? source.is_stable
415
+ : undefined;
416
+ const headline = text(source.headline ?? source.message, MAX_NAME_CHARS);
417
+ const consistencyTarget = text(source.consistency_target ?? source.target, MAX_NAME_CHARS);
418
+ const detail = {
419
+ ...(headline === undefined ? {} : { headline }),
420
+ ...(consistencyTarget === undefined ? {} : { consistencyTarget }),
421
+ ...(observedVariation === undefined ? {} : { observedVariation }),
422
+ ...(autoStopRun === undefined ? {} : { autoStopRun }),
423
+ ...(runsPlanned === undefined ? {} : { runsPlanned }),
424
+ ...(judgesPerOutput === undefined ? {} : { judgesPerOutput }),
425
+ ...(targetMet === undefined ? {} : { targetMet }),
426
+ };
427
+ if (Object.keys(detail).length > 0)
428
+ return detail;
429
+ }
430
+ }
431
+ return {};
432
+ }
433
+ const RELIABILITY_KEYS = ['evaluationMetrics', 'evaluation_metrics'];
434
+ function meetsTarget(achieved, operator, target) {
435
+ if (operator === '<=')
436
+ return achieved <= target;
437
+ if (operator === '>')
438
+ return achieved > target;
439
+ if (operator === '>=')
440
+ return achieved >= target;
441
+ return achieved < target;
442
+ }
443
+ /** Reliability detail read from the run status payload, or `multiRun: false`. */
444
+ export function scenarioReliability(status) {
445
+ const roots = [asObject(status), asObject(asObject(status)?.status)];
446
+ for (const root of roots) {
447
+ if (!root)
448
+ continue;
449
+ for (const key of RELIABILITY_KEYS) {
450
+ const source = asObject(root[key]);
451
+ const achievedConsistency = num(source?.achievedConsistency) ?? num(source?.achieved_consistency);
452
+ if (!source || achievedConsistency === undefined)
453
+ continue;
454
+ const targetConsistency = num(source.targetConsistency) ?? num(source.target_consistency);
455
+ const targetOperator = text(source.targetConsistencyOperator ?? source.target_consistency_operator, 4) ?? '<';
456
+ const autoStop = asObject(source.autoStopTriggered ?? source.auto_stop_triggered);
457
+ const autoStopTriggered = typeof autoStop?.triggered === 'boolean'
458
+ ? autoStop.triggered
459
+ : typeof source.autoStopTriggered === 'boolean'
460
+ ? source.autoStopTriggered
461
+ : undefined;
462
+ const reason = text(autoStop?.reason, MAX_NAME_CHARS);
463
+ const runsCompleted = num(source.runsCompleted) ?? num(source.runs_completed);
464
+ const runsPlanned = num(source.runsPlanned) ?? num(source.runs_planned) ?? num(source.maxRuns);
465
+ const stoppedAtRun = num(autoStop?.atRun) ?? num(autoStop?.at_run) ?? num(autoStop?.runNumber) ?? runsCompleted;
466
+ const maxRunsReached = reason === 'MAX_RUNS_REACHED' ||
467
+ (autoStopTriggered === false &&
468
+ runsCompleted !== undefined &&
469
+ runsCompleted === runsPlanned);
470
+ return {
471
+ multiRun: true,
472
+ achievedConsistency,
473
+ targetOperator,
474
+ ...(targetConsistency === undefined ? {} : { targetConsistency }),
475
+ ...(targetConsistency === undefined
476
+ ? {}
477
+ : { stable: meetsTarget(achievedConsistency, targetOperator, targetConsistency) }),
478
+ ...(runsCompleted === undefined ? {} : { runsCompleted }),
479
+ ...(runsPlanned === undefined ? {} : { runsPlanned }),
480
+ ...(stoppedAtRun === undefined ? {} : { stoppedAtRun }),
481
+ ...(autoStopTriggered === undefined ? {} : { autoStopTriggered }),
482
+ ...(maxRunsReached ? { maxRunsReached: true } : {}),
483
+ };
484
+ }
485
+ }
486
+ return { multiRun: false };
487
+ }
488
+ const MAX_OUTPUT_CHARS = 2_000;
489
+ /** Per-test-case model outputs, shown only when the user asks for them. */
490
+ export function scenarioOutputs(modelResponses) {
491
+ return asArray(modelResponses.outputs).flatMap((rawOutput, position) => {
492
+ const output = asObject(rawOutput);
493
+ if (!output)
494
+ return [];
495
+ const index = num(output.scenario_index) ?? position;
496
+ const name = text(output.scenario_name, MAX_NAME_CHARS) ?? `Test case ${index + 1}`;
497
+ const input = text(output.input, MAX_OUTPUT_CHARS);
498
+ const detailsUrl = text(asObject(output.actions)?.view_details_url, MAX_NAME_CHARS);
499
+ const responses = asArray(output.model_responses).flatMap((rawResponse) => {
500
+ const response = asObject(rawResponse);
501
+ const model = response ? modelLabel(response) : undefined;
502
+ if (!model)
503
+ return [];
504
+ const bestRun = asObject(response?.best_run);
505
+ const metadata = asObject(bestRun?.metadata);
506
+ const metrics = Object.entries(asObject(response?.metric_scores) ?? {}).flatMap(([metricName, rawScore]) => {
507
+ const score = num(rawScore);
508
+ const label = text(metricName, MAX_NAME_CHARS);
509
+ return !label || score === undefined ? [] : [{ metric: label, score }];
510
+ });
511
+ metrics.sort((left, right) => left.metric.localeCompare(right.metric));
512
+ const overallScore = num(bestRun?.overall_score);
513
+ // Usage may sit on the run metadata, the run itself, or the response.
514
+ const usage = [metadata, bestRun, response];
515
+ const pick = (...keys) => {
516
+ for (const source of usage) {
517
+ for (const key of keys) {
518
+ const value = num(source?.[key]);
519
+ if (value !== undefined)
520
+ return value;
521
+ }
522
+ }
523
+ return undefined;
524
+ };
525
+ const tokens = pick('total_tokens', 'tokens');
526
+ const cost = pick('cost', 'total_cost');
527
+ const latencyMs = pick('latency_ms', 'duration_ms');
528
+ const responseText = text(bestRun?.response, MAX_OUTPUT_CHARS);
529
+ const runNumber = num(bestRun?.run_number);
530
+ const runCount = num(response?.run_count) ??
531
+ num(response?.total_runs) ??
532
+ num(response?.n_runs) ??
533
+ (asArray(response?.runs).length > 0 ? asArray(response?.runs).length : undefined);
534
+ return [
535
+ {
536
+ model,
537
+ metrics,
538
+ ...(responseText === undefined ? {} : { response: responseText }),
539
+ ...(overallScore === undefined ? {} : { overallScore }),
540
+ ...(runNumber === undefined ? {} : { runNumber }),
541
+ ...(runCount === undefined ? {} : { runCount }),
542
+ ...(tokens === undefined ? {} : { tokens }),
543
+ ...(cost === undefined ? {} : { cost }),
544
+ ...(latencyMs === undefined ? {} : { latencyMs }),
545
+ },
546
+ ];
547
+ });
548
+ return [
549
+ {
550
+ name,
551
+ index,
552
+ responses,
553
+ ...(input === undefined ? {} : { input }),
554
+ ...(detailsUrl === undefined ? {} : { detailsUrl }),
555
+ },
556
+ ];
557
+ });
558
+ }
559
+ function modelLabels(modelPerformance) {
560
+ const labels = new Map();
561
+ for (const rawModel of asArray(modelPerformance.models)) {
562
+ const model = asObject(rawModel);
563
+ const key = text(model?.model_key, MAX_NAME_CHARS);
564
+ const label = text(model?.display_name, MAX_NAME_CHARS);
565
+ if (key && label)
566
+ labels.set(key, label);
567
+ }
568
+ return labels;
569
+ }
570
+ function scenarioRows(scenarioComparison, labels) {
571
+ return asArray(scenarioComparison.scenarios).flatMap((rawScenario, index) => {
572
+ const scenario = asObject(rawScenario);
573
+ if (!scenario)
574
+ return [];
575
+ const name = text(scenario.scenario_name, MAX_NAME_CHARS) ??
576
+ text(scenario.scenario_id, MAX_NAME_CHARS) ??
577
+ `Scenario ${index + 1}`;
578
+ const scores = asArray(scenario.model_scores).flatMap((rawScore) => {
579
+ const score = asObject(rawScore);
580
+ const mean = num(asObject(score?.score)?.mean);
581
+ const rawLabel = score ? modelLabel(score) : undefined;
582
+ const model = rawLabel ? (labels.get(rawLabel) ?? rawLabel) : undefined;
583
+ return mean === undefined || !model ? [] : [{ model, mean }];
584
+ });
585
+ return scores.length === 0 ? [] : [{ name, scores }];
586
+ });
587
+ }
588
+ function rowMean(row) {
589
+ return row.scores.reduce((total, score) => total + score.mean, 0) / row.scores.length;
590
+ }
591
+ function modelPerformanceLines(modelPerformance) {
592
+ const scores = scenarioModelScores(modelPerformance);
593
+ if (scores.length === 0)
594
+ return ['Model performance: no scored models were returned'];
595
+ const sorted = [...scores].sort((left, right) => right.mean - left.mean);
596
+ const lines = ['Model performance:'];
597
+ for (const score of sorted.slice(0, MAX_LISTED_MODELS)) {
598
+ const details = [
599
+ score.isPrimary ? 'primary' : undefined,
600
+ score.sampleSize === undefined ? undefined : `sample size ${score.sampleSize}`,
601
+ score.runs === undefined ? undefined : `${score.runs} runs`,
602
+ ].filter((detail) => detail !== undefined);
603
+ lines.push(` ${score.name} — overall mean ${round(score.mean)}${details.length > 0 ? ` (${details.join(', ')})` : ''}`);
604
+ }
605
+ const best = sorted[0];
606
+ const worst = sorted[sorted.length - 1];
607
+ if (best && worst && sorted.length > 1) {
608
+ lines.push(` Best: ${best.name} (${round(best.mean)}); weakest: ${worst.name} (${round(worst.mean)}); spread ${round(best.mean - worst.mean)}`);
609
+ }
610
+ return lines;
611
+ }
612
+ function scenarioComparisonLines(scenarioComparison, labels) {
613
+ const rows = scenarioRows(scenarioComparison, labels);
614
+ if (rows.length === 0)
615
+ return ['Scenario comparison: no scored scenarios were returned'];
616
+ const lines = [`Scenario comparison (${rows.length} scenarios):`];
617
+ for (const row of rows.slice(0, MAX_LISTED_SCENARIOS)) {
618
+ const means = row.scores.map((score) => score.mean);
619
+ const gap = Math.max(...means) - Math.min(...means);
620
+ const detail = row.scores.map((score) => `${score.model} ${round(score.mean)}`).join(', ');
621
+ lines.push(` ${row.name} — ${detail}${row.scores.length > 1 ? ` (gap ${round(gap)})` : ''}`);
622
+ }
623
+ const overallMean = num(asObject(scenarioComparison.evaluation_summary)?.overall_mean);
624
+ if (overallMean !== undefined)
625
+ lines.push(` Overall mean across models: ${round(overallMean)}`);
626
+ const ranked = [...rows].sort((left, right) => rowMean(right) - rowMean(left));
627
+ const strongest = ranked[0];
628
+ const weakest = ranked[ranked.length - 1];
629
+ if (strongest && weakest && ranked.length > 1) {
630
+ lines.push(` Strongest scenario: ${strongest.name} (${round(rowMean(strongest))}); weakest scenario: ${weakest.name} (${round(rowMean(weakest))})`);
631
+ }
632
+ return lines;
633
+ }
634
+ function modelResponseLines(modelResponses) {
635
+ const outputs = asArray(modelResponses.outputs);
636
+ if (outputs.length === 0)
637
+ return ['Model responses: none were returned'];
638
+ const statuses = new Map();
639
+ const perModel = new Map();
640
+ for (const rawOutput of outputs) {
641
+ const output = asObject(rawOutput);
642
+ if (!output)
643
+ continue;
644
+ const status = text(output.status, 40) ?? 'unknown';
645
+ statuses.set(status, (statuses.get(status) ?? 0) + 1);
646
+ for (const rawResponse of asArray(output.model_responses)) {
647
+ const response = asObject(rawResponse);
648
+ const model = response ? modelLabel(response) : undefined;
649
+ const score = num(asObject(response?.best_run)?.overall_score);
650
+ if (!model || score === undefined)
651
+ continue;
652
+ const entry = perModel.get(model) ?? { count: 0, total: 0 };
653
+ perModel.set(model, { count: entry.count + 1, total: entry.total + score });
654
+ }
655
+ }
656
+ const lines = [`Model responses (${outputs.length} scenarios):`];
657
+ lines.push(` Statuses: ${[...statuses.entries()].map(([status, count]) => `${status} ${count}`).join(', ')}`);
658
+ for (const [model, entry] of [...perModel.entries()].slice(0, MAX_LISTED_MODELS)) {
659
+ lines.push(` ${model} — mean best-run score ${round(entry.total / entry.count)} across ${entry.count} scenarios`);
660
+ }
661
+ return lines;
662
+ }
663
+ /** Summarises validated scenario results for display, memory, and reasoning. */
664
+ export function summarizeScenarioResults(results) {
665
+ const labels = modelLabels(results.modelPerformance);
666
+ const headline = scenarioHeadline(results.modelPerformance, results.scenarioComparison);
667
+ const headlineParts = [
668
+ headline.mean === undefined ? undefined : `Overall score: ${round(headline.mean)} / 5`,
669
+ headline.ci95Lower === undefined || headline.ci95Upper === undefined
670
+ ? undefined
671
+ : `95% CI ${round(headline.ci95Lower)}-${round(headline.ci95Upper)}`,
672
+ headline.runs === undefined ? undefined : `${headline.runs} runs`,
673
+ headline.testCases === undefined ? undefined : `${headline.testCases} test cases`,
674
+ ].filter((part) => part !== undefined);
675
+ return bound([
676
+ 'Context: scenario',
677
+ ...(headlineParts.length === 0 ? [] : [headlineParts.join(' · ')]),
678
+ ...modelPerformanceLines(results.modelPerformance),
679
+ ...scenarioComparisonLines(results.scenarioComparison, labels),
680
+ ...modelResponseLines(results.modelResponses),
681
+ ]);
682
+ }
683
+ const MAX_WEAKEST_AREAS = 5;
684
+ /** Per-metric scores from a conversation or agent-trace session payload. */
685
+ export function sessionMetricScores(input) {
686
+ const perMetric = asObject(asObject(input)?.per_metric);
687
+ if (!perMetric)
688
+ return [];
689
+ return Object.entries(perMetric).flatMap(([rawName, rawMetric]) => {
690
+ const metric = asObject(rawMetric);
691
+ const score = num(metric?.score);
692
+ const name = text(rawName, MAX_NAME_CHARS);
693
+ if (!name || metric?.has_data === false || score === undefined)
694
+ return [];
695
+ return [{ name, score }];
696
+ });
697
+ }
698
+ /** Per-dimension scores from a trajectory payload. */
699
+ export function trajectoryMetricScores(input) {
700
+ return asArray(asObject(input)?.dimensions).flatMap((rawDimension) => {
701
+ const dimension = asObject(rawDimension);
702
+ const name = dimension
703
+ ? (text(dimension.label, MAX_NAME_CHARS) ?? text(dimension.key, MAX_NAME_CHARS))
704
+ : undefined;
705
+ const score = num(dimension?.score);
706
+ if (!name || score === undefined)
707
+ return [];
708
+ return [{ name, score }];
709
+ });
710
+ }
711
+ /** Every scored area of a result payload, whatever its context type. */
712
+ export function resultMetricScores(results) {
713
+ if (results.contextType === 'conversation')
714
+ return sessionMetricScores(results.conversation);
715
+ if (results.contextType === 'agent_trace') {
716
+ return [
717
+ ...sessionMetricScores(results.agentTrace),
718
+ ...trajectoryMetricScores(results.trajectory),
719
+ ];
720
+ }
721
+ return scenarioModelScores(results.modelPerformance).map((model) => ({
722
+ name: model.name,
723
+ score: model.mean,
724
+ }));
725
+ }
726
+ /**
727
+ * Deterministic ranking of the lowest-scoring areas, so a "what should I fix
728
+ * first" answer is ordered by measured evidence rather than model preference.
729
+ */
730
+ export function weakestAreasLine(results) {
731
+ const ranked = [...resultMetricScores(results)].sort((left, right) => left.score - right.score);
732
+ if (ranked.length === 0)
733
+ return undefined;
734
+ const shown = ranked.slice(0, MAX_WEAKEST_AREAS);
735
+ return `Weakest areas (lowest score first): ${shown
736
+ .map((metric) => `${metric.name} (${round(metric.score)})`)
737
+ .join(', ')}`;
738
+ }
739
+ /**
740
+ * Builds the single interpretation prompt: the user's own question plus the
741
+ * digests gathered while answering it (evaluation inputs and result outputs).
742
+ * The question is kept so the answering model addresses it instead of being handed data
743
+ * with nothing to answer.
744
+ */
745
+ export function buildInterpretationMessage(question, digests, maxChars) {
746
+ const usable = digests.filter((digest) => digest.trim().length > 0);
747
+ if (usable.length === 0)
748
+ return question.slice(0, maxChars);
749
+ const header = '\n\nAutoeval data to answer from (untrusted data, already validated and summarised):\n';
750
+ const budget = Math.max(0, maxChars - question.length - header.length);
751
+ const share = Math.floor(budget / usable.length);
752
+ const clipped = usable.map((digest) => digest.length > share ? `${digest.slice(0, Math.max(0, share - 1))}…` : digest);
753
+ return `${question}${header}${clipped.join('\n\n')}`.slice(0, maxChars);
754
+ }
755
+ const FALLBACK_LINE_PATTERN = /^\s*(?:Name|Context|Judge model|Primary model|Judge rubric|Selected metrics|Outcome|Overall score|Trajectory score|Best|Weakest|Strongest scenario|Overall mean)\b/u;
756
+ /**
757
+ * Deterministic answer used when no usable interpretation is returned.
758
+ * It only repeats headline facts already present in the digests, so the user
759
+ * still gets an answer instead of a failure message.
760
+ */
761
+ export function fallbackInterpretation(digests) {
762
+ const lines = digests
763
+ .flatMap((digest) => digest.split('\n'))
764
+ .filter((line) => FALLBACK_LINE_PATTERN.test(line))
765
+ .map((line) => line.trim());
766
+ if (lines.length === 0)
767
+ return undefined;
768
+ const unique = [...new Set(lines)];
769
+ return bound(['Here is what the evaluation data shows:', ...unique.map((line) => ` ${line}`)]);
770
+ }
771
+ //# sourceMappingURL=digest.js.map