@agent-native/core 0.133.2 → 0.134.0

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 (213) 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/tracking.mdx +25 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/run-manager.ts +7 -0
  7. package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
  8. package/corpus/core/src/client/analytics.ts +105 -1
  9. package/corpus/core/src/client/use-action.ts +6 -0
  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/content/actions/_batch-utils.ts +33 -0
  36. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
  37. package/corpus/templates/content/actions/_content-files.ts +31 -21
  38. package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
  39. package/corpus/templates/content/actions/_database-utils.ts +535 -41
  40. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
  41. package/corpus/templates/content/actions/add-database-item.ts +15 -1
  42. package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
  43. package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
  44. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
  45. package/corpus/templates/content/actions/delete-database-items.ts +8 -2
  46. package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
  47. package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
  48. package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
  49. package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
  50. package/corpus/templates/content/actions/get-content-database.ts +12 -64
  51. package/corpus/templates/content/actions/move-database-item.ts +4 -1
  52. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
  53. package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
  54. package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
  55. package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
  56. package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
  57. package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
  58. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
  59. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
  60. package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
  61. package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
  62. package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
  63. package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
  64. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
  65. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
  66. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
  67. package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
  68. package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
  69. package/corpus/templates/content/app/i18n-data.ts +10 -0
  70. package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
  71. package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
  72. package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
  73. package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
  74. package/corpus/templates/content/parity/matrix.md +2 -1
  75. package/corpus/templates/content/parity/matrix.ts +25 -0
  76. package/corpus/templates/content/shared/api.ts +54 -4
  77. package/corpus/templates/content/shared/database-query.ts +359 -0
  78. package/dist/agent/run-manager.d.ts.map +1 -1
  79. package/dist/agent/run-manager.js +7 -0
  80. package/dist/agent/run-manager.js.map +1 -1
  81. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  82. package/dist/client/agent-chat-adapter.js +12 -0
  83. package/dist/client/agent-chat-adapter.js.map +1 -1
  84. package/dist/client/analytics.d.ts +8 -0
  85. package/dist/client/analytics.d.ts.map +1 -1
  86. package/dist/client/analytics.js +95 -1
  87. package/dist/client/analytics.js.map +1 -1
  88. package/dist/client/use-action.d.ts.map +1 -1
  89. package/dist/client/use-action.js +6 -0
  90. package/dist/client/use-action.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/collab/struct-routes.d.ts +1 -1
  93. package/dist/deploy/build.d.ts.map +1 -1
  94. package/dist/deploy/build.js +38 -1
  95. package/dist/deploy/build.js.map +1 -1
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/notifications/routes.d.ts +3 -3
  98. package/dist/observability/posthog-ai.d.ts +126 -0
  99. package/dist/observability/posthog-ai.d.ts.map +1 -0
  100. package/dist/observability/posthog-ai.js +190 -0
  101. package/dist/observability/posthog-ai.js.map +1 -0
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/observability/routes.d.ts.map +1 -1
  104. package/dist/observability/routes.js +26 -6
  105. package/dist/observability/routes.js.map +1 -1
  106. package/dist/observability/traces.d.ts +9 -0
  107. package/dist/observability/traces.d.ts.map +1 -1
  108. package/dist/observability/traces.js +219 -10
  109. package/dist/observability/traces.js.map +1 -1
  110. package/dist/observability/types.d.ts +10 -0
  111. package/dist/observability/types.d.ts.map +1 -1
  112. package/dist/observability/types.js +1 -0
  113. package/dist/observability/types.js.map +1 -1
  114. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  115. package/dist/secrets/register-framework-secrets.js +14 -0
  116. package/dist/secrets/register-framework-secrets.js.map +1 -1
  117. package/dist/server/agent-run-context.d.ts +8 -0
  118. package/dist/server/agent-run-context.d.ts.map +1 -1
  119. package/dist/server/agent-run-context.js +18 -0
  120. package/dist/server/agent-run-context.js.map +1 -1
  121. package/dist/server/capture-error.d.ts +8 -0
  122. package/dist/server/capture-error.d.ts.map +1 -1
  123. package/dist/server/capture-error.js.map +1 -1
  124. package/dist/server/core-routes-plugin.d.ts +0 -23
  125. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  126. package/dist/server/core-routes-plugin.js +71 -7
  127. package/dist/server/core-routes-plugin.js.map +1 -1
  128. package/dist/server/deploy-environment.d.ts +17 -0
  129. package/dist/server/deploy-environment.d.ts.map +1 -0
  130. package/dist/server/deploy-environment.js +47 -0
  131. package/dist/server/deploy-environment.js.map +1 -0
  132. package/dist/server/error-noise-filter.d.ts +80 -0
  133. package/dist/server/error-noise-filter.d.ts.map +1 -0
  134. package/dist/server/error-noise-filter.js +173 -0
  135. package/dist/server/error-noise-filter.js.map +1 -0
  136. package/dist/server/posthog-config.d.ts +28 -0
  137. package/dist/server/posthog-config.d.ts.map +1 -0
  138. package/dist/server/posthog-config.js +53 -0
  139. package/dist/server/posthog-config.js.map +1 -0
  140. package/dist/server/request-context.d.ts +6 -0
  141. package/dist/server/request-context.d.ts.map +1 -1
  142. package/dist/server/request-context.js.map +1 -1
  143. package/dist/server/sentry-config.d.ts +1 -0
  144. package/dist/server/sentry-config.d.ts.map +1 -1
  145. package/dist/server/sentry-config.js +3 -1
  146. package/dist/server/sentry-config.js.map +1 -1
  147. package/dist/server/sentry-plugin.d.ts.map +1 -1
  148. package/dist/server/sentry-plugin.js +12 -27
  149. package/dist/server/sentry-plugin.js.map +1 -1
  150. package/dist/server/sentry.d.ts.map +1 -1
  151. package/dist/server/sentry.js +8 -124
  152. package/dist/server/sentry.js.map +1 -1
  153. package/dist/server/ssr-handler.d.ts.map +1 -1
  154. package/dist/server/ssr-handler.js +6 -1
  155. package/dist/server/ssr-handler.js.map +1 -1
  156. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  157. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  158. package/dist/shared/mcp-embed-headers.js +1 -1
  159. package/dist/shared/mcp-embed-headers.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  161. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  162. package/dist/tracking/error-capture.d.ts +7 -0
  163. package/dist/tracking/error-capture.d.ts.map +1 -1
  164. package/dist/tracking/error-capture.js +10 -73
  165. package/dist/tracking/error-capture.js.map +1 -1
  166. package/dist/tracking/index.d.ts +1 -0
  167. package/dist/tracking/index.d.ts.map +1 -1
  168. package/dist/tracking/index.js +1 -0
  169. package/dist/tracking/index.js.map +1 -1
  170. package/dist/tracking/posthog-exception.d.ts +103 -0
  171. package/dist/tracking/posthog-exception.d.ts.map +1 -0
  172. package/dist/tracking/posthog-exception.js +181 -0
  173. package/dist/tracking/posthog-exception.js.map +1 -0
  174. package/dist/tracking/providers.d.ts +14 -0
  175. package/dist/tracking/providers.d.ts.map +1 -1
  176. package/dist/tracking/providers.js +63 -11
  177. package/dist/tracking/providers.js.map +1 -1
  178. package/dist/tracking/redaction.d.ts +25 -0
  179. package/dist/tracking/redaction.d.ts.map +1 -0
  180. package/dist/tracking/redaction.js +85 -0
  181. package/dist/tracking/redaction.js.map +1 -0
  182. package/docs/content/observability.mdx +41 -1
  183. package/docs/content/tracking.mdx +25 -0
  184. package/package.json +1 -1
  185. package/src/agent/run-manager.ts +7 -0
  186. package/src/client/agent-chat-adapter.ts +10 -0
  187. package/src/client/analytics.ts +105 -1
  188. package/src/client/use-action.ts +6 -0
  189. package/src/deploy/build.ts +38 -1
  190. package/src/observability/posthog-ai.ts +294 -0
  191. package/src/observability/routes.ts +29 -6
  192. package/src/observability/traces.ts +287 -12
  193. package/src/observability/types.ts +11 -0
  194. package/src/secrets/register-framework-secrets.ts +16 -0
  195. package/src/server/agent-run-context.ts +21 -0
  196. package/src/server/capture-error.ts +8 -0
  197. package/src/server/core-routes-plugin.ts +75 -4
  198. package/src/server/deploy-environment.ts +57 -0
  199. package/src/server/error-noise-filter.ts +268 -0
  200. package/src/server/posthog-config.ts +75 -0
  201. package/src/server/request-context.ts +6 -0
  202. package/src/server/sentry-config.ts +4 -8
  203. package/src/server/sentry-plugin.ts +13 -29
  204. package/src/server/sentry.ts +13 -156
  205. package/src/server/ssr-handler.ts +6 -1
  206. package/src/shared/mcp-embed-headers.ts +1 -1
  207. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  208. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  209. package/src/tracking/error-capture.ts +44 -109
  210. package/src/tracking/index.ts +11 -0
  211. package/src/tracking/posthog-exception.ts +305 -0
  212. package/src/tracking/providers.ts +91 -14
  213. package/src/tracking/redaction.ts +105 -0
