@agent-native/core 0.144.0 → 0.144.2

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 (88) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +1 -1
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +36 -14
  4. package/corpus/templates/clips/app/components/player/player-controls.tsx +0 -3
  5. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -33
  6. package/corpus/templates/clips/app/components/player/video-player.tsx +58 -11
  7. package/corpus/templates/clips/changelog/2026-08-06-trimmed-clips-play-on-a-clean-timeline-without-cut-markers.md +6 -0
  8. package/corpus/templates/dispatch/AGENTS.md +8 -1
  9. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -0
  10. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +30 -15
  11. package/corpus/templates/dispatch/app/i18n-data.ts +131 -0
  12. package/corpus/templates/dispatch/app/routes/admin._index.tsx +4 -0
  13. package/corpus/templates/dispatch/app/routes/admin.agents.tsx +1 -0
  14. package/corpus/templates/dispatch/app/routes/admin.approvals.tsx +1 -0
  15. package/corpus/templates/dispatch/app/routes/admin.apps.$appId.tsx +1 -0
  16. package/corpus/templates/dispatch/app/routes/admin.apps.tsx +1 -0
  17. package/corpus/templates/dispatch/app/routes/admin.audit.tsx +1 -0
  18. package/corpus/templates/dispatch/app/routes/admin.automations.tsx +1 -0
  19. package/corpus/templates/dispatch/app/routes/admin.destinations.tsx +4 -0
  20. package/corpus/templates/dispatch/app/routes/admin.dreams.tsx +1 -0
  21. package/corpus/templates/dispatch/app/routes/admin.identities.tsx +1 -0
  22. package/corpus/templates/dispatch/app/routes/admin.integrations.tsx +4 -0
  23. package/corpus/templates/dispatch/app/routes/admin.messaging.tsx +1 -0
  24. package/corpus/templates/dispatch/app/routes/admin.metrics.tsx +1 -0
  25. package/corpus/templates/dispatch/app/routes/admin.new-app.tsx +1 -0
  26. package/corpus/templates/dispatch/app/routes/admin.operations.tsx +1 -0
  27. package/corpus/templates/dispatch/app/routes/admin.thread-debug.tsx +4 -0
  28. package/corpus/templates/dispatch/app/routes/admin.transactional-email.$appId.$id.tsx +4 -0
  29. package/corpus/templates/dispatch/app/routes/admin.transactional-email.tsx +4 -0
  30. package/corpus/templates/dispatch/app/routes/admin.tsx +1 -0
  31. package/corpus/templates/dispatch/app/routes/admin.vault.tsx +1 -0
  32. package/corpus/templates/dispatch/app/routes/admin.workspace.tsx +1 -0
  33. package/corpus/templates/dispatch/changelog/2026-08-06-dispatch-keeps-everyday-navigation-focused-while-management-.md +6 -0
  34. package/corpus/templates/slides/AGENTS.md +10 -12
  35. package/corpus/templates/slides/actions/export-pptx.ts +68 -0
  36. package/corpus/templates/slides/actions/get-deck.ts +12 -0
  37. package/corpus/templates/slides/actions/import-file.ts +130 -55
  38. package/corpus/templates/slides/actions/import-pptx.ts +108 -8
  39. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  40. package/corpus/templates/slides/actions/update-slide.ts +29 -3
  41. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +11 -0
  42. package/corpus/templates/slides/app/lib/create-deck-generation.ts +72 -11
  43. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +89 -0
  44. package/corpus/templates/slides/app/pages/Index.tsx +85 -18
  45. package/corpus/templates/slides/changelog/2026-08-06-uploaded-decks-keep-their-slides-and-visuals-when-you-improv.md +6 -0
  46. package/corpus/templates/slides/server/handlers/import/html-converter.ts +13 -2
  47. package/corpus/templates/slides/server/lib/chat-attachments.ts +2 -2
  48. package/corpus/templates/slides/server/lib/source-import.ts +167 -0
  49. package/corpus/templates/slides/server/plugins/agent-chat.ts +8 -0
  50. package/dist/client/analytics/index.d.ts +1 -1
  51. package/dist/client/analytics/index.js +1 -1
  52. package/dist/client/analytics-session.d.ts +13 -0
  53. package/dist/client/analytics-session.js +57 -0
  54. package/dist/client/analytics.d.ts +1 -0
  55. package/dist/client/analytics.js +1 -0
  56. package/dist/client/index.d.ts +1 -1
  57. package/dist/client/index.js +1 -1
  58. package/dist/client/track.js +7 -0
  59. package/dist/client/use-action.js +10 -1
  60. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  61. package/dist/jobs/scheduler.js +79 -23
  62. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  63. package/dist/notifications/routes.d.ts +3 -3
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/routes.d.ts +9 -9
  67. package/dist/server/action-routes.js +4 -2
  68. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  69. package/dist/server/agent-run-context.d.ts +4 -2
  70. package/dist/server/agent-run-context.js +5 -3
  71. package/dist/server/core-routes-plugin.js +2 -0
  72. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  73. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
  74. package/dist/tracking/index.d.ts +1 -1
  75. package/dist/tracking/providers.js +12 -1
  76. package/dist/tracking/registry.d.ts +12 -2
  77. package/dist/tracking/registry.js +27 -3
  78. package/dist/tracking/types.d.ts +8 -0
  79. package/docs/content/creating-templates.mdx +1 -1
  80. package/docs/content/getting-started-actions.mdx +38 -189
  81. package/docs/content/getting-started-database.mdx +80 -164
  82. package/docs/content/getting-started-pages.mdx +44 -155
  83. package/docs/content/getting-started.mdx +26 -106
  84. package/docs/content/observability.mdx +16 -0
  85. package/docs/content/tracking.mdx +17 -2
  86. package/package.json +1 -1
  87. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  88. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- count?: undefined;
