@agent-native/core 0.172.2 → 0.172.4

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 (51) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/get-settings.ts +2 -5
  3. package/corpus/templates/calendar/actions/list-events.ts +21 -80
  4. package/corpus/templates/calendar/actions/update-settings.ts +11 -16
  5. package/corpus/templates/calendar/actions/view-screen.ts +25 -10
  6. package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
  7. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
  8. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
  9. package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
  10. package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
  11. package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
  12. package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
  13. package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
  14. package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
  15. package/corpus/templates/calendar/shared/settings.ts +15 -9
  16. package/corpus/templates/calendar/shared/timezone.ts +127 -0
  17. package/corpus/templates/clips/actions/lib/finalize-ended-meetings.ts +37 -0
  18. package/corpus/templates/clips/actions/request-transcript.ts +4 -0
  19. package/corpus/templates/clips/actions/save-browser-transcript.ts +4 -0
  20. package/corpus/templates/clips/desktop/src/app.tsx +436 -365
  21. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +17 -2
  22. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +21 -2
  23. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +1 -0
  24. package/corpus/templates/clips/desktop/src/styles.css +17 -3
  25. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +114 -20
  26. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +130 -15
  27. package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -4
  28. package/corpus/templates/content/actions/_database-row-mutation.ts +22 -2
  29. package/corpus/templates/slides/actions/list-decks.ts +79 -5
  30. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
  31. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
  32. package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
  33. package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
  34. package/corpus/templates/slides/app/pages/Index.tsx +11 -13
  35. package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
  36. package/dist/agent/production-agent.js +5 -1
  37. package/dist/agent/types.d.ts +5 -0
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  40. package/dist/observability/posthog-ai.d.ts +36 -11
  41. package/dist/observability/posthog-ai.js +99 -30
  42. package/dist/observability/routes.js +9 -2
  43. package/dist/observability/traces.js +330 -142
  44. package/dist/progress/routes.d.ts +1 -1
  45. package/dist/resources/handlers.d.ts +3 -3
  46. package/dist/secrets/routes.d.ts +6 -6
  47. package/dist/server/realtime-token.d.ts +1 -1
  48. package/dist/tracking/providers.js +9 -3
  49. package/dist/vite/client.js +1 -0
  50. package/docs/content/observability.mdx +27 -10
  51. package/package.json +3 -3
@@ -24,10 +24,10 @@
24
24
  * schema, and a name it does not define today it may define tomorrow with
25
25
  * different meaning — `input_truncated` / `spans_dropped` cannot collide.
26
26
  *
27
- * Content (`$ai_input` / `$ai_output_choices` / `$ai_input_state` /
28
- * `$ai_output_state`) is gated on config and always OMITTED when disabled.
29
- * Sending `[]` instead would be indistinguishable from a run that genuinely had
30
- * no messages.
27
+ * Content (a generation's `$ai_input` / `$ai_output_choices`, a span's
28
+ * `$ai_input_state` / `$ai_output_state`) is gated on config and always OMITTED
29
+ * when disabled. Sending `[]` instead would be indistinguishable from a run
30
+ * that genuinely had no messages.
31
31
  *
32
32
  * @see https://posthog.com/docs/ai-observability/traces
33
33
  * @see https://posthog.com/docs/ai-observability/spans
@@ -42,12 +42,18 @@ export interface AiErrorDetail {
42
42
  terminal_state?: string;
43
43
  retryable?: boolean;
44
44
  }
45
+ export declare function resolveAiError(isError: boolean, error: AiErrorDetail | undefined): AiErrorDetail | undefined;
45
46
  /**
46
47
  * Serialize a content value under a byte ceiling.
47
48
  *
48
- * Returns `{ truncated: true }` with a placeholder rather than a silently
49
- * shortened payload — a trace that shows half a conversation as if it were the
50
- * whole one is worse than one that says it was cut.
49
+ * A message list keeps the last user message behind a marker entry naming what
50
+ * was dropped — what was asked is what a trace is opened for, and it stays
51
+ * small. Replacing the whole conversation with a placeholder, which is what
52
+ * this did, left nothing at all.
53
+ *
54
+ * Anything else (a string, an object) still becomes a placeholder: there is no
55
+ * message to keep. Either way `truncated` is true, so a cut payload can never
56
+ * be read as a complete one.
51
57
  */
