@obsunified/collector 2.0.1

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/chunk-UGWDOGGH.js +319 -0
  7. package/dist/chunk-UGWDOGGH.js.map +1 -0
  8. package/dist/chunk-XKQTVIRE.js +671 -0
  9. package/dist/chunk-XKQTVIRE.js.map +1 -0
  10. package/dist/index.d.ts +1197 -0
  11. package/dist/index.js +17707 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
  14. package/dist/openai-FJYOQ7WO.js +384 -0
  15. package/dist/openai-FJYOQ7WO.js.map +1 -0
  16. package/package.json +73 -0
  17. package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
  18. package/src/analyses/derive.ts +642 -0
  19. package/src/analyses/index.ts +50 -0
  20. package/src/analyses/investigations.ts +277 -0
  21. package/src/analyses/tier0.ts +568 -0
  22. package/src/auth/dashboard-auth.ts +269 -0
  23. package/src/auth/ingest-auth.ts +147 -0
  24. package/src/durable-objects/tail-hub.test.ts +32 -0
  25. package/src/durable-objects/tail-hub.ts +158 -0
  26. package/src/framework/collector.ts +467 -0
  27. package/src/framework/env.ts +90 -0
  28. package/src/framework/logger.ts +81 -0
  29. package/src/index.ts +211 -0
  30. package/src/lib/action-aggregates.ts +1031 -0
  31. package/src/lib/action-identity.ts +106 -0
  32. package/src/lib/actions-store.ts +397 -0
  33. package/src/lib/ai-pricing.ts +80 -0
  34. package/src/lib/ai-store/calls.ts +148 -0
  35. package/src/lib/ai-store/evaluations.test.ts +47 -0
  36. package/src/lib/ai-store/evaluations.ts +125 -0
  37. package/src/lib/ai-store/helpers.ts +37 -0
  38. package/src/lib/ai-store/retention.ts +17 -0
  39. package/src/lib/ai-store/sessions.ts +277 -0
  40. package/src/lib/ai-store/spans.ts +109 -0
  41. package/src/lib/ai-store/types.ts +112 -0
  42. package/src/lib/ai-store.ts +76 -0
  43. package/src/lib/alerts-store.ts +458 -0
  44. package/src/lib/analyses-runner.ts +439 -0
  45. package/src/lib/analyses-store.test.ts +243 -0
  46. package/src/lib/analyses-store.ts +454 -0
  47. package/src/lib/ask.test.ts +302 -0
  48. package/src/lib/ask.ts +390 -0
  49. package/src/lib/blob-store-s3.ts +213 -0
  50. package/src/lib/blob-store.ts +366 -0
  51. package/src/lib/eval-cases-store.test.ts +171 -0
  52. package/src/lib/eval-cases-store.ts +1033 -0
  53. package/src/lib/evidence-references.test.ts +10 -0
  54. package/src/lib/evidence-references.ts +684 -0
  55. package/src/lib/hash.ts +26 -0
  56. package/src/lib/identity-index/action-lookups.ts +805 -0
  57. package/src/lib/identity-index/constants.ts +1 -0
  58. package/src/lib/identity-index/key-lookups.ts +199 -0
  59. package/src/lib/identity-index/mappers.ts +349 -0
  60. package/src/lib/identity-index/types.ts +196 -0
  61. package/src/lib/identity-index/user-lookup.ts +137 -0
  62. package/src/lib/identity-index.test.ts +624 -0
  63. package/src/lib/identity-index.ts +85 -0
  64. package/src/lib/json.ts +39 -0
  65. package/src/lib/llm.ts +263 -0
  66. package/src/lib/logs-store.ts +188 -0
  67. package/src/lib/metrics-store.test.ts +189 -0
  68. package/src/lib/metrics-store.ts +312 -0
  69. package/src/lib/narrate-gate.test.ts +260 -0
  70. package/src/lib/narrate-gate.ts +253 -0
  71. package/src/lib/openai.test.ts +300 -0
  72. package/src/lib/openai.ts +512 -0
  73. package/src/lib/otlp.ts +212 -0
  74. package/src/lib/parse-pprof.test.ts +64 -0
  75. package/src/lib/parse-pprof.ts +184 -0
  76. package/src/lib/payload-capture.ts +22 -0
  77. package/src/lib/projects-store.ts +207 -0
  78. package/src/lib/propagation-metric.test.ts +196 -0
  79. package/src/lib/propagation-metric.ts +175 -0
  80. package/src/lib/sql-db-dialect.test.ts +22 -0
  81. package/src/lib/sql-db-postgres.ts +229 -0
  82. package/src/lib/sql-db.ts +166 -0
  83. package/src/lib/stage6.test.ts +216 -0
  84. package/src/lib/store/export.ts +65 -0
  85. package/src/lib/store/helpers.ts +367 -0
  86. package/src/lib/store/ingest.ts +68 -0
  87. package/src/lib/store/issues.ts +158 -0
  88. package/src/lib/store/overview.ts +127 -0
  89. package/src/lib/store/retention.ts +10 -0
  90. package/src/lib/store/service-map.ts +384 -0
  91. package/src/lib/store/trace-detail.bench.ts +76 -0
  92. package/src/lib/store/trace-detail.test.ts +410 -0
  93. package/src/lib/store/trace-detail.ts +427 -0
  94. package/src/lib/store/trace-queries.test.ts +29 -0
  95. package/src/lib/store/trace-queries.ts +89 -0
  96. package/src/lib/store-overview.test.ts +77 -0
  97. package/src/lib/store.ts +96 -0
  98. package/src/lib/tail-publisher.ts +64 -0
  99. package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
  100. package/src/lib/test-utils/mem-sql-db.ts +86 -0
  101. package/src/lib/ua-parser.ts +61 -0
  102. package/src/lib/usage-store.ts +567 -0
  103. package/src/migrations/001_telemetry_spans.sql +28 -0
  104. package/src/migrations/002_usage_events.sql +24 -0
  105. package/src/migrations/003_usage_analytics_columns.sql +13 -0
  106. package/src/migrations/004_logs.sql +19 -0
  107. package/src/migrations/005_ai_calls.sql +25 -0
  108. package/src/migrations/006_user_profiles.sql +11 -0
  109. package/src/migrations/007_session_replay_metadata.sql +10 -0
  110. package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
  111. package/src/migrations/009_projects_and_keys.sql +22 -0
  112. package/src/migrations/010_signal_project_id.sql +15 -0
  113. package/src/migrations/011_default_project_seed.sql +3 -0
  114. package/src/migrations/012_alert_rules.sql +17 -0
  115. package/src/migrations/013_alert_evaluations.sql +13 -0
  116. package/src/migrations/014_alert_state.sql +9 -0
  117. package/src/migrations/015_metrics.sql +58 -0
  118. package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
  119. package/src/migrations/017_span_dropped_counts.sql +11 -0
  120. package/src/migrations/018_log_flags.sql +8 -0
  121. package/src/migrations/019_ai_span_payloads.sql +30 -0
  122. package/src/migrations/020_ai_span_evaluations.sql +33 -0
  123. package/src/migrations/021_ai_span_session.sql +13 -0
  124. package/src/migrations/022_span_log_session_id.sql +13 -0
  125. package/src/migrations/023_service_map_indexes.sql +32 -0
  126. package/src/migrations/024_analysis_results.sql +49 -0
  127. package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
  128. package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
  129. package/src/migrations/027_identity_propagation.sql +50 -0
  130. package/src/migrations/028_profile_blobs.sql +57 -0
  131. package/src/migrations/029_telemetry_sdk_name.sql +20 -0
  132. package/src/migrations/030_analysis_claim.sql +18 -0
  133. package/src/migrations/031_agent_action_graph.sql +154 -0
  134. package/src/migrations/032_payload_capture_flags.sql +8 -0
  135. package/src/migrations/033_eval_cases.sql +52 -0
  136. package/src/migrations/034_eval_case_results.sql +23 -0
  137. package/src/migrations/035_metric_exemplars.sql +33 -0
  138. package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
  139. package/src/migrations/037_eval_runs.sql +37 -0
  140. package/src/migrations/038_tool_call_evidence.sql +11 -0
  141. package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
  142. package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
  143. package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
  144. package/src/migrations-postgres/002_usage_events.sql +24 -0
  145. package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
  146. package/src/migrations-postgres/004_logs.sql +19 -0
  147. package/src/migrations-postgres/005_ai_calls.sql +25 -0
  148. package/src/migrations-postgres/006_user_profiles.sql +11 -0
  149. package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
  150. package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
  151. package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
  152. package/src/migrations-postgres/010_signal_project_id.sql +18 -0
  153. package/src/migrations-postgres/011_default_project_seed.sql +5 -0
  154. package/src/migrations-postgres/012_alert_rules.sql +17 -0
  155. package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
  156. package/src/migrations-postgres/014_alert_state.sql +9 -0
  157. package/src/migrations-postgres/015_metrics.sql +47 -0
  158. package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
  159. package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
  160. package/src/migrations-postgres/018_log_flags.sql +2 -0
  161. package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
  162. package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
  163. package/src/migrations-postgres/021_ai_span_session.sql +7 -0
  164. package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
  165. package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
  166. package/src/migrations-postgres/024_analysis_results.sql +41 -0
  167. package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
  168. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
  169. package/src/migrations-postgres/027_identity_propagation.sql +27 -0
  170. package/src/migrations-postgres/028_profile_blobs.sql +48 -0
  171. package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
  172. package/src/migrations-postgres/030_analysis_claim.sql +5 -0
  173. package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
  174. package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
  175. package/src/migrations-postgres/033_eval_cases.sql +55 -0
  176. package/src/migrations-postgres/034_eval_case_results.sql +26 -0
  177. package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
  178. package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
  179. package/src/migrations-postgres/037_eval_runs.sql +40 -0
  180. package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
  181. package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
  182. package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
  183. package/src/migrations-postgres/README.md +40 -0
  184. package/src/otlp/acceptance.test.ts +373 -0
  185. package/src/otlp/decode/body.ts +127 -0
  186. package/src/otlp/decode/logs.ts +96 -0
  187. package/src/otlp/decode/metrics.ts +402 -0
  188. package/src/otlp/decode/traces.ts +92 -0
  189. package/src/otlp/decode/values.ts +150 -0
  190. package/src/otlp/decode.test.ts +800 -0
  191. package/src/otlp/decode.ts +11 -0
  192. package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
  193. package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
  194. package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
  195. package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
  196. package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
  197. package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
  198. package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
  199. package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
  200. package/src/otlp/response.test.ts +158 -0
  201. package/src/otlp/response.ts +129 -0
  202. package/src/plugins/_context.ts +15 -0
  203. package/src/plugins/action-graph-processor/enrichers.ts +45 -0
  204. package/src/plugins/action-graph-processor/redaction.ts +84 -0
  205. package/src/plugins/action-graph-processor.test.ts +1023 -0
  206. package/src/plugins/action-graph-processor.ts +1172 -0
  207. package/src/plugins/action-routes.test.ts +716 -0
  208. package/src/plugins/action-routes.ts +343 -0
  209. package/src/plugins/ai-receiver.ts +254 -0
  210. package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
  211. package/src/plugins/ai-span-payloads-processor.ts +180 -0
  212. package/src/plugins/alerts-evaluator.test.ts +91 -0
  213. package/src/plugins/alerts-evaluator.ts +328 -0
  214. package/src/plugins/alerts-routes.ts +101 -0
  215. package/src/plugins/analyses-routes.ts +138 -0
  216. package/src/plugins/ask-routes.ts +182 -0
  217. package/src/plugins/bot-filter.ts +19 -0
  218. package/src/plugins/connected-routes/manifest.ts +531 -0
  219. package/src/plugins/connected-routes.test.ts +1791 -0
  220. package/src/plugins/connected-routes.ts +1103 -0
  221. package/src/plugins/dashboard-routes.ts +41 -0
  222. package/src/plugins/default-span-enrichment.ts +92 -0
  223. package/src/plugins/eval-cases-routes.test.ts +391 -0
  224. package/src/plugins/eval-cases-routes.ts +529 -0
  225. package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
  226. package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
  227. package/src/plugins/evidence-retrieval-routes.ts +2179 -0
  228. package/src/plugins/gen-ai-normalizer.test.ts +680 -0
  229. package/src/plugins/gen-ai-normalizer.ts +302 -0
  230. package/src/plugins/identity-receiver.ts +96 -0
  231. package/src/plugins/issue-enrichment.ts +132 -0
  232. package/src/plugins/issue-insights.ts +81 -0
  233. package/src/plugins/logs-receiver.ts +160 -0
  234. package/src/plugins/metrics-receiver.ts +91 -0
  235. package/src/plugins/onboarding-routes.ts +54 -0
  236. package/src/plugins/otlp-receiver.ts +124 -0
  237. package/src/plugins/platform-routes.ts +165 -0
  238. package/src/plugins/profile-routes.ts +468 -0
  239. package/src/plugins/projects-routes.ts +92 -0
  240. package/src/plugins/query-routes.ts +220 -0
  241. package/src/plugins/redaction-processor.ts +132 -0
  242. package/src/plugins/replay-query-routes.test.ts +38 -0
  243. package/src/plugins/replay-query-routes.ts +199 -0
  244. package/src/plugins/replay-receiver.ts +81 -0
  245. package/src/plugins/tail-routes.test.ts +47 -0
  246. package/src/plugins/tail-routes.ts +42 -0
  247. package/src/plugins/timeline-routes.ts +244 -0
  248. package/src/plugins/ua-enrichment.ts +27 -0
  249. package/src/plugins/usage-normalization.ts +46 -0
  250. package/src/plugins/usage-privacy.ts +55 -0
  251. package/src/plugins/usage-query-routes.ts +102 -0
  252. package/src/plugins/usage-receiver.ts +129 -0
  253. package/src/plugins/users-query-routes.ts +78 -0
  254. package/src/plugins/utm-enrichment.ts +36 -0
