@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,1172 @@
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_KIND_KEY,
7
+ ACTION_MODEL_NAME_KEY,
8
+ ACTION_NAME_KEY,
9
+ ACTION_PROMPT_VERSION_KEY,
10
+ ACTION_PROVIDER_KEY,
11
+ ACTION_ROOT_ID_KEY,
12
+ ACTION_TOTAL_COST_USD_KEY,
13
+ ACTOR_ID_KEY,
14
+ ACTOR_TYPE_KEY,
15
+ AGENT_AUTONOMY_LEVEL_KEY,
16
+ AGENT_GOAL_KEY,
17
+ AGENT_ID_KEY,
18
+ AGENT_NAME_KEY,
19
+ AGENT_OUTCOME_KEY,
20
+ AGENT_RUN_ID_KEY,
21
+ AGENT_STEP_ID_KEY,
22
+ AGENT_VERSION_KEY,
23
+ AI_PAYLOAD_INPUT_KEY,
24
+ AI_PAYLOAD_OUTPUT_KEY,
25
+ ARTIFACT_CONTENT_KEY,
26
+ ARTIFACT_NAME_KEY,
27
+ ARTIFACT_SHA256_HASH_KEY,
28
+ ARTIFACT_SIZE_BYTES_KEY,
29
+ ARTIFACT_STORAGE_REF_KEY,
30
+ ARTIFACT_TYPE_KEY,
31
+ EVAL_EVALUATOR_NAME_KEY,
32
+ EVAL_EVALUATOR_VERSION_KEY,
33
+ EVAL_PASSED_KEY,
34
+ EVAL_REASONING_KEY,
35
+ EVAL_RUBRIC_KEY,
36
+ EVAL_SCORE_KEY,
37
+ OPENINFERENCE_SPAN_KIND_KEY,
38
+ RETRIEVAL_DOCUMENTS_KEY,
39
+ RETRIEVAL_MAX_RELEVANCE_SCORE_KEY,
40
+ RETRIEVAL_NAME_KEY,
41
+ RETRIEVAL_QUERY_KEY,
42
+ RETRIEVAL_TOTAL_RESULTS_KEY,
43
+ TOOL_APPROVAL_STATE_KEY,
44
+ TOOL_ARGS_KEY,
45
+ TOOL_CALL_ID_KEY,
46
+ TOOL_ERROR_TYPE_KEY,
47
+ TOOL_NAME_KEY,
48
+ TOOL_RESULT_KEY,
49
+ TOOL_SIDE_EFFECT_KEY,
50
+ } from "@obsunified/types/constants";
51
+ import type { CollectorPlugin } from "../framework/collector";
52
+ import { resolveActionIdentity } from "../lib/action-identity";
53
+ import { sha256Hex } from "../lib/hash";
54
+ import { parseJsonRecord } from "../lib/json";
55
+ import { isPayloadCaptureEnabled } from "../lib/payload-capture";
56
+ import { type SqlStatement, sqlDbFor } from "../lib/sql-db";
57
+
58
+ import { enricherPlugins } from "./action-graph-processor/enrichers";
59
+ import { runRedaction } from "./action-graph-processor/redaction";
60
+
61
+ const firstAttr = (
62
+ attrs: Record<string, JsonValue>,
63
+ ...keys: string[]
64
+ ): JsonValue | undefined => {
65
+ for (const key of keys) {
66
+ const value = attrs[key];
67
+ if (value !== undefined && value !== null) return value;
68
+ }
69
+ return undefined;
70
+ };
71
+
72
+ const firstStringAttr = (
73
+ attrs: Record<string, JsonValue>,
74
+ ...keys: string[]
75
+ ): string | undefined => {
76
+ const value = firstAttr(attrs, ...keys);
77
+ return typeof value === "string" ? value : undefined;
78
+ };
79
+
80
+ const firstNumberAttr = (
81
+ attrs: Record<string, JsonValue>,
82
+ ...keys: string[]
83
+ ): number | null => {
84
+ const value = firstAttr(attrs, ...keys);
85
+ if (typeof value === "number" && Number.isFinite(value)) return value;
86
+ if (typeof value === "string") {
87
+ const parsed = Number(value);
88
+ if (Number.isFinite(parsed)) return parsed;
89
+ }
90
+ return null;
91
+ };
92
+
93
+ const toFiniteNumber = (value: unknown): number | null => {
94
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
95
+ if (typeof value === "string") {
96
+ const parsed = Number(value);
97
+ return Number.isFinite(parsed) ? parsed : null;
98
+ }
99
+ return null;
100
+ };
101
+
102
+ const firstBoolLikeAttr = (
103
+ attrs: Record<string, JsonValue>,
104
+ ...keys: string[]
105
+ ): boolean => {
106
+ const value = firstAttr(attrs, ...keys);
107
+ return value === true || value === 1 || value === "1" || value === "true";
108
+ };
109
+
110
+ const firstJsonAttr = (
111
+ attrs: Record<string, JsonValue>,
112
+ ...keys: string[]
113
+ ): string | null => {
114
+ const value = firstAttr(attrs, ...keys);
115
+ if (value === undefined || value === null) return null;
116
+ if (typeof value === "string") {
117
+ const trimmed = value.trim();
118
+ return trimmed.length > 0 ? trimmed : null;
119
+ }
120
+ return JSON.stringify(value);
121
+ };
122
+
123
+ const parseJsonObject = (raw: string): Record<string, JsonValue> | null => {
124
+ try {
125
+ const parsed = JSON.parse(raw);
126
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
127
+ return parsed as Record<string, JsonValue>;
128
+ }
129
+ } catch {
130
+ return null;
131
+ }
132
+ return null;
133
+ };
134
+
135
+ const normalizeMcpAuditJson = (
136
+ attrs: Record<string, JsonValue>,
137
+ ): string | null => {
138
+ const raw = firstJsonAttr(
139
+ attrs,
140
+ "obs.mcp.audit",
141
+ "obs.mcp.audit_json",
142
+ "mcp.audit",
143
+ "mcp.audit_json",
144
+ );
145
+ if (!raw) return null;
146
+
147
+ const parsed = parseJsonObject(raw);
148
+ if (!parsed) return null;
149
+
150
+ const presentFields = Array.isArray(parsed.presentFields)
151
+ ? parsed.presentFields.filter(
152
+ (field): field is string => typeof field === "string",
153
+ )
154
+ : null;
155
+ const allowedFields =
156
+ parsed.allowedFields &&
157
+ typeof parsed.allowedFields === "object" &&
158
+ !Array.isArray(parsed.allowedFields)
159
+ ? (parsed.allowedFields as Record<string, JsonValue>)
160
+ : null;
161
+
162
+ if (!presentFields || !allowedFields) return null;
163
+
164
+ const redactedFields = Array.isArray(parsed.redactedFields)
165
+ ? parsed.redactedFields.filter(
166
+ (field): field is string => typeof field === "string",
167
+ )
168
+ : [];
169
+ const droppedFields = Array.isArray(parsed.droppedFields)
170
+ ? parsed.droppedFields.filter(
171
+ (field): field is string => typeof field === "string",
172
+ )
173
+ : [];
174
+ const hashedFields =
175
+ parsed.hashedFields &&
176
+ typeof parsed.hashedFields === "object" &&
177
+ !Array.isArray(parsed.hashedFields)
178
+ ? (parsed.hashedFields as Record<string, JsonValue>)
179
+ : {};
180
+
181
+ return JSON.stringify({
182
+ schemaVersion: toFiniteNumber(parsed.schemaVersion) ?? 1,
183
+ presentFields,
184
+ allowedFields,
185
+ redactedFields,
186
+ hashedFields,
187
+ droppedFields,
188
+ hasRawMeta: parsed.hasRawMeta === true,
189
+ });
190
+ };
191
+
192
+ const normalizeMcpAuditEnvelope = (
193
+ attrs: Record<string, JsonValue>,
194
+ ): Record<string, JsonValue> | null => {
195
+ const json = normalizeMcpAuditJson(attrs);
196
+ if (!json) return null;
197
+ return parseJsonObject(json);
198
+ };
199
+
200
+ const normalizeMutationArtifactId = (
201
+ attrs: Record<string, JsonValue>,
202
+ ): string | null =>
203
+ firstStringAttr(
204
+ attrs,
205
+ "obs.tool_call.mutation.artifact_id",
206
+ "obs.tool.mutation.artifact_id",
207
+ "tool.mutation.artifact_id",
208
+ ) ?? null;
209
+
210
+ const EVIDENCE_ATTR_KEYS = [
211
+ "obs.mcp.audit",
212
+ "obs.mcp.audit_json",
213
+ "mcp.audit",
214
+ "mcp.audit_json",
215
+ "obs.tool_call.mutation.before",
216
+ "obs.tool_call.mutation.after",
217
+ "obs.tool_call.mutation.diff",
218
+ "obs.tool.mutation.before",
219
+ "obs.tool.mutation.after",
220
+ "obs.tool.mutation.diff",
221
+ "tool.mutation.before",
222
+ "tool.mutation.after",
223
+ "tool.mutation.diff",
224
+ ];
225
+
226
+ const PAYLOAD_ATTR_KEYS = [
227
+ AI_PAYLOAD_INPUT_KEY,
228
+ AI_PAYLOAD_OUTPUT_KEY,
229
+ AGENT_GOAL_KEY,
230
+ "obs.agent_run.goal",
231
+ AGENT_OUTCOME_KEY,
232
+ "obs.agent_run.outcome",
233
+ TOOL_ARGS_KEY,
234
+ "obs.tool_call.args",
235
+ TOOL_RESULT_KEY,
236
+ "obs.tool_call.result",
237
+ RETRIEVAL_QUERY_KEY,
238
+ RETRIEVAL_DOCUMENTS_KEY,
239
+ ARTIFACT_CONTENT_KEY,
240
+ EVAL_REASONING_KEY,
241
+ EVAL_RUBRIC_KEY,
242
+ ...EVIDENCE_ATTR_KEYS,
243
+ ];
244
+
245
+ const stripPayloadAttrs = (
246
+ attrs: Record<string, JsonValue>,
247
+ ): Record<string, JsonValue> => {
248
+ const next = { ...attrs };
249
+ for (const key of PAYLOAD_ATTR_KEYS) {
250
+ delete next[key];
251
+ }
252
+ return next;
253
+ };
254
+
255
+ export type { ActionEnricherPlugin } from "./action-graph-processor/enrichers";
256
+ export {
257
+ clearActionEnricherPlugins,
258
+ registerActionEnricherPlugin,
259
+ } from "./action-graph-processor/enrichers";
260
+ export type {
261
+ PayloadRedactorPlugin,
262
+ RedactionContext,
263
+ } from "./action-graph-processor/redaction";
264
+ export {
265
+ clearRedactionPlugins,
266
+ registerRedactionPlugin,
267
+ runRedaction,
268
+ } from "./action-graph-processor/redaction";
269
+
270
+ export const actionGraphProcessorPlugin: CollectorPlugin = {
271
+ name: "action-graph-processor",
272
+ register(_app, runtime) {
273
+ runtime.addSpanProcessor({
274
+ name: "action-graph-processor",
275
+ async process(spans, context) {
276
+ const db = sqlDbFor(context.env);
277
+ const captureByProject = new Map<string, boolean>();
278
+ const projectCaptureEnabled = async (projectId: string) => {
279
+ const cached = captureByProject.get(projectId);
280
+ if (cached !== undefined) return cached;
281
+ const enabled = await isPayloadCaptureEnabled(
282
+ db,
283
+ projectId,
284
+ context.env,
285
+ );
286
+ captureByProject.set(projectId, enabled);
287
+ return enabled;
288
+ };
289
+
290
+ const actionsToInsert: Record<string, unknown>[] = [];
291
+ const agentRunsToInsert: Record<string, unknown>[] = [];
292
+ const toolCallsToInsert: Record<string, unknown>[] = [];
293
+ const retrievalsToInsert: Record<string, unknown>[] = [];
294
+ const evalsToInsert: Record<string, unknown>[] = [];
295
+ const artifactsToInsert: Record<string, unknown>[] = [];
296
+
297
+ const transformed = await Promise.all(
298
+ spans.map(async (span): Promise<StoredSpan> => {
299
+ const attrs = parseJsonRecord(span.attributesJson);
300
+ const capturePayloads = await projectCaptureEnabled(span.projectId);
301
+
302
+ const obsActionId = attrs[ACTION_ID_KEY];
303
+ const openInferenceKind = attrs[OPENINFERENCE_SPAN_KIND_KEY];
304
+ const genAiAgentId = attrs["gen_ai.agent.id"];
305
+
306
+ const isActionSpan =
307
+ obsActionId !== undefined ||
308
+ openInferenceKind !== undefined ||
309
+ genAiAgentId !== undefined;
310
+ if (!isActionSpan) return span;
311
+
312
+ const actionKind =
313
+ firstStringAttr(attrs, ACTION_KIND_KEY) ??
314
+ (openInferenceKind as string) ??
315
+ "agent.step";
316
+ const identity = await resolveActionIdentity(span, {
317
+ ...attrs,
318
+ [ACTION_KIND_KEY]: actionKind,
319
+ });
320
+ const actionId = identity.actionId;
321
+ const rootActionId = identity.rootActionId;
322
+ const causedByActionId = identity.causedByActionId;
323
+ const trustedAttrs: Record<string, JsonValue> = {
324
+ ...attrs,
325
+ [ACTION_ID_KEY]: actionId,
326
+ [ACTION_ROOT_ID_KEY]: rootActionId,
327
+ [ACTION_CONFIDENCE_KEY]: identity.confidence,
328
+ };
329
+ const mcpAuditEnvelope = normalizeMcpAuditEnvelope(attrs);
330
+ if (mcpAuditEnvelope) {
331
+ trustedAttrs["obs.mcp.audit_envelope"] = mcpAuditEnvelope;
332
+ }
333
+ for (const key of EVIDENCE_ATTR_KEYS) {
334
+ delete trustedAttrs[key];
335
+ }
336
+ delete trustedAttrs["obs.action.agent_run_id"];
337
+ delete trustedAttrs["obs.agent_run.id"];
338
+ if (causedByActionId !== undefined) {
339
+ trustedAttrs[ACTION_CAUSED_BY_ID_KEY] = causedByActionId;
340
+ }
341
+ const actorType =
342
+ firstStringAttr(attrs, ACTOR_TYPE_KEY, "obs.action.actor_type") ??
343
+ "agent";
344
+ const actorId =
345
+ firstStringAttr(attrs, ACTOR_ID_KEY, "obs.action.actor_id") ??
346
+ (genAiAgentId as string) ??
347
+ null;
348
+ const name =
349
+ firstStringAttr(attrs, ACTION_NAME_KEY) ?? span.spanName;
350
+ const status = span.statusCode === 2 ? "error" : "ok";
351
+ const startedAt = span.startTime;
352
+ const endedAt = span.endTime;
353
+ const durationMs = span.durationMs;
354
+ const traceId = span.traceId;
355
+ const spanId = span.spanId;
356
+ const sessionId =
357
+ span.sessionId ?? (attrs["session.id"] as string) ?? null;
358
+ const interactionId =
359
+ span.interactionId ?? (attrs["interaction.id"] as string) ?? null;
360
+ const userId =
361
+ (attrs["user.id"] as string) ??
362
+ (attrs["enduser.id"] as string) ??
363
+ (attrs["obs.user.id"] as string) ??
364
+ null;
365
+
366
+ const agentRunId =
367
+ identity.agentRunId ??
368
+ (actionKind === "agent.run" || actionKind === "agent"
369
+ ? actionId
370
+ : null);
371
+ if (agentRunId !== null) {
372
+ trustedAttrs[AGENT_RUN_ID_KEY] = agentRunId;
373
+ } else {
374
+ delete trustedAttrs[AGENT_RUN_ID_KEY];
375
+ }
376
+
377
+ const stepId =
378
+ firstStringAttr(attrs, AGENT_STEP_ID_KEY, "obs.action.step_id") ??
379
+ null;
380
+ const toolCallId =
381
+ firstStringAttr(
382
+ attrs,
383
+ TOOL_CALL_ID_KEY,
384
+ "obs.action.tool_call_id",
385
+ ) ?? null;
386
+ const promptVersion =
387
+ firstStringAttr(attrs, ACTION_PROMPT_VERSION_KEY) ?? null;
388
+ const modelName =
389
+ firstStringAttr(attrs, ACTION_MODEL_NAME_KEY) ??
390
+ (attrs["llm.model_name"] as string) ??
391
+ null;
392
+ const provider =
393
+ firstStringAttr(attrs, ACTION_PROVIDER_KEY) ??
394
+ (attrs["llm.provider"] as string) ??
395
+ null;
396
+ const totalCostUsd =
397
+ firstNumberAttr(attrs, ACTION_TOTAL_COST_USD_KEY) ??
398
+ firstNumberAttr(
399
+ attrs,
400
+ "llm.cost.total_usd",
401
+ "gen_ai.usage.cost_usd",
402
+ "llm.total_cost_usd",
403
+ );
404
+
405
+ const persistedAttrs = capturePayloads
406
+ ? trustedAttrs
407
+ : stripPayloadAttrs(trustedAttrs);
408
+ const attrsJson = JSON.stringify(persistedAttrs);
409
+
410
+ const actionRecord = {
411
+ id: actionId,
412
+ projectId: span.projectId,
413
+ rootActionId,
414
+ causedByActionId,
415
+ actorType,
416
+ actorId,
417
+ actionKind,
418
+ name,
419
+ status,
420
+ startedAt,
421
+ endedAt,
422
+ durationMs,
423
+ traceId,
424
+ spanId,
425
+ sessionId,
426
+ interactionId,
427
+ userId,
428
+ agentRunId,
429
+ stepId,
430
+ toolCallId,
431
+ promptVersion,
432
+ modelName,
433
+ provider,
434
+ totalCostUsd,
435
+ attrsJson,
436
+ };
437
+
438
+ for (const plugin of enricherPlugins) {
439
+ if (plugin.enrichActionRecord) {
440
+ try {
441
+ await plugin.enrichActionRecord(
442
+ actionRecord,
443
+ span,
444
+ trustedAttrs,
445
+ );
446
+ } catch (err) {
447
+ console.error(
448
+ `[action-enricher-plugin:${plugin.name}] enrichActionRecord failed`,
449
+ err,
450
+ );
451
+ }
452
+ }
453
+ }
454
+
455
+ actionsToInsert.push(actionRecord);
456
+
457
+ // 1. Agent Runs
458
+ const isAgentRun =
459
+ actionKind === "agent.run" ||
460
+ actionKind === "agent" ||
461
+ firstAttr(attrs, AGENT_ID_KEY, "obs.agent_run.agent_id") !==
462
+ undefined;
463
+ if (isAgentRun) {
464
+ const agentRunRecord = {
465
+ id: actionId,
466
+ projectId: span.projectId,
467
+ agentId:
468
+ firstStringAttr(
469
+ attrs,
470
+ AGENT_ID_KEY,
471
+ "obs.agent_run.agent_id",
472
+ ) ??
473
+ (genAiAgentId as string) ??
474
+ "default-agent",
475
+ agentName:
476
+ firstStringAttr(
477
+ attrs,
478
+ AGENT_NAME_KEY,
479
+ "obs.agent_run.agent_name",
480
+ ) ?? span.spanName,
481
+ agentVersion:
482
+ firstStringAttr(
483
+ attrs,
484
+ AGENT_VERSION_KEY,
485
+ "obs.agent_run.agent_version",
486
+ ) ?? "1.0.0",
487
+ goal: capturePayloads
488
+ ? (firstStringAttr(
489
+ attrs,
490
+ AGENT_GOAL_KEY,
491
+ "obs.agent_run.goal",
492
+ ) ??
493
+ (attrs[AI_PAYLOAD_INPUT_KEY] as string) ??
494
+ null)
495
+ : null,
496
+ outcome: capturePayloads
497
+ ? (firstStringAttr(
498
+ attrs,
499
+ AGENT_OUTCOME_KEY,
500
+ "obs.agent_run.outcome",
501
+ ) ??
502
+ (attrs[AI_PAYLOAD_OUTPUT_KEY] as string) ??
503
+ null)
504
+ : null,
505
+ autonomyLevel:
506
+ firstStringAttr(
507
+ attrs,
508
+ AGENT_AUTONOMY_LEVEL_KEY,
509
+ "obs.agent_run.autonomy_level",
510
+ ) ?? "autonomous_write",
511
+ status: span.statusCode === 2 ? "failed" : "success",
512
+ errorMessage: span.statusMessage ?? null,
513
+ totalCostUsd: totalCostUsd ?? 0.0,
514
+ totalDurationMs: span.durationMs,
515
+ metadataJson: attrsJson,
516
+ };
517
+
518
+ for (const plugin of enricherPlugins) {
519
+ if (plugin.enrichAgentRunRecord) {
520
+ try {
521
+ await plugin.enrichAgentRunRecord(
522
+ agentRunRecord,
523
+ span,
524
+ trustedAttrs,
525
+ );
526
+ } catch (err) {
527
+ console.error(
528
+ `[action-enricher-plugin:${plugin.name}] enrichAgentRunRecord failed`,
529
+ err,
530
+ );
531
+ }
532
+ }
533
+ }
534
+
535
+ agentRunsToInsert.push(agentRunRecord);
536
+ }
537
+
538
+ // 2. Tool Calls
539
+ const isToolCall =
540
+ actionKind === "tool" ||
541
+ actionKind === "tool.call" ||
542
+ firstAttr(attrs, TOOL_NAME_KEY, "obs.tool_call.tool_name") !==
543
+ undefined;
544
+ if (isToolCall) {
545
+ const rawArgs =
546
+ firstStringAttr(attrs, TOOL_ARGS_KEY, "obs.tool_call.args") ??
547
+ (attrs[AI_PAYLOAD_INPUT_KEY] as string) ??
548
+ "{}";
549
+ const rawResult =
550
+ firstStringAttr(
551
+ attrs,
552
+ TOOL_RESULT_KEY,
553
+ "obs.tool_call.result",
554
+ ) ??
555
+ (attrs[AI_PAYLOAD_OUTPUT_KEY] as string) ??
556
+ "{}";
557
+
558
+ const argsHash = await sha256Hex(rawArgs);
559
+ const resultHash = await sha256Hex(rawResult);
560
+
561
+ const redactedArgs = capturePayloads
562
+ ? await runRedaction(rawArgs, {
563
+ projectId: span.projectId,
564
+ actionId,
565
+ traceId: span.traceId,
566
+ spanId: span.spanId,
567
+ kind: "tool_call",
568
+ fieldName: "args",
569
+ })
570
+ : null;
571
+ const redactedResult = capturePayloads
572
+ ? await runRedaction(rawResult, {
573
+ projectId: span.projectId,
574
+ actionId,
575
+ traceId: span.traceId,
576
+ spanId: span.spanId,
577
+ kind: "tool_call",
578
+ fieldName: "result",
579
+ })
580
+ : null;
581
+
582
+ const toolCallRecord = {
583
+ id: span.spanId,
584
+ actionId,
585
+ projectId: span.projectId,
586
+ toolName:
587
+ firstStringAttr(
588
+ attrs,
589
+ TOOL_NAME_KEY,
590
+ "obs.tool_call.tool_name",
591
+ ) ?? span.spanName,
592
+ argsHash,
593
+ resultHash,
594
+ errorType:
595
+ firstStringAttr(
596
+ attrs,
597
+ TOOL_ERROR_TYPE_KEY,
598
+ "obs.tool_call.error_type",
599
+ ) ??
600
+ span.statusMessage ??
601
+ null,
602
+ sideEffect: firstBoolLikeAttr(
603
+ attrs,
604
+ TOOL_SIDE_EFFECT_KEY,
605
+ "obs.tool_call.side_effect",
606
+ )
607
+ ? 1
608
+ : 0,
609
+ approvalState:
610
+ firstStringAttr(
611
+ attrs,
612
+ TOOL_APPROVAL_STATE_KEY,
613
+ "obs.tool_call.approval_state",
614
+ ) ?? "suggested",
615
+ argsRedacted:
616
+ redactedArgs === null
617
+ ? null
618
+ : typeof redactedArgs === "string"
619
+ ? redactedArgs
620
+ : JSON.stringify(redactedArgs),
621
+ resultRedacted:
622
+ redactedResult === null
623
+ ? null
624
+ : typeof redactedResult === "string"
625
+ ? redactedResult
626
+ : JSON.stringify(redactedResult),
627
+ mcpAuditJson: normalizeMcpAuditJson(attrs),
628
+ mutationBeforeJson: firstJsonAttr(
629
+ attrs,
630
+ "obs.tool_call.mutation.before",
631
+ "obs.tool.mutation.before",
632
+ "tool.mutation.before",
633
+ ),
634
+ mutationAfterJson: firstJsonAttr(
635
+ attrs,
636
+ "obs.tool_call.mutation.after",
637
+ "obs.tool.mutation.after",
638
+ "tool.mutation.after",
639
+ ),
640
+ mutationDiffJson: firstJsonAttr(
641
+ attrs,
642
+ "obs.tool_call.mutation.diff",
643
+ "obs.tool.mutation.diff",
644
+ "tool.mutation.diff",
645
+ ),
646
+ mutationArtifactId: normalizeMutationArtifactId(attrs),
647
+ };
648
+
649
+ for (const plugin of enricherPlugins) {
650
+ if (plugin.enrichToolCallRecord) {
651
+ try {
652
+ await plugin.enrichToolCallRecord(
653
+ toolCallRecord,
654
+ span,
655
+ trustedAttrs,
656
+ );
657
+ } catch (err) {
658
+ console.error(
659
+ `[action-enricher-plugin:${plugin.name}] enrichToolCallRecord failed`,
660
+ err,
661
+ );
662
+ }
663
+ }
664
+ }
665
+
666
+ toolCallsToInsert.push(toolCallRecord);
667
+ }
668
+
669
+ // 3. Retrieval Events
670
+ const isRetrieval =
671
+ actionKind === "retriever" ||
672
+ actionKind === "retrieval" ||
673
+ attrs[RETRIEVAL_NAME_KEY] !== undefined;
674
+ if (isRetrieval) {
675
+ const rawQuery =
676
+ firstStringAttr(attrs, RETRIEVAL_QUERY_KEY) ??
677
+ (attrs[AI_PAYLOAD_INPUT_KEY] as string) ??
678
+ "";
679
+ const rawDocs =
680
+ firstStringAttr(attrs, RETRIEVAL_DOCUMENTS_KEY) ??
681
+ (attrs[AI_PAYLOAD_OUTPUT_KEY] as string) ??
682
+ "[]";
683
+
684
+ const queryHash = await sha256Hex(rawQuery);
685
+ const redactedDocs = capturePayloads
686
+ ? await runRedaction(rawDocs, {
687
+ projectId: span.projectId,
688
+ actionId,
689
+ traceId: span.traceId,
690
+ spanId: span.spanId,
691
+ kind: "retrieval",
692
+ fieldName: "documents",
693
+ })
694
+ : null;
695
+
696
+ const retrievalRecord = {
697
+ id: span.spanId,
698
+ actionId,
699
+ projectId: span.projectId,
700
+ retrieverName:
701
+ firstStringAttr(attrs, RETRIEVAL_NAME_KEY) ?? span.spanName,
702
+ queryHash,
703
+ documentsJson:
704
+ redactedDocs === null
705
+ ? null
706
+ : typeof redactedDocs === "string"
707
+ ? redactedDocs
708
+ : JSON.stringify(redactedDocs),
709
+ totalResults:
710
+ firstNumberAttr(attrs, RETRIEVAL_TOTAL_RESULTS_KEY) ?? 0,
711
+ maxRelevanceScore: firstNumberAttr(
712
+ attrs,
713
+ RETRIEVAL_MAX_RELEVANCE_SCORE_KEY,
714
+ ),
715
+ durationMs: span.durationMs,
716
+ };
717
+
718
+ for (const plugin of enricherPlugins) {
719
+ if (plugin.enrichRetrievalRecord) {
720
+ try {
721
+ await plugin.enrichRetrievalRecord(
722
+ retrievalRecord,
723
+ span,
724
+ trustedAttrs,
725
+ );
726
+ } catch (err) {
727
+ console.error(
728
+ `[action-enricher-plugin:${plugin.name}] enrichRetrievalRecord failed`,
729
+ err,
730
+ );
731
+ }
732
+ }
733
+ }
734
+
735
+ retrievalsToInsert.push(retrievalRecord);
736
+ }
737
+
738
+ // 4. Evaluation Results
739
+ const isEval =
740
+ actionKind === "evaluator" ||
741
+ actionKind === "eval" ||
742
+ attrs[EVAL_EVALUATOR_NAME_KEY] !== undefined;
743
+ if (isEval) {
744
+ const evalRecord = {
745
+ id: span.spanId,
746
+ actionId,
747
+ projectId: span.projectId,
748
+ evaluatorName:
749
+ firstStringAttr(attrs, EVAL_EVALUATOR_NAME_KEY) ??
750
+ span.spanName,
751
+ evaluatorVersion:
752
+ firstStringAttr(attrs, EVAL_EVALUATOR_VERSION_KEY) ?? "1.0.0",
753
+ score: firstNumberAttr(attrs, EVAL_SCORE_KEY),
754
+ passed:
755
+ attrs[EVAL_PASSED_KEY] !== undefined
756
+ ? firstBoolLikeAttr(attrs, EVAL_PASSED_KEY)
757
+ ? 1
758
+ : 0
759
+ : 1,
760
+ reasoning: firstStringAttr(attrs, EVAL_REASONING_KEY) ?? null,
761
+ rubricJson: firstStringAttr(attrs, EVAL_RUBRIC_KEY) ?? null,
762
+ };
763
+
764
+ for (const plugin of enricherPlugins) {
765
+ if (plugin.enrichEvalRecord) {
766
+ try {
767
+ await plugin.enrichEvalRecord(
768
+ evalRecord,
769
+ span,
770
+ trustedAttrs,
771
+ );
772
+ } catch (err) {
773
+ console.error(
774
+ `[action-enricher-plugin:${plugin.name}] enrichEvalRecord failed`,
775
+ err,
776
+ );
777
+ }
778
+ }
779
+ }
780
+
781
+ evalsToInsert.push(evalRecord);
782
+ }
783
+
784
+ // 5. Generated Artifacts
785
+ const hasArtifact = attrs[ARTIFACT_NAME_KEY] !== undefined;
786
+ if (hasArtifact) {
787
+ const content =
788
+ firstStringAttr(attrs, ARTIFACT_CONTENT_KEY) ?? "";
789
+ const redactedContent = capturePayloads
790
+ ? await runRedaction(content, {
791
+ projectId: span.projectId,
792
+ actionId,
793
+ traceId: span.traceId,
794
+ spanId: span.spanId,
795
+ kind: "artifact",
796
+ fieldName: "content",
797
+ })
798
+ : null;
799
+
800
+ const artifactRecord = {
801
+ id: span.spanId,
802
+ actionId,
803
+ projectId: span.projectId,
804
+ artifactName: attrs[ARTIFACT_NAME_KEY] as string,
805
+ artifactType:
806
+ firstStringAttr(attrs, ARTIFACT_TYPE_KEY) ?? "text",
807
+ storageRef:
808
+ firstStringAttr(attrs, ARTIFACT_STORAGE_REF_KEY) ?? null,
809
+ sizeBytes: firstNumberAttr(attrs, ARTIFACT_SIZE_BYTES_KEY),
810
+ sha256Hash:
811
+ firstStringAttr(attrs, ARTIFACT_SHA256_HASH_KEY) ??
812
+ (await sha256Hex(content)),
813
+ contentPreview:
814
+ redactedContent === null
815
+ ? null
816
+ : typeof redactedContent === "string"
817
+ ? redactedContent
818
+ : JSON.stringify(redactedContent),
819
+ };
820
+
821
+ for (const plugin of enricherPlugins) {
822
+ if (plugin.enrichArtifactRecord) {
823
+ try {
824
+ await plugin.enrichArtifactRecord(
825
+ artifactRecord,
826
+ span,
827
+ trustedAttrs,
828
+ );
829
+ } catch (err) {
830
+ console.error(
831
+ `[action-enricher-plugin:${plugin.name}] enrichArtifactRecord failed`,
832
+ err,
833
+ );
834
+ }
835
+ }
836
+ }
837
+
838
+ artifactsToInsert.push(artifactRecord);
839
+ }
840
+
841
+ // Downstream compatibility: ensure payload routing can join action_id.
842
+ return {
843
+ ...span,
844
+ attributesJson: JSON.stringify(trustedAttrs),
845
+ };
846
+ }),
847
+ );
848
+
849
+ const statements: SqlStatement[] = [];
850
+
851
+ if (agentRunsToInsert.length > 0 && actionsToInsert.length > 0) {
852
+ for (const run of agentRunsToInsert) {
853
+ const runId = String(run.id);
854
+ const childActions = actionsToInsert.filter(
855
+ (action) =>
856
+ action.rootActionId === runId ||
857
+ action.agentRunId === runId ||
858
+ action.id === runId,
859
+ );
860
+ const childCost = childActions
861
+ .filter((action) => action.id !== runId)
862
+ .reduce(
863
+ (sum, action) =>
864
+ sum + (toFiniteNumber(action.totalCostUsd) ?? 0),
865
+ 0,
866
+ );
867
+ const runOwnCost = toFiniteNumber(run.totalCostUsd) ?? 0;
868
+ run.totalCostUsd = Math.max(runOwnCost, childCost);
869
+
870
+ const starts = childActions
871
+ .map((action) =>
872
+ typeof action.startedAt === "string"
873
+ ? Date.parse(action.startedAt)
874
+ : NaN,
875
+ )
876
+ .filter(Number.isFinite);
877
+ const ends = childActions
878
+ .map((action) =>
879
+ typeof action.endedAt === "string"
880
+ ? Date.parse(action.endedAt)
881
+ : NaN,
882
+ )
883
+ .filter(Number.isFinite);
884
+ const wallClockMs =
885
+ starts.length > 0 && ends.length > 0
886
+ ? Math.max(...ends) - Math.min(...starts)
887
+ : null;
888
+ const maxActionDuration = childActions.reduce(
889
+ (max, action) =>
890
+ Math.max(max, toFiniteNumber(action.durationMs) ?? 0),
891
+ 0,
892
+ );
893
+ const existingDuration = toFiniteNumber(run.totalDurationMs) ?? 0;
894
+ run.totalDurationMs = Math.max(
895
+ existingDuration,
896
+ wallClockMs ?? 0,
897
+ maxActionDuration,
898
+ );
899
+ }
900
+ }
901
+
902
+ if (actionsToInsert.length > 0) {
903
+ const stmt = db.prepare(`
904
+ INSERT INTO actions (
905
+ id, project_id, root_action_id, caused_by_action_id, actor_type, actor_id,
906
+ action_kind, name, status, started_at, ended_at, duration_ms, trace_id,
907
+ span_id, session_id, interaction_id, user_id, agent_run_id, step_id,
908
+ tool_call_id, prompt_version, model_name, provider, total_cost_usd, attrs_json
909
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
910
+ ON CONFLICT (id) DO UPDATE SET
911
+ project_id = excluded.project_id,
912
+ root_action_id = excluded.root_action_id,
913
+ caused_by_action_id = excluded.caused_by_action_id,
914
+ actor_type = excluded.actor_type,
915
+ actor_id = excluded.actor_id,
916
+ action_kind = excluded.action_kind,
917
+ name = excluded.name,
918
+ status = excluded.status,
919
+ started_at = excluded.started_at,
920
+ ended_at = excluded.ended_at,
921
+ duration_ms = excluded.duration_ms,
922
+ trace_id = excluded.trace_id,
923
+ span_id = excluded.span_id,
924
+ session_id = excluded.session_id,
925
+ interaction_id = excluded.interaction_id,
926
+ user_id = excluded.user_id,
927
+ agent_run_id = excluded.agent_run_id,
928
+ step_id = excluded.step_id,
929
+ tool_call_id = excluded.tool_call_id,
930
+ prompt_version = excluded.prompt_version,
931
+ model_name = excluded.model_name,
932
+ provider = excluded.provider,
933
+ total_cost_usd = excluded.total_cost_usd,
934
+ attrs_json = excluded.attrs_json
935
+ `);
936
+ for (const r of actionsToInsert) {
937
+ statements.push(
938
+ stmt.bind(
939
+ r.id,
940
+ r.projectId,
941
+ r.rootActionId,
942
+ r.causedByActionId,
943
+ r.actorType,
944
+ r.actorId,
945
+ r.actionKind,
946
+ r.name,
947
+ r.status,
948
+ r.startedAt,
949
+ r.endedAt,
950
+ r.durationMs,
951
+ r.traceId,
952
+ r.spanId,
953
+ r.sessionId,
954
+ r.interactionId,
955
+ r.userId,
956
+ r.agentRunId,
957
+ r.stepId,
958
+ r.toolCallId,
959
+ r.promptVersion,
960
+ r.modelName,
961
+ r.provider,
962
+ r.totalCostUsd,
963
+ r.attrsJson,
964
+ ),
965
+ );
966
+ }
967
+ }
968
+
969
+ if (agentRunsToInsert.length > 0) {
970
+ const stmt = db.prepare(`
971
+ INSERT INTO agent_runs (
972
+ id, project_id, agent_id, agent_name, agent_version, goal, outcome,
973
+ autonomy_level, status, error_message, total_cost_usd, total_duration_ms, metadata_json
974
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
975
+ ON CONFLICT (id) DO UPDATE SET
976
+ project_id = excluded.project_id,
977
+ agent_id = excluded.agent_id,
978
+ agent_name = excluded.agent_name,
979
+ agent_version = excluded.agent_version,
980
+ goal = excluded.goal,
981
+ outcome = excluded.outcome,
982
+ autonomy_level = excluded.autonomy_level,
983
+ status = excluded.status,
984
+ error_message = excluded.error_message,
985
+ total_cost_usd = excluded.total_cost_usd,
986
+ total_duration_ms = excluded.total_duration_ms,
987
+ metadata_json = excluded.metadata_json
988
+ `);
989
+ for (const r of agentRunsToInsert) {
990
+ statements.push(
991
+ stmt.bind(
992
+ r.id,
993
+ r.projectId,
994
+ r.agentId,
995
+ r.agentName,
996
+ r.agentVersion,
997
+ r.goal,
998
+ r.outcome,
999
+ r.autonomyLevel,
1000
+ r.status,
1001
+ r.errorMessage,
1002
+ r.totalCostUsd,
1003
+ r.totalDurationMs,
1004
+ r.metadataJson,
1005
+ ),
1006
+ );
1007
+ }
1008
+ }
1009
+
1010
+ if (toolCallsToInsert.length > 0) {
1011
+ const stmt = db.prepare(`
1012
+ INSERT INTO tool_calls (
1013
+ id, action_id, project_id, tool_name, args_hash, result_hash,
1014
+ error_type, side_effect, approval_state, args_redacted, result_redacted,
1015
+ mcp_audit_json, mutation_before_json, mutation_after_json,
1016
+ mutation_diff_json, mutation_artifact_id
1017
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1018
+ ON CONFLICT (id) DO UPDATE SET
1019
+ action_id = excluded.action_id,
1020
+ project_id = excluded.project_id,
1021
+ tool_name = excluded.tool_name,
1022
+ args_hash = excluded.args_hash,
1023
+ result_hash = excluded.result_hash,
1024
+ error_type = excluded.error_type,
1025
+ side_effect = excluded.side_effect,
1026
+ approval_state = excluded.approval_state,
1027
+ args_redacted = excluded.args_redacted,
1028
+ result_redacted = excluded.result_redacted,
1029
+ mcp_audit_json = excluded.mcp_audit_json,
1030
+ mutation_before_json = excluded.mutation_before_json,
1031
+ mutation_after_json = excluded.mutation_after_json,
1032
+ mutation_diff_json = excluded.mutation_diff_json,
1033
+ mutation_artifact_id = excluded.mutation_artifact_id
1034
+ `);
1035
+ for (const r of toolCallsToInsert) {
1036
+ statements.push(
1037
+ stmt.bind(
1038
+ r.id,
1039
+ r.actionId,
1040
+ r.projectId,
1041
+ r.toolName,
1042
+ r.argsHash,
1043
+ r.resultHash,
1044
+ r.errorType,
1045
+ r.sideEffect,
1046
+ r.approvalState,
1047
+ r.argsRedacted,
1048
+ r.resultRedacted,
1049
+ r.mcpAuditJson,
1050
+ r.mutationBeforeJson,
1051
+ r.mutationAfterJson,
1052
+ r.mutationDiffJson,
1053
+ r.mutationArtifactId,
1054
+ ),
1055
+ );
1056
+ }
1057
+ }
1058
+
1059
+ if (retrievalsToInsert.length > 0) {
1060
+ const stmt = db.prepare(`
1061
+ INSERT INTO retrieval_events (
1062
+ id, action_id, project_id, retriever_name, query_hash, documents_json,
1063
+ total_results, max_relevance_score, duration_ms
1064
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1065
+ ON CONFLICT (id) DO UPDATE SET
1066
+ action_id = excluded.action_id,
1067
+ project_id = excluded.project_id,
1068
+ retriever_name = excluded.retriever_name,
1069
+ query_hash = excluded.query_hash,
1070
+ documents_json = excluded.documents_json,
1071
+ total_results = excluded.total_results,
1072
+ max_relevance_score = excluded.max_relevance_score,
1073
+ duration_ms = excluded.duration_ms
1074
+ `);
1075
+ for (const r of retrievalsToInsert) {
1076
+ statements.push(
1077
+ stmt.bind(
1078
+ r.id,
1079
+ r.actionId,
1080
+ r.projectId,
1081
+ r.retrieverName,
1082
+ r.queryHash,
1083
+ r.documentsJson,
1084
+ r.totalResults,
1085
+ r.maxRelevanceScore,
1086
+ r.durationMs,
1087
+ ),
1088
+ );
1089
+ }
1090
+ }
1091
+
1092
+ if (evalsToInsert.length > 0) {
1093
+ const stmt = db.prepare(`
1094
+ INSERT INTO eval_results (
1095
+ id, action_id, project_id, evaluator_name, evaluator_version,
1096
+ score, passed, reasoning, rubric_json
1097
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1098
+ ON CONFLICT (id) DO UPDATE SET
1099
+ action_id = excluded.action_id,
1100
+ project_id = excluded.project_id,
1101
+ evaluator_name = excluded.evaluator_name,
1102
+ evaluator_version = excluded.evaluator_version,
1103
+ score = excluded.score,
1104
+ passed = excluded.passed,
1105
+ reasoning = excluded.reasoning,
1106
+ rubric_json = excluded.rubric_json
1107
+ `);
1108
+ for (const r of evalsToInsert) {
1109
+ statements.push(
1110
+ stmt.bind(
1111
+ r.id,
1112
+ r.actionId,
1113
+ r.projectId,
1114
+ r.evaluatorName,
1115
+ r.evaluatorVersion,
1116
+ r.score,
1117
+ r.passed,
1118
+ r.reasoning,
1119
+ r.rubricJson,
1120
+ ),
1121
+ );
1122
+ }
1123
+ }
1124
+
1125
+ if (artifactsToInsert.length > 0) {
1126
+ const stmt = db.prepare(`
1127
+ INSERT INTO artifacts (
1128
+ id, action_id, project_id, artifact_name, artifact_type,
1129
+ storage_ref, size_bytes, sha256_hash, content_preview
1130
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1131
+ ON CONFLICT (id) DO UPDATE SET
1132
+ action_id = excluded.action_id,
1133
+ project_id = excluded.project_id,
1134
+ artifact_name = excluded.artifact_name,
1135
+ artifact_type = excluded.artifact_type,
1136
+ storage_ref = excluded.storage_ref,
1137
+ size_bytes = excluded.size_bytes,
1138
+ sha256_hash = excluded.sha256_hash,
1139
+ content_preview = excluded.content_preview
1140
+ `);
1141
+ for (const r of artifactsToInsert) {
1142
+ statements.push(
1143
+ stmt.bind(
1144
+ r.id,
1145
+ r.actionId,
1146
+ r.projectId,
1147
+ r.artifactName,
1148
+ r.artifactType,
1149
+ r.storageRef,
1150
+ r.sizeBytes,
1151
+ r.sha256Hash,
1152
+ r.contentPreview,
1153
+ ),
1154
+ );
1155
+ }
1156
+ }
1157
+
1158
+ if (statements.length > 0) {
1159
+ try {
1160
+ await db.batch(statements);
1161
+ } catch (err) {
1162
+ context.logger.error("[action-graph-processor] write failed", {
1163
+ error: err instanceof Error ? err.message : String(err),
1164
+ });
1165
+ }
1166
+ }
1167
+
1168
+ return transformed;
1169
+ },
1170
+ });
1171
+ },
1172
+ };