@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,716 @@
1
+ import { Hono } from "hono";
2
+ import { describe, expect, it } from "vitest";
3
+ import { CollectorRuntime } from "../framework/collector";
4
+ import type { CollectorEnv } from "../framework/env";
5
+ import type {
6
+ AutonomousReviewResult,
7
+ CostAttributionResult,
8
+ ToolReliabilityResult,
9
+ VersionComparisonResult,
10
+ } from "../lib/action-aggregates";
11
+ import type { EntityManifestExtended } from "../lib/identity-index";
12
+ import { MemSqlDb } from "../lib/test-utils/mem-sql-db";
13
+ import { actionRoutesPlugin } from "./action-routes";
14
+
15
+ const actionRoot = {
16
+ id: "run-123",
17
+ project_id: "default",
18
+ root_action_id: "run-123",
19
+ caused_by_action_id: null,
20
+ actor_type: "agent",
21
+ actor_id: "billing-agent",
22
+ action_kind: "agent.run",
23
+ name: "Billing agent",
24
+ status: "ok",
25
+ started_at: "2026-05-22T00:00:00.000Z",
26
+ ended_at: "2026-05-22T00:00:05.000Z",
27
+ duration_ms: 5000,
28
+ trace_id: "trace-123",
29
+ span_id: "span-root",
30
+ session_id: null,
31
+ interaction_id: null,
32
+ user_id: null,
33
+ agent_run_id: "run-123",
34
+ step_id: null,
35
+ tool_call_id: null,
36
+ prompt_version: "billing-v2",
37
+ model_name: "claude-sonnet-4",
38
+ provider: "anthropic",
39
+ total_cost_usd: 0.02,
40
+ attrs_json: "{}",
41
+ };
42
+
43
+ const actionChild = {
44
+ ...actionRoot,
45
+ id: "action-tool",
46
+ caused_by_action_id: "run-123",
47
+ action_kind: "tool.call",
48
+ name: "Update invoice",
49
+ span_id: "span-tool",
50
+ tool_call_id: "tool-1",
51
+ };
52
+
53
+ const agentRun = {
54
+ id: "run-123",
55
+ project_id: "default",
56
+ agent_id: "billing-agent",
57
+ agent_name: "Billing Agent",
58
+ agent_version: "2.0.0",
59
+ goal: "Resolve invoice mismatch",
60
+ outcome: "Updated invoice",
61
+ autonomy_level: "human_approved_write",
62
+ status: "success",
63
+ error_message: null,
64
+ total_cost_usd: 0.02,
65
+ total_duration_ms: 5000,
66
+ metadata_json: "{}",
67
+ };
68
+
69
+ const toolCall = {
70
+ id: "tool-1",
71
+ action_id: "action-tool",
72
+ project_id: "default",
73
+ tool_name: "update_invoice",
74
+ args_hash: "args-hash",
75
+ result_hash: "result-hash",
76
+ error_type: null,
77
+ side_effect: 1,
78
+ approval_state: "human_approved",
79
+ args_redacted: "{}",
80
+ result_redacted: "{}",
81
+ };
82
+
83
+ const setup = (db: MemSqlDb) => {
84
+ const app = new Hono<{ Bindings: CollectorEnv }>();
85
+ const runtime = new CollectorRuntime();
86
+ actionRoutesPlugin.register(app, runtime);
87
+ const env: CollectorEnv = {
88
+ DB: db as unknown as D1Database,
89
+ };
90
+ return (path: string) => app.request(path, { method: "GET" }, env);
91
+ };
92
+
93
+ type ActionRouteBody = {
94
+ action: { id: string };
95
+ manifest: EntityManifestExtended;
96
+ };
97
+
98
+ type AgentRunRouteBody = {
99
+ agentRun: { id: string };
100
+ manifest: EntityManifestExtended;
101
+ };
102
+
103
+ type ToolCallRouteBody = {
104
+ toolCall: { id: string; actionId: string };
105
+ manifest: EntityManifestExtended;
106
+ };
107
+
108
+ const actionGraphDb = () =>
109
+ new MemSqlDb({
110
+ first: (sql, binds) => {
111
+ if (sql.includes("FROM actions")) {
112
+ return binds.includes("run-123") || binds.includes("action-tool")
113
+ ? actionRoot
114
+ : null;
115
+ }
116
+ if (sql.includes("FROM agent_runs")) {
117
+ return binds.includes("run-123") ? agentRun : null;
118
+ }
119
+ if (sql.includes("FROM tool_calls")) {
120
+ return binds.includes("tool-1") ? { action_id: "action-tool" } : null;
121
+ }
122
+ return null;
123
+ },
124
+ all: (sql) => {
125
+ if (sql.includes("FROM actions")) return [actionRoot, actionChild];
126
+ if (sql.includes("FROM agent_runs")) return [agentRun];
127
+ if (sql.includes("FROM tool_calls")) return [toolCall];
128
+ return [];
129
+ },
130
+ });
131
+
132
+ describe("actionRoutesPlugin", () => {
133
+ it("returns direct action detail with the full action graph manifest", async () => {
134
+ const res = await setup(actionGraphDb())("/internal/actions/run-123");
135
+
136
+ expect(res.status).toBe(200);
137
+ const body = (await res.json()) as ActionRouteBody;
138
+ expect(body.action.id).toBe("run-123");
139
+ expect(body.manifest.actions).toHaveLength(2);
140
+ expect(body.manifest.agentRuns[0].id).toBe("run-123");
141
+ expect(body.manifest.toolCalls[0].id).toBe("tool-1");
142
+ });
143
+
144
+ it("returns direct agent run detail with the full run manifest", async () => {
145
+ const res = await setup(actionGraphDb())("/internal/agent-runs/run-123");
146
+
147
+ expect(res.status).toBe(200);
148
+ const body = (await res.json()) as AgentRunRouteBody;
149
+ expect(body.agentRun.id).toBe("run-123");
150
+ expect(body.manifest.actions.map((a: { id: string }) => a.id)).toContain(
151
+ "action-tool",
152
+ );
153
+ });
154
+
155
+ it("resolves a tool call through its causal action", async () => {
156
+ const res = await setup(actionGraphDb())("/internal/tool-calls/tool-1");
157
+
158
+ expect(res.status).toBe(200);
159
+ const body = (await res.json()) as ToolCallRouteBody;
160
+ expect(body.toolCall.id).toBe("tool-1");
161
+ expect(body.toolCall.actionId).toBe("action-tool");
162
+ expect(body.manifest.actions).toHaveLength(2);
163
+ });
164
+
165
+ it("compares normalized action step sequences for two agent runs", async () => {
166
+ const leftRoot = {
167
+ ...actionRoot,
168
+ id: "left-run",
169
+ root_action_id: "left-run",
170
+ };
171
+ const rightRoot = {
172
+ ...actionRoot,
173
+ id: "right-run",
174
+ root_action_id: "right-run",
175
+ };
176
+ const leftTool = {
177
+ ...actionChild,
178
+ id: "left-tool",
179
+ root_action_id: "left-run",
180
+ caused_by_action_id: "left-run",
181
+ name: "Update invoice",
182
+ status: "ok",
183
+ duration_ms: 100,
184
+ };
185
+ const rightTool = {
186
+ ...actionChild,
187
+ id: "right-tool",
188
+ root_action_id: "right-run",
189
+ caused_by_action_id: "right-run",
190
+ name: "Update invoice",
191
+ status: "error",
192
+ duration_ms: 250,
193
+ };
194
+ const db = new MemSqlDb({
195
+ first: (_sql, binds) => {
196
+ if (binds.includes("left-run")) return leftRoot;
197
+ if (binds.includes("right-run")) return rightRoot;
198
+ return null;
199
+ },
200
+ all: (sql, binds) => {
201
+ if (sql.includes("FROM actions")) {
202
+ if (binds.includes("left-run")) return [leftRoot, leftTool];
203
+ if (binds.includes("right-run")) return [rightRoot, rightTool];
204
+ }
205
+ if (sql.includes("FROM agent_runs")) {
206
+ return [
207
+ {
208
+ ...agentRun,
209
+ id: binds.includes("left-run") ? "left-run" : "right-run",
210
+ },
211
+ ];
212
+ }
213
+ if (sql.includes("FROM tool_calls")) {
214
+ if (binds.includes("left-tool")) {
215
+ return [{ ...toolCall, id: "left-call", action_id: "left-tool" }];
216
+ }
217
+ if (binds.includes("right-tool")) {
218
+ return [{ ...toolCall, id: "right-call", action_id: "right-tool" }];
219
+ }
220
+ }
221
+ return [];
222
+ },
223
+ });
224
+
225
+ const res = await setup(db)(
226
+ "/internal/actions/compare?left=left-run&right=right-run",
227
+ );
228
+
229
+ expect(res.status).toBe(200);
230
+ const body = (await res.json()) as {
231
+ stepComparisons: Array<{ changeType: string; changedFields: string[] }>;
232
+ };
233
+ expect(
234
+ body.stepComparisons.some((step) => step.changeType === "changed"),
235
+ ).toBe(true);
236
+ expect(
237
+ body.stepComparisons.some((step) =>
238
+ step.changedFields.includes("status"),
239
+ ),
240
+ ).toBe(true);
241
+ });
242
+
243
+ it("returns 404 for missing actions, agent runs, and tool calls", async () => {
244
+ const fetch = setup(new MemSqlDb({ first: () => null, all: () => [] }));
245
+
246
+ expect((await fetch("/internal/actions/missing")).status).toBe(404);
247
+ expect((await fetch("/internal/agent-runs/missing")).status).toBe(404);
248
+ expect((await fetch("/internal/tool-calls/missing")).status).toBe(404);
249
+ });
250
+
251
+ it("returns tool reliability aggregate shape with explicit null/zero unsupported metrics", async () => {
252
+ const fetch = setup(
253
+ new MemSqlDb({
254
+ all: (sql) => {
255
+ if (
256
+ sql.includes("FROM tool_calls t") &&
257
+ sql.includes("GROUP BY t.tool_name")
258
+ ) {
259
+ return [
260
+ {
261
+ tool_name: "update_invoice",
262
+ call_count: 4,
263
+ error_count: 2,
264
+ timeout_count: 1,
265
+ malformed_argument_count: 1,
266
+ side_effect_count: 3,
267
+ },
268
+ {
269
+ tool_name: "lookup_customer",
270
+ call_count: 1,
271
+ error_count: 0,
272
+ timeout_count: 0,
273
+ malformed_argument_count: 0,
274
+ side_effect_count: 0,
275
+ },
276
+ ];
277
+ }
278
+ if (sql.includes("FROM tool_calls t")) {
279
+ return [
280
+ {
281
+ tool_name: "update_invoice",
282
+ duration_ms: 100,
283
+ agent_id: "billing-agent",
284
+ agent_label: "Billing Agent",
285
+ workflow_id: "invoice-workflow",
286
+ workflow_label: "invoice-workflow",
287
+ action_id: "action-tool",
288
+ agent_run_id: "run-123",
289
+ trace_id: "trace-123",
290
+ tool_call_id: "tool-1",
291
+ eval_id: "eval-1",
292
+ action_label: "Update invoice",
293
+ status: "error",
294
+ occurred_at: "2026-05-22T00:00:01.000Z",
295
+ },
296
+ {
297
+ tool_name: "update_invoice",
298
+ duration_ms: 200,
299
+ agent_id: "billing-agent",
300
+ agent_label: "Billing Agent",
301
+ workflow_id: "invoice-workflow",
302
+ workflow_label: "invoice-workflow",
303
+ },
304
+ {
305
+ tool_name: "update_invoice",
306
+ duration_ms: 400,
307
+ agent_id: "billing-agent",
308
+ agent_label: "Billing Agent",
309
+ workflow_id: "invoice-workflow",
310
+ workflow_label: "invoice-workflow",
311
+ },
312
+ {
313
+ tool_name: "update_invoice",
314
+ duration_ms: 800,
315
+ agent_id: "refund-agent",
316
+ agent_label: "Refund Agent",
317
+ workflow_id: null,
318
+ workflow_label: null,
319
+ },
320
+ {
321
+ tool_name: "lookup_customer",
322
+ duration_ms: null,
323
+ agent_id: null,
324
+ agent_label: null,
325
+ workflow_id: null,
326
+ workflow_label: null,
327
+ },
328
+ ];
329
+ }
330
+ return [];
331
+ },
332
+ }),
333
+ );
334
+
335
+ const res = await fetch(
336
+ "/internal/actions/aggregates/tool-reliability?hours=24&limit=2",
337
+ );
338
+
339
+ expect(res.status).toBe(200);
340
+ const body = (await res.json()) as ToolReliabilityResult;
341
+ expect(body.windowHours).toBe(24);
342
+ expect(body.tools).toHaveLength(2);
343
+ expect(body.tools[0]).toMatchObject({
344
+ toolName: "update_invoice",
345
+ callCount: 4,
346
+ p50LatencyMs: 200,
347
+ p95LatencyMs: 800,
348
+ errorCount: 2,
349
+ errorRate: 0.5,
350
+ timeoutCount: 1,
351
+ timeoutRate: 0.25,
352
+ retryCount: 0,
353
+ malformedArgumentCount: 1,
354
+ sideEffectCount: 3,
355
+ });
356
+ expect(body.tools[0].topCausingAgents[0]).toEqual({
357
+ id: "billing-agent",
358
+ label: "Billing Agent",
359
+ count: 3,
360
+ });
361
+ expect(body.tools[0].topCausingWorkflows[0]).toEqual({
362
+ id: "invoice-workflow",
363
+ label: "invoice-workflow",
364
+ count: 3,
365
+ });
366
+ expect(body.tools[0].exemplars[0]).toMatchObject({
367
+ actionId: "action-tool",
368
+ agentRunId: "run-123",
369
+ traceId: "trace-123",
370
+ toolCallId: "tool-1",
371
+ evalId: "eval-1",
372
+ label: "Update invoice",
373
+ status: "error",
374
+ });
375
+ expect(body.tools[1]).toMatchObject({
376
+ toolName: "lookup_customer",
377
+ p50LatencyMs: null,
378
+ p95LatencyMs: null,
379
+ retryCount: 0,
380
+ malformedArgumentCount: 0,
381
+ });
382
+ });
383
+
384
+ it("returns cost attribution aggregate shape across agent run action tool user tenant workflow dimensions", async () => {
385
+ const fetch = setup(
386
+ new MemSqlDb({
387
+ all: (sql) => {
388
+ if (sql.includes("ar.agent_id AS dimension_key")) {
389
+ return [
390
+ {
391
+ dimension_key: "billing-agent",
392
+ action_id: "run-1",
393
+ agent_run_id: "run-1",
394
+ trace_id: "trace-agent-cost",
395
+ tool_call_id: null,
396
+ eval_id: "eval-agent-cost",
397
+ label: "Billing Agent",
398
+ status: "success",
399
+ occurred_at: "2026-05-22T00:00:00.000Z",
400
+ },
401
+ ];
402
+ }
403
+ if (sql.includes("ar.id AS dimension_key")) {
404
+ return [
405
+ {
406
+ dimension_key: "run-1",
407
+ action_id: "run-1",
408
+ agent_run_id: "run-1",
409
+ trace_id: "trace-run-cost",
410
+ tool_call_id: null,
411
+ eval_id: null,
412
+ label: "Billing Agent",
413
+ status: "success",
414
+ occurred_at: "2026-05-22T00:00:00.000Z",
415
+ },
416
+ ];
417
+ }
418
+ if (sql.includes("t.tool_name AS dimension_key")) {
419
+ return [
420
+ {
421
+ dimension_key: "update_invoice",
422
+ action_id: "action-tool",
423
+ agent_run_id: "run-1",
424
+ trace_id: "trace-tool-cost",
425
+ tool_call_id: "tool-1",
426
+ eval_id: null,
427
+ label: "Update invoice",
428
+ status: "ok",
429
+ occurred_at: "2026-05-22T00:00:01.000Z",
430
+ },
431
+ ];
432
+ }
433
+ if (sql.includes("FROM agent_runs ar")) {
434
+ if (sql.includes("GROUP BY ar.agent_id")) {
435
+ return [
436
+ {
437
+ key: "billing-agent",
438
+ label: "Billing Agent",
439
+ total_cost_usd: 0.3,
440
+ action_count: 0,
441
+ agent_run_count: 2,
442
+ tool_call_count: 0,
443
+ },
444
+ ];
445
+ }
446
+ return [
447
+ {
448
+ key: "run-1",
449
+ label: "Billing Agent",
450
+ total_cost_usd: 0.2,
451
+ action_count: 0,
452
+ agent_run_count: 1,
453
+ tool_call_count: 0,
454
+ },
455
+ ];
456
+ }
457
+ if (
458
+ sql.includes("FROM tool_calls t") &&
459
+ sql.includes("COUNT(*) AS tool_call_count")
460
+ ) {
461
+ return [
462
+ {
463
+ key: "update_invoice",
464
+ label: "update_invoice",
465
+ total_cost_usd: 0.07,
466
+ action_count: 2,
467
+ agent_run_count: 1,
468
+ tool_call_count: 4,
469
+ },
470
+ ];
471
+ }
472
+ if (sql.includes("a.model_name AS key")) {
473
+ return [
474
+ {
475
+ key: "gpt-4o",
476
+ label: "gpt-4o",
477
+ total_cost_usd: 0.13,
478
+ action_count: 3,
479
+ agent_run_count: 1,
480
+ tool_call_count: 0,
481
+ },
482
+ ];
483
+ }
484
+ if (sql.includes("a.provider AS key")) {
485
+ return [
486
+ {
487
+ key: "openai",
488
+ label: "openai",
489
+ total_cost_usd: 0.13,
490
+ action_count: 3,
491
+ agent_run_count: 1,
492
+ tool_call_count: 0,
493
+ },
494
+ ];
495
+ }
496
+ if (sql.includes("a.prompt_version AS key")) {
497
+ return [
498
+ {
499
+ key: "billing-v3",
500
+ label: "billing-v3",
501
+ total_cost_usd: 0.11,
502
+ action_count: 2,
503
+ agent_run_count: 1,
504
+ tool_call_count: 0,
505
+ },
506
+ ];
507
+ }
508
+ if (sql.includes("a.user_id AS key")) {
509
+ return [
510
+ {
511
+ key: "user-123",
512
+ label: "user-123",
513
+ total_cost_usd: 0.09,
514
+ action_count: 2,
515
+ agent_run_count: 1,
516
+ tool_call_count: 0,
517
+ },
518
+ ];
519
+ }
520
+ if (sql.includes("target_tenant")) {
521
+ return [
522
+ {
523
+ key: "acme_corp",
524
+ label: "acme_corp",
525
+ total_cost_usd: 0.08,
526
+ action_count: 1,
527
+ agent_run_count: 1,
528
+ tool_call_count: 0,
529
+ },
530
+ ];
531
+ }
532
+ if (sql.includes("workflow_id") || sql.includes("task_id")) {
533
+ return [
534
+ {
535
+ key: "invoice-workflow",
536
+ label: "invoice-workflow",
537
+ total_cost_usd: 0.06,
538
+ action_count: 1,
539
+ agent_run_count: 1,
540
+ tool_call_count: 0,
541
+ },
542
+ ];
543
+ }
544
+ return [];
545
+ },
546
+ }),
547
+ );
548
+
549
+ const res = await fetch(
550
+ "/internal/actions/aggregates/cost-attribution?hours=48&limit=10",
551
+ );
552
+
553
+ expect(res.status).toBe(200);
554
+ const body = (await res.json()) as CostAttributionResult;
555
+ expect(body.windowHours).toBe(48);
556
+ expect(body.byAgent[0]).toMatchObject({
557
+ dimension: "agent",
558
+ key: "billing-agent",
559
+ totalCostUsd: 0.3,
560
+ agentRunCount: 2,
561
+ });
562
+ expect(body.byRun[0]).toMatchObject({
563
+ dimension: "run",
564
+ key: "run-1",
565
+ totalCostUsd: 0.2,
566
+ });
567
+ expect(body.byModel[0]).toMatchObject({
568
+ dimension: "model",
569
+ key: "gpt-4o",
570
+ totalCostUsd: 0.13,
571
+ });
572
+ expect(body.byProvider[0].key).toBe("openai");
573
+ expect(body.byPromptVersion[0].key).toBe("billing-v3");
574
+ expect(body.byTool[0]).toMatchObject({
575
+ dimension: "tool",
576
+ key: "update_invoice",
577
+ toolCallCount: 4,
578
+ });
579
+ expect(body.byAgent[0].exemplars[0]).toMatchObject({
580
+ actionId: "run-1",
581
+ agentRunId: "run-1",
582
+ traceId: "trace-agent-cost",
583
+ evalId: "eval-agent-cost",
584
+ });
585
+ expect(body.byRun[0].exemplars[0]).toMatchObject({
586
+ actionId: "run-1",
587
+ agentRunId: "run-1",
588
+ traceId: "trace-run-cost",
589
+ });
590
+ expect(body.byTool[0].exemplars[0]).toMatchObject({
591
+ actionId: "action-tool",
592
+ agentRunId: "run-1",
593
+ traceId: "trace-tool-cost",
594
+ toolCallId: "tool-1",
595
+ });
596
+ expect(body.byUser[0].key).toBe("user-123");
597
+ expect(body.byTenant[0].key).toBe("acme_corp");
598
+ expect(body.byWorkflow[0].key).toBe("invoice-workflow");
599
+ });
600
+
601
+ it("returns autonomous-write review rows for side-effecting autonomous tools", async () => {
602
+ const fetch = setup(
603
+ new MemSqlDb({
604
+ all: () => [
605
+ {
606
+ id: "tool-1",
607
+ tool_name: "update_invoice",
608
+ action_id: "action-tool",
609
+ action_name: "Update invoice",
610
+ agent_run_id: "run-123",
611
+ agent_name: "Billing Agent",
612
+ agent_version: "2.0.0",
613
+ autonomy_level: "autonomous_write",
614
+ side_effect: 1,
615
+ approval_state: "bypassed",
616
+ status: "error",
617
+ error_snippet: "policy violation",
618
+ trace_id: "trace-123",
619
+ occurred_at: "2026-05-22T00:00:01.000Z",
620
+ },
621
+ ],
622
+ }),
623
+ );
624
+
625
+ const res = await fetch(
626
+ "/internal/actions/aggregates/autonomous-review?hours=24&limit=10",
627
+ );
628
+
629
+ expect(res.status).toBe(200);
630
+ const body = (await res.json()) as AutonomousReviewResult;
631
+ expect(body.rows[0]).toMatchObject({
632
+ id: "tool-1",
633
+ toolName: "update_invoice",
634
+ agentRunId: "run-123",
635
+ approvalState: "bypassed",
636
+ status: "error",
637
+ });
638
+ });
639
+
640
+ it("returns prompt and agent version diff metrics", async () => {
641
+ const fetch = setup(
642
+ new MemSqlDb({
643
+ all: (sql) => {
644
+ if (sql.includes("root.id AS action_id")) {
645
+ return [
646
+ {
647
+ action_id: "run-version",
648
+ agent_run_id: "run-version",
649
+ trace_id: "trace-version",
650
+ tool_call_id: null,
651
+ eval_id: "eval-version",
652
+ label: "Billing Agent",
653
+ status: "failed",
654
+ occurred_at: "2026-05-22T00:00:00.000Z",
655
+ },
656
+ ];
657
+ }
658
+ return [
659
+ {
660
+ version: "v3",
661
+ run_count: 10,
662
+ success_count: 9,
663
+ avg_duration_ms: 300,
664
+ avg_cost_usd: 0.02,
665
+ eval_count: 8,
666
+ passed_eval_count: 7,
667
+ avg_eval_score: 0.92,
668
+ tool_count: 20,
669
+ tool_error_count: 1,
670
+ },
671
+ {
672
+ version: "v2",
673
+ run_count: 10,
674
+ success_count: 8,
675
+ avg_duration_ms: 500,
676
+ avg_cost_usd: 0.04,
677
+ eval_count: 8,
678
+ passed_eval_count: 6,
679
+ avg_eval_score: 0.82,
680
+ tool_count: 20,
681
+ tool_error_count: 3,
682
+ },
683
+ ];
684
+ },
685
+ }),
686
+ );
687
+
688
+ const res = await fetch(
689
+ "/internal/actions/aggregates/version-diff?baseline=v2&target=v3",
690
+ );
691
+
692
+ expect(res.status).toBe(200);
693
+ const body = (await res.json()) as VersionComparisonResult;
694
+ expect(body.baselineVersion).toBe("v2");
695
+ expect(body.targetVersion).toBe("v3");
696
+ expect(body.metrics.map((metric) => metric.label)).toContain(
697
+ "Success Rate",
698
+ );
699
+ expect(
700
+ body.metrics.find((metric) => metric.label === "Average Run Cost")
701
+ ?.deltaDirection,
702
+ ).toBe("positive");
703
+ expect(body.baselineExemplars[0]).toMatchObject({
704
+ actionId: "run-version",
705
+ agentRunId: "run-version",
706
+ traceId: "trace-version",
707
+ evalId: "eval-version",
708
+ });
709
+ expect(body.targetExemplars[0]).toMatchObject({
710
+ actionId: "run-version",
711
+ agentRunId: "run-version",
712
+ traceId: "trace-version",
713
+ evalId: "eval-version",
714
+ });
715
+ });
716
+ });