@@ -13,6 +13,7 @@
13
13
  * automatically by the core-routes plugin).
14
14
  */
15
15
 
16
+ import { reshapeTrackedExceptionProperties } from "./posthog-exception.js";
16
17
  import { registerTrackingProvider } from "./registry.js";
17
18
  import type { TrackingProvider, TrackingEvent } from "./types.js";
18
19
 
@@ -162,27 +163,61 @@ function isPostHogAiObservabilityEvent(eventName: string): boolean {
162
163
  return eventName.startsWith("$ai_");
163
164
  }
164
165
 
165
- function createPostHogProvider(apiKey: string, host: string): TrackingProvider {
166
+ /**
167
+ * `$ai_*` and `$exception` are ingested through PostHog's dedicated endpoint
168
+ * rather than `/capture/`, and `$exception` additionally has to carry
169
+ * `$exception_list` — the framework's own `captureException()` emits camelCase
170
+ * fields that PostHog would otherwise render as an empty, ungroupable issue.
171
+ */
172
+ function createPostHogProvider(
173
+ apiKey: string,
174
+ host: string,
175
+ errorTracking: boolean,
176
+ ): TrackingProvider {
177
+ const sendToEventsEndpoint = (
178
+ event: TrackingEvent,
179
+ properties: Record<string, unknown> | undefined,
180
+ distinctId: string,
181
+ ): void => {
182
+ enqueue(
183
+ `${host}/i/v0/e/`,
184
+ JSON.stringify({
185
+ api_key: apiKey,
186
+ event: event.name,
187
+ properties: {
188
+ distinct_id: distinctId,
189
+ ...properties,
190
+ timestamp: event.timestamp,
191
+ },
192
+ }),
193
+ );
194
+ };
195
+
166
196
  return {
167
197
  name: "posthog",
168
198
  track(event: TrackingEvent) {
169
199
  const distinctId = event.userId || "anonymous";
170
200
  if (isPostHogAiObservabilityEvent(event.name)) {
171
- enqueue(
172
- `${host}/i/v0/e/`,
173
- JSON.stringify({
174
- api_key: apiKey,
175
- event: event.name,
176
- properties: {
177
- distinct_id: distinctId,
178
- ...event.properties,
179
- timestamp: event.timestamp,
180
- },
181
- }),
182
- );
201
+ sendToEventsEndpoint(event, event.properties, distinctId);
183
202
  return;
184
203
  }
185
204
 
205
+ if (event.name === "$exception") {
206
+ // `POSTHOG_ERROR_TRACKING=false` keeps product analytics flowing while
207
+ // another backend owns crashes. Drop rather than downgrade to
208
+ // `/capture/`: a malformed exception is what this branch exists to
209
+ // prevent.
210
+ if (!errorTracking) return;
211
+ const reshaped = reshapeTrackedExceptionProperties(event.properties);
212
+ if (reshaped) {
213
+ sendToEventsEndpoint(event, reshaped, distinctId);
214
+ return;
215
+ }
216
+ // No recognizable exception fields. Fall through to `/capture/` so the
217
+ // event is still recorded as-is rather than becoming an issue with
218
+ // nothing in it.
219
+ }
220
+
186
221
  enqueue(
187
222
  `${host}/capture/`,
188
223
  JSON.stringify({
@@ -213,6 +248,42 @@ function createPostHogProvider(apiKey: string, host: string): TrackingProvider {
213
248
  };
214
249
  }
215
250
 
251
+ /**
252
+ * Send one event to PostHog only, bypassing the provider fan-out.
253
+ *
254
+ * For payloads whose *shape* is PostHog-specific and whose *content* other
255
+ * backends must not receive. `track()` broadcasts to every configured provider,
256
+ * so a PostHog-only integration (e.g. enabling a survey id) would otherwise
257
+ * start exporting that integration's content — including user-authored text —
258
+ * to Mixpanel, Amplitude, webhooks, and Agent Native Analytics as a side
259
+ * effect nobody opted into.
260
+ *
261
+ * Returns `false` when PostHog is not configured, so callers can tell "not
262
+ * sent" from "sent".
263
+ */
264
+ export function sendPostHogEvent(
265
+ name: string,
266
+ properties: Record<string, unknown>,
267
+ distinctId: string,
268
+ ): boolean {
269
+ const apiKey = process.env.POSTHOG_API_KEY;
270
+ if (!apiKey) return false;
271
+ const host = (process.env.POSTHOG_HOST || POSTHOG_DEFAULT_HOST).replace(
272
+ /\/+$/,
273
+ "",
274
+ );
275
+ enqueue(
276
+ `${host}/capture/`,
277
+ JSON.stringify({
278
+ api_key: apiKey,
279
+ event: name,
280
+ distinct_id: distinctId,
281
+ properties: { ...properties, timestamp: new Date().toISOString() },
282
+ }),
283
+ );
284
+ return true;
285
+ }
286
+
216
287
  // ─── Mixpanel ──────────────────────────────────────────────────────────────
217
288
 
218
289
  function createMixpanelProvider(token: string): TrackingProvider {
@@ -383,7 +454,13 @@ export function registerBuiltinProviders(): void {
383
454
  /\/+$/,
384
455
  "",
385
456
  );
386
- registerTrackingProvider(createPostHogProvider(posthogKey, host));
457
+ registerTrackingProvider(
458
+ createPostHogProvider(
459
+ posthogKey,
460
+ host,
461
+ process.env.POSTHOG_ERROR_TRACKING?.trim().toLowerCase() !== "false",
462
+ ),
463
+ );
387
464
  }
388
465
 
389
466
  const mixpanelToken = process.env.MIXPANEL_TOKEN;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Bounding and redaction helpers shared by every exception emitter.
3
+ *
4
+ * Kept free of `process.env` and Node built-ins so the browser bundle can use
5
+ * the same rules — an exception shaped one way on the server and another way
6
+ * in the client is how a leak ships in only one of them.
7
+ */
8
+
9
+ export const MAX_MESSAGE_LENGTH = 1000;
10
+ export const MAX_STACK_LENGTH = 8000;
11
+ export const MAX_TAGS = 30;
12
+ export const MAX_EXTRA_KEYS = 30;
13
+ export const MAX_EXTRA_VALUE_LENGTH = 1000;
14
+
15
+ const SECRET_RE = /\b(?:bearer|basic)\s+[^\s]+/gi;
16
+
17
+ export const SECRET_KEY_RE =
18
+ /(?:authorization|cookie|set[-_]?cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|credential)/i;
19
+
20
+ export function redact(value: string): string {
21
+ return value
22
+ .replace(SECRET_RE, (match) => `${match.split(/\s+/, 1)[0]} <redacted>`)
23
+ .replace(
24
+ /([A-Za-z0-9_$.-]*(?:authorization|cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|credential)[A-Za-z0-9_$.-]*\s*[:=]\s*)([^\s,;}]+)/gi,
25
+ "$1<redacted>",
26
+ );
27
+ }
28
+
29
+ export function boundedText(value: unknown, max: number): string {
30
+ const text = typeof value === "string" ? value : String(value ?? "");
31
+ const safe = redact(text);
32
+ return safe.length > max ? safe.slice(0, max) : safe;
33
+ }
34
+
35
+ export function safeValue(value: unknown, depth = 2): unknown {
36
+ if (
37
+ value == null ||
38
+ typeof value === "boolean" ||
39
+ typeof value === "number"
40
+ ) {
41
+ return value;
42
+ }
43
+ if (typeof value === "string")
44
+ return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
45
+ if (depth <= 0) return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
46
+ if (Array.isArray(value)) {
47
+ return value.slice(0, 20).map((item) => safeValue(item, depth - 1));
48
+ }
49
+ if (typeof value === "object") {
50
+ const out: Record<string, unknown> = {};
51
+ for (const [key, child] of Object.entries(value)) {
52
+ if (Object.keys(out).length >= MAX_EXTRA_KEYS) break;
53
+ const safeKey = boundedText(key, 100);
54
+ out[safeKey] = SECRET_KEY_RE.test(safeKey)
55
+ ? "<redacted>"
56
+ : safeValue(child, depth - 1);
57
+ }
58
+ return out;
59
+ }
60
+ return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
61
+ }
62
+
63
+ export function safeTags(
64
+ tags: Record<string, string | undefined> | undefined,
65
+ ): Record<string, string> {
66
+ const out: Record<string, string> = {};
67
+ for (const [key, value] of Object.entries(tags ?? {})) {
68
+ if (Object.keys(out).length >= MAX_TAGS) break;
69
+ // Skip, don't stop: callers build tag objects with optional entries
70
+ // (`{ ...tags, route, method, userAgent }`), so breaking on the first
71
+ // undefined dropped every tag after it depending on key order.
72
+ if (value == null) continue;
73
+ const safeKey = boundedText(key, 100);
74
+ out[safeKey] = SECRET_KEY_RE.test(safeKey)
75
+ ? "<redacted>"
76
+ : boundedText(value, 200);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ export interface ExceptionParts {
82
+ type: string;
83
+ message: string;
84
+ stack?: string;
85
+ }
86
+
87
+ /** Split an unknown thrown value into bounded, redacted type/message/stack. */
88
+ export function exceptionParts(error: unknown): ExceptionParts {
89
+ if (error instanceof Error) {
90
+ return {
91
+ type: boundedText(error.name || "Error", 200),
92
+ message: boundedText(
93
+ error.message || error.name || "Error",
94
+ MAX_MESSAGE_LENGTH,
95
+ ),
96
+ ...(error.stack
97
+ ? { stack: boundedText(error.stack, MAX_STACK_LENGTH) }
98
+ : {}),
99
+ };
100
+ }
101
+ return {
102
+ type: "Error",
103
+ message: boundedText(error, MAX_MESSAGE_LENGTH),
104
+ };
105
+ }
@@ -1,3 +1,5 @@
1
+ import { getDialect, type Dialect } from "@agent-native/core/db";
2
+
1
3
  export function chunks<T>(items: T[], size: number): T[][] {
2
4
  if (items.length === 0) return [];
3
5
  const out: T[][] = [];
@@ -6,3 +8,34 @@ export function chunks<T>(items: T[], size: number): T[][] {
6
8
  }
7
9
  return out;
8
10
  }
11
+
12
+ export function bulkChunkSizeForColumnCount(
13
+ columnCount: number,
14
+ dialect: Dialect = getDialect(),
15
+ ) {
16
+ // D1 rejects statements with more than 100 bound params, so derive every
17
+ // bulk chunk from the statement's column count instead of a fixed row count.
18
+ const budget = dialect === "d1" ? 90 : 900;
19
+ return Math.max(1, Math.floor(budget / Math.max(1, columnCount)));
20
+ }
21
+
22
+ export async function processWithConcurrency<T>(
23
+ items: readonly T[],
24
+ concurrency: number,
25
+ worker: (item: T) => Promise<void>,
26
+ ) {
27
+ let nextIndex = 0;
28
+ const workerCount = Math.min(
29
+ items.length,
30
+ Math.max(1, Math.floor(concurrency)),
31
+ );
32
+ await Promise.all(
33
+ Array.from({ length: workerCount }, async () => {
34
+ while (nextIndex < items.length) {
35
+ const item = items[nextIndex];
36
+ nextIndex += 1;
37
+ if (item !== undefined) await worker(item);
38
+ }
39
+ }),
40
+ );
41
+ }
@@ -163,6 +163,7 @@ type BuilderMcpToolResult = {
163
163
  const BUILDER_CMS_DEFAULT_READ_LIMIT = 500;
164
164
  const BUILDER_CMS_MAX_READ_LIMIT = 10_000;
165
165
  const BUILDER_CMS_PAGE_SIZE = 100;
166
+ const BUILDER_CMS_PROJECTED_READ_CONCURRENCY = 8;
166
167
  const BUILDER_CMS_READ_RETRIES = 2;
167
168
  const BUILDER_CMS_METADATA_ENTRY_FIELD_PATHS = [
168
169
  "id",
@@ -192,6 +193,10 @@ const BUILDER_CMS_HEAVY_BODY_FIELD_PATHS = [
192
193
  const BUILDER_CMS_FIELD_PATH_PATTERN =
193
194
  /^[A-Za-z0-9_$-]+(?:\.[A-Za-z0-9_$-]+)*$/;
194
195
 
196
+ type BuilderCmsContentPageResult =
197
+ | { pageLimit: number; pageEntries: BuilderCmsSourceEntry[] }
198
+ | { pageLimit: number; error: string };
199
+
195
200
  function normalizeBuilderCmsListFieldPath(fieldPath: string) {
196
201
  const trimmed = fieldPath.trim();
197
202
  if (!trimmed || !BUILDER_CMS_FIELD_PATH_PATTERN.test(trimmed)) return null;
@@ -218,6 +223,36 @@ function normalizeBuilderCmsListFieldPath(fieldPath: string) {
218
223
  return normalized;
219
224
  }
220
225
 
226
+ function preserveProjectedBuilderFieldAbsence(
227
+ read: BuilderCmsReadResult,
228
+ fieldPaths: readonly string[] | undefined,
229
+ rawData: boolean | undefined,
230
+ ): BuilderCmsReadResult {
231
+ if (read.state !== "live" || rawData === true || !fieldPaths?.length) {
232
+ return read;
233
+ }
234
+ const projectedFieldPaths = [
235
+ ...new Set(
236
+ fieldPaths
237
+ .map(normalizeBuilderCmsListFieldPath)
238
+ .filter((fieldPath): fieldPath is string => fieldPath !== null),
239
+ ),
240
+ ];
241
+ if (projectedFieldPaths.length === 0) return read;
242
+ return {
243
+ ...read,
244
+ entries: read.entries.map((entry) => {
245
+ const sourceValues = { ...entry.sourceValues };
246
+ for (const fieldPath of projectedFieldPaths) {
247
+ if (!Object.prototype.hasOwnProperty.call(sourceValues, fieldPath)) {
248
+ sourceValues[fieldPath] = null;
249
+ }
250
+ }
251
+ return { ...entry, sourceValues };
252
+ }),
253
+ };
254
+ }
255
+
221
256
  export function builderCmsListEntryFields(fieldPaths: readonly string[] = []) {
222
257
  const fields = new Map<string, string>();
223
258
  for (const fieldPath of [
@@ -752,6 +787,7 @@ async function initializeBuilderMcp(args: {
752
787
  async function readBuilderCmsContentEntriesViaMcp(args: {
753
788
  model: string;
754
789
  fieldPaths?: readonly string[];
790
+ includeBodies?: boolean;
755
791
  rawData?: boolean;
756
792
  limit?: number;
757
793
  maxPages?: number;
@@ -775,7 +811,9 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
775
811
  const contentEntries: BuilderCmsSourceEntry[] = [];
776
812
  const fields = args.rawData
777
813
  ? undefined
778
- : builderCmsListEntryFields(args.fieldPaths);
814
+ : args.includeBodies
815
+ ? `${builderCmsListEntryFields(args.fieldPaths)},${BUILDER_CMS_HEAVY_BODY_FIELD_PATHS.join(",")}`
816
+ : builderCmsListEntryFields(args.fieldPaths);
779
817
  const seenContentIds = new Set<string>();
780
818
  let pagesRead = 0;
781
819
  let hasMore = false;
@@ -941,6 +979,8 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
941
979
  async function readBuilderCmsContentEntriesViaContentApi(args: {
942
980
  model: string;
943
981
  fieldPaths?: readonly string[];
982
+ includeBodies?: boolean;
983
+ allowCached?: boolean;
944
984
  rawData?: boolean;
945
985
  limit?: number;
946
986
  maxPages?: number;
@@ -959,14 +999,21 @@ async function readBuilderCmsContentEntriesViaContentApi(args: {
959
999
  // Fidelity reads intentionally do neither: Builder's raw data object is the
960
1000
  // clone contract, including unresolved references and every nested field.
961
1001
  url.searchParams.set("enrich", args.rawData === true ? "false" : "true");
962
- url.searchParams.set("noCache", "true");
963
- url.searchParams.set(
964
- "cachebust",
965
- args.rawData === true ? String(Date.now()) : "true",
966
- );
1002
+ if (args.allowCached !== true || args.rawData === true) {
1003
+ url.searchParams.set("noCache", "true");
1004
+ url.searchParams.set(
1005
+ "cachebust",
1006
+ args.rawData === true ? String(Date.now()) : "true",
1007
+ );
1008
+ }
967
1009
  url.searchParams.set("includeUnpublished", "true");
968
1010
  if (args.rawData !== true) {
969
- url.searchParams.set("fields", builderCmsListEntryFields(args.fieldPaths));
1011
+ url.searchParams.set(
1012
+ "fields",
1013
+ args.includeBodies
1014
+ ? `${builderCmsListEntryFields(args.fieldPaths)},${BUILDER_CMS_HEAVY_BODY_FIELD_PATHS.join(",")}`
1015
+ : builderCmsListEntryFields(args.fieldPaths),
1016
+ );
970
1017
  }
971
1018
 
972
1019
  const limit = readLimit(args.limit);
@@ -978,73 +1025,103 @@ async function readBuilderCmsContentEntriesViaContentApi(args: {
978
1025
  const seenIds = new Set<string>();
979
1026
  let pagesRead = 0;
980
1027
  let hasMore = false;
981
- for (
982
- let offset = startOffset;
983
- entries.length < limit;
984
- offset += BUILDER_CMS_PAGE_SIZE
985
- ) {
986
- const pageUrl = new URL(url);
987
- const pageLimit = readPageLimit(limit - entries.length);
988
- pageUrl.searchParams.set("limit", String(pageLimit));
989
- pageUrl.searchParams.set("offset", String(offset));
1028
+ const concurrency =
1029
+ args.rawData === true ? 1 : BUILDER_CMS_PROJECTED_READ_CONCURRENCY;
1030
+ const errorResult = (message: string): BuilderCmsReadResult => ({
1031
+ state: "error",
1032
+ entries: [],
1033
+ fetchedAt,
1034
+ message,
1035
+ progress: {
1036
+ requestedLimit: limit,
1037
+ pageSize: BUILDER_CMS_PAGE_SIZE,
1038
+ startOffset,
1039
+ nextOffset: startOffset + entries.length,
1040
+ fetchedEntryCount: startOffset + entries.length,
1041
+ hasMore,
1042
+ partial: Boolean(args.maxPages) && hasMore,
1043
+ readMode: "builder-api",
1044
+ },
1045
+ });
990
1046
 
991
- let response: Response;
992
- try {
993
- response = await fetchBuilderContentPage({
994
- fetchImpl: args.fetchImpl,
995
- url: pageUrl,
996
- privateKey: args.privateKey,
997
- });
998
- } catch (error) {
999
- return {
1000
- state: "error",
1001
- entries: [],
1002
- fetchedAt,
1003
- message:
1004
- error instanceof Error
1005
- ? `Builder CMS read failed: ${error.message}`
1006
- : "Builder CMS read failed.",
1007
- progress: {
1008
- requestedLimit: limit,
1009
- pageSize: BUILDER_CMS_PAGE_SIZE,
1010
- startOffset,
1011
- nextOffset: startOffset + entries.length,
1012
- fetchedEntryCount: startOffset + entries.length,
1013
- hasMore,
1014
- partial: Boolean(args.maxPages) && hasMore,
1015
- readMode: "builder-api",
1047
+ while (entries.length < limit) {
1048
+ const pagesRemaining = args.maxPages
1049
+ ? args.maxPages - pagesRead
1050
+ : Number.POSITIVE_INFINITY;
1051
+ if (pagesRemaining <= 0) break;
1052
+ const pageCount = Math.min(
1053
+ pagesRead === 0 ? 1 : concurrency,
1054
+ pagesRemaining,
1055
+ Math.ceil((limit - entries.length) / BUILDER_CMS_PAGE_SIZE),
1056
+ );
1057
+ const pageRequests = Array.from({ length: pageCount }, (_, index) => {
1058
+ const pageUrl = new URL(url);
1059
+ const pageLimit = readPageLimit(
1060
+ limit - entries.length - index * BUILDER_CMS_PAGE_SIZE,
1061
+ );
1062
+ pageUrl.searchParams.set("limit", String(pageLimit));
1063
+ pageUrl.searchParams.set(
1064
+ "offset",
1065
+ String(startOffset + (pagesRead + index) * BUILDER_CMS_PAGE_SIZE),
1066
+ );
1067
+ return { pageLimit, pageUrl };
1068
+ });
1069
+ const pageResults = await Promise.all(
1070
+ pageRequests.map(
1071
+ async ({
1072
+ pageLimit,
1073
+ pageUrl,
1074
+ }): Promise<BuilderCmsContentPageResult> => {
1075
+ try {
1076
+ const response = await fetchBuilderContentPage({
1077
+ fetchImpl: args.fetchImpl,
1078
+ url: pageUrl,
1079
+ privateKey: args.privateKey,
1080
+ });
1081
+ if (!response.ok) {
1082
+ return {
1083
+ pageLimit,
1084
+ error: `Builder CMS read failed with HTTP ${response.status}.`,
1085
+ };
1086
+ }
1087
+ const json = (await response.json()) as unknown;
1088
+ const pageEntries = entryArrayFromResponse(json)
1089
+ .map((entry) => normalizeBuilderCmsApiEntry(entry, args.model))
1090
+ .filter((entry): entry is BuilderCmsSourceEntry =>
1091
+ Boolean(entry),
1092
+ );
1093
+ return { pageLimit, pageEntries };
1094
+ } catch (error) {
1095
+ return {
1096
+ pageLimit,
1097
+ error:
1098
+ error instanceof Error
1099
+ ? `Builder CMS read failed: ${error.message}`
1100
+ : "Builder CMS read failed.",
1101
+ };
1102
+ }
1016
1103
  },
1017
- };
1018
- }
1104
+ ),
1105
+ );
1019
1106
 
1020
- if (!response.ok) {
1021
- return {
1022
- state: "error",
1023
- entries: [],
1024
- fetchedAt,
1025
- message: `Builder CMS read failed with HTTP ${response.status}.`,
1026
- progress: {
1027
- requestedLimit: limit,
1028
- pageSize: BUILDER_CMS_PAGE_SIZE,
1029
- startOffset,
1030
- nextOffset: startOffset + entries.length,
1031
- fetchedEntryCount: startOffset + entries.length,
1032
- hasMore,
1033
- partial: Boolean(args.maxPages) && hasMore,
1034
- readMode: "builder-api",
1035
- },
1036
- };
1107
+ let stoppedOnShortPage = false;
1108
+ for (const pageResult of pageResults) {
1109
+ if ("error" in pageResult) return errorResult(pageResult.error);
1110
+ const appended = appendUniqueBuilderEntries(
1111
+ entries,
1112
+ seenIds,
1113
+ pageResult.pageEntries,
1114
+ );
1115
+ pagesRead += 1;
1116
+ hasMore =
1117
+ pageResult.pageEntries.length >= pageResult.pageLimit && appended > 0;
1118
+ if (args.maxPages && pagesRead >= args.maxPages) break;
1119
+ if (!hasMore) {
1120
+ stoppedOnShortPage = true;
1121
+ break;
1122
+ }
1037
1123
  }
1038
-
1039
- const json = (await response.json()) as unknown;
1040
- const pageEntries = entryArrayFromResponse(json)
1041
- .map((entry) => normalizeBuilderCmsApiEntry(entry, args.model))
1042
- .filter((entry): entry is BuilderCmsSourceEntry => Boolean(entry));
1043
- const appended = appendUniqueBuilderEntries(entries, seenIds, pageEntries);
1044
- pagesRead += 1;
1045
- hasMore = pageEntries.length >= pageLimit && appended > 0;
1046
- if (args.maxPages && pagesRead >= args.maxPages) break;
1047
- if (!hasMore) break;
1124
+ if (stoppedOnShortPage || !hasMore) break;
1048
1125
  }
1049
1126
 
1050
1127
  return {
@@ -1231,6 +1308,8 @@ export async function readBuilderCmsModelFields(args: {
1231
1308
  export async function readBuilderCmsContentEntries(args: {
1232
1309
  model: string;
1233
1310
  fieldPaths?: readonly string[];
1311
+ includeBodies?: boolean;
1312
+ allowCached?: boolean;
1234
1313
  rawData?: boolean;
1235
1314
  requirePrivateKey?: boolean;
1236
1315
  limit?: number;
@@ -1265,6 +1344,8 @@ export async function readBuilderCmsContentEntries(args: {
1265
1344
  const contentApiRead = await readBuilderCmsContentEntriesViaContentApi({
1266
1345
  model: args.model,
1267
1346
  fieldPaths: args.fieldPaths,
1347
+ includeBodies: args.includeBodies,
1348
+ allowCached: args.allowCached,
1268
1349
  rawData: args.rawData,
1269
1350
  limit: args.limit,
1270
1351
  maxPages: args.maxPages,
@@ -1273,22 +1354,33 @@ export async function readBuilderCmsContentEntries(args: {
1273
1354
  publicKey,
1274
1355
  privateKey: privateKey ?? undefined,
1275
1356
  });
1276
- if (contentApiRead.state === "live") return contentApiRead;
1357
+ if (contentApiRead.state === "live") {
1358
+ return preserveProjectedBuilderFieldAbsence(
1359
+ contentApiRead,
1360
+ args.fieldPaths,
1361
+ args.rawData,
1362
+ );
1363
+ }
1277
1364
  if (!privateKey) return contentApiRead;
1278
1365
  }
1279
1366
 
1280
1367
  if (privateKey) {
1281
1368
  try {
1282
- return await readBuilderCmsContentEntriesViaMcp({
1283
- model: args.model,
1284
- fieldPaths: args.fieldPaths,
1285
- rawData: args.rawData,
1286
- limit: args.limit,
1287
- maxPages: args.maxPages,
1288
- offset: args.offset,
1289
- fetchImpl,
1290
- privateKey,
1291
- });
1369
+ return preserveProjectedBuilderFieldAbsence(
1370
+ await readBuilderCmsContentEntriesViaMcp({
1371
+ model: args.model,
1372
+ fieldPaths: args.fieldPaths,
1373
+ includeBodies: args.includeBodies,
1374
+ rawData: args.rawData,
1375
+ limit: args.limit,
1376
+ maxPages: args.maxPages,
1377
+ offset: args.offset,
1378
+ fetchImpl,
1379
+ privateKey,
1380
+ }),
1381
+ args.fieldPaths,
1382
+ args.rawData,
1383
+ );
1292
1384
  } catch (error) {
1293
1385
  return {
1294
1386
  state: "error",