@agent-native/core 0.133.3 → 0.134.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 (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +62 -0
  3. package/corpus/core/docs/content/observability.mdx +41 -1
  4. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  5. package/corpus/core/docs/content/tracking.mdx +25 -0
  6. package/corpus/core/package.json +1 -1
  7. package/corpus/core/src/agent/run-manager.ts +7 -0
  8. package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
  9. package/corpus/core/src/client/analytics.ts +105 -1
  10. package/corpus/core/src/deploy/build.ts +38 -1
  11. package/corpus/core/src/observability/posthog-ai.ts +294 -0
  12. package/corpus/core/src/observability/routes.ts +29 -6
  13. package/corpus/core/src/observability/traces.ts +287 -12
  14. package/corpus/core/src/observability/types.ts +11 -0
  15. package/corpus/core/src/secrets/register-framework-secrets.ts +16 -0
  16. package/corpus/core/src/server/agent-run-context.ts +21 -0
  17. package/corpus/core/src/server/capture-error.ts +8 -0
  18. package/corpus/core/src/server/core-routes-plugin.ts +75 -4
  19. package/corpus/core/src/server/deploy-environment.ts +57 -0
  20. package/corpus/core/src/server/error-noise-filter.ts +268 -0
  21. package/corpus/core/src/server/posthog-config.ts +75 -0
  22. package/corpus/core/src/server/request-context.ts +6 -0
  23. package/corpus/core/src/server/sentry-config.ts +4 -8
  24. package/corpus/core/src/server/sentry-plugin.ts +13 -29
  25. package/corpus/core/src/server/sentry.ts +13 -156
  26. package/corpus/core/src/server/ssr-handler.ts +6 -1
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  30. package/corpus/core/src/tracking/error-capture.ts +44 -109
  31. package/corpus/core/src/tracking/index.ts +11 -0
  32. package/corpus/core/src/tracking/posthog-exception.ts +305 -0
  33. package/corpus/core/src/tracking/providers.ts +91 -14
  34. package/corpus/core/src/tracking/redaction.ts +105 -0
  35. package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  36. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  37. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  38. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  39. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  40. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  41. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  43. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  44. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  45. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  46. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  47. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  48. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  49. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  50. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  51. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  52. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  53. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  54. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  55. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  56. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  57. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  58. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  59. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  60. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  61. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  62. package/corpus/templates/content/app/i18n-data.ts +130 -0
  63. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  64. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  65. package/corpus/templates/content/parity/matrix.md +1 -1
  66. package/corpus/templates/content/parity/matrix.ts +4 -5
  67. package/corpus/templates/content/shared/api.ts +4 -0
  68. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  69. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  70. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  71. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  72. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  73. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  74. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  75. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  76. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  77. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  78. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  79. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  80. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  81. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  82. package/corpus/templates/design/package.json +4 -0
  83. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  84. package/dist/agent/run-manager.d.ts.map +1 -1
  85. package/dist/agent/run-manager.js +7 -0
  86. package/dist/agent/run-manager.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +12 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +8 -0
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js +95 -1
  93. package/dist/client/analytics.js.map +1 -1
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/collab/struct-routes.d.ts +1 -1
  96. package/dist/deploy/build.d.ts.map +1 -1
  97. package/dist/deploy/build.js +38 -1
  98. package/dist/deploy/build.js.map +1 -1
  99. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  100. package/dist/notifications/routes.d.ts +1 -1
  101. package/dist/observability/posthog-ai.d.ts +126 -0
  102. package/dist/observability/posthog-ai.d.ts.map +1 -0
  103. package/dist/observability/posthog-ai.js +190 -0
  104. package/dist/observability/posthog-ai.js.map +1 -0
  105. package/dist/observability/routes.d.ts +3 -3
  106. package/dist/observability/routes.d.ts.map +1 -1
  107. package/dist/observability/routes.js +26 -6
  108. package/dist/observability/routes.js.map +1 -1
  109. package/dist/observability/traces.d.ts +9 -0
  110. package/dist/observability/traces.d.ts.map +1 -1
  111. package/dist/observability/traces.js +219 -10
  112. package/dist/observability/traces.js.map +1 -1
  113. package/dist/observability/types.d.ts +10 -0
  114. package/dist/observability/types.d.ts.map +1 -1
  115. package/dist/observability/types.js +1 -0
  116. package/dist/observability/types.js.map +1 -1
  117. package/dist/progress/routes.d.ts +1 -1
  118. package/dist/resources/handlers.d.ts +1 -1
  119. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  120. package/dist/secrets/register-framework-secrets.js +14 -0
  121. package/dist/secrets/register-framework-secrets.js.map +1 -1
  122. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  123. package/dist/server/agent-run-context.d.ts +8 -0
  124. package/dist/server/agent-run-context.d.ts.map +1 -1
  125. package/dist/server/agent-run-context.js +18 -0
  126. package/dist/server/agent-run-context.js.map +1 -1
  127. package/dist/server/capture-error.d.ts +8 -0
  128. package/dist/server/capture-error.d.ts.map +1 -1
  129. package/dist/server/capture-error.js.map +1 -1
  130. package/dist/server/core-routes-plugin.d.ts +0 -23
  131. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  132. package/dist/server/core-routes-plugin.js +71 -7
  133. package/dist/server/core-routes-plugin.js.map +1 -1
  134. package/dist/server/deploy-environment.d.ts +17 -0
  135. package/dist/server/deploy-environment.d.ts.map +1 -0
  136. package/dist/server/deploy-environment.js +47 -0
  137. package/dist/server/deploy-environment.js.map +1 -0
  138. package/dist/server/error-noise-filter.d.ts +80 -0
  139. package/dist/server/error-noise-filter.d.ts.map +1 -0
  140. package/dist/server/error-noise-filter.js +173 -0
  141. package/dist/server/error-noise-filter.js.map +1 -0
  142. package/dist/server/posthog-config.d.ts +28 -0
  143. package/dist/server/posthog-config.d.ts.map +1 -0
  144. package/dist/server/posthog-config.js +53 -0
  145. package/dist/server/posthog-config.js.map +1 -0
  146. package/dist/server/request-context.d.ts +6 -0
  147. package/dist/server/request-context.d.ts.map +1 -1
  148. package/dist/server/request-context.js.map +1 -1
  149. package/dist/server/sentry-config.d.ts +1 -0
  150. package/dist/server/sentry-config.d.ts.map +1 -1
  151. package/dist/server/sentry-config.js +3 -1
  152. package/dist/server/sentry-config.js.map +1 -1
  153. package/dist/server/sentry-plugin.d.ts.map +1 -1
  154. package/dist/server/sentry-plugin.js +12 -27
  155. package/dist/server/sentry-plugin.js.map +1 -1
  156. package/dist/server/sentry.d.ts.map +1 -1
  157. package/dist/server/sentry.js +8 -124
  158. package/dist/server/sentry.js.map +1 -1
  159. package/dist/server/ssr-handler.d.ts.map +1 -1
  160. package/dist/server/ssr-handler.js +6 -1
  161. package/dist/server/ssr-handler.js.map +1 -1
  162. package/dist/server/transcribe-voice.d.ts +1 -1
  163. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  164. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  165. package/dist/shared/mcp-embed-headers.js +1 -1
  166. package/dist/shared/mcp-embed-headers.js.map +1 -1
  167. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  168. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  169. package/dist/tracking/error-capture.d.ts +7 -0
  170. package/dist/tracking/error-capture.d.ts.map +1 -1
  171. package/dist/tracking/error-capture.js +10 -73
  172. package/dist/tracking/error-capture.js.map +1 -1
  173. package/dist/tracking/index.d.ts +1 -0
  174. package/dist/tracking/index.d.ts.map +1 -1
  175. package/dist/tracking/index.js +1 -0
  176. package/dist/tracking/index.js.map +1 -1
  177. package/dist/tracking/posthog-exception.d.ts +103 -0
  178. package/dist/tracking/posthog-exception.d.ts.map +1 -0
  179. package/dist/tracking/posthog-exception.js +181 -0
  180. package/dist/tracking/posthog-exception.js.map +1 -0
  181. package/dist/tracking/providers.d.ts +14 -0
  182. package/dist/tracking/providers.d.ts.map +1 -1
  183. package/dist/tracking/providers.js +63 -11
  184. package/dist/tracking/providers.js.map +1 -1
  185. package/dist/tracking/redaction.d.ts +25 -0
  186. package/dist/tracking/redaction.d.ts.map +1 -0
  187. package/dist/tracking/redaction.js +85 -0
  188. package/dist/tracking/redaction.js.map +1 -0
  189. package/docs/content/observability.mdx +41 -1
  190. package/docs/content/template-content-developers.mdx +1 -1
  191. package/docs/content/tracking.mdx +25 -0
  192. package/package.json +1 -1
  193. package/src/agent/run-manager.ts +7 -0
  194. package/src/client/agent-chat-adapter.ts +10 -0
  195. package/src/client/analytics.ts +105 -1
  196. package/src/deploy/build.ts +38 -1
  197. package/src/observability/posthog-ai.ts +294 -0
  198. package/src/observability/routes.ts +29 -6
  199. package/src/observability/traces.ts +287 -12
  200. package/src/observability/types.ts +11 -0
  201. package/src/secrets/register-framework-secrets.ts +16 -0
  202. package/src/server/agent-run-context.ts +21 -0
  203. package/src/server/capture-error.ts +8 -0
  204. package/src/server/core-routes-plugin.ts +75 -4
  205. package/src/server/deploy-environment.ts +57 -0
  206. package/src/server/error-noise-filter.ts +268 -0
  207. package/src/server/posthog-config.ts +75 -0
  208. package/src/server/request-context.ts +6 -0
  209. package/src/server/sentry-config.ts +4 -8
  210. package/src/server/sentry-plugin.ts +13 -29
  211. package/src/server/sentry.ts +13 -156
  212. package/src/server/ssr-handler.ts +6 -1
  213. package/src/shared/mcp-embed-headers.ts +1 -1
  214. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  215. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  216. package/src/tracking/error-capture.ts +44 -109
  217. package/src/tracking/index.ts +11 -0
  218. package/src/tracking/posthog-exception.ts +305 -0
  219. package/src/tracking/providers.ts +91 -14
  220. package/src/tracking/redaction.ts +105 -0
  221. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -3,6 +3,15 @@ import type {
3
3
  AgentLoopUsage,
4
4
  } from "../agent/production-agent.js";
5
5
  import type { AgentChatEvent, AgentToolInput } from "../agent/types.js";
6
+ import { getRequestContext } from "../server/request-context.js";
7
+ import {
8
+ MAX_AI_CONTENT_BYTES,
9
+ MAX_AI_SPANS_PER_RUN,
10
+ boundAiContent,
11
+ emitAiSpanEvent,
12
+ emitAiTraceEvent,
13
+ toAiErrorDetail,
14
+ } from "./posthog-ai.js";
6
15
  import { type AgentSpan, endAgentSpan, startAgentSpan } from "./tracing.js";
7
16
  import { trackingIdentityProperties } from "./tracking-identity.js";
8
17
  import type { TraceSpan, TraceSummary, ObservabilityConfig } from "./types.js";
@@ -117,6 +126,17 @@ function emitLlmGenerationTrackingEvent(args: {
117
126
  variantId: string;
118
127
  }>;
119
128
  modelSelectionSource?: string;
129
+ /**
130
+ * PostHog content fields. Each is `undefined` unless the matching capture
131
+ * flag is on, and is then OMITTED from the event — never sent as `[]`, which
132
+ * PostHog would render as "the model was called with no messages".
133
+ */
134
+ aiInput?: unknown;
135
+ aiOutputChoices?: unknown;
136
+ aiTools?: unknown;
137
+ aiInputTruncated?: boolean;
138
+ aiOutputTruncated?: boolean;
139
+ browserSessionId?: string;
120
140
  }): void {
121
141
  const provider = llmProviderFromEngine(args.engineName, args.model);
122
142
  const costUsd =
@@ -128,6 +148,15 @@ function emitLlmGenerationTrackingEvent(args: {
128
148
  ? args.inputTokens + args.outputTokens
129
149
  : undefined;
130
150
  const error = args.errorMessage ?? undefined;
151
+ const terminalCode =
152
+ args.terminalOutcome?.state === "failed" ||
153
+ args.terminalOutcome?.state === "input_required"
154
+ ? args.terminalOutcome.code
155
+ : undefined;
156
+ const terminalRetryable =
157
+ args.terminalOutcome?.state === "failed"
158
+ ? args.terminalOutcome.retryable
159
+ : undefined;
131
160
  const properties: Record<string, unknown> = {
132
161
  ...trackingIdentityProperties(),
133
162
  source: "agent_observability",
@@ -154,15 +183,8 @@ function emitLlmGenerationTrackingEvent(args: {
154
183
  tools: args.tools,
155
184
  tools_truncated: args.toolsTruncated,
156
185
  terminal_state: args.terminalOutcome?.state,
157
- terminal_code:
158
- args.terminalOutcome?.state === "failed" ||
159
- args.terminalOutcome?.state === "input_required"
160
- ? args.terminalOutcome.code
161
- : undefined,
162
- terminal_retryable:
163
- args.terminalOutcome?.state === "failed"
164
- ? args.terminalOutcome.retryable
165
- : undefined,
186
+ terminal_code: terminalCode,
187
+ terminal_retryable: terminalRetryable,
166
188
  delegated: args.delegation ? true : undefined,
167
189
  delegation_protocol: args.delegation?.protocol,
168
190
  caller_app: args.delegation?.callerApp,
@@ -175,19 +197,36 @@ function emitLlmGenerationTrackingEvent(args: {
175
197
  $ai_trace_id: args.runId,
176
198
  $ai_session_id: args.threadId ?? undefined,
177
199
  $ai_span_id: args.llmSpanId,
178
- $ai_span_name: "agent_run",
179
- $ai_parent_id: args.parentSpanId,
200
+ $ai_span_name: args.model,
201
+ // Parent is the run's trace, not the internal `agent_run` span id — the
202
+ // latter is never emitted to PostHog, so pointing at it orphaned the
203
+ // generation and PostHog rendered a placeholder trace around it.
204
+ $ai_parent_id: args.runId,
180
205
  $ai_model: args.model,
181
206
  $ai_provider: provider,
182
207
  $ai_input_tokens: args.inputTokens,
183
208
  $ai_output_tokens: args.outputTokens,
184
209
  $ai_latency: Math.round((args.durationMs / 1000) * 1000) / 1000,
185
210
  $ai_is_error: args.status === "error",
186
- $ai_error: error,
211
+ $ai_error:
212
+ args.status === "error"
213
+ ? toAiErrorDetail(error, {
214
+ state: args.terminalOutcome?.state,
215
+ code: terminalCode,
216
+ retryable: terminalRetryable,
217
+ })
218
+ : undefined,
187
219
  $ai_cache_read_input_tokens: args.cacheReadTokens,
188
220
  $ai_cache_creation_input_tokens: args.cacheWriteTokens,
189
221
  $ai_request_count: 1,
190
222
  $ai_total_cost_usd: costUsd,
223
+ $ai_input: args.aiInput,
224
+ $ai_output_choices: args.aiOutputChoices,
225
+ $ai_tools: args.aiTools,
226
+ $ai_input_truncated: args.aiInputTruncated || undefined,
227
+ $ai_output_truncated: args.aiOutputTruncated || undefined,
228
+ $ai_time_to_first_token: args.firstTokenMs,
229
+ $session_id: args.browserSessionId,
191
230
  };
192
231
  if (args.experimentAssignments?.length) {
193
232
  properties.experiment_ids = args.experimentAssignments
@@ -220,6 +259,96 @@ function emitLlmGenerationTrackingEvent(args: {
220
259
  }
221
260
  }
222
261
 
262
+ /**
263
+ * Build the PostHog content fields for a run's `$ai_generation`.
264
+ *
265
+ * This is one generation per run carrying the run's whole message list, not one
266
+ * per model round-trip: the engine layer reports aggregate usage only
267
+ * (`onUsage`) and exposes no per-step hook. Consequence to know when reading a
268
+ * trace — per-round-trip latency and intermediate assistant turns are not
269
+ * visible; a multi-step run collapses into a single generation node.
270
+ *
271
+ * `$ai_output_choices` is emitted whenever tool calls happened even with
272
+ * `capturePrompts` off, because PostHog derives `$ai_tools_called` /
273
+ * `$ai_tool_call_count` from tool-call blocks inside it and nothing else. The
274
+ * assistant's text content stays gated; only the structural call list ships.
275
+ */
276
+ function buildGenerationContent(args: {
277
+ config: ObservabilityConfig;
278
+ messages: unknown;
279
+ tools: unknown;
280
+ assistantText: string;
281
+ toolSpans: TraceSpan[];
282
+ }): {
283
+ aiInput?: unknown;
284
+ aiOutputChoices?: unknown;
285
+ aiTools?: unknown;
286
+ aiInputTruncated?: boolean;
287
+ aiOutputTruncated?: boolean;
288
+ } {
289
+ const { config } = args;
290
+
291
+ const input = config.capturePrompts
292
+ ? boundAiContent(redactSensitiveFields(args.messages))
293
+ : undefined;
294
+
295
+ const toolCalls = args.toolSpans
296
+ .slice(0, MAX_TRACKED_GENERATION_TOOL_CALLS)
297
+ .map((span) => ({
298
+ type: "function" as const,
299
+ id: span.id,
300
+ function: {
301
+ name: span.name,
302
+ // Already redacted at span construction, and only present when
303
+ // `captureToolArgs` is on.
304
+ ...((span.metadata as { input?: unknown } | null)?.input !== undefined
305
+ ? { arguments: (span.metadata as { input?: unknown }).input }
306
+ : {}),
307
+ },
308
+ }));
309
+
310
+ const hasChoice = config.capturePrompts || toolCalls.length > 0;
311
+ const output = hasChoice
312
+ ? boundAiContent([
313
+ {
314
+ role: "assistant",
315
+ ...(config.capturePrompts
316
+ ? { content: redactToolErrorMessage(args.assistantText) }
317
+ : {}),
318
+ ...(toolCalls.length ? { tool_calls: toolCalls } : {}),
319
+ },
320
+ ])
321
+ : undefined;
322
+
323
+ const toolList = Array.isArray(args.tools)
324
+ ? args.tools
325
+ .filter(
326
+ (tool): tool is { name: string; description?: string } =>
327
+ !!tool && typeof (tool as { name?: unknown }).name === "string",
328
+ )
329
+ .map((tool) => ({
330
+ type: "function" as const,
331
+ function: {
332
+ name: tool.name,
333
+ ...(typeof tool.description === "string"
334
+ ? { description: tool.description }
335
+ : {}),
336
+ },
337
+ }))
338
+ : [];
339
+
340
+ return {
341
+ aiInput: input?.value,
342
+ aiOutputChoices: output?.value,
343
+ // Tool definitions are app configuration rather than user content, so they
344
+ // are not gated — without them a trace shows calls to tools nobody can
345
+ // identify. Schemas are excluded to keep the event small.
346
+ aiTools: toolList.length ? toolList : undefined,
347
+ aiInputTruncated: input?.truncated,
348
+ aiOutputTruncated: output?.truncated,
349
+ };
350
+ }
351
+
223
352
  /** Keys whose values are stripped from persisted tool inputs when
224
353
  * `captureToolArgs` is enabled. Matched case-insensitively and tolerant
225
354
  * of `_` / `-` separators. M14 in the MCP/A2A audit: tool calls
@@ -324,6 +453,15 @@ export async function instrumentAgentLoop(opts: {
324
453
  };
325
454
  /** Raw user-authored message before prompt/context enrichment. */
326
455
  sentimentInput?: string;
456
+ /**
457
+ * Browser session id of the request that started this run, when it came from
458
+ * a page. Emitted as PostHog's `$session_id` so agent traces join to session
459
+ * replay — distinct from `$ai_session_id`, which is the thread.
460
+ *
461
+ * Defaults to the in-flight request context, which the agent-chat route
462
+ * populates from the `X-Agent-Native-Session-Id` header.
463
+ */
464
+ browserSessionId?: string;
327
465
  classifyError?: (error: unknown) =>
328
466
  | {
329
467
  status?: "success" | "error";
@@ -348,6 +486,11 @@ export async function instrumentAgentLoop(opts: {
348
486
  .catch(() => null)
349
487
  : Promise.resolve(null);
350
488
 
489
+ // Falls back to the in-flight request so callers deep in the agent stack
490
+ // don't have to thread it down by hand.
491
+ const browserSessionId =
492
+ opts.browserSessionId ?? getRequestContext()?.browserSessionId;
493
+
351
494
  // Optional OpenTelemetry root span for this run. No-ops unless a host has
352
495
  // installed `@opentelemetry/api` and registered a provider. The promise is
353
496
  // resolved before the loop runs so child tool/model spans can parent under
@@ -389,6 +532,10 @@ export async function instrumentAgentLoop(opts: {
389
532
  const toolNameToCounters = new Map<string, number[]>();
390
533
  const toolCallIdToCounter = new Map<string, number>();
391
534
  const generationToolCalls = new Map<number, GenerationToolCall>();
535
+ // Assistant text, accumulated only when prompt capture is on so a disabled
536
+ // config never holds message content in memory in the first place.
537
+ const assistantTextParts: string[] = [];
538
+ let assistantTextLength = 0;
392
539
 
393
540
  let toolCallCount = 0;
394
541
  let successfulTools = 0;
@@ -432,6 +579,14 @@ export async function instrumentAgentLoop(opts: {
432
579
 
433
580
  const instrumentedSend = (event: AgentChatEvent): void => {
434
581
  try {
582
+ if (
583
+ config.capturePrompts &&
584
+ event.type === "text" &&
585
+ assistantTextLength < MAX_AI_CONTENT_BYTES
586
+ ) {
587
+ assistantTextParts.push(event.text);
588
+ assistantTextLength += event.text.length;
589
+ }
435
590
  // Some guardrails intentionally stop the loop by emitting a terminal
436
591
  // event and returning usage instead of throwing. Preserve that terminal
437
592
  // state in telemetry so a tripwire/loop-limit/provider error cannot be
@@ -741,6 +896,13 @@ export async function instrumentAgentLoop(opts: {
741
896
  ? Math.max(0, usage.firstEngineEventAtMs - runStart)
742
897
  : undefined;
743
898
  const llmSpanId = spanId();
899
+ const generationContent = buildGenerationContent({
900
+ config,
901
+ messages: loopOpts.messages,
902
+ tools: loopOpts.tools,
903
+ assistantText: assistantTextParts.join(""),
904
+ toolSpans: spans.filter((s) => s.spanType === "tool_call"),
905
+ });
744
906
  const llmSpan: TraceSpan = {
745
907
  id: llmSpanId,
746
908
  runId,
@@ -798,6 +960,8 @@ export async function instrumentAgentLoop(opts: {
798
960
  createdAt: runStart,
799
961
  experimentAssignments: opts.experimentAssignments,
800
962
  modelSelectionSource: opts.modelSelectionSource,
963
+ browserSessionId,
964
+ ...generationContent,
801
965
  });
802
966
  }
803
967
 
@@ -822,6 +986,117 @@ export async function instrumentAgentLoop(opts: {
822
986
  };
823
987
  spans.push(parentSpan);
824
988
 
989
+ // PostHog LLM analytics: the run is a `$ai_trace`, each tool call an
990
+ // `$ai_span` under it. Emitted from the collected spans rather than from a
991
+ // second instrumentation pass, so the tree PostHog shows and the tree we
992
+ // persist cannot drift apart.
993
+ try {
994
+ const aiError =
995
+ runStatus === "error"
996
+ ? toAiErrorDetail(errorMessage, {
997
+ state: terminalOutcome?.state,
998
+ code:
999
+ terminalOutcome?.state === "failed" ||
1000
+ terminalOutcome?.state === "input_required"
1001
+ ? terminalOutcome.code
1002
+ : undefined,
1003
+ retryable:
1004
+ terminalOutcome?.state === "failed"
1005
+ ? terminalOutcome.retryable
1006
+ : undefined,
1007
+ })
1008
+ : undefined;
1009
+ const provider = llmProviderFromEngine(
1010
+ typeof loopOpts.engine?.name === "string"
1011
+ ? loopOpts.engine.name
1012
+ : undefined,
1013
+ usage?.model ?? loopOpts.model,
1014
+ );
1015
+
1016
+ const toolSpans = config.captureLlmSpans
1017
+ ? spans.filter((s) => s.spanType === "tool_call")
1018
+ : [];
1019
+ const emittedToolSpans = toolSpans.slice(0, MAX_AI_SPANS_PER_RUN);
1020
+ const droppedToolSpans = toolSpans.length - emittedToolSpans.length;
1021
+
1022
+ emitAiTraceEvent({
1023
+ runId,
1024
+ threadId,
1025
+ userId,
1026
+ spanName: "agent_run",
1027
+ model: usage?.model ?? loopOpts.model,
1028
+ provider,
1029
+ latencySeconds: Math.round(totalDurationMs) / 1000,
1030
+ isError: runStatus === "error",
1031
+ error: aiError,
1032
+ inputTokens: usage?.usageReported ? usage.inputTokens : undefined,
1033
+ outputTokens: usage?.usageReported ? usage.outputTokens : undefined,
1034
+ costUsd: usage?.usageReported
1035
+ ? costUsdFromCenticents(costCentsX100)
1036
+ : undefined,
1037
+ createdAt: runStart,
1038
+ browserSessionId,
1039
+ extraProperties: {
1040
+ ...trackingIdentityProperties(),
1041
+ source: "agent_observability",
1042
+ run_id: runId,
1043
+ thread_id: threadId,
1044
+ // A truncated run must not read as a complete one.
1045
+ ...(droppedToolSpans > 0
1046
+ ? {
1047
+ $ai_spans_dropped: droppedToolSpans,
1048
+ $ai_spans_emitted: emittedToolSpans.length,
1049
+ }
1050
+ : {}),
1051
+ },
1052
+ });
1053
+
1054
+ for (const span of emittedToolSpans) {
1055
+ // `span.errorMessage` is the raw tool result. It routinely contains
1056
+ // upstream response bodies with Authorization headers and standalone
1057
+ // API keys, so it gets the same redaction + bounding the generation
1058
+ // event's `tools[].error_message` already applies, and the same
1059
+ // `captureToolResults` gate — exporting it here otherwise reintroduced
1060
+ // the leak that gate exists to prevent. `$ai_is_error` still marks the
1061
+ // failure when the content is withheld.
1062
+ const toolErrorMessage =
1063
+ span.status === "error" &&
1064
+ span.errorMessage &&
1065
+ config.captureToolResults
1066
+ ? truncateToolErrorMessage(
1067
+ redactToolErrorMessage(span.errorMessage),
1068
+ )
1069
+ : undefined;
1070
+
1071
+ emitAiSpanEvent({
1072
+ runId,
1073
+ threadId,
1074
+ userId,
1075
+ spanId: span.id,
1076
+ spanName: span.name,
1077
+ latencySeconds: Math.round(span.durationMs) / 1000,
1078
+ isError: span.status === "error",
1079
+ error: toolErrorMessage
1080
+ ? toAiErrorDetail(toolErrorMessage)
1081
+ : undefined,
1082
+ createdAt: span.createdAt,
1083
+ browserSessionId,
1084
+ // `metadata.input` is already redacted and only present when
1085
+ // `captureToolArgs` is on; absent stays absent.
1086
+ inputState: (span.metadata as { input?: unknown } | null)?.input,
1087
+ outputState: toolErrorMessage,
1088
+ extraProperties: {
1089
+ ...trackingIdentityProperties(),
1090
+ source: "agent_observability",
1091
+ span_type: "tool_call",
1092
+ },
1093
+ });
1094
+ }
1095
+ // coercion-ok: a throw here would skip trace persistence below
1096
+ } catch {
1097
+ // LLM analytics must never affect the run or trace persistence.
1098
+ }
1099
+
825
1100
  const summary: TraceSummary = {
826
1101
  runId,
827
1102
  threadId,
@@ -175,9 +175,19 @@ export interface ExperimentMetricResult {
175
175
 
176
176
  export interface ObservabilityConfig {
177
177
  enabled: boolean;
178
+ /**
179
+ * Export prompt and completion content (`$ai_input`, `$ai_output_choices`)
180
+ * to configured LLM-analytics backends. Off by default: message bodies are
181
+ * user data, and a trace backend is not a place to put it without a decision.
182
+ *
183
+ * When off the fields are OMITTED, never sent empty — an empty array is
184
+ * indistinguishable from a genuinely empty prompt.
185
+ */
178
186
  capturePrompts: boolean;
179
187
  captureToolArgs: boolean;
180
188
  captureToolResults: boolean;
189
+ /** Emit one `$ai_span` per tool call alongside the run's `$ai_trace`. */
190
+ captureLlmSpans: boolean;
181
191
  evalSampleRate: number;
182
192
  /**
183
193
  * Classify the raw user message as positive, negative, or neutral. Off by
@@ -203,6 +213,7 @@ export const DEFAULT_OBSERVABILITY_CONFIG: ObservabilityConfig = {
203
213
  capturePrompts: false,
204
214
  captureToolArgs: false,
205
215
  captureToolResults: false,
216
+ captureLlmSpans: true,
206
217
  evalSampleRate: 0,
207
218
  inferredSentimentEnabled: false,
208
219
  inferredSentimentSampleRate: 0,
@@ -140,6 +140,22 @@ export function registerFrameworkSecrets(): void {
140
140
  });
141
141
  }
142
142
 
143
+ // PostHog — product analytics, error tracking, and LLM analytics. One key
144
+ // arms all three; `POSTHOG_ERROR_TRACKING=false` opts out of exceptions
145
+ // while keeping analytics.
146
+ if (!getRequiredSecret("POSTHOG_API_KEY")) {
147
+ registerRequiredSecret({
148
+ key: "POSTHOG_API_KEY",
149
+ label: "PostHog project API key",
150
+ description:
151
+ "Sends product analytics, server exceptions, and LLM traces to PostHog. Set POSTHOG_HOST for self-hosted or EU projects.",
152
+ docsUrl: "https://posthog.com/docs/getting-started/install",
153
+ scope: "workspace",
154
+ kind: "api-key",
155
+ required: false,
156
+ });
157
+ }
158
+
143
159
  // Web-search tool backends — optional; the tool selects the first
144
160
  // configured manual key at call time, then falls back to Builder Connect.
145
161
  const webSearchKeys: Array<{
@@ -78,6 +78,25 @@ export function readAgentRunTimezone(event: H3Event): string | undefined {
78
78
  : undefined;
79
79
  }
80
80
 
81
+ /**
82
+ * The caller's browser analytics session id, when the page sent one.
83
+ *
84
+ * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
85
+ * trace joins to the session replay it happened in. Distinct from
86
+ * `$ai_session_id`, which is the conversation thread.
87
+ */
88
+ export function readAgentRunBrowserSessionId(
89
+ event: H3Event,
90
+ ): string | undefined {
91
+ const raw = readHeaderValue(event, "x-agent-native-session-id");
92
+ const value = Array.isArray(raw) ? raw[0] : raw;
93
+ return typeof value === "string" &&
94
+ value.trim().length > 0 &&
95
+ value.trim().length < 128
96
+ ? value.trim()
97
+ : undefined;
98
+ }
99
+
81
100
  export function seedAgentRunOwnerContext(
82
101
  event: H3Event,
83
102
  ownerContext: AgentRunOwnerContext,
@@ -186,6 +205,7 @@ export async function resolveAgentRunRequestContext(options: {
186
205
  }): Promise<RequestContext> {
187
206
  const orgId = await resolveAgentRunOrgId(options);
188
207
  const timezone = readAgentRunTimezone(options.event);
208
+ const browserSessionId = readAgentRunBrowserSessionId(options.event);
189
209
  const waitUntil = requestWaitUntil(options.event);
190
210
  const run = {
191
211
  ...(options.isBackgroundWorker ? { isBackgroundWorker: true } : {}),
@@ -196,6 +216,7 @@ export async function resolveAgentRunRequestContext(options: {
196
216
  userName: options.ownerContext.name,
197
217
  orgId,
198
218
  timezone,
219
+ ...(browserSessionId ? { browserSessionId } : {}),
199
220
  ...(Object.keys(run).length > 0 ? { run } : {}),
200
221
  };
201
222
  }
@@ -11,6 +11,14 @@ export interface CaptureErrorContext {
11
11
  extra?: Record<string, unknown>;
12
12
  /** Grouped diagnostic cards shown on the captured event. */
13
13
  contexts?: Record<string, Record<string, unknown>>;
14
+ /**
15
+ * The agent run this error belongs to, when it belongs to one.
16
+ *
17
+ * Emitted as a top-level `$ai_trace_id` so an error-tracking issue and the
18
+ * LLM trace it came from resolve to each other. Nesting the run id inside
19
+ * `extra` (as callers did before) puts it somewhere no backend can join on.
20
+ */
21
+ aiTraceId?: string;
14
22
  }
15
23
 
16
24
  export type CaptureErrorProvider = (
@@ -157,7 +157,12 @@ import {
157
157
  readDeployCredentialEnv,
158
158
  resolveSecret,
159
159
  } from "./credential-provider.js";
160
+ import {
161
+ resolveDeployEnvironment,
162
+ resolveServerRelease,
163
+ } from "./deploy-environment.js";
160
164
  import { createEmbedStartRouteHandler } from "./embed-route.js";
165
+ import { shouldReportError } from "./error-noise-filter.js";
161
166
  import {
162
167
  getH3App,
163
168
  awaitBootstrap,
@@ -178,7 +183,11 @@ import { createOpenRouteHandler } from "./open-route.js";
178
183
  import { createPollEventsHandler } from "./poll-events.js";
179
184
  import { createPollHandler } from "./poll.js";
180
185
  import { createRealtimeTokenHandler } from "./realtime-token.js";
181
- import { runWithRequestContext } from "./request-context.js";
186
+ import {
187
+ getRequestContext,
188
+ hasRequestContext,
189
+ runWithRequestContext,
190
+ } from "./request-context.js";
182
191
  import {
183
192
  findUnsupportedScopedKeyNames,
184
193
  saveKeyValuesToScopedSecrets,
@@ -1298,6 +1307,53 @@ export async function readLegacyCoreRouteInitSettings(
1298
1307
  * PUT /_agent-native/application-state/compose/:id — upsert compose draft
1299
1308
  * DELETE /_agent-native/application-state/compose/:id — delete compose draft
1300
1309
  */
1310
+ /**
1311
+ * Route every Nitro route error through the provider-agnostic `captureError()`
1312
+ * registry, filtered by the shared noise rules.
1313
+ *
1314
+ * This lives here rather than in `sentry-plugin.ts` because that plugin bails
1315
+ * out when no `SENTRY_DSN` is configured — wiring the hook there meant an app
1316
+ * running PostHog (or any other backend) with no Sentry project reported no
1317
+ * route errors at all, while still looking configured.
1318
+ */
1319
+ function wireRouteErrorCapture(nitroApp: any): void {
1320
+ nitroApp.hooks?.hook?.(
1321
+ "error",
1322
+ (error: unknown, ctx?: { event?: H3Event }) => {
1323
+ try {
1324
+ const event = ctx?.event;
1325
+ const route = (() => {
1326
+ try {
1327
+ return event?.url?.pathname;
1328
+ // coercion-ok: a missing route tag must not suppress the error
1329
+ } catch {
1330
+ return undefined;
1331
+ }
1332
+ })();
1333
+ const userAgent = (() => {
1334
+ try {
1335
+ return event ? getHeader(event, "user-agent") : undefined;
1336
+ // coercion-ok: a missing UA tag must not suppress the error
1337
+ } catch {
1338
+ return undefined;
1339
+ }
1340
+ })();
1341
+
1342
+ if (!shouldReportError(error, { tags: { route } })) return;
1343
+
1344
+ captureError(error, {
1345
+ route,
1346
+ method: event ? getMethod(event) : undefined,
1347
+ userAgent,
1348
+ });
1349
+ // coercion-ok: rethrowing here would replace the app's real error
1350
+ } catch {
1351
+ // Error reporting must never escape into Nitro's error path.
1352
+ }
1353
+ },
1354
+ );
1355
+ }
1356
+
1301
1357
  export function createCoreRoutesPlugin(
1302
1358
  options: CoreRoutesPluginOptions = {},
1303
1359
  ): NitroPluginDef {
@@ -1375,14 +1431,29 @@ export function createCoreRoutesPlugin(
1375
1431
  // already registered the same key win.
1376
1432
  registerFrameworkSecrets();
1377
1433
  registerBuiltinProviders();
1378
- registerErrorCaptureProvider("agent-native-analytics", (error, context) =>
1434
+ // Named for the destination it actually reaches: every configured
1435
+ // tracking provider (PostHog, Mixpanel, Amplitude, Agent Native
1436
+ // Analytics, webhook), not just one of them.
1437
+ registerErrorCaptureProvider("tracking", (error, context) => {
1438
+ // Attribute to the in-flight request's user so server exceptions and
1439
+ // that same person's browser events share one `distinct_id`.
1440
+ const requestContext = hasRequestContext()
1441
+ ? getRequestContext()
1442
+ : undefined;
1379
1443
  captureException(error, {
1380
1444
  ...context,
1381
1445
  handled: false,
1382
1446
  runtime: "node",
1383
1447
  source: "server",
1384
- }),
1385
- );
1448
+ release: resolveServerRelease(),
1449
+ environment: resolveDeployEnvironment(),
1450
+ ...(requestContext?.userEmail
1451
+ ? { userId: requestContext.userEmail }
1452
+ : {}),
1453
+ ...(requestContext?.orgId ? { orgId: requestContext.orgId } : {}),
1454
+ });
1455
+ });
1456
+ wireRouteErrorCapture(nitroApp);
1386
1457
  registerBuiltinNotificationChannels();
1387
1458
 
1388
1459
  try {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Deployment identity shared by every error-reporting backend.
3
+ *
4
+ * These were originally Sentry-private helpers. They describe the deployment,
5
+ * not the vendor, and a second backend that reports errors without them
6
+ * produces issues nobody can tie to a release.
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ function firstNonEmpty(
14
+ ...values: Array<string | undefined>
15
+ ): string | undefined {
16
+ for (const value of values) {
17
+ const trimmed = value?.trim();
18
+ if (trimmed) return trimmed;
19
+ }
20
+ return undefined;
21
+ }
22
+
23
+ /** The deploy environment name, e.g. `production`, `deploy-preview`. */
24
+ export function resolveDeployEnvironment(): string {
25
+ return (
26
+ firstNonEmpty(
27
+ process.env.SENTRY_ENVIRONMENT,
28
+ process.env.NETLIFY_CONTEXT,
29
+ process.env.VERCEL_ENV,
30
+ process.env.NODE_ENV,
31
+ ) ?? "production"
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Resolve the agent-native version baked into core's package.json so the
37
+ * reported "release" reflects the running framework version. Mirrors how the
38
+ * CLI computes `_version` — same dist layout, same fallback string. Guarded so
39
+ * a missing/unreadable package.json never crashes server boot.
40
+ */
41
+ export function resolveServerRelease(): string {
42
+ const explicit = process.env.AGENT_NATIVE_RELEASE;
43
+ if (explicit) return explicit;
44
+ try {
45
+ const here = path.dirname(fileURLToPath(import.meta.url));
46
+ // dist/server/deploy-environment.js → ../../package.json
47
+ const pkgPath = path.resolve(here, "../../package.json");
48
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
49
+ version?: string;
50
+ };
51
+ if (pkg?.version) return `agent-native-server@${pkg.version}`;
52
+ // coercion-ok: falls through to the distinct "unknown" release marker
53
+ } catch {
54
+ // ignore — fall through to "unknown"
55
+ }
56
+ return "agent-native-server@unknown";
57
+ }