@@ -0,0 +1,529 @@
1
+ import type {
2
+ EvidenceReference,
3
+ EvidenceReferenceContract as EvidenceReferenceContractType,
4
+ JsonValue,
5
+ } from "@obsunified/types";
6
+ import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
7
+ import type { CollectorPlugin } from "../framework/collector";
8
+ import {
9
+ type EvalCase,
10
+ type EvalCaseInput,
11
+ EvalCaseSourceNotFoundError,
12
+ type EvalCaseSourceType,
13
+ EvalCasesStore,
14
+ type EvalRunInput,
15
+ isEvalCaseSourceType,
16
+ isEvalRunStatus,
17
+ } from "../lib/eval-cases-store";
18
+ import { sourceLinkEvidenceReferences } from "../lib/evidence-references";
19
+ import { randomHex } from "../lib/hash";
20
+ import { sqlDbFor } from "../lib/sql-db";
21
+ import { getProjectId } from "./_context";
22
+
23
+ interface EvalCaseRequestBody {
24
+ sourceEntityType?: unknown;
25
+ sourceEntityId?: unknown;
26
+ source?: {
27
+ type?: unknown;
28
+ id?: unknown;
29
+ agentRunId?: unknown;
30
+ actionId?: unknown;
31
+ aiCallId?: unknown;
32
+ toolCallId?: unknown;
33
+ traceId?: unknown;
34
+ spanId?: unknown;
35
+ };
36
+ name?: unknown;
37
+ expectedOutcome?: unknown;
38
+ rubric?: JsonValue | null;
39
+ redactedPrompt?: JsonValue | null;
40
+ referencePayload?: JsonValue | null;
41
+ metadata?: unknown;
42
+ }
43
+
44
+ interface EvalCaseResultResponse {
45
+ id: string;
46
+ projectId: string;
47
+ evalCaseId: string;
48
+ runId: string;
49
+ passed: boolean;
50
+ score: number | null;
51
+ actualOutcome: string | null;
52
+ details: unknown;
53
+ createdAt: string;
54
+ evidenceReferences?: EvidenceReference[];
55
+ evidenceContract?: EvidenceReferenceContractType;
56
+ }
57
+
58
+ interface EvalRunRequestBody {
59
+ id?: unknown;
60
+ runId?: unknown;
61
+ evalCaseId?: unknown;
62
+ status?: unknown;
63
+ candidate?: unknown;
64
+ startedAt?: unknown;
65
+ endedAt?: unknown;
66
+ totalCount?: unknown;
67
+ passCount?: unknown;
68
+ failCount?: unknown;
69
+ averageScore?: unknown;
70
+ metadata?: unknown;
71
+ }
72
+
73
+ const asOptionalString = (value: unknown): string | null => {
74
+ if (value === undefined || value === null) return null;
75
+ return typeof value === "string" ? value : null;
76
+ };
77
+
78
+ const asRecord = (value: unknown): Record<string, JsonValue> | null => {
79
+ if (value === undefined || value === null) return {};
80
+ if (typeof value !== "object" || Array.isArray(value)) return null;
81
+ return value as Record<string, JsonValue>;
82
+ };
83
+
84
+ const asNonNegativeNumber = (value: unknown): number | undefined | null => {
85
+ if (value === undefined) return undefined;
86
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
87
+ ? value
88
+ : null;
89
+ };
90
+
91
+ const badRequest = (message: string) => ({
92
+ error: "Bad Request",
93
+ message,
94
+ });
95
+
96
+ const parseLimit = (value: string | undefined): number => {
97
+ const parsed = Number.parseInt(value ?? "50", 10);
98
+ if (!Number.isFinite(parsed)) return 50;
99
+ return Math.min(200, Math.max(1, parsed));
100
+ };
101
+
102
+ const parseJsonMaybe = (value: unknown): unknown => {
103
+ if (value === null || value === undefined) return null;
104
+ return typeof value === "string" ? JSON.parse(value) : value;
105
+ };
106
+
107
+ const evalCaseResultEvidenceReferences = (
108
+ evalCase: EvalCase,
109
+ resultId: string,
110
+ ): EvidenceReference[] =>
111
+ sourceLinkEvidenceReferences(
112
+ {
113
+ sourceLabel: `Eval case result "${resultId}"`,
114
+ sourceId: resultId,
115
+ sourceKind: "eval_case_result",
116
+ sourceRoute: `#/evaluations?case=${encodeURIComponent(evalCase.id)}&result=${encodeURIComponent(resultId)}`,
117
+ sourceName: evalCase.name,
118
+ },
119
+ evalCase,
120
+ );
121
+
122
+ export const evalCasesRoutesPlugin: CollectorPlugin = {
123
+ name: "eval-cases-routes",
124
+ register(app) {
125
+ app.post("/internal/eval-cases", async (c) => {
126
+ let body: EvalCaseRequestBody;
127
+ try {
128
+ body = (await c.req.json()) as EvalCaseRequestBody;
129
+ } catch {
130
+ return c.json(badRequest("body must be JSON"), 400);
131
+ }
132
+
133
+ const sourceType = body.sourceEntityType ?? body.source?.type;
134
+ if (!isEvalCaseSourceType(sourceType)) {
135
+ return c.json(
136
+ badRequest(
137
+ "sourceEntityType must be one of: agent_run, action, ai_call, tool_call, trace",
138
+ ),
139
+ 400,
140
+ );
141
+ }
142
+ const sourceId = asOptionalString(
143
+ body.sourceEntityId ?? body.source?.id,
144
+ )?.trim();
145
+ if (!sourceId) {
146
+ return c.json(badRequest("sourceEntityId is required"), 400);
147
+ }
148
+ const name = asOptionalString(body.name)?.trim();
149
+ if (!name) {
150
+ return c.json(badRequest("name is required"), 400);
151
+ }
152
+ const expectedOutcome = asOptionalString(body.expectedOutcome);
153
+ if (
154
+ body.expectedOutcome !== undefined &&
155
+ body.expectedOutcome !== null &&
156
+ expectedOutcome === null
157
+ ) {
158
+ return c.json(badRequest("expectedOutcome must be a string"), 400);
159
+ }
160
+ const metadata = asRecord(body.metadata);
161
+ if (metadata === null) {
162
+ return c.json(badRequest("metadata must be a JSON object"), 400);
163
+ }
164
+
165
+ const input: EvalCaseInput = {
166
+ sourceEntityType: sourceType,
167
+ sourceEntityId: sourceId,
168
+ name,
169
+ expectedOutcome,
170
+ rubric: body.rubric ?? null,
171
+ redactedPrompt: body.redactedPrompt ?? null,
172
+ referencePayload: body.referencePayload ?? null,
173
+ metadata,
174
+ source: {
175
+ sourceAgentRunId: asOptionalString(body.source?.agentRunId),
176
+ sourceActionId: asOptionalString(body.source?.actionId),
177
+ sourceAiCallId: asOptionalString(body.source?.aiCallId),
178
+ sourceToolCallId: asOptionalString(body.source?.toolCallId),
179
+ sourceTraceId: asOptionalString(body.source?.traceId),
180
+ sourceSpanId: asOptionalString(body.source?.spanId),
181
+ },
182
+ };
183
+
184
+ const store = new EvalCasesStore(sqlDbFor(c.env));
185
+ try {
186
+ const evalCase = await store.createCase(getProjectId(c), input);
187
+ return c.json({ evalCase }, 201);
188
+ } catch (err) {
189
+ if (err instanceof EvalCaseSourceNotFoundError) {
190
+ return c.json(
191
+ {
192
+ error: "Not Found",
193
+ message: err.message,
194
+ },
195
+ 404,
196
+ );
197
+ }
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ return c.json(badRequest(message), 400);
200
+ }
201
+ });
202
+
203
+ app.get("/internal/eval-cases", async (c) => {
204
+ const rawSourceType = c.req.query("sourceEntityType");
205
+ const sourceEntityType =
206
+ rawSourceType === undefined
207
+ ? undefined
208
+ : isEvalCaseSourceType(rawSourceType)
209
+ ? rawSourceType
210
+ : null;
211
+ if (sourceEntityType === null) {
212
+ return c.json(badRequest("sourceEntityType is invalid"), 400);
213
+ }
214
+ const store = new EvalCasesStore(sqlDbFor(c.env));
215
+ const evalCases = await store.listCases({
216
+ projectId: getProjectId(c),
217
+ sourceEntityType: sourceEntityType as EvalCaseSourceType | undefined,
218
+ sourceEntityId: c.req.query("sourceEntityId"),
219
+ limit: parseLimit(c.req.query("limit")),
220
+ });
221
+ return c.json({ evalCases });
222
+ });
223
+
224
+ app.get("/internal/eval-cases/:id", async (c) => {
225
+ const id = c.req.param("id");
226
+ if (!id) return c.json(badRequest("id is required"), 400);
227
+ const store = new EvalCasesStore(sqlDbFor(c.env));
228
+ const evalCase = await store.getCase(getProjectId(c), id);
229
+ if (!evalCase) {
230
+ return c.json(
231
+ { error: "Not Found", message: "Eval case not found" },
232
+ 404,
233
+ );
234
+ }
235
+ return c.json({ evalCase });
236
+ });
237
+
238
+ app.post("/internal/eval-runs", async (c) => {
239
+ let body: EvalRunRequestBody;
240
+ try {
241
+ body = (await c.req.json()) as EvalRunRequestBody;
242
+ } catch {
243
+ return c.json(badRequest("body must be JSON"), 400);
244
+ }
245
+
246
+ const id = asOptionalString(body.id ?? body.runId)?.trim();
247
+ if ((body.id ?? body.runId) !== undefined && !id) {
248
+ return c.json(badRequest("id must be a non-empty string"), 400);
249
+ }
250
+ const evalCaseId = asOptionalString(body.evalCaseId)?.trim() || null;
251
+ if (
252
+ body.evalCaseId !== undefined &&
253
+ body.evalCaseId !== null &&
254
+ !evalCaseId
255
+ ) {
256
+ return c.json(badRequest("evalCaseId must be a string"), 400);
257
+ }
258
+ const status = body.status ?? "running";
259
+ if (!isEvalRunStatus(status)) {
260
+ return c.json(badRequest("status is invalid"), 400);
261
+ }
262
+ const metadata = asRecord(body.metadata);
263
+ if (metadata === null) {
264
+ return c.json(badRequest("metadata must be a JSON object"), 400);
265
+ }
266
+ const candidate = asRecord(body.candidate);
267
+ if (candidate === null) {
268
+ return c.json(badRequest("candidate must be a JSON object"), 400);
269
+ }
270
+
271
+ const totalCount = asNonNegativeNumber(body.totalCount);
272
+ const passCount = asNonNegativeNumber(body.passCount);
273
+ const failCount = asNonNegativeNumber(body.failCount);
274
+ const averageScore = asNonNegativeNumber(body.averageScore);
275
+ if (totalCount === null) {
276
+ return c.json(
277
+ badRequest("totalCount must be a non-negative number"),
278
+ 400,
279
+ );
280
+ }
281
+ if (passCount === null) {
282
+ return c.json(
283
+ badRequest("passCount must be a non-negative number"),
284
+ 400,
285
+ );
286
+ }
287
+ if (failCount === null) {
288
+ return c.json(
289
+ badRequest("failCount must be a non-negative number"),
290
+ 400,
291
+ );
292
+ }
293
+ if (averageScore === null && body.averageScore !== null) {
294
+ return c.json(
295
+ badRequest("averageScore must be a non-negative number"),
296
+ 400,
297
+ );
298
+ }
299
+
300
+ const input: EvalRunInput = {
301
+ id: id ?? undefined,
302
+ evalCaseId,
303
+ status,
304
+ candidate: {
305
+ agentId: asOptionalString(candidate.agentId),
306
+ agentVersion: asOptionalString(candidate.agentVersion),
307
+ promptId: asOptionalString(candidate.promptId),
308
+ promptVersion: asOptionalString(candidate.promptVersion),
309
+ modelProvider: asOptionalString(candidate.modelProvider),
310
+ model: asOptionalString(candidate.model),
311
+ modelVersion: asOptionalString(candidate.modelVersion),
312
+ },
313
+ startedAt: asOptionalString(body.startedAt),
314
+ endedAt: asOptionalString(body.endedAt),
315
+ totalCount: totalCount ?? undefined,
316
+ passCount: passCount ?? undefined,
317
+ failCount: failCount ?? undefined,
318
+ averageScore: averageScore ?? null,
319
+ metadata,
320
+ };
321
+
322
+ const store = new EvalCasesStore(sqlDbFor(c.env));
323
+ try {
324
+ const evalRun = await store.createRun(getProjectId(c), input);
325
+ return c.json({ evalRun }, 201);
326
+ } catch (err) {
327
+ const message = err instanceof Error ? err.message : String(err);
328
+ if (message.includes("does not reference a case")) {
329
+ return c.json({ error: "Not Found", message }, 404);
330
+ }
331
+ return c.json(badRequest(message), 400);
332
+ }
333
+ });
334
+
335
+ app.get("/internal/eval-runs", async (c) => {
336
+ const rawStatus = c.req.query("status");
337
+ const status =
338
+ rawStatus === undefined
339
+ ? undefined
340
+ : isEvalRunStatus(rawStatus)
341
+ ? rawStatus
342
+ : null;
343
+ if (status === null) {
344
+ return c.json(badRequest("status is invalid"), 400);
345
+ }
346
+ const store = new EvalCasesStore(sqlDbFor(c.env));
347
+ const evalRuns = await store.listRuns({
348
+ projectId: getProjectId(c),
349
+ evalCaseId: c.req.query("evalCaseId"),
350
+ status,
351
+ limit: parseLimit(c.req.query("limit")),
352
+ });
353
+ return c.json({ evalRuns });
354
+ });
355
+
356
+ app.get("/internal/eval-runs/:id", async (c) => {
357
+ const id = c.req.param("id");
358
+ if (!id) return c.json(badRequest("id is required"), 400);
359
+ const store = new EvalCasesStore(sqlDbFor(c.env));
360
+ const evalRun = await store.getRun(getProjectId(c), id);
361
+ if (!evalRun) {
362
+ return c.json(
363
+ { error: "Not Found", message: "Eval run not found" },
364
+ 404,
365
+ );
366
+ }
367
+ return c.json({ evalRun });
368
+ });
369
+
370
+ app.post("/internal/eval-cases/:id/results", async (c) => {
371
+ const evalCaseId = c.req.param("id");
372
+ if (!evalCaseId) return c.json(badRequest("id is required"), 400);
373
+
374
+ let body: {
375
+ runId?: unknown;
376
+ passed?: unknown;
377
+ score?: unknown;
378
+ actualOutcome?: unknown;
379
+ details?: unknown;
380
+ };
381
+ try {
382
+ body = (await c.req.json()) as typeof body;
383
+ } catch {
384
+ return c.json(badRequest("body must be JSON"), 400);
385
+ }
386
+
387
+ if (body.passed === undefined || typeof body.passed !== "boolean") {
388
+ return c.json(badRequest("passed must be a boolean"), 400);
389
+ }
390
+
391
+ const db = sqlDbFor(c.env);
392
+
393
+ // Verify case exists
394
+ const store = new EvalCasesStore(db);
395
+ const projectId = getProjectId(c);
396
+ const evalCase = await store.getCase(getProjectId(c), evalCaseId);
397
+ if (!evalCase) {
398
+ return c.json(
399
+ { error: "Not Found", message: "Eval case not found" },
400
+ 404,
401
+ );
402
+ }
403
+
404
+ const id = randomHex(16);
405
+ const runId =
406
+ typeof body.runId === "string" && body.runId.trim()
407
+ ? body.runId.trim()
408
+ : `run_${randomHex(8)}`;
409
+ const evalRun = await store.getRun(projectId, runId);
410
+ if (evalRun?.evalCaseId && evalRun.evalCaseId !== evalCaseId) {
411
+ return c.json(
412
+ badRequest("runId belongs to a different eval case"),
413
+ 400,
414
+ );
415
+ }
416
+ const passed = body.passed ? 1 : 0;
417
+ const score =
418
+ typeof body.score === "number" && Number.isFinite(body.score)
419
+ ? body.score
420
+ : null;
421
+ const actualOutcome =
422
+ typeof body.actualOutcome === "string" ? body.actualOutcome : null;
423
+ const details_json =
424
+ body.details === undefined ? null : JSON.stringify(body.details);
425
+ const now = new Date().toISOString();
426
+
427
+ await db
428
+ .prepare(
429
+ `INSERT INTO eval_case_results (
430
+ id, project_id, eval_case_id, run_id, passed, score, actual_outcome, details_json, created_at
431
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
432
+ )
433
+ .bind(
434
+ id,
435
+ projectId,
436
+ evalCaseId,
437
+ runId,
438
+ passed,
439
+ score,
440
+ actualOutcome,
441
+ details_json,
442
+ now,
443
+ )
444
+ .run();
445
+
446
+ if (evalRun) await store.refreshRunSummary(projectId, runId);
447
+ const refreshedRun = evalRun
448
+ ? await store.getRun(projectId, runId)
449
+ : null;
450
+ const evalCaseResult: EvalCaseResultResponse = {
451
+ id,
452
+ projectId,
453
+ evalCaseId,
454
+ runId,
455
+ passed: body.passed,
456
+ score,
457
+ actualOutcome,
458
+ details: body.details ?? null,
459
+ createdAt: now,
460
+ evidenceReferences: evalCaseResultEvidenceReferences(evalCase, id),
461
+ evidenceContract: EVIDENCE_REFERENCE_CONTRACT,
462
+ };
463
+
464
+ return c.json(
465
+ {
466
+ evalCaseResult,
467
+ evalRun: refreshedRun,
468
+ },
469
+ 201,
470
+ );
471
+ });
472
+
473
+ app.get("/internal/eval-cases/:id/results", async (c) => {
474
+ const evalCaseId = c.req.param("id");
475
+ if (!evalCaseId) return c.json(badRequest("id is required"), 400);
476
+
477
+ const db = sqlDbFor(c.env);
478
+
479
+ const rows = await db
480
+ .prepare(
481
+ `SELECT * FROM eval_case_results
482
+ WHERE project_id = ? AND eval_case_id = ?
483
+ ORDER BY created_at DESC`,
484
+ )
485
+ .bind(getProjectId(c), evalCaseId)
486
+ .all<{
487
+ id: string;
488
+ project_id: string;
489
+ eval_case_id: string;
490
+ run_id: string;
491
+ passed: number;
492
+ score: number | null;
493
+ actual_outcome: string | null;
494
+ details_json: string | null;
495
+ created_at: string;
496
+ }>();
497
+
498
+ const store = new EvalCasesStore(db);
499
+ const evalCase = await store.getCase(getProjectId(c), evalCaseId);
500
+ if (!evalCase) {
501
+ return c.json(
502
+ { error: "Not Found", message: "Eval case not found" },
503
+ 404,
504
+ );
505
+ }
506
+
507
+ const evalCaseResults: EvalCaseResultResponse[] = rows.results.map(
508
+ (row) => ({
509
+ id: row.id,
510
+ projectId: row.project_id,
511
+ evalCaseId: row.eval_case_id,
512
+ runId: row.run_id,
513
+ passed: row.passed === 1,
514
+ score: row.score,
515
+ actualOutcome: row.actual_outcome,
516
+ details: parseJsonMaybe(row.details_json),
517
+ createdAt: row.created_at,
518
+ evidenceReferences: evalCaseResultEvidenceReferences(
519
+ evalCase,
520
+ row.id,
521
+ ),
522
+ evidenceContract: EVIDENCE_REFERENCE_CONTRACT,
523
+ }),
524
+ );
525
+
526
+ return c.json({ evalCaseResults });
527
+ });
528
+ },
529
+ };