@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,106 @@
1
+ import type { JsonValue, StoredSpan } from "@obsunified/types";
2
+ import {
3
+ ACTION_CAUSED_BY_ID_KEY,
4
+ ACTION_CONFIDENCE_KEY,
5
+ ACTION_ID_KEY,
6
+ ACTION_ID_RE,
7
+ ACTION_KIND_KEY,
8
+ ACTION_ROOT_ID_KEY,
9
+ ActionConfidence,
10
+ AGENT_RUN_ID_KEY,
11
+ } from "@obsunified/types/constants";
12
+ import { sha256Hex } from "./hash";
13
+
14
+ export interface ResolvedActionIdentity {
15
+ actionId: string;
16
+ rootActionId: string;
17
+ causedByActionId: string | null;
18
+ agentRunId: string | null;
19
+ confidence: ActionConfidence;
20
+ }
21
+
22
+ export async function deriveActionId(
23
+ projectId: string,
24
+ traceId: string,
25
+ spanId: string,
26
+ ): Promise<string> {
27
+ const input = `${projectId}:${traceId}:${spanId}`;
28
+ const hash = await sha256Hex(input);
29
+ const hex = hash.substring(0, 32);
30
+ let num = BigInt(`0x${hex}`);
31
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
32
+ let encoded = "";
33
+ for (let i = 0; i < 26; i++) {
34
+ const remainder = Number(num % 32n);
35
+ encoded = alphabet[remainder] + encoded;
36
+ num = num / 32n;
37
+ }
38
+ return encoded;
39
+ }
40
+
41
+ export const asValidActionId = (value: unknown): string | undefined => {
42
+ if (typeof value !== "string") return undefined;
43
+ const trimmed = value.trim();
44
+ return ACTION_ID_RE.test(trimmed) ? trimmed : undefined;
45
+ };
46
+
47
+ const asConfidence = (value: unknown): ActionConfidence | undefined =>
48
+ value === ActionConfidence.Explicit || value === ActionConfidence.Fallback
49
+ ? value
50
+ : undefined;
51
+
52
+ export async function resolveActionIdentity(
53
+ span: Pick<StoredSpan, "projectId" | "traceId" | "spanId" | "parentSpanId">,
54
+ attrs: Record<string, JsonValue>,
55
+ ): Promise<ResolvedActionIdentity> {
56
+ const explicitActionId = asValidActionId(attrs[ACTION_ID_KEY]);
57
+ const actionId =
58
+ explicitActionId ??
59
+ (await deriveActionId(span.projectId, span.traceId, span.spanId));
60
+ const confidence =
61
+ asConfidence(attrs[ACTION_CONFIDENCE_KEY]) ??
62
+ (explicitActionId ? ActionConfidence.Explicit : ActionConfidence.Fallback);
63
+ const actionKind =
64
+ typeof attrs[ACTION_KIND_KEY] === "string" ? attrs[ACTION_KIND_KEY] : null;
65
+
66
+ const rootActionId =
67
+ asValidActionId(attrs[ACTION_ROOT_ID_KEY]) ??
68
+ asValidActionId(attrs[AGENT_RUN_ID_KEY]) ??
69
+ asValidActionId(attrs["obs.action.agent_run_id"]) ??
70
+ asValidActionId(attrs["obs.agent_run.id"]) ??
71
+ (actionKind === "agent.run" || actionKind === "agent"
72
+ ? actionId
73
+ : await deriveActionId(
74
+ span.projectId,
75
+ span.traceId,
76
+ span.traceId.substring(0, 16),
77
+ ));
78
+
79
+ const explicitCausedByActionId = asValidActionId(
80
+ attrs[ACTION_CAUSED_BY_ID_KEY],
81
+ );
82
+ let causedByActionId: string | null;
83
+ if (explicitCausedByActionId) {
84
+ causedByActionId = explicitCausedByActionId;
85
+ } else if (attrs[ACTION_CAUSED_BY_ID_KEY] === null) {
86
+ causedByActionId = null;
87
+ } else {
88
+ causedByActionId = span.parentSpanId
89
+ ? await deriveActionId(span.projectId, span.traceId, span.parentSpanId)
90
+ : null;
91
+ }
92
+
93
+ const agentRunId =
94
+ asValidActionId(attrs[AGENT_RUN_ID_KEY]) ??
95
+ asValidActionId(attrs["obs.action.agent_run_id"]) ??
96
+ asValidActionId(attrs["obs.agent_run.id"]) ??
97
+ (actionKind === "agent.run" || actionKind === "agent" ? actionId : null);
98
+
99
+ return {
100
+ actionId,
101
+ rootActionId,
102
+ causedByActionId,
103
+ agentRunId,
104
+ confidence,
105
+ };
106
+ }
@@ -0,0 +1,397 @@
1
+ import type {
2
+ ActionRef,
3
+ AgentRunRef,
4
+ ArtifactRef,
5
+ EvalResultRef,
6
+ RetrievalEventRef,
7
+ ToolCallRef,
8
+ } from "./identity-index";
9
+ import {
10
+ mapAction,
11
+ mapAgentRun,
12
+ mapArtifact,
13
+ mapEvalResult,
14
+ mapRetrievalEvent,
15
+ mapToolCall,
16
+ } from "./identity-index/mappers";
17
+ import type { SqlDb } from "./sql-db";
18
+
19
+ export type ActionRecord = Omit<ActionRef, "projectId"> & {
20
+ projectId: string;
21
+ };
22
+
23
+ export type AgentRunRecord = Omit<AgentRunRef, "projectId"> & {
24
+ projectId: string;
25
+ };
26
+
27
+ export type ToolCallRecord = Omit<ToolCallRef, "projectId" | "sideEffect"> & {
28
+ projectId: string;
29
+ sideEffect: boolean | number;
30
+ };
31
+
32
+ export type RetrievalEventRecord = Omit<RetrievalEventRef, "projectId"> & {
33
+ projectId: string;
34
+ };
35
+
36
+ export type EvalResultRecord = Omit<EvalResultRef, "projectId" | "passed"> & {
37
+ projectId: string;
38
+ passed: boolean | number;
39
+ };
40
+
41
+ export type ArtifactRecord = Omit<ArtifactRef, "projectId"> & {
42
+ projectId: string;
43
+ };
44
+
45
+ export class ActionStore {
46
+ constructor(private readonly db: SqlDb) {}
47
+
48
+ async upsertAction(action: ActionRecord): Promise<void> {
49
+ await this.db
50
+ .prepare(`
51
+ INSERT INTO actions (
52
+ id, project_id, root_action_id, caused_by_action_id, actor_type, actor_id,
53
+ action_kind, name, status, started_at, ended_at, duration_ms, trace_id,
54
+ span_id, session_id, interaction_id, user_id, agent_run_id, step_id,
55
+ tool_call_id, prompt_version, model_name, provider, total_cost_usd,
56
+ attrs_json
57
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
58
+ ON CONFLICT(id) DO UPDATE SET
59
+ project_id = excluded.project_id,
60
+ root_action_id = excluded.root_action_id,
61
+ caused_by_action_id = excluded.caused_by_action_id,
62
+ actor_type = excluded.actor_type,
63
+ actor_id = excluded.actor_id,
64
+ action_kind = excluded.action_kind,
65
+ name = excluded.name,
66
+ status = excluded.status,
67
+ started_at = excluded.started_at,
68
+ ended_at = excluded.ended_at,
69
+ duration_ms = excluded.duration_ms,
70
+ trace_id = excluded.trace_id,
71
+ span_id = excluded.span_id,
72
+ session_id = excluded.session_id,
73
+ interaction_id = excluded.interaction_id,
74
+ user_id = excluded.user_id,
75
+ agent_run_id = excluded.agent_run_id,
76
+ step_id = excluded.step_id,
77
+ tool_call_id = excluded.tool_call_id,
78
+ prompt_version = excluded.prompt_version,
79
+ model_name = excluded.model_name,
80
+ provider = excluded.provider,
81
+ total_cost_usd = excluded.total_cost_usd,
82
+ attrs_json = excluded.attrs_json
83
+ `)
84
+ .bind(
85
+ action.id,
86
+ action.projectId,
87
+ action.rootActionId,
88
+ action.causedByActionId,
89
+ action.actorType,
90
+ action.actorId,
91
+ action.actionKind,
92
+ action.name,
93
+ action.status,
94
+ action.startedAt,
95
+ action.endedAt,
96
+ action.durationMs,
97
+ action.traceId,
98
+ action.spanId,
99
+ action.sessionId,
100
+ action.interactionId,
101
+ action.userId,
102
+ action.agentRunId,
103
+ action.stepId,
104
+ action.toolCallId,
105
+ action.promptVersion,
106
+ action.modelName,
107
+ action.provider,
108
+ action.totalCostUsd,
109
+ action.attrsJson,
110
+ )
111
+ .run();
112
+ }
113
+
114
+ async upsertAgentRun(run: AgentRunRecord): Promise<void> {
115
+ await this.db
116
+ .prepare(`
117
+ INSERT INTO agent_runs (
118
+ id, project_id, agent_id, agent_name, agent_version, goal, outcome,
119
+ autonomy_level, status, error_message, total_cost_usd,
120
+ total_duration_ms, metadata_json
121
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
122
+ ON CONFLICT(id) DO UPDATE SET
123
+ project_id = excluded.project_id,
124
+ agent_id = excluded.agent_id,
125
+ agent_name = excluded.agent_name,
126
+ agent_version = excluded.agent_version,
127
+ goal = excluded.goal,
128
+ outcome = excluded.outcome,
129
+ autonomy_level = excluded.autonomy_level,
130
+ status = excluded.status,
131
+ error_message = excluded.error_message,
132
+ total_cost_usd = excluded.total_cost_usd,
133
+ total_duration_ms = excluded.total_duration_ms,
134
+ metadata_json = excluded.metadata_json
135
+ `)
136
+ .bind(
137
+ run.id,
138
+ run.projectId,
139
+ run.agentId,
140
+ run.agentName,
141
+ run.agentVersion,
142
+ run.goal,
143
+ run.outcome,
144
+ run.autonomyLevel,
145
+ run.status,
146
+ run.errorMessage,
147
+ run.totalCostUsd,
148
+ run.totalDurationMs,
149
+ run.metadataJson,
150
+ )
151
+ .run();
152
+ }
153
+
154
+ async upsertToolCall(toolCall: ToolCallRecord): Promise<void> {
155
+ await this.db
156
+ .prepare(`
157
+ INSERT INTO tool_calls (
158
+ id, action_id, project_id, tool_name, args_hash, result_hash,
159
+ error_type, side_effect, approval_state, args_redacted, result_redacted,
160
+ mcp_audit_json, mutation_before_json, mutation_after_json,
161
+ mutation_diff_json, mutation_artifact_id
162
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
163
+ ON CONFLICT(id) DO UPDATE SET
164
+ action_id = excluded.action_id,
165
+ project_id = excluded.project_id,
166
+ tool_name = excluded.tool_name,
167
+ args_hash = excluded.args_hash,
168
+ result_hash = excluded.result_hash,
169
+ error_type = excluded.error_type,
170
+ side_effect = excluded.side_effect,
171
+ approval_state = excluded.approval_state,
172
+ args_redacted = excluded.args_redacted,
173
+ result_redacted = excluded.result_redacted,
174
+ mcp_audit_json = excluded.mcp_audit_json,
175
+ mutation_before_json = excluded.mutation_before_json,
176
+ mutation_after_json = excluded.mutation_after_json,
177
+ mutation_diff_json = excluded.mutation_diff_json,
178
+ mutation_artifact_id = excluded.mutation_artifact_id
179
+ `)
180
+ .bind(
181
+ toolCall.id,
182
+ toolCall.actionId,
183
+ toolCall.projectId,
184
+ toolCall.toolName,
185
+ toolCall.argsHash,
186
+ toolCall.resultHash,
187
+ toolCall.errorType,
188
+ toolCall.sideEffect ? 1 : 0,
189
+ toolCall.approvalState,
190
+ toolCall.argsRedacted,
191
+ toolCall.resultRedacted,
192
+ toolCall.mcpAuditJson,
193
+ toolCall.mutationBeforeJson,
194
+ toolCall.mutationAfterJson,
195
+ toolCall.mutationDiffJson,
196
+ toolCall.mutationArtifactId,
197
+ )
198
+ .run();
199
+ }
200
+
201
+ async upsertRetrievalEvent(event: RetrievalEventRecord): Promise<void> {
202
+ await this.db
203
+ .prepare(`
204
+ INSERT INTO retrieval_events (
205
+ id, action_id, project_id, retriever_name, query_hash, documents_json,
206
+ total_results, max_relevance_score, duration_ms
207
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
208
+ ON CONFLICT(id) DO UPDATE SET
209
+ action_id = excluded.action_id,
210
+ project_id = excluded.project_id,
211
+ retriever_name = excluded.retriever_name,
212
+ query_hash = excluded.query_hash,
213
+ documents_json = excluded.documents_json,
214
+ total_results = excluded.total_results,
215
+ max_relevance_score = excluded.max_relevance_score,
216
+ duration_ms = excluded.duration_ms
217
+ `)
218
+ .bind(
219
+ event.id,
220
+ event.actionId,
221
+ event.projectId,
222
+ event.retrieverName,
223
+ event.queryHash,
224
+ event.documentsJson,
225
+ event.totalResults,
226
+ event.maxRelevanceScore,
227
+ event.durationMs,
228
+ )
229
+ .run();
230
+ }
231
+
232
+ async upsertEvalResult(result: EvalResultRecord): Promise<void> {
233
+ await this.db
234
+ .prepare(`
235
+ INSERT INTO eval_results (
236
+ id, action_id, project_id, evaluator_name, evaluator_version, score,
237
+ passed, reasoning, rubric_json
238
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
239
+ ON CONFLICT(id) DO UPDATE SET
240
+ action_id = excluded.action_id,
241
+ project_id = excluded.project_id,
242
+ evaluator_name = excluded.evaluator_name,
243
+ evaluator_version = excluded.evaluator_version,
244
+ score = excluded.score,
245
+ passed = excluded.passed,
246
+ reasoning = excluded.reasoning,
247
+ rubric_json = excluded.rubric_json
248
+ `)
249
+ .bind(
250
+ result.id,
251
+ result.actionId,
252
+ result.projectId,
253
+ result.evaluatorName,
254
+ result.evaluatorVersion,
255
+ result.score,
256
+ result.passed ? 1 : 0,
257
+ result.reasoning,
258
+ result.rubricJson,
259
+ )
260
+ .run();
261
+ }
262
+
263
+ async upsertArtifact(artifact: ArtifactRecord): Promise<void> {
264
+ await this.db
265
+ .prepare(`
266
+ INSERT INTO artifacts (
267
+ id, action_id, project_id, artifact_name, artifact_type, storage_ref,
268
+ size_bytes, sha256_hash, content_preview
269
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
270
+ ON CONFLICT(id) DO UPDATE SET
271
+ action_id = excluded.action_id,
272
+ project_id = excluded.project_id,
273
+ artifact_name = excluded.artifact_name,
274
+ artifact_type = excluded.artifact_type,
275
+ storage_ref = excluded.storage_ref,
276
+ size_bytes = excluded.size_bytes,
277
+ sha256_hash = excluded.sha256_hash,
278
+ content_preview = excluded.content_preview
279
+ `)
280
+ .bind(
281
+ artifact.id,
282
+ artifact.actionId,
283
+ artifact.projectId,
284
+ artifact.artifactName,
285
+ artifact.artifactType,
286
+ artifact.storageRef,
287
+ artifact.sizeBytes,
288
+ artifact.sha256Hash,
289
+ artifact.contentPreview,
290
+ )
291
+ .run();
292
+ }
293
+
294
+ async getAction(
295
+ projectId: string,
296
+ actionId: string,
297
+ ): Promise<ActionRef | null> {
298
+ const row = await this.db
299
+ .prepare(`SELECT * FROM actions WHERE project_id = ? AND id = ? LIMIT 1`)
300
+ .bind(projectId, actionId)
301
+ .first<Parameters<typeof mapAction>[0]>();
302
+ return row ? mapAction(row) : null;
303
+ }
304
+
305
+ async listActionsByRoot(
306
+ projectId: string,
307
+ rootActionId: string,
308
+ limit = 100,
309
+ ): Promise<ActionRef[]> {
310
+ const rows = await this.db
311
+ .prepare(
312
+ `SELECT * FROM actions
313
+ WHERE project_id = ? AND root_action_id = ?
314
+ ORDER BY started_at ASC LIMIT ?`,
315
+ )
316
+ .bind(projectId, rootActionId, limit)
317
+ .all<Parameters<typeof mapAction>[0]>();
318
+ return rows.results.map(mapAction);
319
+ }
320
+
321
+ async getAgentRun(
322
+ projectId: string,
323
+ agentRunId: string,
324
+ ): Promise<AgentRunRef | null> {
325
+ const row = await this.db
326
+ .prepare(
327
+ `SELECT * FROM agent_runs WHERE project_id = ? AND id = ? LIMIT 1`,
328
+ )
329
+ .bind(projectId, agentRunId)
330
+ .first<Parameters<typeof mapAgentRun>[0]>();
331
+ return row ? mapAgentRun(row) : null;
332
+ }
333
+
334
+ async listToolCallsByAction(
335
+ projectId: string,
336
+ actionId: string,
337
+ limit = 100,
338
+ ): Promise<ToolCallRef[]> {
339
+ const rows = await this.db
340
+ .prepare(
341
+ `SELECT * FROM tool_calls
342
+ WHERE project_id = ? AND action_id = ?
343
+ LIMIT ?`,
344
+ )
345
+ .bind(projectId, actionId, limit)
346
+ .all<Parameters<typeof mapToolCall>[0]>();
347
+ return rows.results.map(mapToolCall);
348
+ }
349
+
350
+ async listRetrievalEventsByAction(
351
+ projectId: string,
352
+ actionId: string,
353
+ limit = 100,
354
+ ): Promise<RetrievalEventRef[]> {
355
+ const rows = await this.db
356
+ .prepare(
357
+ `SELECT * FROM retrieval_events
358
+ WHERE project_id = ? AND action_id = ?
359
+ LIMIT ?`,
360
+ )
361
+ .bind(projectId, actionId, limit)
362
+ .all<Parameters<typeof mapRetrievalEvent>[0]>();
363
+ return rows.results.map(mapRetrievalEvent);
364
+ }
365
+
366
+ async listEvalResultsByAction(
367
+ projectId: string,
368
+ actionId: string,
369
+ limit = 100,
370
+ ): Promise<EvalResultRef[]> {
371
+ const rows = await this.db
372
+ .prepare(
373
+ `SELECT * FROM eval_results
374
+ WHERE project_id = ? AND action_id = ?
375
+ LIMIT ?`,
376
+ )
377
+ .bind(projectId, actionId, limit)
378
+ .all<Parameters<typeof mapEvalResult>[0]>();
379
+ return rows.results.map(mapEvalResult);
380
+ }
381
+
382
+ async listArtifactsByAction(
383
+ projectId: string,
384
+ actionId: string,
385
+ limit = 100,
386
+ ): Promise<ArtifactRef[]> {
387
+ const rows = await this.db
388
+ .prepare(
389
+ `SELECT * FROM artifacts
390
+ WHERE project_id = ? AND action_id = ?
391
+ LIMIT ?`,
392
+ )
393
+ .bind(projectId, actionId, limit)
394
+ .all<Parameters<typeof mapArtifact>[0]>();
395
+ return rows.results.map(mapArtifact);
396
+ }
397
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Vendor pricing table for computing llm.cost.total_usd from token counts
3
+ * when a span didn't report cost directly. Conservative — only covers the
4
+ * models we see most often; unknown models get $0 and we log once per model.
5
+ *
6
+ * Prices are USD per 1M tokens. Keep in sync with vendor rate cards.
7
+ * Values as of mid-2026; when stale, bump and add a migration note.
8
+ */
9
+
10
+ export interface ModelPricing {
11
+ inputPer1M: number;
12
+ outputPer1M: number;
13
+ }
14
+
15
+ const PRICING: Record<string, ModelPricing> = {
16
+ // OpenAI
17
+ "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
18
+ "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
19
+ "gpt-4-turbo": { inputPer1M: 10, outputPer1M: 30 },
20
+ "gpt-4": { inputPer1M: 30, outputPer1M: 60 },
21
+ "gpt-3.5-turbo": { inputPer1M: 0.5, outputPer1M: 1.5 },
22
+ o1: { inputPer1M: 15, outputPer1M: 60 },
23
+ "o1-mini": { inputPer1M: 3, outputPer1M: 12 },
24
+
25
+ // Anthropic
26
+ "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15 },
27
+ "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4 },
28
+ "claude-3-opus": { inputPer1M: 15, outputPer1M: 75 },
29
+ "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15 },
30
+ "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25 },
31
+ "claude-opus-4": { inputPer1M: 15, outputPer1M: 75 },
32
+ "claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15 },
33
+
34
+ // Google
35
+ "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5 },
36
+ "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3 },
37
+ "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 },
38
+ };
39
+
40
+ // Resolve a model identifier to its pricing by longest-prefix match so
41
+ // `gpt-4o-2024-11-20` hits `gpt-4o`, `claude-3-5-sonnet-20240620` hits
42
+ // `claude-3-5-sonnet`, etc.
43
+ const lookupCache = new Map<string, ModelPricing | null>();
44
+
45
+ export function getModelPricing(
46
+ model: string | null | undefined,
47
+ ): ModelPricing | null {
48
+ if (!model) return null;
49
+ const normalized = model.toLowerCase();
50
+ const cached = lookupCache.get(normalized);
51
+ if (cached !== undefined) return cached;
52
+
53
+ let best: { key: string; pricing: ModelPricing } | null = null;
54
+ for (const [key, pricing] of Object.entries(PRICING)) {
55
+ const matchesModel = normalized === key || normalized.startsWith(`${key}-`);
56
+ if (matchesModel && (!best || key.length > best.key.length)) {
57
+ best = { key, pricing };
58
+ }
59
+ }
60
+ const result = best?.pricing ?? null;
61
+ lookupCache.set(normalized, result);
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Compute USD cost from token counts using the pricing table. Returns null
67
+ * when the model is unknown — caller should fall back to the cost attribute
68
+ * on the span (if any) rather than reporting $0.
69
+ */
70
+ export function computeCost(
71
+ model: string | null | undefined,
72
+ promptTokens: number | null | undefined,
73
+ completionTokens: number | null | undefined,
74
+ ): number | null {
75
+ const pricing = getModelPricing(model);
76
+ if (!pricing) return null;
77
+ const input = ((promptTokens ?? 0) * pricing.inputPer1M) / 1_000_000;
78
+ const output = ((completionTokens ?? 0) * pricing.outputPer1M) / 1_000_000;
79
+ return input + output;
80
+ }