20
19
  updated: number;
21
20
  error?: undefined;
22
21
  ok?: undefined;
23
- } | {
24
22
  count?: undefined;
23
+ } | {
25
24
  updated?: undefined;
26
25
  error: string;
27
26
  ok?: undefined;
28
- } | {
29
27
  count?: undefined;
28
+ } | {
30
29
  updated?: undefined;
31
30
  error?: undefined;
32
31
  ok: boolean;
32
+ count?: undefined;
33
33
  }>>;
@@ -75,8 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- id?: undefined;
79
78
  deleted?: undefined;
79
+ id?: undefined;
80
80
  providers: {
81
81
  id: string;
82
82
  label: string;
@@ -93,9 +93,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
93
93
  label?: undefined;
94
94
  message?: undefined;
95
95
  } | {
96
- id?: undefined;
97
96
  deleted?: undefined;
98
97
  count?: undefined;
98
+ id?: undefined;
99
99
  providers?: undefined;
100
100
  found: boolean;
101
101
  provider: import("../custom-registry.js").CustomProviderConfig;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- ok?: undefined;
38
37
  status?: undefined;
38
+ ok?: undefined;
39
39
  } | {
40
- error?: undefined;
41
40
  ok: boolean;
42
41
  status: string;
42
+ error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
- error?: undefined;
49
48
  ok: boolean;
50
49
  removed: boolean;
50
+ error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
- ok?: undefined;
58
57
  error: string;
59
58
  note?: undefined;
59
+ ok?: undefined;
60
60
  } | {
61
- error?: undefined;
62
61
  ok: boolean;
63
62
  note?: undefined;
64
- } | {
65
63
  error?: undefined;
64
+ } | {
66
65
  ok: boolean;
67
66
  note: string;
67
+ error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
- error?: undefined;
99
98
  ok: boolean;
100
99
  key: string;
101
- } | {
102
100
  error?: undefined;
101
+ } | {
103
102
  ok: boolean;
104
103
  removed: boolean;
104
+ error?: undefined;
105
105
  }>>;
@@ -7,7 +7,7 @@ import { readBody } from "../server/h3-helpers.js";
7
7
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
8
8
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
9
9
  import { actionCallIsReadOnly, notifyActionChange } from "./action-change.js";
10
- import { seedAgentRunOwnerContext, } from "./agent-run-context.js";
10
+ import { readBrowserSessionIdHeader, seedAgentRunOwnerContext, } from "./agent-run-context.js";
11
11
  import { captureError } from "./capture-error.js";
