@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,1033 @@
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 { sourceLinkEvidenceReferences } from "./evidence-references";
8
+ import { randomHex } from "./hash";
9
+ import { parseJsonArray, parseJsonValue } from "./json";
10
+ import type { SqlDb } from "./sql-db";
11
+
12
+ export type EvalCaseSourceType =
13
+ | "agent_run"
14
+ | "action"
15
+ | "ai_call"
16
+ | "tool_call"
17
+ | "trace";
18
+
19
+ export interface EvalCaseSourceLinks {
20
+ sourceAgentRunId?: string | null;
21
+ sourceActionId?: string | null;
22
+ sourceAiCallId?: string | null;
23
+ sourceToolCallId?: string | null;
24
+ sourceTraceId?: string | null;
25
+ sourceSpanId?: string | null;
26
+ }
27
+
28
+ export interface EvalCaseInput {
29
+ sourceEntityType: EvalCaseSourceType;
30
+ sourceEntityId: string;
31
+ name: string;
32
+ expectedOutcome?: string | null;
33
+ rubric?: JsonValue | null;
34
+ redactedPrompt?: JsonValue | null;
35
+ referencePayload?: JsonValue | null;
36
+ metadata?: Record<string, JsonValue>;
37
+ source?: EvalCaseSourceLinks;
38
+ }
39
+
40
+ export interface EvalCase extends Required<EvalCaseSourceLinks> {
41
+ id: string;
42
+ projectId: string;
43
+ sourceEntityType: EvalCaseSourceType;
44
+ sourceEntityId: string;
45
+ name: string;
46
+ expectedOutcome: string | null;
47
+ rubric: JsonValue | null;
48
+ redactedPrompt: JsonValue | null;
49
+ referencePayload: JsonValue | null;
50
+ metadata: Record<string, JsonValue>;
51
+ createdAt: string;
52
+ updatedAt: string;
53
+ evidenceReferences?: EvidenceReference[];
54
+ evidenceContract?: EvidenceReferenceContractType;
55
+ }
56
+
57
+ export interface EvalCaseListOptions {
58
+ projectId: string;
59
+ sourceEntityType?: EvalCaseSourceType;
60
+ sourceEntityId?: string;
61
+ limit?: number;
62
+ }
63
+
64
+ export type EvalRunStatus =
65
+ | "pending"
66
+ | "running"
67
+ | "completed"
68
+ | "failed"
69
+ | "canceled";
70
+
71
+ export interface EvalRunCandidate {
72
+ agentId: string | null;
73
+ agentVersion: string | null;
74
+ promptId: string | null;
75
+ promptVersion: string | null;
76
+ modelProvider: string | null;
77
+ model: string | null;
78
+ modelVersion: string | null;
79
+ }
80
+
81
+ export interface EvalRunSourceCase {
82
+ id: string;
83
+ name: string;
84
+ sourceEntityType: string;
85
+ sourceEntityId: string;
86
+ sourceAgentRunId: string | null;
87
+ sourceActionId: string | null;
88
+ sourceAiCallId: string | null;
89
+ sourceToolCallId: string | null;
90
+ sourceTraceId: string | null;
91
+ sourceSpanId: string | null;
92
+ evidenceReferences?: EvidenceReference[];
93
+ evidenceContract?: EvidenceReferenceContractType;
94
+ }
95
+
96
+ export interface EvalRun {
97
+ id: string;
98
+ projectId: string;
99
+ evalCaseId: string | null;
100
+ status: EvalRunStatus;
101
+ candidate: EvalRunCandidate;
102
+ startedAt: string | null;
103
+ endedAt: string | null;
104
+ totalCount: number;
105
+ passCount: number;
106
+ failCount: number;
107
+ averageScore: number | null;
108
+ metadata: Record<string, JsonValue>;
109
+ createdAt: string;
110
+ sourceEvalCase?: EvalRunSourceCase | null;
111
+ evidenceReferences?: EvidenceReference[];
112
+ evidenceContract?: EvidenceReferenceContractType;
113
+ }
114
+
115
+ export interface EvalRunInput {
116
+ id?: string;
117
+ evalCaseId?: string | null;
118
+ status?: EvalRunStatus;
119
+ candidate?: Partial<EvalRunCandidate>;
120
+ startedAt?: string | null;
121
+ endedAt?: string | null;
122
+ totalCount?: number;
123
+ passCount?: number;
124
+ failCount?: number;
125
+ averageScore?: number | null;
126
+ metadata?: Record<string, JsonValue>;
127
+ }
128
+
129
+ export interface EvalRunsListOptions {
130
+ projectId: string;
131
+ evalCaseId?: string;
132
+ status?: EvalRunStatus;
133
+ limit?: number;
134
+ }
135
+
136
+ interface EvalRunRow {
137
+ id: string;
138
+ project_id: string;
139
+ eval_case_id: string | null;
140
+ candidate_agent_id: string | null;
141
+ candidate_agent_version: string | null;
142
+ candidate_prompt_id: string | null;
143
+ candidate_prompt_version: string | null;
144
+ candidate_model_provider: string | null;
145
+ candidate_model: string | null;
146
+ candidate_model_version: string | null;
147
+ status: string;
148
+ started_at: string | null;
149
+ ended_at: string | null;
150
+ total_count: number;
151
+ pass_count: number;
152
+ fail_count: number;
153
+ average_score: number | null;
154
+ metadata_json: unknown;
155
+ created_at: string;
156
+ case_id?: string | null;
157
+ case_name?: string | null;
158
+ case_source_entity_type?: string | null;
159
+ case_source_entity_id?: string | null;
160
+ case_source_agent_run_id?: string | null;
161
+ case_source_action_id?: string | null;
162
+ case_source_ai_call_id?: string | null;
163
+ case_source_tool_call_id?: string | null;
164
+ case_source_trace_id?: string | null;
165
+ case_source_span_id?: string | null;
166
+ }
167
+
168
+ interface EvalCaseRow {
169
+ id: string;
170
+ project_id: string;
171
+ source_entity_type: string;
172
+ source_entity_id: string;
173
+ source_agent_run_id: string | null;
174
+ source_action_id: string | null;
175
+ source_ai_call_id: string | null;
176
+ source_tool_call_id: string | null;
177
+ source_trace_id: string | null;
178
+ source_span_id: string | null;
179
+ name: string;
180
+ expected_outcome: string | null;
181
+ rubric_json: unknown;
182
+ redacted_prompt_json: unknown;
183
+ reference_payload_json: unknown;
184
+ metadata_json: unknown;
185
+ created_at: string;
186
+ updated_at: string;
187
+ }
188
+
189
+ interface ActionSourceRow {
190
+ id: string;
191
+ agent_run_id: string | null;
192
+ tool_call_id: string | null;
193
+ trace_id: string | null;
194
+ span_id: string | null;
195
+ }
196
+
197
+ interface AiCallSourceRow {
198
+ call_id: string;
199
+ trace_id: string | null;
200
+ span_id: string | null;
201
+ request_json: unknown;
202
+ response_json: unknown;
203
+ }
204
+
205
+ interface ToolCallSourceRow {
206
+ id: string;
207
+ action_id: string | null;
208
+ tool_name: string | null;
209
+ args_hash: string | null;
210
+ result_hash: string | null;
211
+ mcp_audit_json?: unknown;
212
+ mutation_before_json?: unknown;
213
+ mutation_after_json?: unknown;
214
+ mutation_diff_json?: unknown;
215
+ mutation_artifact_id?: string | null;
216
+ }
217
+
218
+ interface SpanSourceRow {
219
+ trace_id: string;
220
+ span_id: string;
221
+ }
222
+
223
+ interface PayloadRow {
224
+ input_json: unknown;
225
+ output_json: unknown;
226
+ }
227
+
228
+ interface RetrievalEventRow {
229
+ documents_json: unknown;
230
+ }
231
+
232
+ export class EvalCaseSourceNotFoundError extends Error {
233
+ constructor(
234
+ readonly projectId: string,
235
+ readonly sourceEntityType: EvalCaseSourceType,
236
+ readonly sourceEntityId: string,
237
+ ) {
238
+ super(
239
+ `Source ${sourceEntityType}:${sourceEntityId} not found in project ${projectId}`,
240
+ );
241
+ this.name = "EvalCaseSourceNotFoundError";
242
+ }
243
+ }
244
+
245
+ const SOURCE_TYPES: EvalCaseSourceType[] = [
246
+ "agent_run",
247
+ "action",
248
+ "ai_call",
249
+ "tool_call",
250
+ "trace",
251
+ ];
252
+
253
+ const EVAL_RUN_STATUSES: EvalRunStatus[] = [
254
+ "pending",
255
+ "running",
256
+ "completed",
257
+ "failed",
258
+ "canceled",
259
+ ];
260
+
261
+ export const isEvalCaseSourceType = (
262
+ value: unknown,
263
+ ): value is EvalCaseSourceType =>
264
+ typeof value === "string" &&
265
+ SOURCE_TYPES.includes(value as EvalCaseSourceType);
266
+
267
+ export const isEvalRunStatus = (value: unknown): value is EvalRunStatus =>
268
+ typeof value === "string" &&
269
+ EVAL_RUN_STATUSES.includes(value as EvalRunStatus);
270
+
271
+ const parseJsonField = (value: unknown): JsonValue | null => {
272
+ if (value === null || value === undefined) return null;
273
+ if (typeof value === "string") return parseJsonValue(value);
274
+ return value as JsonValue;
275
+ };
276
+
277
+ const parseJsonRecordField = (value: unknown): Record<string, JsonValue> => {
278
+ const parsed = parseJsonField(value);
279
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
280
+ ? (parsed as Record<string, JsonValue>)
281
+ : {};
282
+ };
283
+
284
+ const jsonString = (value: JsonValue | null | undefined): string | null =>
285
+ value === undefined || value === null ? null : JSON.stringify(value);
286
+
287
+ const compactLinks = (
288
+ links: EvalCaseSourceLinks,
289
+ ): Record<string, JsonValue> => {
290
+ const out: Record<string, JsonValue> = {};
291
+ if (links.sourceAgentRunId) out.agentRunId = links.sourceAgentRunId;
292
+ if (links.sourceActionId) out.actionId = links.sourceActionId;
293
+ if (links.sourceAiCallId) out.aiCallId = links.sourceAiCallId;
294
+ if (links.sourceToolCallId) out.toolCallId = links.sourceToolCallId;
295
+ if (links.sourceTraceId) out.traceId = links.sourceTraceId;
296
+ if (links.sourceSpanId) out.spanId = links.sourceSpanId;
297
+ return out;
298
+ };
299
+
300
+ const clampLimit = (limit: number | undefined): number =>
301
+ Math.min(
302
+ 200,
303
+ Math.max(1, Number.isFinite(limit ?? NaN) ? (limit ?? 50) : 50),
304
+ );
305
+
306
+ const nonNegativeInteger = (value: number | undefined): number => {
307
+ if (value === undefined) return 0;
308
+ if (!Number.isFinite(value) || value < 0) {
309
+ throw new Error("summary counters must be non-negative numbers");
310
+ }
311
+ return Math.floor(value);
312
+ };
313
+
314
+ const optionalScore = (value: number | null | undefined): number | null => {
315
+ if (value === undefined || value === null) return null;
316
+ if (!Number.isFinite(value)) throw new Error("averageScore must be a number");
317
+ return value;
318
+ };
319
+
320
+ const evalCaseEvidenceReferences = (evalCase: EvalCase): EvidenceReference[] =>
321
+ sourceLinkEvidenceReferences(
322
+ {
323
+ sourceLabel: `Eval case "${evalCase.name}"`,
324
+ sourceId: evalCase.id,
325
+ sourceKind: "eval_case",
326
+ sourceRoute: `#/evaluations?case=${encodeURIComponent(evalCase.id)}`,
327
+ sourceName: evalCase.name,
328
+ },
329
+ evalCase,
330
+ );
331
+
332
+ export const rowToEvalCase = (row: EvalCaseRow): EvalCase => {
333
+ const evalCase: EvalCase = {
334
+ id: row.id,
335
+ projectId: row.project_id,
336
+ sourceEntityType: row.source_entity_type as EvalCaseSourceType,
337
+ sourceEntityId: row.source_entity_id,
338
+ sourceAgentRunId: row.source_agent_run_id ?? null,
339
+ sourceActionId: row.source_action_id ?? null,
340
+ sourceAiCallId: row.source_ai_call_id ?? null,
341
+ sourceToolCallId: row.source_tool_call_id ?? null,
342
+ sourceTraceId: row.source_trace_id ?? null,
343
+ sourceSpanId: row.source_span_id ?? null,
344
+ name: row.name,
345
+ expectedOutcome: row.expected_outcome ?? null,
346
+ rubric: parseJsonField(row.rubric_json),
347
+ redactedPrompt: parseJsonField(row.redacted_prompt_json),
348
+ referencePayload: parseJsonField(row.reference_payload_json),
349
+ metadata: parseJsonRecordField(row.metadata_json),
350
+ createdAt: row.created_at,
351
+ updatedAt: row.updated_at,
352
+ };
353
+ evalCase.evidenceReferences = evalCaseEvidenceReferences(evalCase);
354
+ evalCase.evidenceContract = EVIDENCE_REFERENCE_CONTRACT;
355
+ return evalCase;
356
+ };
357
+
358
+ const evalRunEvidenceReferences = (run: EvalRun): EvidenceReference[] => {
359
+ if (!run.sourceEvalCase) return [];
360
+ return sourceLinkEvidenceReferences(
361
+ {
362
+ sourceLabel: `Eval run "${run.id}"`,
363
+ sourceId: run.id,
364
+ sourceKind: "eval_run",
365
+ sourceRoute: `#/evaluations?run=${encodeURIComponent(run.id)}`,
366
+ sourceName: run.sourceEvalCase.name,
367
+ },
368
+ run.sourceEvalCase,
369
+ );
370
+ };
371
+
372
+ const rowToEvalRun = (row: EvalRunRow): EvalRun => {
373
+ const sourceEvalCase: EvalRunSourceCase | null =
374
+ row.case_id && row.case_name
375
+ ? {
376
+ id: row.case_id,
377
+ name: row.case_name,
378
+ sourceEntityType: row.case_source_entity_type ?? "",
379
+ sourceEntityId: row.case_source_entity_id ?? "",
380
+ sourceAgentRunId: row.case_source_agent_run_id ?? null,
381
+ sourceActionId: row.case_source_action_id ?? null,
382
+ sourceAiCallId: row.case_source_ai_call_id ?? null,
383
+ sourceToolCallId: row.case_source_tool_call_id ?? null,
384
+ sourceTraceId: row.case_source_trace_id ?? null,
385
+ sourceSpanId: row.case_source_span_id ?? null,
386
+ }
387
+ : null;
388
+ if (sourceEvalCase) {
389
+ sourceEvalCase.evidenceReferences = sourceLinkEvidenceReferences(
390
+ {
391
+ sourceLabel: `Eval case "${sourceEvalCase.name}"`,
392
+ sourceId: sourceEvalCase.id,
393
+ sourceKind: "eval_case",
394
+ sourceRoute: `#/evaluations?case=${encodeURIComponent(sourceEvalCase.id)}`,
395
+ sourceName: sourceEvalCase.name,
396
+ },
397
+ sourceEvalCase,
398
+ );
399
+ sourceEvalCase.evidenceContract = EVIDENCE_REFERENCE_CONTRACT;
400
+ }
401
+
402
+ const run: EvalRun = {
403
+ id: row.id,
404
+ projectId: row.project_id,
405
+ evalCaseId: row.eval_case_id ?? null,
406
+ status: isEvalRunStatus(row.status) ? row.status : "pending",
407
+ candidate: {
408
+ agentId: row.candidate_agent_id ?? null,
409
+ agentVersion: row.candidate_agent_version ?? null,
410
+ promptId: row.candidate_prompt_id ?? null,
411
+ promptVersion: row.candidate_prompt_version ?? null,
412
+ modelProvider: row.candidate_model_provider ?? null,
413
+ model: row.candidate_model ?? null,
414
+ modelVersion: row.candidate_model_version ?? null,
415
+ },
416
+ startedAt: row.started_at ?? null,
417
+ endedAt: row.ended_at ?? null,
418
+ totalCount: row.total_count ?? 0,
419
+ passCount: row.pass_count ?? 0,
420
+ failCount: row.fail_count ?? 0,
421
+ averageScore: row.average_score ?? null,
422
+ metadata: parseJsonRecordField(row.metadata_json),
423
+ createdAt: row.created_at,
424
+ sourceEvalCase,
425
+ };
426
+ run.evidenceReferences = evalRunEvidenceReferences(run);
427
+ run.evidenceContract = EVIDENCE_REFERENCE_CONTRACT;
428
+ return run;
429
+ };
430
+
431
+ export class EvalCasesStore {
432
+ constructor(private readonly db: SqlDb) {}
433
+
434
+ async createCase(projectId: string, input: EvalCaseInput): Promise<EvalCase> {
435
+ if (!projectId)
436
+ throw new Error("EvalCasesStore.createCase: projectId is required");
437
+ if (!isEvalCaseSourceType(input.sourceEntityType)) {
438
+ throw new Error("sourceEntityType is invalid");
439
+ }
440
+ const sourceEntityId = input.sourceEntityId.trim();
441
+ if (!sourceEntityId) throw new Error("sourceEntityId is required");
442
+ const name = input.name.trim();
443
+ if (!name) throw new Error("name is required");
444
+
445
+ const hydrated = await this.hydrateSource(
446
+ projectId,
447
+ input.sourceEntityType,
448
+ sourceEntityId,
449
+ input.source ?? {},
450
+ );
451
+ const payload = await this.findPayload(projectId, hydrated.links);
452
+ const autoMetadata = await this.buildMetadata(projectId, hydrated.links);
453
+ const metadata = {
454
+ ...(input.metadata ?? {}),
455
+ ...autoMetadata,
456
+ sourceLinks: compactLinks(hydrated.links),
457
+ };
458
+ const now = new Date().toISOString();
459
+ const evalCase: EvalCase = {
460
+ id: randomHex(16),
461
+ projectId,
462
+ sourceEntityType: input.sourceEntityType,
463
+ sourceEntityId,
464
+ sourceAgentRunId: hydrated.links.sourceAgentRunId ?? null,
465
+ sourceActionId: hydrated.links.sourceActionId ?? null,
466
+ sourceAiCallId: hydrated.links.sourceAiCallId ?? null,
467
+ sourceToolCallId: hydrated.links.sourceToolCallId ?? null,
468
+ sourceTraceId: hydrated.links.sourceTraceId ?? null,
469
+ sourceSpanId: hydrated.links.sourceSpanId ?? null,
470
+ name,
471
+ expectedOutcome: input.expectedOutcome ?? null,
472
+ rubric: input.rubric ?? null,
473
+ redactedPrompt: input.redactedPrompt ?? payload?.input ?? null,
474
+ referencePayload: input.referencePayload ?? payload?.output ?? null,
475
+ metadata,
476
+ createdAt: now,
477
+ updatedAt: now,
478
+ };
479
+ evalCase.evidenceReferences = evalCaseEvidenceReferences(evalCase);
480
+
481
+ await this.db
482
+ .prepare(
483
+ `INSERT INTO eval_cases (
484
+ id, project_id, source_entity_type, source_entity_id,
485
+ source_agent_run_id, source_action_id, source_ai_call_id,
486
+ source_tool_call_id, source_trace_id, source_span_id,
487
+ name, expected_outcome, rubric_json, redacted_prompt_json,
488
+ reference_payload_json, metadata_json, created_at, updated_at
489
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
490
+ )
491
+ .bind(
492
+ evalCase.id,
493
+ evalCase.projectId,
494
+ evalCase.sourceEntityType,
495
+ evalCase.sourceEntityId,
496
+ evalCase.sourceAgentRunId,
497
+ evalCase.sourceActionId,
498
+ evalCase.sourceAiCallId,
499
+ evalCase.sourceToolCallId,
500
+ evalCase.sourceTraceId,
501
+ evalCase.sourceSpanId,
502
+ evalCase.name,
503
+ evalCase.expectedOutcome,
504
+ jsonString(evalCase.rubric),
505
+ jsonString(evalCase.redactedPrompt),
506
+ jsonString(evalCase.referencePayload),
507
+ JSON.stringify(evalCase.metadata),
508
+ evalCase.createdAt,
509
+ evalCase.updatedAt,
510
+ )
511
+ .run();
512
+
513
+ return evalCase;
514
+ }
515
+
516
+ async getCase(projectId: string, id: string): Promise<EvalCase | null> {
517
+ const row = await this.db
518
+ .prepare(
519
+ `SELECT * FROM eval_cases WHERE project_id = ? AND id = ? LIMIT 1`,
520
+ )
521
+ .bind(projectId, id)
522
+ .first<EvalCaseRow>();
523
+ return row ? rowToEvalCase(row) : null;
524
+ }
525
+
526
+ async listCases(options: EvalCaseListOptions): Promise<EvalCase[]> {
527
+ if (!options.projectId)
528
+ throw new Error("EvalCasesStore.listCases: projectId is required");
529
+ const limit = clampLimit(options.limit);
530
+ let sql = `SELECT * FROM eval_cases WHERE project_id = ?`;
531
+ const params: unknown[] = [options.projectId];
532
+ if (options.sourceEntityType) {
533
+ sql += ` AND source_entity_type = ?`;
534
+ params.push(options.sourceEntityType);
535
+ }
536
+ if (options.sourceEntityId) {
537
+ sql += ` AND source_entity_id = ?`;
538
+ params.push(options.sourceEntityId);
539
+ }
540
+ sql += ` ORDER BY created_at DESC LIMIT ?`;
541
+ params.push(limit);
542
+
543
+ const rows = await this.db
544
+ .prepare(sql)
545
+ .bind(...params)
546
+ .all<EvalCaseRow>();
547
+ return rows.results.map(rowToEvalCase);
548
+ }
549
+
550
+ async createRun(projectId: string, input: EvalRunInput): Promise<EvalRun> {
551
+ if (!projectId)
552
+ throw new Error("EvalCasesStore.createRun: projectId is required");
553
+ const id = input.id?.trim() || `run_${randomHex(8)}`;
554
+ const evalCaseId = input.evalCaseId?.trim() || null;
555
+ const status = input.status ?? "running";
556
+ if (!isEvalRunStatus(status)) throw new Error("status is invalid");
557
+ if (evalCaseId) {
558
+ const evalCase = await this.getCase(projectId, evalCaseId);
559
+ if (!evalCase) throw new Error("evalCaseId does not reference a case");
560
+ }
561
+
562
+ const candidate = input.candidate ?? {};
563
+ const totalCount = nonNegativeInteger(input.totalCount);
564
+ const passCount = nonNegativeInteger(input.passCount);
565
+ const failCount = nonNegativeInteger(input.failCount);
566
+ const averageScore = optionalScore(input.averageScore);
567
+ const now = new Date().toISOString();
568
+ const startedAt =
569
+ input.startedAt === undefined
570
+ ? status === "pending"
571
+ ? null
572
+ : now
573
+ : input.startedAt;
574
+ const endedAt = input.endedAt ?? null;
575
+
576
+ await this.db
577
+ .prepare(
578
+ `INSERT INTO eval_runs (
579
+ id, project_id, eval_case_id,
580
+ candidate_agent_id, candidate_agent_version,
581
+ candidate_prompt_id, candidate_prompt_version,
582
+ candidate_model_provider, candidate_model, candidate_model_version,
583
+ status, started_at, ended_at,
584
+ total_count, pass_count, fail_count, average_score,
585
+ metadata_json, created_at
586
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
587
+ )
588
+ .bind(
589
+ id,
590
+ projectId,
591
+ evalCaseId,
592
+ candidate.agentId ?? null,
593
+ candidate.agentVersion ?? null,
594
+ candidate.promptId ?? null,
595
+ candidate.promptVersion ?? null,
596
+ candidate.modelProvider ?? null,
597
+ candidate.model ?? null,
598
+ candidate.modelVersion ?? null,
599
+ status,
600
+ startedAt,
601
+ endedAt,
602
+ totalCount,
603
+ passCount,
604
+ failCount,
605
+ averageScore,
606
+ JSON.stringify(input.metadata ?? {}),
607
+ now,
608
+ )
609
+ .run();
610
+
611
+ const run = await this.getRun(projectId, id);
612
+ if (!run) throw new Error("EvalCasesStore.createRun: insert failed");
613
+ return run;
614
+ }
615
+
616
+ async getRun(projectId: string, id: string): Promise<EvalRun | null> {
617
+ const row = await this.db
618
+ .prepare(
619
+ `${this.evalRunSelectSql()}
620
+ WHERE r.project_id = ? AND r.id = ?
621
+ LIMIT 1`,
622
+ )
623
+ .bind(projectId, id)
624
+ .first<EvalRunRow>();
625
+ return row ? rowToEvalRun(row) : null;
626
+ }
627
+
628
+ async listRuns(options: EvalRunsListOptions): Promise<EvalRun[]> {
629
+ if (!options.projectId)
630
+ throw new Error("EvalCasesStore.listRuns: projectId is required");
631
+ if (options.status && !isEvalRunStatus(options.status)) {
632
+ throw new Error("status is invalid");
633
+ }
634
+ const limit = clampLimit(options.limit);
635
+ let sql = `${this.evalRunSelectSql()} WHERE r.project_id = ?`;
636
+ const params: unknown[] = [options.projectId];
637
+ if (options.evalCaseId) {
638
+ sql += ` AND r.eval_case_id = ?`;
639
+ params.push(options.evalCaseId);
640
+ }
641
+ if (options.status) {
642
+ sql += ` AND r.status = ?`;
643
+ params.push(options.status);
644
+ }
645
+ sql += ` ORDER BY r.created_at DESC LIMIT ?`;
646
+ params.push(limit);
647
+
648
+ const rows = await this.db
649
+ .prepare(sql)
650
+ .bind(...params)
651
+ .all<EvalRunRow>();
652
+ return rows.results.map(rowToEvalRun);
653
+ }
654
+
655
+ async refreshRunSummary(projectId: string, id: string): Promise<void> {
656
+ const found = await this.db
657
+ .prepare(
658
+ `SELECT id FROM eval_runs WHERE project_id = ? AND id = ? LIMIT 1`,
659
+ )
660
+ .bind(projectId, id)
661
+ .first<{ id: string }>();
662
+ if (!found) return;
663
+
664
+ await this.db
665
+ .prepare(
666
+ `UPDATE eval_runs
667
+ SET total_count = (
668
+ SELECT COUNT(*) FROM eval_case_results
669
+ WHERE project_id = ? AND run_id = ?
670
+ ),
671
+ pass_count = (
672
+ SELECT COUNT(*) FROM eval_case_results
673
+ WHERE project_id = ? AND run_id = ? AND passed = 1
674
+ ),
675
+ fail_count = (
676
+ SELECT COUNT(*) FROM eval_case_results
677
+ WHERE project_id = ? AND run_id = ? AND passed = 0
678
+ ),
679
+ average_score = (
680
+ SELECT AVG(score) FROM eval_case_results
681
+ WHERE project_id = ? AND run_id = ? AND score IS NOT NULL
682
+ )
683
+ WHERE project_id = ? AND id = ?`,
684
+ )
685
+ .bind(
686
+ projectId,
687
+ id,
688
+ projectId,
689
+ id,
690
+ projectId,
691
+ id,
692
+ projectId,
693
+ id,
694
+ projectId,
695
+ id,
696
+ )
697
+ .run();
698
+ }
699
+
700
+ private evalRunSelectSql(): string {
701
+ return `SELECT
702
+ r.*,
703
+ c.id AS case_id,
704
+ c.name AS case_name,
705
+ c.source_entity_type AS case_source_entity_type,
706
+ c.source_entity_id AS case_source_entity_id,
707
+ c.source_agent_run_id AS case_source_agent_run_id,
708
+ c.source_action_id AS case_source_action_id,
709
+ c.source_ai_call_id AS case_source_ai_call_id,
710
+ c.source_tool_call_id AS case_source_tool_call_id,
711
+ c.source_trace_id AS case_source_trace_id,
712
+ c.source_span_id AS case_source_span_id
713
+ FROM eval_runs r
714
+ LEFT JOIN eval_cases c
715
+ ON c.project_id = r.project_id AND c.id = r.eval_case_id`;
716
+ }
717
+
718
+ private async hydrateSource(
719
+ projectId: string,
720
+ sourceEntityType: EvalCaseSourceType,
721
+ sourceEntityId: string,
722
+ explicit: EvalCaseSourceLinks,
723
+ ): Promise<{ links: EvalCaseSourceLinks }> {
724
+ const links: EvalCaseSourceLinks = { ...explicit };
725
+
726
+ switch (sourceEntityType) {
727
+ case "action": {
728
+ const action = await this.getActionSource(projectId, sourceEntityId);
729
+ if (!action)
730
+ throw new EvalCaseSourceNotFoundError(
731
+ projectId,
732
+ sourceEntityType,
733
+ sourceEntityId,
734
+ );
735
+ links.sourceActionId = action.id;
736
+ links.sourceAgentRunId ??= action.agent_run_id;
737
+ links.sourceToolCallId ??= action.tool_call_id;
738
+ links.sourceTraceId ??= action.trace_id;
739
+ links.sourceSpanId ??= action.span_id;
740
+ break;
741
+ }
742
+ case "agent_run": {
743
+ const found = await this.db
744
+ .prepare(
745
+ `SELECT id FROM agent_runs WHERE project_id = ? AND id = ? LIMIT 1`,
746
+ )
747
+ .bind(projectId, sourceEntityId)
748
+ .first<{ id: string }>();
749
+ if (!found)
750
+ throw new EvalCaseSourceNotFoundError(
751
+ projectId,
752
+ sourceEntityType,
753
+ sourceEntityId,
754
+ );
755
+ links.sourceAgentRunId = sourceEntityId;
756
+ const action = await this.db
757
+ .prepare(
758
+ `SELECT id, agent_run_id, tool_call_id, trace_id, span_id
759
+ FROM actions
760
+ WHERE project_id = ? AND (id = ? OR agent_run_id = ?)
761
+ ORDER BY started_at ASC LIMIT 1`,
762
+ )
763
+ .bind(projectId, sourceEntityId, sourceEntityId)
764
+ .first<ActionSourceRow>();
765
+ if (action) {
766
+ links.sourceActionId ??= action.id;
767
+ links.sourceTraceId ??= action.trace_id;
768
+ links.sourceSpanId ??= action.span_id;
769
+ }
770
+ break;
771
+ }
772
+ case "ai_call": {
773
+ const call = explicit.sourceTraceId
774
+ ? await this.db
775
+ .prepare(
776
+ `SELECT call_id, trace_id, span_id, request_json, response_json
777
+ FROM ai_calls
778
+ WHERE project_id = ?
779
+ AND trace_id = ?
780
+ AND (call_id = ? OR span_id = ?)
781
+ LIMIT 1`,
782
+ )
783
+ .bind(
784
+ projectId,
785
+ explicit.sourceTraceId,
786
+ sourceEntityId,
787
+ sourceEntityId,
788
+ )
789
+ .first<AiCallSourceRow>()
790
+ : await this.db
791
+ .prepare(
792
+ `SELECT call_id, trace_id, span_id, request_json, response_json
793
+ FROM ai_calls
794
+ WHERE project_id = ? AND (call_id = ? OR span_id = ?)
795
+ LIMIT 1`,
796
+ )
797
+ .bind(projectId, sourceEntityId, sourceEntityId)
798
+ .first<AiCallSourceRow>();
799
+ if (!call)
800
+ throw new EvalCaseSourceNotFoundError(
801
+ projectId,
802
+ sourceEntityType,
803
+ sourceEntityId,
804
+ );
805
+ links.sourceAiCallId = call.call_id;
806
+ links.sourceTraceId ??= call.trace_id;
807
+ links.sourceSpanId ??= call.span_id;
808
+ break;
809
+ }
810
+ case "tool_call": {
811
+ const tool = await this.getToolCallSource(projectId, sourceEntityId);
812
+ if (!tool)
813
+ throw new EvalCaseSourceNotFoundError(
814
+ projectId,
815
+ sourceEntityType,
816
+ sourceEntityId,
817
+ );
818
+ links.sourceToolCallId = tool.id;
819
+ links.sourceActionId ??= tool.action_id;
820
+ if (tool.action_id) {
821
+ const action = await this.getActionSource(projectId, tool.action_id);
822
+ links.sourceAgentRunId ??= action?.agent_run_id;
823
+ links.sourceTraceId ??= action?.trace_id;
824
+ links.sourceSpanId ??= action?.span_id;
825
+ }
826
+ break;
827
+ }
828
+ case "trace": {
829
+ const span = await this.db
830
+ .prepare(
831
+ `SELECT trace_id, span_id
832
+ FROM telemetry_spans
833
+ WHERE project_id = ? AND trace_id = ?
834
+ ORDER BY start_time ASC LIMIT 1`,
835
+ )
836
+ .bind(projectId, sourceEntityId)
837
+ .first<SpanSourceRow>();
838
+ if (!span)
839
+ throw new EvalCaseSourceNotFoundError(
840
+ projectId,
841
+ sourceEntityType,
842
+ sourceEntityId,
843
+ );
844
+ links.sourceTraceId = span.trace_id;
845
+ links.sourceSpanId ??= span.span_id;
846
+ break;
847
+ }
848
+ }
849
+
850
+ return { links };
851
+ }
852
+
853
+ private async getActionSource(
854
+ projectId: string,
855
+ actionId: string,
856
+ ): Promise<ActionSourceRow | null> {
857
+ return this.db
858
+ .prepare(
859
+ `SELECT id, agent_run_id, tool_call_id, trace_id, span_id
860
+ FROM actions
861
+ WHERE project_id = ? AND id = ? LIMIT 1`,
862
+ )
863
+ .bind(projectId, actionId)
864
+ .first<ActionSourceRow>();
865
+ }
866
+
867
+ private async getToolCallSource(
868
+ projectId: string,
869
+ toolCallId: string,
870
+ ): Promise<ToolCallSourceRow | null> {
871
+ return this.db
872
+ .prepare(
873
+ `SELECT id, action_id, tool_name, args_hash, result_hash,
874
+ mcp_audit_json, mutation_before_json, mutation_after_json,
875
+ mutation_diff_json, mutation_artifact_id
876
+ FROM tool_calls
877
+ WHERE project_id = ? AND id = ? LIMIT 1`,
878
+ )
879
+ .bind(projectId, toolCallId)
880
+ .first<ToolCallSourceRow>();
881
+ }
882
+
883
+ private async findPayload(
884
+ projectId: string,
885
+ links: EvalCaseSourceLinks,
886
+ ): Promise<{ input: JsonValue | null; output: JsonValue | null } | null> {
887
+ let row: PayloadRow | null = null;
888
+ if (links.sourceTraceId && links.sourceSpanId) {
889
+ row = await this.db
890
+ .prepare(
891
+ `SELECT input_json, output_json
892
+ FROM ai_span_payloads
893
+ WHERE project_id = ? AND trace_id = ? AND span_id = ?
894
+ LIMIT 1`,
895
+ )
896
+ .bind(projectId, links.sourceTraceId, links.sourceSpanId)
897
+ .first<PayloadRow>();
898
+ }
899
+ if (!row && links.sourceActionId) {
900
+ row = await this.db
901
+ .prepare(
902
+ `SELECT input_json, output_json
903
+ FROM ai_span_payloads
904
+ WHERE project_id = ? AND action_id = ?
905
+ ORDER BY received_at DESC LIMIT 1`,
906
+ )
907
+ .bind(projectId, links.sourceActionId)
908
+ .first<PayloadRow>();
909
+ }
910
+ if (!row && links.sourceTraceId) {
911
+ row = await this.db
912
+ .prepare(
913
+ `SELECT input_json, output_json
914
+ FROM ai_span_payloads
915
+ WHERE project_id = ? AND trace_id = ?
916
+ ORDER BY received_at DESC LIMIT 1`,
917
+ )
918
+ .bind(projectId, links.sourceTraceId)
919
+ .first<PayloadRow>();
920
+ }
921
+ if (!row) return null;
922
+ return {
923
+ input: parseJsonField(row.input_json),
924
+ output: parseJsonField(row.output_json),
925
+ };
926
+ }
927
+
928
+ private async buildMetadata(
929
+ projectId: string,
930
+ links: EvalCaseSourceLinks,
931
+ ): Promise<Record<string, JsonValue>> {
932
+ const linkedSpans: JsonValue[] = [];
933
+ if (links.sourceTraceId && links.sourceSpanId) {
934
+ linkedSpans.push({
935
+ traceId: links.sourceTraceId,
936
+ spanId: links.sourceSpanId,
937
+ });
938
+ }
939
+
940
+ const toolHashes: JsonValue[] = [];
941
+ if (links.sourceToolCallId) {
942
+ const tool = await this.getToolCallSource(
943
+ projectId,
944
+ links.sourceToolCallId,
945
+ );
946
+ if (tool) toolHashes.push(toolHashMetadata(tool));
947
+ } else if (links.sourceActionId) {
948
+ const rows = await this.db
949
+ .prepare(
950
+ `SELECT id, action_id, tool_name, args_hash, result_hash,
951
+ mcp_audit_json, mutation_before_json, mutation_after_json,
952
+ mutation_diff_json, mutation_artifact_id
953
+ FROM tool_calls
954
+ WHERE project_id = ? AND action_id = ?
955
+ LIMIT 25`,
956
+ )
957
+ .bind(projectId, links.sourceActionId)
958
+ .all<ToolCallSourceRow>();
959
+ for (const row of rows.results) {
960
+ toolHashes.push(toolHashMetadata(row));
961
+ }
962
+ }
963
+
964
+ const documentRefs: JsonValue[] = [];
965
+ if (links.sourceActionId) {
966
+ const rows = await this.db
967
+ .prepare(
968
+ `SELECT documents_json
969
+ FROM retrieval_events
970
+ WHERE project_id = ? AND action_id = ?
971
+ LIMIT 25`,
972
+ )
973
+ .bind(projectId, links.sourceActionId)
974
+ .all<RetrievalEventRow>();
975
+ for (const row of rows.results) {
976
+ for (const doc of parseJsonArray(
977
+ typeof row.documents_json === "string"
978
+ ? row.documents_json
979
+ : JSON.stringify(row.documents_json ?? []),
980
+ )) {
981
+ documentRefs.push(doc as JsonValue);
982
+ }
983
+ }
984
+ }
985
+
986
+ const metadata: Record<string, JsonValue> = {};
987
+ if (linkedSpans.length > 0) metadata.linkedSpans = linkedSpans;
988
+ if (toolHashes.length > 0) metadata.toolHashes = toolHashes;
989
+ if (documentRefs.length > 0) metadata.documentRefs = documentRefs;
990
+ return metadata;
991
+ }
992
+ }
993
+
994
+ const toolHashMetadata = (tool: ToolCallSourceRow): JsonValue => {
995
+ const metadata: Record<string, JsonValue> = {
996
+ toolCallId: tool.id,
997
+ toolName: tool.tool_name,
998
+ argsHash: tool.args_hash,
999
+ resultHash: tool.result_hash,
1000
+ };
1001
+
1002
+ const mcpAudit = parseEvidenceJson(tool.mcp_audit_json);
1003
+ if (mcpAudit !== null) metadata.mcpAudit = mcpAudit;
1004
+
1005
+ const mutationEvidence: Record<string, JsonValue> = {};
1006
+ const before = parseEvidenceJson(tool.mutation_before_json);
1007
+ const after = parseEvidenceJson(tool.mutation_after_json);
1008
+ const diff = parseEvidenceJson(tool.mutation_diff_json);
1009
+ if (before !== null) mutationEvidence.before = before;
1010
+ if (after !== null) mutationEvidence.after = after;
1011
+ if (diff !== null) mutationEvidence.diff = diff;
1012
+ if (tool.mutation_artifact_id) {
1013
+ mutationEvidence.artifactId = tool.mutation_artifact_id;
1014
+ }
1015
+ if (Object.keys(mutationEvidence).length > 0) {
1016
+ metadata.mutationEvidence = mutationEvidence;
1017
+ }
1018
+
1019
+ return metadata;
1020
+ };
1021
+
1022
+ const parseEvidenceJson = (value: unknown): JsonValue | null => {
1023
+ if (value === null || value === undefined) return null;
1024
+ if (typeof value === "string") return parseJsonValue(value);
1025
+ if (
1026
+ typeof value === "object" ||
1027
+ typeof value === "number" ||
1028
+ typeof value === "boolean"
1029
+ ) {
1030
+ return value as JsonValue;
1031
+ }
1032
+ return null;
1033
+ };