52
58
  export declare function boundAiContent(value: unknown): {
53
59
  value: unknown;
@@ -66,6 +72,10 @@ export interface AiTraceEventInput {
66
72
  durationMs: number;
67
73
  isError: boolean;
68
74
  error?: AiErrorDetail;
75
+ /** What KIND of failure this was — a terminal code, a cut-off reason, an
76
+ * engine error code. A classification, never content, so it ships even when
77
+ * the message is withheld. */
78
+ errorType?: string;
69
79
  inputTokens?: number;
70
80
  outputTokens?: number;
71
81
  costUsd?: number;
@@ -73,11 +83,15 @@ export interface AiTraceEventInput {
73
83
  /** Browser session id, when the run originated from a page. Links the trace
74
84
  * to PostHog session replay. */
75
85
  browserSessionId?: string;
76
- /** Omitted unless `capturePrompts` is on. */
77
- inputState?: unknown;
78
- outputState?: unknown;
79
86
  extraProperties?: Record<string, unknown>;
80
87
  }
88
+ /**
89
+ * The run's prompt and answer are NOT carried here. Every round-trip emits a
90
+ * generation with its own `$ai_input` / `$ai_output_choices`, so a trace-level
91
+ * copy repeated the first call's prompt and the last call's answer on a second
92
+ * event. The trace owns what only it knows: the run's name, failure, metadata
93
+ * and totals.
94
+ */
81
95
  export declare function emitAiTraceEvent(input: AiTraceEventInput): void;
82
96
  export interface AiSpanEventInput {
83
97
  runId: string;
@@ -90,6 +104,9 @@ export interface AiSpanEventInput {
90
104
  latencySeconds: number;
91
105
  isError: boolean;
92
106
  error?: AiErrorDetail;
107
+ /** The tool failure's class — `tool_error`, `interrupted`. A classification,
108
+ * never the tool's output, so it ships with `captureToolResults` off. */
109
+ errorType?: string;
93
110
  createdAt: number;
94
111
  browserSessionId?: string;
95
112
  /** Omitted unless `captureToolArgs` / `captureToolResults` are on. */
@@ -103,8 +120,12 @@ export interface AiFeedbackSurveyInput {
103
120
  threadId: string | null;
104
121
  userId: string | null;
105
122
  feedbackType: "thumbs_up" | "thumbs_down" | "category" | "text";
106
- /** The submitted value: sentiment label, chosen category, or free text. */
123
+ /** The submitted value: chosen category or free text. Ignored for thumbs,
124
+ * which answer a rating question and travel as PostHog's choice index. */
107
125
  value: string;
126
+ /** Groups the events of one response. A thumbs-down and the follow-up text
127
+ * it opens are two `survey sent` events answering two questions of the SAME
128
+ * response, so they must share this id. */
108
129
  submissionId: string;
109
130
  model?: string;
110
131
  browserSessionId?: string;
@@ -117,6 +138,10 @@ export interface AiFeedbackSurveyInput {
117
138
  * nowhere. Returns `false` and emits nothing when no survey id is configured:
118
139
  * inventing one would produce events attached to a survey that does not exist.
119
140
  *
141
+ * A thumbs vote answers the survey's first question; a category or free-text
142
+ * answer answers the follow-up question after it. Both share the caller's
143
+ * `submissionId`, which is how PostHog joins them into one response.
144
+ *
120
145
  * Sent to PostHog ONLY, not through `track()`. The survey response carries the
121
146
  * user's free-text feedback verbatim, and configuring a PostHog survey id must
122
147
  * not silently start shipping that text to Mixpanel, Amplitude, webhooks, or
@@ -24,10 +24,10 @@
24
24
  * schema, and a name it does not define today it may define tomorrow with
25
25
  * different meaning — `input_truncated` / `spans_dropped` cannot collide.
26
26
  *
27
- * Content (`$ai_input` / `$ai_output_choices` / `$ai_input_state` /
28
- * `$ai_output_state`) is gated on config and always OMITTED when disabled.
29
- * Sending `[]` instead would be indistinguishable from a run that genuinely had
30
- * no messages.
27
+ * Content (a generation's `$ai_input` / `$ai_output_choices`, a span's
28
+ * `$ai_input_state` / `$ai_output_state`) is gated on config and always OMITTED
29
+ * when disabled. Sending `[]` instead would be indistinguishable from a run
30
+ * that genuinely had no messages.
31
31
  *
32
32
  * @see https://posthog.com/docs/ai-observability/traces
33
33
  * @see https://posthog.com/docs/ai-observability/spans
@@ -38,6 +38,21 @@ import { boundedText } from "../tracking/redaction.js";
38
38
  export const MAX_AI_CONTENT_BYTES = 128 * 1024;
39
39
  /** Hard ceiling on emitted `$ai_span` events per run. */
40
40
  export const MAX_AI_SPANS_PER_RUN = 100;
41
+ /**
42
+ * `$ai_is_error: true` with no `$ai_error` is a dead end: the reader learns
43
+ * something broke and nothing else, which is how a failed tool span reached
44
+ * PostHog saying only "error". Emitters fall back to this rather than shipping
45
+ * the flag alone — a layer that failed without reporting a message is itself
46
+ * worth seeing, and is not the same as a message we chose to withhold.
47
+ */
48
+ const UNREPORTED_ERROR = {
49
+ message: "failed without a reported error message",
50
+ };
51
+ export function resolveAiError(isError, error) {
52
+ if (!isError)
53
+ return undefined;
54
+ return error ?? UNREPORTED_ERROR;
55
+ }
41
56
  function trackAiEvent(name, properties, userId, occurredAt) {
42
57
  for (const key of Object.keys(properties)) {
43
58
  if (properties[key] === undefined)
@@ -55,12 +70,24 @@ function trackAiEvent(name, properties, userId, occurredAt) {
55
70
  // Tracking must never affect the agent run or trace persistence.
56
71
  }
57
72
  }
73
+ function utf8Bytes(text) {
74
+ return typeof Buffer !== "undefined"
75
+ ? Buffer.byteLength(text, "utf8")
76
+ : new TextEncoder().encode(text).length;
77
+ }
78
+ /** Room reserved for the marker entry that reports what was dropped. */
79
+ const OMISSION_MARKER_BYTES = 512;
58
80
  /**
59
81
  * Serialize a content value under a byte ceiling.
60
82
  *
61
- * Returns `{ truncated: true }` with a placeholder rather than a silently
62
- * shortened payload — a trace that shows half a conversation as if it were the
63
- * whole one is worse than one that says it was cut.
83
+ * A message list keeps the last user message behind a marker entry naming what
84
+ * was dropped — what was asked is what a trace is opened for, and it stays
85
+ * small. Replacing the whole conversation with a placeholder, which is what
86
+ * this did, left nothing at all.
87
+ *
88
+ * Anything else (a string, an object) still becomes a placeholder: there is no
89
+ * message to keep. Either way `truncated` is true, so a cut payload can never
90
+ * be read as a complete one.
64
91
  */
65
92
  export function boundAiContent(value) {
66
93
  let serialized;
@@ -72,23 +99,45 @@ export function boundAiContent(value) {
72
99
  }
73
100
  if (serialized === undefined)
74
101
  return { value: undefined, truncated: false };
75
- const bytes = typeof Buffer !== "undefined"
76
- ? Buffer.byteLength(serialized, "utf8")
77
- : new TextEncoder().encode(serialized).length;
102
+ const bytes = utf8Bytes(serialized);
78
103
  if (bytes <= MAX_AI_CONTENT_BYTES)
79
104
  return { value, truncated: false };
105
+ if (Array.isArray(value)) {
106
+ // What was ASKED is the one thing worth rescuing from an oversized
107
+ // transcript: the rest is context the thread itself still holds, and
108
+ // keeping as much of it as fits just ships the ceiling on every event.
109
+ const lastUser = value.findLast((entry) => !!entry &&
110
+ typeof entry === "object" &&
111
+ entry.role === "user");
112
+ const kept = lastUser !== undefined &&
113
+ utf8Bytes(JSON.stringify(lastUser) ?? "null") <=
114
+ MAX_AI_CONTENT_BYTES - OMISSION_MARKER_BYTES
115
+ ? [lastUser]
116
+ : [];
117
+ return {
118
+ value: [
119
+ {
120
+ role: "system",
121
+ content: `[${value.length - kept.length} message(s) omitted: ${bytes} bytes exceeded the ${MAX_AI_CONTENT_BYTES}-byte trace content limit]`,
122
+ },
123
+ ...kept,
124
+ ],
125
+ truncated: true,
126
+ };
127
+ }
80
128
  return {
81
129
  value: `[truncated: ${bytes} bytes exceeded the ${MAX_AI_CONTENT_BYTES}-byte trace content limit]`,
82
130
  truncated: true,
83
131
  };
84
132
  }
133
+ /**
134
+ * The run's prompt and answer are NOT carried here. Every round-trip emits a
135
+ * generation with its own `$ai_input` / `$ai_output_choices`, so a trace-level
136
+ * copy repeated the first call's prompt and the last call's answer on a second
137
+ * event. The trace owns what only it knows: the run's name, failure, metadata
138
+ * and totals.
139
+ */
85
140
  export function emitAiTraceEvent(input) {
86
- const inputContent = input.inputState === undefined
87
- ? undefined
88
- : boundAiContent(input.inputState);
89
- const outputContent = input.outputState === undefined
90
- ? undefined
91
- : boundAiContent(input.outputState);
92
141
  trackAiEvent("$ai_trace", {
93
142
  ...input.extraProperties,
94
143
  $ai_trace_id: input.runId,
@@ -97,15 +146,14 @@ export function emitAiTraceEvent(input) {
97
146
  $ai_model: input.model,
98
147
  $ai_provider: input.provider,
99
148
  $ai_is_error: input.isError,
100
- $ai_error: input.error,
149
+ $ai_error: resolveAiError(input.isError, input.error),
150
+ $ai_error_type: input.isError
151
+ ? (input.errorType ?? "run_error")
152
+ : undefined,
101
153
  duration_ms: Math.round(input.durationMs),
102
154
  input_tokens: input.inputTokens,
103
155
  output_tokens: input.outputTokens,
104
156
  cost_usd: input.costUsd,
105
- $ai_input_state: inputContent?.value,
106
- $ai_output_state: outputContent?.value,
107
- input_truncated: inputContent?.truncated || undefined,
108
- output_truncated: outputContent?.truncated || undefined,
109
157
  $session_id: input.browserSessionId,
110
158
  created_at: new Date(input.createdAt).toISOString(),
111
159
  }, input.userId, input.createdAt);
@@ -126,7 +174,10 @@ export function emitAiSpanEvent(input) {
126
174
  $ai_span_name: input.spanName,
127
175
  $ai_latency: input.latencySeconds,
128
176
  $ai_is_error: input.isError,
129
- $ai_error: input.error,
177
+ $ai_error: resolveAiError(input.isError, input.error),
178
+ $ai_error_type: input.isError
179
+ ? (input.errorType ?? "tool_error")
180
+ : undefined,
130
181
  $ai_input_state: inputContent?.value,
131
182
  $ai_output_state: outputContent?.value,
132
183
  input_truncated: inputContent?.truncated || undefined,
@@ -135,6 +186,14 @@ export function emitAiSpanEvent(input) {
135
186
  created_at: new Date(input.createdAt).toISOString(),
136
187
  }, input.userId, input.createdAt);
137
188
  }
189
+ /**
190
+ * PostHog's thumbs survey is a choice question, and the response is the
191
+ * choice's 1-based index — not a label. Sending "thumbs_up" put a string where
192
+ * the rating belongs, so the vote counted as an unrecognized answer.
193
+ *
194
+ * @see https://posthog.com/docs/ai-observability/user-feedback/manual-event-capture
195
+ */
196
+ const THUMB_RESPONSE_INDEX = { thumbs_up: 1, thumbs_down: 2 };
138
197
  /**
139
198
  * Emit PostHog's documented manual feedback event for an LLM trace.
140
199
  *
@@ -143,6 +202,10 @@ export function emitAiSpanEvent(input) {
143
202
  * nowhere. Returns `false` and emits nothing when no survey id is configured:
144
203
  * inventing one would produce events attached to a survey that does not exist.
145
204
  *
205
+ * A thumbs vote answers the survey's first question; a category or free-text
206
+ * answer answers the follow-up question after it. Both share the caller's
207
+ * `submissionId`, which is how PostHog joins them into one response.
208
+ *
146
209
  * Sent to PostHog ONLY, not through `track()`. The survey response carries the
147
210
  * user's free-text feedback verbatim, and configuring a PostHog survey id must
148
211
  * not silently start shipping that text to Mixpanel, Amplitude, webhooks, or
@@ -155,17 +218,23 @@ export function emitAiFeedbackSurveyEvent(input) {
155
218
  const surveyId = process.env.POSTHOG_AI_FEEDBACK_SURVEY_ID?.trim();
156
219
  if (!surveyId)
157
220
  return false;
158
- const questionId = process.env.POSTHOG_AI_FEEDBACK_SURVEY_QUESTION_ID?.trim();
159
- // PostHog accepts `$survey_response` for a single-question survey and
160
- // `$survey_response_<questionId>` when the survey has named questions.
161
- const responseKey = questionId
162
- ? `$survey_response_${questionId}`
163
- : "$survey_response";
221
+ const thumbResponse = input.feedbackType === "thumbs_up" || input.feedbackType === "thumbs_down"
222
+ ? THUMB_RESPONSE_INDEX[input.feedbackType]
223
+ : undefined;
224
+ // `$survey_response` is the survey's first question — the thumbs — and
225
+ // `$survey_response_1` the follow-up asked after a thumbs-down. Putting the
226
+ // free text on the first key instead would file prose where the rating
227
+ // belongs and skew every thumbs metric in the survey.
228
+ const responses = thumbResponse !== undefined
229
+ ? { $survey_response: thumbResponse }
230
+ : { $survey_response_1: input.value };
164
231
  const properties = {
165
232
  $survey_id: surveyId,
166
- [responseKey]: input.value,
233
+ ...responses,
167
234
  $survey_submission_id: input.submissionId,
168
- $survey_completed: true,
235
+ // A thumbs-down opens the follow-up question, so the response is not
236
+ // complete until that answer arrives (or the user closes it without one).
237
+ $survey_completed: input.feedbackType !== "thumbs_down",
169
238
  $ai_trace_id: input.runId ?? undefined,
170
239
  $ai_session_id: input.threadId ?? undefined,
171
240
  $ai_model: input.model,
@@ -216,8 +216,15 @@ export function createObservabilityHandler() {
216
216
  threadId,
217
217
  userId: owner,
218
218
  feedbackType,
219
- value: isThumb ? feedbackType : value,
220
- submissionId: id,
219
+ value,
220
+ // One PostHog response per rated message, not per row: a thumbs-down
221
+ // and the free text it opens are two answers to the same survey, and
222
+ // a fresh id per row would file them as two unrelated responses.
223
+ // Falls back to the row id when there is no message to key on, which
224
+ // groups nothing — the honest outcome when nothing can be grouped.
225
+ submissionId: runId && typeof body.messageSeq === "number"
226
+ ? `${runId}:${body.messageSeq}`
227
+ : id,
221
228
  model,
222
229
  browserSessionId: getRequestContext()?.browserSessionId,
223
230
  });