12
12
  import { getAllowedCorsOrigin as resolveAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
13
13
  import { resolveEmbedSessionFromRequest, resolvedEmbedCapabilityScope, } from "./embed-session.js";
@@ -158,7 +158,7 @@ function handleOptionsRequest(event) {
158
158
  }
159
159
  setResponseHeader(event, "Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
160
160
  setResponseHeader(event, "Access-Control-Allow-Headers", cors.credentials
161
- ? `Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,${EMBED_TARGET_HEADER}`
161
+ ? `Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,${EMBED_TARGET_HEADER}`
162
162
  : `${MCP_EMBED_CORS_ALLOW_HEADERS},X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id`);
163
163
  }
164
164
  setResponseStatus(event, 204);
@@ -350,12 +350,14 @@ export function mountActionRoutes(nitroApp, actions, options) {
350
350
  : undefined;
351
351
  }
352
352
  const timezone = readTimezoneHeader(event);
353
+ const browserSessionId = readBrowserSessionIdHeader(event);
353
354
  return runWithRequestContext({
354
355
  userEmail,
355
356
  userName,
356
357
  orgId,
357
358
  authCapability,
358
359
  timezone,
360
+ browserSessionId,
359
361
  requestOrigin: getRequestURL(event).origin,
360
362
  // Captured here because this is the last layer that still holds
361
363
  // the h3 event; everything below reads it off the request store.
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -14,9 +14,11 @@ export declare function readAgentRunTimezone(event: H3Event): string | undefined
14
14
  *
15
15
  * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
16
16
  * trace joins to the session replay it happened in. Distinct from
17
- * `$ai_session_id`, which is the conversation thread.
17
+ * `$ai_session_id`, which is the conversation thread. Read by both the agent
18
+ * run path and the HTTP action route, so one visit correlates across the UI's
19
+ * action calls and the agent's.
18
20
  */
19
- export declare function readAgentRunBrowserSessionId(event: H3Event): string | undefined;
21
+ export declare function readBrowserSessionIdHeader(event: H3Event): string | undefined;
20
22
  export declare function seedAgentRunOwnerContext(event: H3Event, ownerContext: AgentRunOwnerContext): AgentRunOwnerContext;
21
23
  export declare function seedBackgroundAgentRunOwnerContext(event: H3Event, runId: string): Promise<AgentRunOwnerContext | null>;
22
24
  export declare function resolveAgentRunOwnerContext(event: H3Event, options?: {
@@ -49,9 +49,11 @@ export function readAgentRunTimezone(event) {
49
49
  *
50
50
  * Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
51
51
  * trace joins to the session replay it happened in. Distinct from
52
- * `$ai_session_id`, which is the conversation thread.
52
+ * `$ai_session_id`, which is the conversation thread. Read by both the agent
53
+ * run path and the HTTP action route, so one visit correlates across the UI's
54
+ * action calls and the agent's.
53
55
  */
54
- export function readAgentRunBrowserSessionId(event) {
56
+ export function readBrowserSessionIdHeader(event) {
55
57
  const raw = readHeaderValue(event, "x-agent-native-session-id");
56
58
  const value = Array.isArray(raw) ? raw[0] : raw;
57
59
  return typeof value === "string" &&
@@ -140,7 +142,7 @@ export async function resolveAgentRunOrgId(options) {
140
142
  export async function resolveAgentRunRequestContext(options) {
141
143
  const orgId = await resolveAgentRunOrgId(options);
142
144
  const timezone = readAgentRunTimezone(options.event);
143
- const browserSessionId = readAgentRunBrowserSessionId(options.event);
145
+ const browserSessionId = readBrowserSessionIdHeader(options.event);
144
146
  const waitUntil = requestWaitUntil(options.event);
145
147
  const run = {
146
148
  ...(options.isBackgroundWorker ? { isBackgroundWorker: true } : {}),
@@ -33,6 +33,7 @@ import { registerBuiltinProviders } from "../tracking/providers.js";
33
33
  import { validateTrackPayload } from "../tracking/route.js";
34
34
  import { createAutomationsHandler } from "../triggers/routes.js";
35
35
  import { createAgentEngineApiKeyHandler } from "./agent-engine-api-key-route.js";
36
+ import { readBrowserSessionIdHeader } from "./agent-run-context.js";
36
37
  import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js";
37
38
  import { getAppName } from "./app-name.js";
38
39
  import { getSession } from "./auth.js";
@@ -2544,6 +2545,7 @@ export function createCoreRoutesPlugin(options = {}) {
2544
2545
  try {
2545
2546
  track(validation.name, properties, {
2546
2547
  userId: userEmail,
2548
+ sessionId: readBrowserSessionIdHeader(event),
2547
2549
  });
2548
2550
  }
2549
2551
  catch {
@@ -283,7 +283,11 @@ same best-effort fan-out as other tracking events.
283
283
  upward through `$ai_parent_id` so a backend can rebuild the tree.
284
284
  `$ai_session_id` is the thread; the browser session is separate and ships as
285
285
  `$session_id`, read from `X-Agent-Native-Session-Id` via
286
- `RequestContext.browserSessionId`. Emission lives in `posthog-ai.ts`.
286
+ `RequestContext.browserSessionId`. The agent chat and the action client both
287
+ send that header, so a UI action call and the agent's own call during one
288
+ visit share a session. `setAnalyticsSessionId()` from
289
+ `@agent-native/core/client/analytics` pins a custom id and opts it out of the
290
+ 30-minute idle rotation. Emission lives in `posthog-ai.ts`.
287
291
  - Agent Native Analytics shape: the same event lands in `analytics_events` with
288
292
  mirrored query-friendly properties such as `run_id`, `thread_id`,
289
293
  `cost_cents_x100`, `duration_ms`, `tool_calls`, `successful_tools`,
@@ -18,19 +18,26 @@ The tracking system provides a single `track()` call that fans out to all regist
18
18
  ## How It Works
19
19
 
20
20
  1. At server startup, `registerBuiltinProviders()` checks env vars and registers any configured providers.
21
- 2. Application code calls `track(eventName, properties, meta)` from actions, plugins, or server routes.
21
+ 2. Application code calls `track(eventName, properties, source)` from actions, plugins, or server routes.
22
22
  3. The registry fans out the event to every registered provider. Errors are caught and logged -- a failing provider never crashes the caller.
23
23
  4. Built-in providers batch HTTP calls (flush every 10 seconds or 50 events, whichever comes first).
24
24
 
25
25
  ## API
26
26
 
27
- ### `track(name, properties?, meta?)`
27
+ ### `track(name, properties?, source?)`
28
28
 
29
- Fire an analytics event.
29
+ Fire an analytics event. `source` is either a `{ userId, anonymousId, sessionId }`
30
+ meta object or an action's `ctx` passed straight through.
30
31
 
31
32
  ```ts
32
33
  import { track } from "@agent-native/core/tracking";
33
34
 
35
+ // From an action — pass ctx; userId comes from ctx.userEmail.
36
+ run: async ({ name }, ctx) => {
37
+ track("meal.logged", { mealName: name, calories: 350 }, ctx);
38
+ };
39
+
40
+ // From a plugin or route with no ctx.
34
41
  track(
35
42
  "meal.logged",
36
43
  { mealName: "Salad", calories: 350 },
@@ -38,6 +45,16 @@ track(
38
45
  );
39
46
  ```
40
47
 
48
+ The caller's browser session comes from the ambient request context
49
+ (`RequestContext.browserSessionId`, set from the `X-Agent-Native-Session-Id`
50
+ header), so it resolves the same whether the UI called the action or the agent
51
+ did. Pass `sessionId` in the meta object to override it — routes that run
52
+ outside a request context, such as `/_agent-native/track`, do exactly that.
53
+ Providers map it to their own session field: `$session_id` for PostHog (which
54
+ joins the event to session replay), `session_id` as a property for Mixpanel and
55
+ Amplitude, a top-level `sessionId` for webhooks and Agent Native Analytics. It
56
+ is absent for callers with no browser — cron, CLI, MCP, A2A.
57
+
41
58
  ### `identify(userId, traits?)`
42
59
 
43
60
  Identify a user with traits. Forwarded to providers that support it.
@@ -1,4 +1,4 @@
1
- export { track, identify, flushTracking, registerTrackingProvider, unregisterTrackingProvider, listTrackingProviders, } from "./registry.js";
1
+ export { track, identify, flushTracking, registerTrackingProvider, unregisterTrackingProvider, listTrackingProviders, type TrackingMeta, type TrackingSource, } from "./registry.js";
2
2
  export { registerBuiltinProviders } from "./providers.js";
3
3
  export { captureException, type TrackingExceptionContext, type TrackingExceptionLevel, } from "./error-capture.js";
4
4
  export { errorToPostHogExceptionProperties, parseStackFrames, reshapeTrackedExceptionProperties, toPostHogExceptionProperties, type PostHogExceptionEntry, type PostHogExceptionInput, type PostHogExceptionLevel, type PostHogExceptionProperties, type PostHogStackFrame, } from "./posthog-exception.js";
@@ -134,6 +134,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
134
134
  properties: {
135
135
  distinct_id: distinctId,
136
136
  ...properties,
137
+ ...(event.sessionId ? { $session_id: event.sessionId } : {}),
137
138
  timestamp: event.timestamp,
138
139
  },
139
140
  }));
@@ -168,6 +169,7 @@ function createPostHogProvider(apiKey, host, errorTracking) {
168
169
  distinct_id: distinctId,
169
170
  properties: {
170
171
  ...event.properties,
172
+ ...(event.sessionId ? { $session_id: event.sessionId } : {}),
171
173
  timestamp: event.timestamp,
172
174
  },
173
175
  }));
@@ -225,6 +227,9 @@ function createMixpanelProvider(token) {
225
227
  ? new Date(event.timestamp).getTime() / 1000
226
228
  : undefined,
227
229
  ...event.properties,
230
+ // Mixpanel's own `$session_id` is numeric and assigned by its SDK, so
231
+ // the browser session lands as a plain property here.
232
+ ...(event.sessionId ? { session_id: event.sessionId } : {}),
228
233
  },
229
234
  };
230
235
  enqueue("https://api.mixpanel.com/track", JSON.stringify([data]));
@@ -253,7 +258,11 @@ function createAmplitudeProvider(apiKey) {
253
258
  {
254
259
  event_type: event.name,
255
260
  user_id: event.userId || "anonymous",
256
- event_properties: event.properties,
261
+ // Amplitude's top-level `session_id` must be a numeric epoch, so
262
+ // the browser session ships as an event property instead.
263
+ event_properties: event.sessionId
264
+ ? { ...event.properties, session_id: event.sessionId }
265
+ : event.properties,
257
266
  time: event.timestamp
258
267
  ? new Date(event.timestamp).getTime()
259
268
  : undefined,
@@ -290,6 +299,7 @@ function createWebhookProvider(url, authHeader) {
290
299
  event: event.name,
291
300
  properties: event.properties,
292
301
  userId: event.userId,
302
+ sessionId: event.sessionId,
293
303
  timestamp: event.timestamp,
294
304
  }), extra);
295
305
  },
@@ -318,6 +328,7 @@ function createAgentNativeAnalyticsProvider(publicKey, endpoint) {
318
328
  properties: event.properties ?? {},
319
329
  userId: event.userId,
320
330
  anonymousId: event.anonymousId,
331
+ sessionId: event.sessionId,
321
332
  timestamp: event.timestamp,
322
333
  }), undefined, { flushImmediately });
323
334
  },
@@ -1,10 +1,20 @@
1
+ import type { ActionRunContext } from "../action.js";
1
2
  import type { TrackingProvider } from "./types.js";
2
3
  export declare function registerTrackingProvider(provider: TrackingProvider): void;
3
4
  export declare function unregisterTrackingProvider(name: string): boolean;
4
5
  export declare function listTrackingProviders(): string[];
5
- export declare function track(name: string, properties?: Record<string, unknown>, meta?: {
6
+ export interface TrackingMeta {
6
7
  userId?: string;
7
8
  anonymousId?: string;
8
- }): void;
9
+ /** Overrides the ambient request's browser session. */
10
+ sessionId?: string;
11
+ }
12
+ /**
13
+ * Who an event is attributed to. Pass an action's `ctx` straight through —
14
+ * `track("project_created", { template }, ctx)` — instead of restating
15
+ * `{ userId: ctx.userEmail }` at every call site.
16
+ */
17
+ export type TrackingSource = TrackingMeta | ActionRunContext;
18
+ export declare function track(name: string, properties?: Record<string, unknown>, source?: TrackingSource): void;
9
19
  export declare function identify(userId: string, traits?: Record<string, unknown>): void;
10
20
  export declare function flushTracking(): Promise<void[]>;
@@ -1,3 +1,4 @@
1
+ import { getRequestContext } from "../server/request-context.js";
1
2
  const REGISTRY_KEY = Symbol.for("@agent-native/core/tracking.registry");
2
3
  function getRegistry() {
3
4
  const g = globalThis;
@@ -20,13 +21,36 @@ export function unregisterTrackingProvider(name) {
20
21
  export function listTrackingProviders() {
21
22
  return Array.from(getRegistry().keys());
22
23
  }
23
- export function track(name, properties, meta) {
24
+ // `caller` is required on ActionRunContext and absent from TrackingMeta, so it
25
+ // is the one field that tells the two apart without the caller declaring which
26
+ // shape it passed.
27
+ function isActionRunContext(source) {
28
+ return typeof source.caller === "string";
29
+ }
30
+ function resolveTrackingSource(source) {
31
+ // The browser session rides the request, not the caller's arguments, so it
32
+ // resolves the same way whether the UI called the action or the agent did.
33
+ const ambientSessionId = getRequestContext()?.browserSessionId;
34
+ if (!source)
35
+ return { sessionId: ambientSessionId };
36
+ if (isActionRunContext(source)) {
37
+ return { userId: source.userEmail, sessionId: ambientSessionId };
38
+ }
39
+ return {
40
+ userId: source.userId,
41
+ anonymousId: source.anonymousId,
42
+ sessionId: source.sessionId ?? ambientSessionId,
43
+ };
44
+ }
45
+ export function track(name, properties, source) {
46
+ const { userId, anonymousId, sessionId } = resolveTrackingSource(source);
24
47
  const event = {
25
48
  name,
26
49
  properties,
27
50
  timestamp: new Date().toISOString(),
28
- userId: meta?.userId,
29
- anonymousId: meta?.anonymousId,
51
+ userId,
52
+ anonymousId,
53
+ sessionId,
30
54
  };
31
55
  for (const provider of getRegistry().values()) {
32
56
  try {
@@ -4,6 +4,14 @@ export interface TrackingEvent {
4
4
  timestamp?: string;
5
5
  userId?: string;
6
6
  anonymousId?: string;
7
+ /**
8
+ * Browser session the event belongs to, so a server-side event joins the
9
+ * visit that caused it. Carried as a typed field rather than a property so
10
+ * each provider maps it to its own session concept instead of every backend
11
+ * receiving another backend's reserved key. `undefined` for callers with no
12
+ * browser — cron, CLI, MCP, A2A — which is a real distinction, not a gap.
13
+ */
14
+ sessionId?: string;
7
15
  }
8
16
  export interface TrackingProvider {
9
17
  name: string;
@@ -49,7 +49,7 @@ npx @agent-native/core@latest create my-platform
49
49
 
50
50
  Chat gives you auth, durable chat threads, SQL-backed resources, tools, application state, actions, and polling sync. You add the domain model and product UI.
51
51
 
52
- If you are not building a reusable UI template yet, use the headless on-ramp in [Getting Started](/docs/getting-started#create-your-app): define one action, run it with `pnpm agent`, and add UI later when the workflow needs a durable surface.
52
+ If you are not building a reusable UI template yet, use [Automation-First Apps](/docs/pure-agent-apps): define actions, run them without a browser, and add UI later when the workflow needs a durable surface.
53
53
 
54
54
  ## Project Structure {#project-structure}
55
55