@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,1031 @@
1
+ import type { SqlDb } from "./sql-db";
2
+ import { dialectFor } from "./sql-db";
3
+
4
+ const TOP_CAUSERS_LIMIT = 5;
5
+ const EXEMPLAR_LIMIT = 3;
6
+
7
+ export interface ActionAggregateExemplar {
8
+ actionId: string;
9
+ agentRunId: string | null;
10
+ traceId: string | null;
11
+ toolCallId: string | null;
12
+ evalId: string | null;
13
+ label: string | null;
14
+ status: string | null;
15
+ occurredAt: string | null;
16
+ }
17
+
18
+ export interface ToolReliabilityCauser {
19
+ id: string;
20
+ label: string | null;
21
+ count: number;
22
+ }
23
+
24
+ export interface ToolReliabilityAggregate {
25
+ toolName: string;
26
+ callCount: number;
27
+ p50LatencyMs: number | null;
28
+ p95LatencyMs: number | null;
29
+ errorCount: number;
30
+ errorRate: number;
31
+ timeoutCount: number;
32
+ timeoutRate: number;
33
+ retryCount: number;
34
+ malformedArgumentCount: number;
35
+ sideEffectCount: number;
36
+ topCausingAgents: ToolReliabilityCauser[];
37
+ topCausingWorkflows: ToolReliabilityCauser[];
38
+ exemplars: ActionAggregateExemplar[];
39
+ }
40
+
41
+ export interface ToolReliabilityResult {
42
+ projectId: string;
43
+ windowHours: number;
44
+ tools: ToolReliabilityAggregate[];
45
+ generatedAt: string;
46
+ }
47
+
48
+ export interface CostAttributionRow {
49
+ dimension: string;
50
+ key: string | null;
51
+ label: string | null;
52
+ totalCostUsd: number;
53
+ actionCount: number;
54
+ agentRunCount: number;
55
+ toolCallCount: number;
56
+ exemplars: ActionAggregateExemplar[];
57
+ }
58
+
59
+ export interface CostAttributionResult {
60
+ projectId: string;
61
+ windowHours: number;
62
+ byAgent: CostAttributionRow[];
63
+ byRun: CostAttributionRow[];
64
+ byModel: CostAttributionRow[];
65
+ byProvider: CostAttributionRow[];
66
+ byPromptVersion: CostAttributionRow[];
67
+ byTool: CostAttributionRow[];
68
+ byUser: CostAttributionRow[];
69
+ byTenant: CostAttributionRow[];
70
+ byWorkflow: CostAttributionRow[];
71
+ generatedAt: string;
72
+ }
73
+
74
+ export interface AutonomousReviewRow {
75
+ id: string;
76
+ toolName: string;
77
+ actionId: string;
78
+ actionName: string;
79
+ agentRunId: string;
80
+ agentName: string;
81
+ agentVersion: string;
82
+ autonomyLevel: string;
83
+ sideEffect: boolean;
84
+ approvalState: string;
85
+ status: "ok" | "error";
86
+ errorSnippet: string | null;
87
+ traceId: string;
88
+ occurredAt: string;
89
+ mutationEvidence: boolean;
90
+ mutationArtifactId: string | null;
91
+ }
92
+
93
+ export interface AutonomousReviewResult {
94
+ projectId: string;
95
+ windowHours: number;
96
+ rows: AutonomousReviewRow[];
97
+ timestamp: string;
98
+ }
99
+
100
+ export interface VersionDiffMetric {
101
+ label: string;
102
+ baselineValue: string | number;
103
+ targetValue: string | number;
104
+ deltaValue: string | number | null;
105
+ deltaDirection: "positive" | "negative" | "neutral";
106
+ }
107
+
108
+ export interface VersionComparisonResult {
109
+ projectId: string;
110
+ baselineVersion: string;
111
+ targetVersion: string;
112
+ metrics: VersionDiffMetric[];
113
+ baselineExemplars: ActionAggregateExemplar[];
114
+ targetExemplars: ActionAggregateExemplar[];
115
+ timestamp: string;
116
+ }
117
+
118
+ interface ToolAggregateRow {
119
+ tool_name: string;
120
+ call_count: number;
121
+ error_count: number | null;
122
+ timeout_count: number | null;
123
+ malformed_argument_count: number | null;
124
+ side_effect_count: number | null;
125
+ }
126
+
127
+ interface ToolDetailRow {
128
+ tool_name: string;
129
+ duration_ms: number | null;
130
+ agent_id: string | null;
131
+ agent_label: string | null;
132
+ workflow_id: string | null;
133
+ workflow_label: string | null;
134
+ action_id: string | null;
135
+ agent_run_id: string | null;
136
+ trace_id: string | null;
137
+ tool_call_id: string | null;
138
+ eval_id: string | null;
139
+ action_label: string | null;
140
+ status: string | null;
141
+ occurred_at: string | null;
142
+ }
143
+
144
+ interface CostRow {
145
+ key: string | null;
146
+ label: string | null;
147
+ total_cost_usd: number | null;
148
+ action_count: number | null;
149
+ agent_run_count: number | null;
150
+ tool_call_count: number | null;
151
+ }
152
+
153
+ interface ExemplarSqlRow {
154
+ dimension_key?: string | null;
155
+ action_id: string | null;
156
+ agent_run_id: string | null;
157
+ trace_id: string | null;
158
+ tool_call_id: string | null;
159
+ eval_id: string | null;
160
+ label: string | null;
161
+ status: string | null;
162
+ occurred_at: string | null;
163
+ }
164
+
165
+ interface AutonomousReviewSqlRow {
166
+ id: string;
167
+ tool_name: string;
168
+ action_id: string;
169
+ action_name: string | null;
170
+ agent_run_id: string | null;
171
+ agent_name: string | null;
172
+ agent_version: string | null;
173
+ autonomy_level: string | null;
174
+ side_effect: number | null;
175
+ approval_state: string | null;
176
+ status: string | null;
177
+ error_snippet: string | null;
178
+ trace_id: string | null;
179
+ occurred_at: string | null;
180
+ mutation_evidence: number | null;
181
+ mutation_artifact_id: string | null;
182
+ }
183
+
184
+ interface VersionStatsRow {
185
+ version: string;
186
+ run_count: number | null;
187
+ success_count: number | null;
188
+ avg_duration_ms: number | null;
189
+ avg_cost_usd: number | null;
190
+ eval_count: number | null;
191
+ passed_eval_count: number | null;
192
+ avg_eval_score: number | null;
193
+ tool_count: number | null;
194
+ tool_error_count: number | null;
195
+ }
196
+
197
+ const toNumber = (value: unknown): number => {
198
+ const n = typeof value === "number" ? value : Number(value);
199
+ return Number.isFinite(n) ? n : 0;
200
+ };
201
+
202
+ const percentile = (values: number[], p: number): number | null => {
203
+ const sorted = values
204
+ .filter((v) => Number.isFinite(v) && v >= 0)
205
+ .sort((a, b) => a - b);
206
+ if (sorted.length === 0) return null;
207
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
208
+ return sorted[Math.min(Math.max(index, 0), sorted.length - 1)] ?? null;
209
+ };
210
+
211
+ const topCausers = (
212
+ rows: ToolDetailRow[],
213
+ idKey: "agent_id" | "workflow_id",
214
+ labelKey: "agent_label" | "workflow_label",
215
+ ): ToolReliabilityCauser[] => {
216
+ const counts = new Map<string, { label: string | null; count: number }>();
217
+ for (const row of rows) {
218
+ const id = row[idKey];
219
+ if (!id) continue;
220
+ const current = counts.get(id) ?? { label: row[labelKey], count: 0 };
221
+ current.count += 1;
222
+ if (!current.label && row[labelKey]) current.label = row[labelKey];
223
+ counts.set(id, current);
224
+ }
225
+ return [...counts.entries()]
226
+ .map(([id, value]) => ({ id, label: value.label, count: value.count }))
227
+ .sort((a, b) => b.count - a.count || a.id.localeCompare(b.id))
228
+ .slice(0, TOP_CAUSERS_LIMIT);
229
+ };
230
+
231
+ const exemplarFromRow = (row: ExemplarSqlRow): ActionAggregateExemplar => ({
232
+ actionId: row.action_id ?? "",
233
+ agentRunId: row.agent_run_id ?? null,
234
+ traceId: row.trace_id ?? null,
235
+ toolCallId: row.tool_call_id ?? null,
236
+ evalId: row.eval_id ?? null,
237
+ label: row.label ?? null,
238
+ status: row.status ?? null,
239
+ occurredAt: row.occurred_at ?? null,
240
+ });
241
+
242
+ const exemplarsFromRows = (rows: ExemplarSqlRow[]): ActionAggregateExemplar[] =>
243
+ rows
244
+ .filter((row) => row.action_id)
245
+ .map(exemplarFromRow)
246
+ .slice(0, EXEMPLAR_LIMIT);
247
+
248
+ const appendExemplar = (
249
+ map: Map<string, ActionAggregateExemplar[]>,
250
+ key: string | null,
251
+ row: ExemplarSqlRow,
252
+ ) => {
253
+ if (key === null || key === undefined || !row.action_id) return;
254
+ const current = map.get(key) ?? [];
255
+ if (current.length >= EXEMPLAR_LIMIT) return;
256
+ current.push(exemplarFromRow(row));
257
+ map.set(key, current);
258
+ };
259
+
260
+ const costRows = (
261
+ dimension: string,
262
+ rows: CostRow[],
263
+ exemplars = new Map<string, ActionAggregateExemplar[]>(),
264
+ ): CostAttributionRow[] =>
265
+ rows.map((row) => ({
266
+ dimension,
267
+ key: row.key,
268
+ label: row.label,
269
+ totalCostUsd: toNumber(row.total_cost_usd),
270
+ actionCount: toNumber(row.action_count),
271
+ agentRunCount: toNumber(row.agent_run_count),
272
+ toolCallCount: toNumber(row.tool_call_count),
273
+ exemplars: row.key ? (exemplars.get(row.key) ?? []) : [],
274
+ }));
275
+
276
+ const inClause = (values: string[]): string => values.map(() => "?").join(", ");
277
+
278
+ const actionTenantExpr = (db: SqlDb, alias = "a"): string => {
279
+ const dialect = dialectFor(db);
280
+ return `COALESCE(
281
+ ${dialect.jsonText(`${alias}.attrs_json`, '$."obs.tenant.id"')},
282
+ ${dialect.jsonText(`${alias}.attrs_json`, '$."tenant.id"')},
283
+ ${dialect.jsonText(`${alias}.attrs_json`, "$.tenant_id")},
284
+ ${dialect.jsonText(`${alias}.attrs_json`, "$.target_tenant")}
285
+ )`;
286
+ };
287
+
288
+ const actionWorkflowExpr = (db: SqlDb, alias = "a"): string => {
289
+ const dialect = dialectFor(db);
290
+ return `COALESCE(
291
+ CASE WHEN ${alias}.actor_type = 'workflow' THEN ${alias}.actor_id ELSE NULL END,
292
+ ${dialect.jsonText(`${alias}.attrs_json`, '$."obs.workflow.id"')},
293
+ ${dialect.jsonText(`${alias}.attrs_json`, '$."workflow.id"')},
294
+ ${dialect.jsonText(`${alias}.attrs_json`, "$.workflow_id")},
295
+ ${dialect.jsonText(`${alias}.attrs_json`, "$.task_id")}
296
+ )`;
297
+ };
298
+
299
+ export const getToolReliabilityAggregates = async (
300
+ db: SqlDb,
301
+ projectId: string,
302
+ windowHours: number,
303
+ limit: number,
304
+ ): Promise<ToolReliabilityResult> => {
305
+ const dialect = dialectFor(db);
306
+ const since = dialect.sinceHours("?");
307
+
308
+ const aggregateRows = await db
309
+ .prepare(`
310
+ SELECT
311
+ t.tool_name,
312
+ COUNT(*) AS call_count,
313
+ SUM(CASE WHEN t.error_type IS NOT NULL AND t.error_type <> '' THEN 1 ELSE 0 END) AS error_count,
314
+ SUM(CASE
315
+ WHEN LOWER(COALESCE(t.error_type, '')) LIKE '%timeout%'
316
+ OR LOWER(COALESCE(t.error_type, '')) = 'deadline_exceeded'
317
+ THEN 1 ELSE 0
318
+ END) AS timeout_count,
319
+ SUM(CASE
320
+ WHEN LOWER(COALESCE(t.error_type, '')) LIKE '%malformed%'
321
+ OR LOWER(COALESCE(t.error_type, '')) LIKE '%invalid_argument%'
322
+ OR LOWER(COALESCE(t.error_type, '')) LIKE '%invalid_arguments%'
323
+ OR LOWER(COALESCE(t.error_type, '')) LIKE '%schema_validation%'
324
+ OR LOWER(COALESCE(t.error_type, '')) = 'bad_request'
325
+ THEN 1 ELSE 0
326
+ END) AS malformed_argument_count,
327
+ SUM(CASE WHEN t.side_effect <> 0 THEN 1 ELSE 0 END) AS side_effect_count
328
+ FROM tool_calls t
329
+ LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
330
+ WHERE t.project_id = ? AND (a.started_at IS NULL OR a.started_at >= ${since})
331
+ GROUP BY t.tool_name
332
+ ORDER BY call_count DESC, t.tool_name ASC
333
+ LIMIT ?
334
+ `)
335
+ .bind(projectId, windowHours, limit)
336
+ .all<ToolAggregateRow>();
337
+
338
+ const toolNames = aggregateRows.results.map((row) => row.tool_name);
339
+ if (toolNames.length === 0) {
340
+ return {
341
+ projectId,
342
+ windowHours,
343
+ tools: [],
344
+ generatedAt: new Date().toISOString(),
345
+ };
346
+ }
347
+
348
+ const workflowIdExpr = `COALESCE(
349
+ ${actionWorkflowExpr(db, "a")},
350
+ ${actionWorkflowExpr(db, "root")}
351
+ )`;
352
+
353
+ const detailRows = await db
354
+ .prepare(`
355
+ SELECT
356
+ t.tool_name,
357
+ a.duration_ms,
358
+ COALESCE(
359
+ ar.agent_id,
360
+ CASE WHEN a.actor_type = 'agent' THEN a.actor_id ELSE NULL END,
361
+ CASE WHEN root.actor_type = 'agent' THEN root.actor_id ELSE NULL END
362
+ ) AS agent_id,
363
+ COALESCE(
364
+ ar.agent_name,
365
+ CASE WHEN a.actor_type = 'agent' THEN a.name ELSE NULL END,
366
+ CASE WHEN root.actor_type = 'agent' THEN root.name ELSE NULL END
367
+ ) AS agent_label,
368
+ ${workflowIdExpr} AS workflow_id,
369
+ ${workflowIdExpr} AS workflow_label,
370
+ a.id AS action_id,
371
+ COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
372
+ COALESCE(a.trace_id, root.trace_id) AS trace_id,
373
+ t.id AS tool_call_id,
374
+ er.id AS eval_id,
375
+ COALESCE(a.name, t.tool_name) AS action_label,
376
+ a.status,
377
+ a.started_at AS occurred_at
378
+ FROM tool_calls t
379
+ LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
380
+ LEFT JOIN actions root ON root.project_id = t.project_id AND root.id = a.root_action_id
381
+ LEFT JOIN agent_runs ar ON ar.project_id = t.project_id AND ar.id = COALESCE(a.agent_run_id, a.root_action_id)
382
+ LEFT JOIN eval_results er ON er.project_id = t.project_id AND er.action_id = a.id
383
+ WHERE t.project_id = ?
384
+ AND (a.started_at IS NULL OR a.started_at >= ${since})
385
+ AND t.tool_name IN (${inClause(toolNames)})
386
+ ORDER BY
387
+ CASE WHEN a.status = 'error' OR t.error_type IS NOT NULL THEN 0 ELSE 1 END,
388
+ a.started_at DESC
389
+ `)
390
+ .bind(projectId, windowHours, ...toolNames)
391
+ .all<ToolDetailRow>();
392
+
393
+ const detailsByTool = new Map<string, ToolDetailRow[]>();
394
+ for (const row of detailRows.results) {
395
+ const rows = detailsByTool.get(row.tool_name) ?? [];
396
+ rows.push(row);
397
+ detailsByTool.set(row.tool_name, rows);
398
+ }
399
+
400
+ return {
401
+ projectId,
402
+ windowHours,
403
+ tools: aggregateRows.results.map((row) => {
404
+ const callCount = toNumber(row.call_count);
405
+ const errorCount = toNumber(row.error_count);
406
+ const timeoutCount = toNumber(row.timeout_count);
407
+ const details = detailsByTool.get(row.tool_name) ?? [];
408
+ const durations = details
409
+ .map((detail) => detail.duration_ms)
410
+ .filter((duration): duration is number => duration !== null);
411
+ return {
412
+ toolName: row.tool_name,
413
+ callCount,
414
+ p50LatencyMs: percentile(durations, 50),
415
+ p95LatencyMs: percentile(durations, 95),
416
+ errorCount,
417
+ errorRate: callCount > 0 ? errorCount / callCount : 0,
418
+ timeoutCount,
419
+ timeoutRate: callCount > 0 ? timeoutCount / callCount : 0,
420
+ retryCount: 0,
421
+ malformedArgumentCount: toNumber(row.malformed_argument_count),
422
+ sideEffectCount: toNumber(row.side_effect_count),
423
+ topCausingAgents: topCausers(details, "agent_id", "agent_label"),
424
+ topCausingWorkflows: topCausers(
425
+ details,
426
+ "workflow_id",
427
+ "workflow_label",
428
+ ),
429
+ exemplars: exemplarsFromRows(
430
+ details.map((detail) => ({
431
+ action_id: detail.action_id,
432
+ agent_run_id: detail.agent_run_id,
433
+ trace_id: detail.trace_id,
434
+ tool_call_id: detail.tool_call_id,
435
+ eval_id: detail.eval_id,
436
+ label: detail.action_label,
437
+ status: detail.status,
438
+ occurred_at: detail.occurred_at,
439
+ })),
440
+ ),
441
+ };
442
+ }),
443
+ generatedAt: new Date().toISOString(),
444
+ };
445
+ };
446
+
447
+ const groupByActionDimension = async (
448
+ db: SqlDb,
449
+ projectId: string,
450
+ windowHours: number,
451
+ limit: number,
452
+ dimension: string,
453
+ keyExpr: string,
454
+ labelExpr = keyExpr,
455
+ ): Promise<CostAttributionRow[]> => {
456
+ const dialect = dialectFor(db);
457
+ const since = dialect.sinceHours("?");
458
+ const rows = await db
459
+ .prepare(`
460
+ SELECT
461
+ ${keyExpr} AS key,
462
+ ${labelExpr} AS label,
463
+ COALESCE(SUM(COALESCE(a.total_cost_usd, 0)), 0) AS total_cost_usd,
464
+ COUNT(*) AS action_count,
465
+ COUNT(DISTINCT a.agent_run_id) AS agent_run_count,
466
+ 0 AS tool_call_count
467
+ FROM actions a
468
+ WHERE a.project_id = ? AND a.started_at >= ${since}
469
+ GROUP BY 1, 2
470
+ ORDER BY total_cost_usd DESC, action_count DESC
471
+ LIMIT ?
472
+ `)
473
+ .bind(projectId, windowHours, limit)
474
+ .all<CostRow>();
475
+ const keys = rows.results
476
+ .map((row) => row.key)
477
+ .filter((key): key is string => !!key);
478
+ if (keys.length === 0) return costRows(dimension, rows.results);
479
+
480
+ const exemplarRows = await db
481
+ .prepare(`
482
+ SELECT
483
+ ${keyExpr} AS dimension_key,
484
+ a.id AS action_id,
485
+ COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
486
+ a.trace_id,
487
+ a.tool_call_id,
488
+ er.id AS eval_id,
489
+ a.name AS label,
490
+ a.status,
491
+ a.started_at AS occurred_at
492
+ FROM actions a
493
+ LEFT JOIN eval_results er ON er.project_id = a.project_id AND er.action_id = a.id
494
+ WHERE a.project_id = ?
495
+ AND a.started_at >= ${since}
496
+ AND ${keyExpr} IN (${inClause(keys)})
497
+ ORDER BY COALESCE(a.total_cost_usd, 0) DESC, a.started_at DESC
498
+ `)
499
+ .bind(projectId, windowHours, ...keys)
500
+ .all<ExemplarSqlRow>();
501
+ const exemplars = new Map<string, ActionAggregateExemplar[]>();
502
+ for (const row of exemplarRows.results) {
503
+ appendExemplar(exemplars, row.dimension_key ?? null, row);
504
+ }
505
+ return costRows(dimension, rows.results, exemplars);
506
+ };
507
+
508
+ const getAgentCostExemplars = async (
509
+ db: SqlDb,
510
+ projectId: string,
511
+ windowHours: number,
512
+ keys: string[],
513
+ ): Promise<Map<string, ActionAggregateExemplar[]>> => {
514
+ if (keys.length === 0) return new Map();
515
+ const dialect = dialectFor(db);
516
+ const since = dialect.sinceHours("?");
517
+ const rows = await db
518
+ .prepare(`
519
+ SELECT
520
+ ar.agent_id AS dimension_key,
521
+ root.id AS action_id,
522
+ ar.id AS agent_run_id,
523
+ root.trace_id,
524
+ root.tool_call_id,
525
+ er.id AS eval_id,
526
+ ar.agent_name AS label,
527
+ ar.status,
528
+ root.started_at AS occurred_at
529
+ FROM agent_runs ar
530
+ LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
531
+ LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = root.id
532
+ WHERE ar.project_id = ?
533
+ AND (root.started_at IS NULL OR root.started_at >= ${since})
534
+ AND ar.agent_id IN (${inClause(keys)})
535
+ ORDER BY COALESCE(ar.total_cost_usd, 0) DESC, root.started_at DESC
536
+ `)
537
+ .bind(projectId, windowHours, ...keys)
538
+ .all<ExemplarSqlRow>();
539
+ const exemplars = new Map<string, ActionAggregateExemplar[]>();
540
+ for (const row of rows.results) {
541
+ appendExemplar(exemplars, row.dimension_key ?? null, row);
542
+ }
543
+ return exemplars;
544
+ };
545
+
546
+ const getRunCostExemplars = async (
547
+ db: SqlDb,
548
+ projectId: string,
549
+ windowHours: number,
550
+ keys: string[],
551
+ ): Promise<Map<string, ActionAggregateExemplar[]>> => {
552
+ if (keys.length === 0) return new Map();
553
+ const dialect = dialectFor(db);
554
+ const since = dialect.sinceHours("?");
555
+ const rows = await db
556
+ .prepare(`
557
+ SELECT
558
+ ar.id AS dimension_key,
559
+ root.id AS action_id,
560
+ ar.id AS agent_run_id,
561
+ root.trace_id,
562
+ root.tool_call_id,
563
+ er.id AS eval_id,
564
+ ar.agent_name AS label,
565
+ ar.status,
566
+ root.started_at AS occurred_at
567
+ FROM agent_runs ar
568
+ LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
569
+ LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = root.id
570
+ WHERE ar.project_id = ?
571
+ AND (root.started_at IS NULL OR root.started_at >= ${since})
572
+ AND ar.id IN (${inClause(keys)})
573
+ ORDER BY COALESCE(ar.total_cost_usd, 0) DESC, root.started_at DESC
574
+ `)
575
+ .bind(projectId, windowHours, ...keys)
576
+ .all<ExemplarSqlRow>();
577
+ const exemplars = new Map<string, ActionAggregateExemplar[]>();
578
+ for (const row of rows.results) {
579
+ appendExemplar(exemplars, row.dimension_key ?? null, row);
580
+ }
581
+ return exemplars;
582
+ };
583
+
584
+ const getToolCostExemplars = async (
585
+ db: SqlDb,
586
+ projectId: string,
587
+ windowHours: number,
588
+ keys: string[],
589
+ ): Promise<Map<string, ActionAggregateExemplar[]>> => {
590
+ if (keys.length === 0) return new Map();
591
+ const dialect = dialectFor(db);
592
+ const since = dialect.sinceHours("?");
593
+ const rows = await db
594
+ .prepare(`
595
+ SELECT
596
+ t.tool_name AS dimension_key,
597
+ a.id AS action_id,
598
+ COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
599
+ COALESCE(a.trace_id, root.trace_id) AS trace_id,
600
+ t.id AS tool_call_id,
601
+ er.id AS eval_id,
602
+ COALESCE(a.name, t.tool_name) AS label,
603
+ a.status,
604
+ a.started_at AS occurred_at
605
+ FROM tool_calls t
606
+ LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
607
+ LEFT JOIN actions root ON root.project_id = t.project_id AND root.id = a.root_action_id
608
+ LEFT JOIN eval_results er ON er.project_id = t.project_id AND er.action_id = a.id
609
+ WHERE t.project_id = ?
610
+ AND (a.started_at IS NULL OR a.started_at >= ${since})
611
+ AND t.tool_name IN (${inClause(keys)})
612
+ ORDER BY COALESCE(a.total_cost_usd, 0) DESC, a.started_at DESC
613
+ `)
614
+ .bind(projectId, windowHours, ...keys)
615
+ .all<ExemplarSqlRow>();
616
+ const exemplars = new Map<string, ActionAggregateExemplar[]>();
617
+ for (const row of rows.results) {
618
+ appendExemplar(exemplars, row.dimension_key ?? null, row);
619
+ }
620
+ return exemplars;
621
+ };
622
+
623
+ export const getCostAttributionAggregates = async (
624
+ db: SqlDb,
625
+ projectId: string,
626
+ windowHours: number,
627
+ limit: number,
628
+ ): Promise<CostAttributionResult> => {
629
+ const dialect = dialectFor(db);
630
+ const since = dialect.sinceHours("?");
631
+ const tenantExpr = actionTenantExpr(db, "a");
632
+ const workflowExpr = actionWorkflowExpr(db, "a");
633
+
634
+ const [
635
+ agentRows,
636
+ runRows,
637
+ byModel,
638
+ byProvider,
639
+ byPromptVersion,
640
+ toolRows,
641
+ byUser,
642
+ byTenant,
643
+ byWorkflow,
644
+ ] = await Promise.all([
645
+ db
646
+ .prepare(`
647
+ SELECT
648
+ ar.agent_id AS key,
649
+ ar.agent_name AS label,
650
+ COALESCE(SUM(COALESCE(ar.total_cost_usd, 0)), 0) AS total_cost_usd,
651
+ 0 AS action_count,
652
+ COUNT(*) AS agent_run_count,
653
+ 0 AS tool_call_count
654
+ FROM agent_runs ar
655
+ LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
656
+ WHERE ar.project_id = ? AND (root.started_at IS NULL OR root.started_at >= ${since})
657
+ GROUP BY ar.agent_id, ar.agent_name
658
+ ORDER BY total_cost_usd DESC, agent_run_count DESC
659
+ LIMIT ?
660
+ `)
661
+ .bind(projectId, windowHours, limit)
662
+ .all<CostRow>(),
663
+ db
664
+ .prepare(`
665
+ SELECT
666
+ ar.id AS key,
667
+ ar.agent_name AS label,
668
+ COALESCE(ar.total_cost_usd, 0) AS total_cost_usd,
669
+ 0 AS action_count,
670
+ 1 AS agent_run_count,
671
+ 0 AS tool_call_count
672
+ FROM agent_runs ar
673
+ LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
674
+ WHERE ar.project_id = ? AND (root.started_at IS NULL OR root.started_at >= ${since})
675
+ ORDER BY total_cost_usd DESC
676
+ LIMIT ?
677
+ `)
678
+ .bind(projectId, windowHours, limit)
679
+ .all<CostRow>(),
680
+ groupByActionDimension(
681
+ db,
682
+ projectId,
683
+ windowHours,
684
+ limit,
685
+ "model",
686
+ "a.model_name",
687
+ ),
688
+ groupByActionDimension(
689
+ db,
690
+ projectId,
691
+ windowHours,
692
+ limit,
693
+ "provider",
694
+ "a.provider",
695
+ ),
696
+ groupByActionDimension(
697
+ db,
698
+ projectId,
699
+ windowHours,
700
+ limit,
701
+ "prompt_version",
702
+ "a.prompt_version",
703
+ ),
704
+ db
705
+ .prepare(`
706
+ SELECT
707
+ t.tool_name AS key,
708
+ t.tool_name AS label,
709
+ COALESCE(SUM(COALESCE(a.total_cost_usd, 0)), 0) AS total_cost_usd,
710
+ COUNT(DISTINCT a.id) AS action_count,
711
+ COUNT(DISTINCT a.agent_run_id) AS agent_run_count,
712
+ COUNT(*) AS tool_call_count
713
+ FROM tool_calls t
714
+ LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
715
+ WHERE t.project_id = ? AND (a.started_at IS NULL OR a.started_at >= ${since})
716
+ GROUP BY t.tool_name
717
+ ORDER BY total_cost_usd DESC, tool_call_count DESC
718
+ LIMIT ?
719
+ `)
720
+ .bind(projectId, windowHours, limit)
721
+ .all<CostRow>(),
722
+ groupByActionDimension(
723
+ db,
724
+ projectId,
725
+ windowHours,
726
+ limit,
727
+ "user",
728
+ "a.user_id",
729
+ ),
730
+ groupByActionDimension(
731
+ db,
732
+ projectId,
733
+ windowHours,
734
+ limit,
735
+ "tenant",
736
+ tenantExpr,
737
+ ),
738
+ groupByActionDimension(
739
+ db,
740
+ projectId,
741
+ windowHours,
742
+ limit,
743
+ "workflow",
744
+ workflowExpr,
745
+ ),
746
+ ]);
747
+ const agentKeys = agentRows.results
748
+ .map((row) => row.key)
749
+ .filter((key): key is string => !!key);
750
+ const runKeys = runRows.results
751
+ .map((row) => row.key)
752
+ .filter((key): key is string => !!key);
753
+ const toolKeys = toolRows.results
754
+ .map((row) => row.key)
755
+ .filter((key): key is string => !!key);
756
+ const [agentExemplars, runExemplars, toolExemplars] = await Promise.all([
757
+ getAgentCostExemplars(db, projectId, windowHours, agentKeys),
758
+ getRunCostExemplars(db, projectId, windowHours, runKeys),
759
+ getToolCostExemplars(db, projectId, windowHours, toolKeys),
760
+ ]);
761
+
762
+ return {
763
+ projectId,
764
+ windowHours,
765
+ byAgent: costRows("agent", agentRows.results, agentExemplars),
766
+ byRun: costRows("run", runRows.results, runExemplars),
767
+ byModel,
768
+ byProvider,
769
+ byPromptVersion,
770
+ byTool: costRows("tool", toolRows.results, toolExemplars),
771
+ byUser,
772
+ byTenant,
773
+ byWorkflow,
774
+ generatedAt: new Date().toISOString(),
775
+ };
776
+ };
777
+
778
+ export const getAutonomousReviewAggregates = async (
779
+ db: SqlDb,
780
+ projectId: string,
781
+ windowHours: number,
782
+ limit: number,
783
+ ): Promise<AutonomousReviewResult> => {
784
+ const dialect = dialectFor(db);
785
+ const since = dialect.sinceHours("?");
786
+ const rows = await db
787
+ .prepare(`
788
+ SELECT
789
+ t.id,
790
+ t.tool_name,
791
+ t.action_id,
792
+ a.name AS action_name,
793
+ COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
794
+ ar.agent_name,
795
+ ar.agent_version,
796
+ COALESCE(ar.autonomy_level, 'autonomous_write') AS autonomy_level,
797
+ t.side_effect,
798
+ COALESCE(t.approval_state, 'suggested') AS approval_state,
799
+ COALESCE(a.status, 'ok') AS status,
800
+ t.error_type AS error_snippet,
801
+ COALESCE(a.trace_id, root.trace_id) AS trace_id,
802
+ COALESCE(a.started_at, root.started_at) AS occurred_at,
803
+ CASE
804
+ WHEN t.mutation_before_json IS NOT NULL
805
+ OR t.mutation_after_json IS NOT NULL
806
+ OR t.mutation_diff_json IS NOT NULL
807
+ OR t.mutation_artifact_id IS NOT NULL
808
+ THEN 1 ELSE 0
809
+ END AS mutation_evidence,
810
+ t.mutation_artifact_id
811
+ FROM tool_calls t
812
+ LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
813
+ LEFT JOIN actions root ON root.project_id = t.project_id AND root.id = a.root_action_id
814
+ LEFT JOIN agent_runs ar ON ar.project_id = t.project_id AND ar.id = COALESCE(a.agent_run_id, a.root_action_id)
815
+ WHERE t.project_id = ?
816
+ AND t.side_effect <> 0
817
+ AND COALESCE(ar.autonomy_level, '') = 'autonomous_write'
818
+ AND (a.started_at IS NULL OR a.started_at >= ${since})
819
+ ORDER BY occurred_at DESC
820
+ LIMIT ?
821
+ `)
822
+ .bind(projectId, windowHours, limit)
823
+ .all<AutonomousReviewSqlRow>();
824
+
825
+ return {
826
+ projectId,
827
+ windowHours,
828
+ rows: rows.results.map((row) => ({
829
+ id: row.id,
830
+ toolName: row.tool_name,
831
+ actionId: row.action_id,
832
+ actionName: row.action_name ?? row.tool_name,
833
+ agentRunId: row.agent_run_id ?? "",
834
+ agentName: row.agent_name ?? "Unknown agent",
835
+ agentVersion: row.agent_version ?? "unknown",
836
+ autonomyLevel: row.autonomy_level ?? "autonomous_write",
837
+ sideEffect: row.side_effect !== 0,
838
+ approvalState: row.approval_state ?? "suggested",
839
+ status: row.status === "error" ? "error" : "ok",
840
+ errorSnippet: row.error_snippet,
841
+ traceId: row.trace_id ?? "",
842
+ occurredAt: row.occurred_at ?? new Date(0).toISOString(),
843
+ mutationEvidence: row.mutation_evidence !== 0,
844
+ mutationArtifactId: row.mutation_artifact_id ?? null,
845
+ })),
846
+ timestamp: new Date().toISOString(),
847
+ };
848
+ };
849
+
850
+ const formatPercent = (value: number): string => `${(value * 100).toFixed(1)}%`;
851
+ const directionForHigherBetter = (
852
+ baseline: number,
853
+ target: number,
854
+ ): "positive" | "negative" | "neutral" =>
855
+ target === baseline ? "neutral" : target > baseline ? "positive" : "negative";
856
+ const directionForLowerBetter = (
857
+ baseline: number,
858
+ target: number,
859
+ ): "positive" | "negative" | "neutral" =>
860
+ target === baseline ? "neutral" : target < baseline ? "positive" : "negative";
861
+
862
+ const getVersionExemplars = async (
863
+ db: SqlDb,
864
+ projectId: string,
865
+ version: string,
866
+ ): Promise<ActionAggregateExemplar[]> => {
867
+ if (!version || version === "unknown") return [];
868
+ const rows = await db
869
+ .prepare(`
870
+ SELECT
871
+ root.id AS action_id,
872
+ ar.id AS agent_run_id,
873
+ root.trace_id,
874
+ root.tool_call_id,
875
+ er.id AS eval_id,
876
+ ar.agent_name AS label,
877
+ ar.status,
878
+ root.started_at AS occurred_at
879
+ FROM agent_runs ar
880
+ LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
881
+ LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = root.id
882
+ WHERE ar.project_id = ? AND ar.agent_version = ?
883
+ ORDER BY
884
+ CASE WHEN ar.status <> 'success' THEN 0 ELSE 1 END,
885
+ root.started_at DESC
886
+ LIMIT ?
887
+ `)
888
+ .bind(projectId, version, EXEMPLAR_LIMIT)
889
+ .all<ExemplarSqlRow>();
890
+ return exemplarsFromRows(rows.results);
891
+ };
892
+
893
+ export const getVersionDiffAggregates = async (
894
+ db: SqlDb,
895
+ projectId: string,
896
+ baselineVersion?: string,
897
+ targetVersion?: string,
898
+ ): Promise<VersionComparisonResult> => {
899
+ const rows = await db
900
+ .prepare(`
901
+ SELECT
902
+ ar.agent_version AS version,
903
+ COUNT(DISTINCT ar.id) AS run_count,
904
+ SUM(CASE WHEN ar.status = 'success' THEN 1 ELSE 0 END) AS success_count,
905
+ AVG(ar.total_duration_ms) AS avg_duration_ms,
906
+ AVG(ar.total_cost_usd) AS avg_cost_usd,
907
+ COUNT(er.id) AS eval_count,
908
+ SUM(CASE WHEN er.passed <> 0 THEN 1 ELSE 0 END) AS passed_eval_count,
909
+ AVG(er.score) AS avg_eval_score,
910
+ COUNT(t.id) AS tool_count,
911
+ SUM(CASE WHEN t.error_type IS NOT NULL AND t.error_type <> '' THEN 1 ELSE 0 END) AS tool_error_count
912
+ FROM agent_runs ar
913
+ LEFT JOIN actions a ON a.project_id = ar.project_id AND a.agent_run_id = ar.id
914
+ LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = a.id
915
+ LEFT JOIN tool_calls t ON t.project_id = ar.project_id AND t.action_id = a.id
916
+ WHERE ar.project_id = ?
917
+ GROUP BY ar.agent_version
918
+ ORDER BY MAX(COALESCE(a.started_at, '')) DESC, ar.agent_version DESC
919
+ LIMIT 20
920
+ `)
921
+ .bind(projectId)
922
+ .all<VersionStatsRow>();
923
+
924
+ const versions = rows.results.filter((row) => row.version);
925
+ const baseline =
926
+ versions.find((row) => row.version === baselineVersion) ??
927
+ versions[1] ??
928
+ versions[0];
929
+ const target =
930
+ versions.find((row) => row.version === targetVersion) ??
931
+ versions[0] ??
932
+ baseline;
933
+
934
+ const metric = (
935
+ label: string,
936
+ baselineValue: number,
937
+ targetValue: number,
938
+ format: (value: number) => string | number,
939
+ direction: "higher" | "lower",
940
+ ): VersionDiffMetric => ({
941
+ label,
942
+ baselineValue: format(baselineValue),
943
+ targetValue: format(targetValue),
944
+ deltaValue:
945
+ typeof format(targetValue - baselineValue) === "number"
946
+ ? format(targetValue - baselineValue)
947
+ : `${targetValue - baselineValue >= 0 ? "+" : ""}${format(targetValue - baselineValue)}`,
948
+ deltaDirection:
949
+ direction === "higher"
950
+ ? directionForHigherBetter(baselineValue, targetValue)
951
+ : directionForLowerBetter(baselineValue, targetValue),
952
+ });
953
+
954
+ const empty = {
955
+ version: "unknown",
956
+ run_count: 0,
957
+ success_count: 0,
958
+ avg_duration_ms: 0,
959
+ avg_cost_usd: 0,
960
+ eval_count: 0,
961
+ passed_eval_count: 0,
962
+ avg_eval_score: 0,
963
+ tool_count: 0,
964
+ tool_error_count: 0,
965
+ } satisfies VersionStatsRow;
966
+ const b = baseline ?? empty;
967
+ const t = target ?? empty;
968
+ const bRuns = toNumber(b.run_count);
969
+ const tRuns = toNumber(t.run_count);
970
+ const bTools = toNumber(b.tool_count);
971
+ const tTools = toNumber(t.tool_count);
972
+ const bEvals = toNumber(b.eval_count);
973
+ const tEvals = toNumber(t.eval_count);
974
+ const [baselineExemplars, targetExemplars] = await Promise.all([
975
+ getVersionExemplars(db, projectId, b.version),
976
+ getVersionExemplars(db, projectId, t.version),
977
+ ]);
978
+
979
+ return {
980
+ projectId,
981
+ baselineVersion: b.version,
982
+ targetVersion: t.version,
983
+ metrics: [
984
+ metric(
985
+ "Success Rate",
986
+ bRuns > 0 ? toNumber(b.success_count) / bRuns : 0,
987
+ tRuns > 0 ? toNumber(t.success_count) / tRuns : 0,
988
+ formatPercent,
989
+ "higher",
990
+ ),
991
+ metric(
992
+ "Evaluation Score",
993
+ toNumber(b.avg_eval_score),
994
+ toNumber(t.avg_eval_score),
995
+ (value) => value.toFixed(2),
996
+ "higher",
997
+ ),
998
+ metric(
999
+ "Evaluation Pass Rate",
1000
+ bEvals > 0 ? toNumber(b.passed_eval_count) / bEvals : 0,
1001
+ tEvals > 0 ? toNumber(t.passed_eval_count) / tEvals : 0,
1002
+ formatPercent,
1003
+ "higher",
1004
+ ),
1005
+ metric(
1006
+ "Average Run Latency",
1007
+ toNumber(b.avg_duration_ms),
1008
+ toNumber(t.avg_duration_ms),
1009
+ (value) => `${Math.round(value)}ms`,
1010
+ "lower",
1011
+ ),
1012
+ metric(
1013
+ "Average Run Cost",
1014
+ toNumber(b.avg_cost_usd),
1015
+ toNumber(t.avg_cost_usd),
1016
+ (value) => `$${value.toFixed(4)}`,
1017
+ "lower",
1018
+ ),
1019
+ metric(
1020
+ "Tool Error Rate",
1021
+ bTools > 0 ? toNumber(b.tool_error_count) / bTools : 0,
1022
+ tTools > 0 ? toNumber(t.tool_error_count) / tTools : 0,
1023
+ formatPercent,
1024
+ "lower",
1025
+ ),
1026
+ ],
1027
+ baselineExemplars,
1028
+ targetExemplars,
1029
+ timestamp: new Date().toISOString(),
1030
+ };
1031
+ };