@agent-native/core 0.119.4 → 0.119.6

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 (150) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +1 -0
  5. package/corpus/core/src/agent/engine/index.ts +2 -0
  6. package/corpus/core/src/agent/engine/registry.ts +95 -2
  7. package/corpus/core/src/deploy/build.ts +144 -2
  8. package/corpus/core/src/deploy/workspace-deploy.ts +123 -1
  9. package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +22 -0
  10. package/corpus/core/src/integrations/integration-durable-dispatch.ts +190 -0
  11. package/corpus/core/src/integrations/pending-tasks-retry-job.ts +175 -87
  12. package/corpus/core/src/integrations/pending-tasks-store.ts +114 -11
  13. package/corpus/core/src/integrations/plugin.ts +113 -60
  14. package/corpus/core/src/integrations/task-queue-stats.ts +68 -15
  15. package/corpus/core/src/integrations/webhook-handler.ts +21 -46
  16. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +8 -0
  17. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
  18. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +9 -1
  19. package/corpus/core/src/server/auth.ts +6 -0
  20. package/corpus/core/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
  21. package/corpus/core/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
  22. package/corpus/core/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
  23. package/corpus/templates/dispatch/changelog/2026-07-21-recover-stalled-slack-agent-work.md +6 -0
  24. package/corpus/templates/slides/actions/patch-deck.ts +2 -1
  25. package/corpus/templates/slides/changelog/2026-07-23-opening-or-refining-a-deck-no-longer-crashes-with-a-blank-er.md +6 -0
  26. package/corpus/templates/slides/shared/aspect-ratios.ts +4 -1
  27. package/corpus/templates/tasks/.oxfmtrc.json +8 -0
  28. package/corpus/templates/tasks/AGENTS.md +34 -41
  29. package/corpus/templates/tasks/README.md +13 -6
  30. package/corpus/templates/tasks/actions/bulk-delete-inbox-items.ts +10 -8
  31. package/corpus/templates/tasks/actions/bulk-delete-tasks.ts +10 -8
  32. package/corpus/templates/tasks/actions/bulk-mark-inbox-items-ready.ts +1 -1
  33. package/corpus/templates/tasks/actions/bulk-update-tasks.ts +12 -10
  34. package/corpus/templates/tasks/actions/create-inbox-item.ts +7 -4
  35. package/corpus/templates/tasks/actions/create-task.ts +2 -1
  36. package/corpus/templates/tasks/actions/delete-inbox-item.ts +6 -4
  37. package/corpus/templates/tasks/actions/delete-task.ts +1 -1
  38. package/corpus/templates/tasks/actions/list-inbox-items.ts +4 -2
  39. package/corpus/templates/tasks/actions/list-visible-task-fields.ts +3 -1
  40. package/corpus/templates/tasks/actions/mark-inbox-item-ready.ts +6 -4
  41. package/corpus/templates/tasks/actions/navigate.ts +16 -14
  42. package/corpus/templates/tasks/actions/reorder-inbox-items.ts +9 -7
  43. package/corpus/templates/tasks/actions/update-inbox-item.ts +6 -4
  44. package/corpus/templates/tasks/actions/update-task.ts +12 -9
  45. package/corpus/templates/tasks/actions/update-visible-task-fields.ts +10 -8
  46. package/corpus/templates/tasks/actions/view-screen.ts +1 -1
  47. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +15 -0
  48. package/corpus/templates/tasks/changelog/2026-07-23-task-detail-extensions.md +6 -0
  49. package/corpus/templates/tasks/drizzle.config.ts +3 -0
  50. package/corpus/templates/tasks/package.json +4 -1
  51. package/corpus/templates/tasks/server/db/schema.ts +0 -10
  52. package/corpus/templates/tasks/server/db/test-tasks-table.ts +0 -5
  53. package/corpus/templates/tasks/server/plugins/agent-chat.ts +1 -1
  54. package/corpus/templates/tasks/server/plugins/db.ts +43 -10
  55. package/corpus/templates/tasks/server/user-config/store.ts +42 -81
  56. package/corpus/templates/tasks/tsconfig.json +1 -2
  57. package/corpus/templates/tasks/vitest.config.ts +10 -1
  58. package/dist/agent/durable-background.d.ts +1 -0
  59. package/dist/agent/durable-background.d.ts.map +1 -1
  60. package/dist/agent/durable-background.js +1 -0
  61. package/dist/agent/durable-background.js.map +1 -1
  62. package/dist/agent/engine/index.d.ts +1 -1
  63. package/dist/agent/engine/index.d.ts.map +1 -1
  64. package/dist/agent/engine/index.js +1 -1
  65. package/dist/agent/engine/index.js.map +1 -1
  66. package/dist/agent/engine/registry.d.ts +32 -1
  67. package/dist/agent/engine/registry.d.ts.map +1 -1
  68. package/dist/agent/engine/registry.js +75 -3
  69. package/dist/agent/engine/registry.js.map +1 -1
  70. package/dist/deploy/build.d.ts +3 -0
  71. package/dist/deploy/build.d.ts.map +1 -1
  72. package/dist/deploy/build.js +106 -3
  73. package/dist/deploy/build.js.map +1 -1
  74. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  75. package/dist/deploy/workspace-deploy.js +103 -2
  76. package/dist/deploy/workspace-deploy.js.map +1 -1
  77. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  78. package/dist/integrations/integration-durable-dispatch-config.d.ts +7 -0
  79. package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -0
  80. package/dist/integrations/integration-durable-dispatch-config.js +16 -0
  81. package/dist/integrations/integration-durable-dispatch-config.js.map +1 -0
  82. package/dist/integrations/integration-durable-dispatch.d.ts +23 -0
  83. package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -0
  84. package/dist/integrations/integration-durable-dispatch.js +115 -0
  85. package/dist/integrations/integration-durable-dispatch.js.map +1 -0
  86. package/dist/integrations/pending-tasks-retry-job.d.ts +13 -1
  87. package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
  88. package/dist/integrations/pending-tasks-retry-job.js +133 -74
  89. package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
  90. package/dist/integrations/pending-tasks-store.d.ts +14 -2
  91. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  92. package/dist/integrations/pending-tasks-store.js +75 -11
  93. package/dist/integrations/pending-tasks-store.js.map +1 -1
  94. package/dist/integrations/plugin.d.ts.map +1 -1
  95. package/dist/integrations/plugin.js +88 -55
  96. package/dist/integrations/plugin.js.map +1 -1
  97. package/dist/integrations/task-queue-stats.d.ts +15 -3
  98. package/dist/integrations/task-queue-stats.d.ts.map +1 -1
  99. package/dist/integrations/task-queue-stats.js +45 -15
  100. package/dist/integrations/task-queue-stats.js.map +1 -1
  101. package/dist/integrations/webhook-handler.d.ts +1 -1
  102. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  103. package/dist/integrations/webhook-handler.js +18 -42
  104. package/dist/integrations/webhook-handler.js.map +1 -1
  105. package/dist/notifications/routes.d.ts +2 -2
  106. package/dist/observability/routes.d.ts +5 -5
  107. package/dist/progress/routes.d.ts +1 -1
  108. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  109. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  110. package/dist/resources/handlers.d.ts +1 -1
  111. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  112. package/dist/scripts/agent-engines/list-agent-engines.js +8 -2
  113. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  114. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  115. package/dist/scripts/agent-engines/manage-agent-engine.js +5 -2
  116. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  117. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  118. package/dist/scripts/agent-engines/set-agent-engine.js +9 -2
  119. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  120. package/dist/secrets/routes.d.ts +3 -3
  121. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  122. package/dist/server/auth.d.ts.map +1 -1
  123. package/dist/server/auth.js +5 -0
  124. package/dist/server/auth.js.map +1 -1
  125. package/dist/server/realtime-token.d.ts +1 -1
  126. package/dist/server/transcribe-voice.d.ts +1 -1
  127. package/dist/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
  128. package/dist/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
  129. package/dist/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
  130. package/package.json +1 -1
  131. package/src/agent/durable-background.ts +1 -0
  132. package/src/agent/engine/index.ts +2 -0
  133. package/src/agent/engine/registry.ts +95 -2
  134. package/src/deploy/build.ts +144 -2
  135. package/src/deploy/workspace-deploy.ts +123 -1
  136. package/src/integrations/integration-durable-dispatch-config.ts +22 -0
  137. package/src/integrations/integration-durable-dispatch.ts +190 -0
  138. package/src/integrations/pending-tasks-retry-job.ts +175 -87
  139. package/src/integrations/pending-tasks-store.ts +114 -11
  140. package/src/integrations/plugin.ts +113 -60
  141. package/src/integrations/task-queue-stats.ts +68 -15
  142. package/src/integrations/webhook-handler.ts +21 -46
  143. package/src/scripts/agent-engines/list-agent-engines.ts +8 -0
  144. package/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
  145. package/src/scripts/agent-engines/set-agent-engine.ts +9 -1
  146. package/src/server/auth.ts +6 -0
  147. package/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
  148. package/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
  149. package/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
  150. package/corpus/templates/tasks/.prettierrc +0 -5
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1660
31
+ - core files: 1662
32
32
  - toolkit files: 159
33
- - template files: 6327
33
+ - template files: 6331
@@ -1,5 +1,19 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.119.6
4
+
5
+ ### Patch Changes
6
+
7
+ - f3c3523: Add an opt-in, scoped Netlify background handoff and scheduled recovery sweep for durable messaging-integration tasks.
8
+
9
+ ## 0.119.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 13eb9f7: Fix AI SDK provider engines being unusable on bundled serverless deploys, and stop rewriting custom model IDs on save/read.
14
+ - **Serverless package detection.** `isAgentEnginePackageInstalled` relied on `require.resolve`, which fails on bundled serverless runtimes (Vercel/Netlify via Nitro) where optional provider packages (`ai`, `@ai-sdk/*`) are inlined into the function bundle. Every engine-usability gate then rejected the AI SDK engines and the agent silently fell back to the native Anthropic engine. Package resolution now treats a resolve miss as available only when there is real evidence of a bundled runtime (Vercel/Netlify markers, or a bundle-output module path), and defers to the engine's own dynamic `import()` as the real gate. Generic container/Lambda/Cloud Run deploys that ship a real `node_modules` still surface genuine "package not installed" misses.
15
+ - **Custom model preservation.** `normalizeModelForEngine` replaced any unrecognized model ID with the engine default when the settings actions passed a static registry entry, so a custom OpenAI-compatible gateway model (e.g. an Ollama model) reverted to the OpenAI default on save and read. The set/list/app-default actions now resolve the OpenAI-compatible-endpoint capability (`resolveEnginePreservesCustomModels`) and pass it through, preserving custom IDs verbatim — including version-shaped IDs — while first-party OpenAI still normalizes unknown IDs to a supported model.
16
+
3
17
  ## 0.119.4
4
18
 
5
19
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.119.4",
3
+ "version": "0.119.6",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -85,6 +85,7 @@ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_B
85
85
  */
86
86
  export const AGENT_BACKGROUND_PROCESSOR_FIELD = "__agentNativeProcessor";
87
87
  export const AGENT_BACKGROUND_PROCESSOR_A2A = "a2a";
88
+ export const AGENT_BACKGROUND_PROCESSOR_INTEGRATION = "integration";
88
89
  export const AGENT_BACKGROUND_PROCESSOR_ROUTE = "route";
89
90
  export const AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD =
90
91
  "__agentNativeProcessorRoute";
@@ -25,6 +25,8 @@ export {
25
25
  getConfiguredEngineNameForRequest,
26
26
  getStoredModelForEngine,
27
27
  normalizeModelForEngine,
28
+ resolveEnginePreservesCustomModels,
29
+ type NormalizeModelOptions,
28
30
  detectEngineFromEnv,
29
31
  detectEngineFromEnvForRequest,
30
32
  detectEngineFromUserSecrets,
@@ -96,6 +96,42 @@ function packageNameFromInstallSpecifier(specifier: string): string | null {
96
96
  return versionIndex === -1 ? trimmed : trimmed.slice(0, versionIndex);
97
97
  }
98
98
 
99
+ /**
100
+ * True only when there is positive evidence this module is executing from a
101
+ * bundled serverless function where optional dependencies were inlined into the
102
+ * bundle and are therefore NOT resolvable via `require.resolve` — even though
103
+ * the dynamic `import()` the engine uses to load them still works.
104
+ *
105
+ * Deliberately narrow. The Nitro Vercel/Netlify presets (which agent-native's
106
+ * own `deploy` command emits) inline optional peers and always set these env
107
+ * markers, so they are a reliable signal. Other serverless runtimes — a
108
+ * container on Cloud Run / Google Cloud Functions (`K_SERVICE` /
109
+ * `FUNCTION_TARGET`), or a plain AWS Lambda — commonly ship a real
110
+ * `node_modules` where `require.resolve` is authoritative; there a resolve miss
111
+ * means the package is genuinely absent and must NOT be masked. Those runtimes
112
+ * are still covered *when the code is actually bundled*, via the module-path
113
+ * check below, which stays false for a normal `node_modules` layout.
114
+ */
115
+ function isBundledServerlessRuntime(): boolean {
116
+ const env = process.env;
117
+ // Nitro's Vercel/Netlify presets inline optional peers into the function
118
+ // bundle; these platforms always set these markers.
119
+ if (env.VERCEL || env.NETLIFY) return true;
120
+ // Otherwise require direct evidence that this module is running from inside a
121
+ // bundle output directory (Vercel's `/var/task`, Nitro's `.output/server`,
122
+ // inlined `_libs`). This is the real signal that `require.resolve` cannot be
123
+ // trusted; it stays false for normal `node_modules` layouts (dev, tests, and
124
+ // container/Lambda/Cloud Run deploys that ship their dependencies), so a
125
+ // genuine "package not installed" miss still surfaces there.
126
+ try {
127
+ return /[\\/](?:_libs|\.vercel|\.netlify|\.output)[\\/]|\/var\/task\//.test(
128
+ import.meta.url ?? "",
129
+ );
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
99
135
  function canResolvePackage(packageName: string): boolean {
100
136
  const cached = _packageAvailabilityCache.get(packageName);
101
137
  if (cached !== undefined) return cached;
@@ -104,7 +140,15 @@ function canResolvePackage(packageName: string): boolean {
104
140
  require.resolve(packageName);
105
141
  available = true;
106
142
  } catch {
107
- available = false;
143
+ // Bundled serverless runtimes (e.g. Nitro on Vercel/Netlify) inline optional
144
+ // provider packages into the function bundle, so require.resolve cannot find
145
+ // them even though the dynamic `import()` the engine actually uses to load
146
+ // them works. Treat them as available there and let the engine's own import
147
+ // be the real gate — it already fails with a clear "pnpm add …" message when
148
+ // the package is genuinely missing. Without this, every engine-usability
149
+ // gate rejects the AI-SDK engines at runtime and the agent silently falls
150
+ // back to the native Anthropic engine.
151
+ available = isBundledServerlessRuntime();
108
152
  }
109
153
  _packageAvailabilityCache.set(packageName, available);
110
154
  return available;
@@ -179,17 +223,41 @@ function findLatestSupportedVersionMatch(
179
223
  return best?.model;
180
224
  }
181
225
 
226
+ export interface NormalizeModelOptions {
227
+ /**
228
+ * Force unrecognized (custom) model IDs to be kept verbatim, as if
229
+ * `engine.preserveCustomModels` were set on a live engine instance.
230
+ *
231
+ * The settings actions call `normalizeModelForEngine` with a static registry
232
+ * ENTRY, which never carries the runtime `preserveCustomModels` flag — that
233
+ * is only set on the engine INSTANCE created with an OpenAI-compatible
234
+ * `baseUrl`. They resolve the capability with
235
+ * {@link resolveEnginePreservesCustomModels} and pass it here so a gateway
236
+ * model (e.g. an Ollama `gemma4`) is not rewritten to the OpenAI default on
237
+ * save/read. First-party OpenAI (no gateway) leaves this unset, so an unknown
238
+ * or invalid model still normalizes to a supported one.
239
+ */
240
+ preserveCustomModels?: boolean;
241
+ }
242
+
182
243
  export function normalizeModelForEngine(
183
244
  engine: Pick<
184
245
  AgentEngine,
185
246
  "name" | "defaultModel" | "supportedModels" | "preserveCustomModels"
186
247
  >,
187
248
  model: string | null | undefined,
249
+ options: NormalizeModelOptions = {},
188
250
  ): string {
189
251
  const candidate = typeof model === "string" ? model.trim() : "";
190
252
  if (!candidate) return engine.defaultModel;
191
253
 
192
- if (engine.preserveCustomModels) return candidate;
254
+ // Preserve custom IDs verbatim BEFORE any catalog/version matching, so a
255
+ // version-shaped gateway model that happens to share a family with a
256
+ // built-in model (e.g. `gpt-5.4` on an OpenAI-compatible endpoint) is not
257
+ // rewritten to a catalog entry.
258
+ if (engine.preserveCustomModels || options.preserveCustomModels) {
259
+ return candidate;
260
+ }
193
261
 
194
262
  if (engine.supportedModels.length === 0) return candidate;
195
263
 
@@ -203,6 +271,31 @@ export function normalizeModelForEngine(
203
271
  );
204
272
  }
205
273
 
274
+ /**
275
+ * Whether models saved or read for this engine ENTRY should be preserved
276
+ * verbatim instead of normalized against the built-in catalog.
277
+ *
278
+ * `normalizeModelForEngine` honors a live engine's `preserveCustomModels`, but
279
+ * that flag is only set on an AI SDK engine INSTANCE when the OpenAI provider
280
+ * is pointed at an OpenAI-compatible gateway (a custom base URL — e.g. Ollama
281
+ * Cloud or LiteLLM), whose model IDs are not in the built-in OpenAI catalog.
282
+ * The static registry entry the settings actions pass to
283
+ * `normalizeModelForEngine` cannot carry that runtime flag, so this async
284
+ * helper reproduces the same decision — `ai-sdk:openai` AND a resolved base URL
285
+ * — from the request's stored/deploy config. First-party OpenAI (no gateway)
286
+ * returns false so an unknown/invalid model still normalizes to a supported one.
287
+ */
288
+ export async function resolveEnginePreservesCustomModels(
289
+ entry: Pick<AgentEngineEntry, "name">,
290
+ ): Promise<boolean> {
291
+ if (entry.name !== "ai-sdk:openai") return false;
292
+ try {
293
+ return Boolean(await resolveOpenAiBaseUrl());
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
206
299
  function assertAgentEnginePackageInstalled(entry: AgentEngineEntry): void {
207
300
  if (isAgentEnginePackageInstalled(entry)) return;
208
301
  const installHint = entry.installPackage
@@ -24,10 +24,16 @@ import {
24
24
  AGENT_BACKGROUND_FUNCTION_NAME,
25
25
  AGENT_BACKGROUND_PROCESSOR_A2A,
26
26
  AGENT_BACKGROUND_PROCESSOR_FIELD,
27
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
27
28
  AGENT_BACKGROUND_PROCESSOR_ROUTE,
28
29
  AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
29
30
  AGENT_CHAT_PROCESS_RUN_PATH,
30
31
  } from "../agent/durable-background.js";
32
+ import {
33
+ INTEGRATION_RETRY_SWEEP_PATH,
34
+ INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
35
+ isIntegrationDurableDispatchConfigured,
36
+ } from "../integrations/integration-durable-dispatch-config.js";
31
37
  import { normalizeAppBasePath } from "../server/app-base-path.js";
32
38
  import {
33
39
  DEFAULT_SSR_CDN_CACHE_CONTROL,
@@ -2369,6 +2375,13 @@ export function isDurableBackgroundDeployEnabled(): boolean {
2369
2375
  return v === "1" || v === "true" || v === "yes" || v === "on";
2370
2376
  }
2371
2377
 
2378
+ export const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME =
2379
+ "server-integration-recovery";
2380
+
2381
+ export function isIntegrationDurableDispatchDeployEnabled(): boolean {
2382
+ return isIntegrationDurableDispatchConfigured();
2383
+ }
2384
+
2372
2385
  const NETLIFY_KEEP_WARM_FUNCTION_NAME = "agent-native-keep-warm";
2373
2386
 
2374
2387
  /**
@@ -2536,10 +2549,16 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
2536
2549
 
2537
2550
  const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
2538
2551
  const a2aProcessTaskPath = JSON.stringify("/_agent-native/a2a/_process-task");
2552
+ const integrationProcessTaskPath = JSON.stringify(
2553
+ "/_agent-native/integrations/process-task",
2554
+ );
2539
2555
  const backgroundProcessorField = JSON.stringify(
2540
2556
  AGENT_BACKGROUND_PROCESSOR_FIELD,
2541
2557
  );
2542
2558
  const backgroundProcessorA2A = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A);
2559
+ const backgroundProcessorIntegration = JSON.stringify(
2560
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
2561
+ );
2543
2562
  const backgroundProcessorRoute = JSON.stringify(
2544
2563
  AGENT_BACKGROUND_PROCESSOR_ROUTE,
2545
2564
  );
@@ -2558,8 +2577,10 @@ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
2558
2577
  // The framework route the Nitro router dispatches to (the _process-run plugin).
2559
2578
  const PROCESS_RUN_PATH = ${processRunPath};
2560
2579
  const A2A_PROCESS_TASK_PATH = ${a2aProcessTaskPath};
2580
+ const INTEGRATION_PROCESS_TASK_PATH = ${integrationProcessTaskPath};
2561
2581
  const BACKGROUND_PROCESSOR_FIELD = ${backgroundProcessorField};
2562
2582
  const BACKGROUND_PROCESSOR_A2A = ${backgroundProcessorA2A};
2583
+ const BACKGROUND_PROCESSOR_INTEGRATION = ${backgroundProcessorIntegration};
2563
2584
  const BACKGROUND_PROCESSOR_ROUTE = ${backgroundProcessorRoute};
2564
2585
  const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${backgroundProcessorRouteField};
2565
2586
 
@@ -2570,6 +2591,12 @@ function processorPathFromBody(body) {
2570
2591
  if (parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_A2A) {
2571
2592
  return A2A_PROCESS_TASK_PATH;
2572
2593
  }
2594
+ if (
2595
+ parsed?.[BACKGROUND_PROCESSOR_FIELD] ===
2596
+ BACKGROUND_PROCESSOR_INTEGRATION
2597
+ ) {
2598
+ return INTEGRATION_PROCESS_TASK_PATH;
2599
+ }
2573
2600
  const route = parsed?.[BACKGROUND_PROCESSOR_ROUTE_FIELD];
2574
2601
  if (
2575
2602
  parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
@@ -2658,6 +2685,78 @@ export const config = {
2658
2685
  );
2659
2686
  }
2660
2687
 
2688
+ export function emitSingleTemplateNetlifyIntegrationRecoveryFunction(
2689
+ projectCwd: string,
2690
+ ): void {
2691
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
2692
+ const serverDir = path.join(internalDir, "server");
2693
+ if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
2694
+ console.warn(
2695
+ "[build] Integration recovery emit skipped: expected Nitro Netlify function " +
2696
+ "at .netlify/functions-internal/server/main.mjs was not found.",
2697
+ );
2698
+ return;
2699
+ }
2700
+ const functionName = NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME;
2701
+ const dest = path.join(internalDir, functionName);
2702
+ fs.rmSync(dest, { recursive: true, force: true });
2703
+ copyDir(serverDir, dest);
2704
+ fs.rmSync(path.join(dest, "server.mjs"), { force: true });
2705
+
2706
+ const entry = `import { createHmac } from "node:crypto";
2707
+
2708
+ const SWEEP_PATH = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_PATH)};
2709
+ const SWEEP_SUBJECT = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT)};
2710
+
2711
+ function enabled() {
2712
+ const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
2713
+ if (!raw) return false;
2714
+ return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
2715
+ }
2716
+
2717
+ function token(secret) {
2718
+ const timestamp = Date.now();
2719
+ const signature = createHmac("sha256", secret)
2720
+ .update(\`${INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
2721
+ .digest("hex");
2722
+ return \`\${timestamp}.\${signature}\`;
2723
+ }
2724
+
2725
+ let cachedHandler;
2726
+
2727
+ export default async function handler(request, context) {
2728
+ if (!enabled()) return new Response(null, { status: 204 });
2729
+ const secret = process.env.A2A_SECRET;
2730
+ if (!secret) {
2731
+ console.error("[integration-recovery] A2A_SECRET is required; sweep skipped");
2732
+ return new Response(null, { status: 204 });
2733
+ }
2734
+ cachedHandler ??= (await import("./main.mjs")).default;
2735
+ const url = new URL(request.url);
2736
+ url.pathname = SWEEP_PATH;
2737
+ const rewritten = new Request(url.toString(), {
2738
+ method: "POST",
2739
+ headers: {
2740
+ Authorization: \`Bearer \${token(secret)}\`,
2741
+ "Content-Type": "application/json",
2742
+ },
2743
+ body: JSON.stringify({ taskId: SWEEP_SUBJECT }),
2744
+ });
2745
+ return cachedHandler(rewritten, context);
2746
+ }
2747
+
2748
+ export const config = {
2749
+ name: "integration pending-task recovery",
2750
+ generator: "agent-native build",
2751
+ schedule: "* * * * *",
2752
+ nodeBundler: "none",
2753
+ includedFiles: ["**"],
2754
+ preferStatic: false,
2755
+ };
2756
+ `;
2757
+ fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
2758
+ }
2759
+
2661
2760
  /**
2662
2761
  * Nitro's Netlify preset can emit a harmful fallback rewrite to
2663
2762
  * `/.netlify/functions/server`. With `config.path: "/*"`, that default URL is
@@ -2950,7 +3049,10 @@ export function assertSingleTemplateNetlifyBuildOutput(
2950
3049
  );
2951
3050
  }
2952
3051
 
2953
- if (isDurableBackgroundDeployEnabled()) {
3052
+ if (
3053
+ isDurableBackgroundDeployEnabled() ||
3054
+ isIntegrationDurableDispatchDeployEnabled()
3055
+ ) {
2954
3056
  const backgroundDir = path.join(
2955
3057
  internalDir,
2956
3058
  AGENT_BACKGROUND_FUNCTION_NAME,
@@ -2987,6 +3089,32 @@ export function assertSingleTemplateNetlifyBuildOutput(
2987
3089
  }
2988
3090
  }
2989
3091
 
3092
+ if (isIntegrationDurableDispatchDeployEnabled()) {
3093
+ const recoveryEntryPath = path.join(
3094
+ internalDir,
3095
+ NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME,
3096
+ `${NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME}.mjs`,
3097
+ );
3098
+ if (!fs.existsSync(recoveryEntryPath)) {
3099
+ failures.push(
3100
+ `integration durable dispatch is enabled but ${path.relative(
3101
+ projectCwd,
3102
+ recoveryEntryPath,
3103
+ )} was not emitted`,
3104
+ );
3105
+ } else {
3106
+ const recoveryEntry = fs.readFileSync(recoveryEntryPath, "utf-8");
3107
+ if (!/\bschedule\s*:\s*["']\* \* \* \* \*["']/.test(recoveryEntry)) {
3108
+ failures.push(
3109
+ `integration recovery entry ${path.relative(
3110
+ projectCwd,
3111
+ recoveryEntryPath,
3112
+ )} is missing the one-minute schedule`,
3113
+ );
3114
+ }
3115
+ }
3116
+ }
3117
+
2990
3118
  if (failures.length > 0) {
2991
3119
  throw new Error(
2992
3120
  "[deploy] Netlify deploy guard failed; refusing to publish an output " +
@@ -3638,7 +3766,10 @@ export default bundle;
3638
3766
  // bundle, so the chat `_process-run` POST lands on Netlify's async (15-min)
3639
3767
  // function. When not opted in this is a no-op and the single-function
3640
3768
  // deploy is byte-for-byte unchanged.
3641
- if (isDurableBackgroundDeployEnabled()) {
3769
+ if (
3770
+ isDurableBackgroundDeployEnabled() ||
3771
+ isIntegrationDurableDispatchDeployEnabled()
3772
+ ) {
3642
3773
  try {
3643
3774
  emitSingleTemplateNetlifyBackgroundFunction(cwd);
3644
3775
  } catch (err) {
@@ -3649,6 +3780,17 @@ export default bundle;
3649
3780
  }
3650
3781
  }
3651
3782
 
3783
+ if (isIntegrationDurableDispatchDeployEnabled()) {
3784
+ try {
3785
+ emitSingleTemplateNetlifyIntegrationRecoveryFunction(cwd);
3786
+ } catch (err) {
3787
+ console.warn(
3788
+ "[build] Failed to emit integration recovery Netlify function (non-fatal):",
3789
+ err instanceof Error ? err.message : err,
3790
+ );
3791
+ }
3792
+ }
3793
+
3652
3794
  writeSingleTemplateNetlifyRedirects(cwd);
3653
3795
  assertSingleTemplateNetlifyBuildOutput(cwd);
3654
3796
  }
@@ -21,10 +21,16 @@ import path from "path";
21
21
  import {
22
22
  AGENT_BACKGROUND_PROCESSOR_A2A,
23
23
  AGENT_BACKGROUND_PROCESSOR_FIELD,
24
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
24
25
  AGENT_BACKGROUND_PROCESSOR_ROUTE,
25
26
  AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
26
27
  AGENT_CHAT_PROCESS_RUN_PATH,
27
28
  } from "../agent/durable-background.js";
29
+ import {
30
+ INTEGRATION_RETRY_SWEEP_PATH,
31
+ INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
32
+ isIntegrationDurableDispatchConfigured,
33
+ } from "../integrations/integration-durable-dispatch-config.js";
28
34
  import { findWorkspaceRoot } from "../scripts/utils.js";
29
35
  import {
30
36
  DEFAULT_WORKSPACE_APP_AUDIENCE,
@@ -677,9 +683,19 @@ function copyNetlifyFunctionIntoWorkspace(
677
683
  // Durable background agent runs (default-ON; opt out with a falsy
678
684
  // AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: when explicitly opted out
679
685
  // this emits nothing and the single-function deploy is unchanged.
680
- if (isDurableBackgroundDeployEnabled()) {
686
+ const integrationDurableDispatch =
687
+ app === "dispatch" && isIntegrationDurableDispatchConfigured();
688
+ if (isDurableBackgroundDeployEnabled() || integrationDurableDispatch) {
681
689
  emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
682
690
  }
691
+ if (integrationDurableDispatch) {
692
+ emitNetlifyIntegrationRecoveryFunction(
693
+ workspaceRoot,
694
+ app,
695
+ src,
696
+ workspaceApps,
697
+ );
698
+ }
683
699
  }
684
700
 
685
701
  /**
@@ -753,6 +769,7 @@ function emitNetlifyBackgroundFunction(
753
769
  // incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
754
770
  const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
755
771
  const a2aProcessTaskPath = `${basePath}/_agent-native/a2a/_process-task`;
772
+ const integrationProcessTaskPath = `${basePath}/_agent-native/integrations/process-task`;
756
773
  const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
757
774
  // is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
758
775
  // function (the deployed Lambda name is not guaranteed to end in -background). A
@@ -764,8 +781,10 @@ const basePath = ${JSON.stringify(basePath)};
764
781
  // The base-path-prefixed framework route the Nitro router dispatches to.
765
782
  const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
766
783
  const A2A_PROCESS_TASK_PATH = ${JSON.stringify(a2aProcessTaskPath)};
784
+ const INTEGRATION_PROCESS_TASK_PATH = ${JSON.stringify(integrationProcessTaskPath)};
767
785
  const BACKGROUND_PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
768
786
  const BACKGROUND_PROCESSOR_A2A = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A)};
787
+ const BACKGROUND_PROCESSOR_INTEGRATION = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION)};
769
788
  const BACKGROUND_PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
770
789
  const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
771
790
 
@@ -776,6 +795,12 @@ function processorPathFromBody(body) {
776
795
  if (parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_A2A) {
777
796
  return A2A_PROCESS_TASK_PATH;
778
797
  }
798
+ if (
799
+ parsed?.[BACKGROUND_PROCESSOR_FIELD] ===
800
+ BACKGROUND_PROCESSOR_INTEGRATION
801
+ ) {
802
+ return INTEGRATION_PROCESS_TASK_PATH;
803
+ }
779
804
  const route = parsed?.[BACKGROUND_PROCESSOR_ROUTE_FIELD];
780
805
  if (
781
806
  parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
@@ -862,6 +887,103 @@ export const config = {
862
887
  );
863
888
  }
864
889
 
890
+ function emitNetlifyIntegrationRecoveryFunction(
891
+ workspaceRoot: string,
892
+ app: string,
893
+ srcServerDir: string,
894
+ workspaceApps: WorkspaceAppManifestEntry[],
895
+ ): void {
896
+ const functionName = `${app}-integration-recovery`;
897
+ const dest = path.join(netlifyFunctionsDir(workspaceRoot), functionName);
898
+ fs.rmSync(dest, { recursive: true, force: true });
899
+ copyDir(srcServerDir, dest);
900
+ fs.rmSync(path.join(dest, "server.mjs"), { force: true });
901
+
902
+ const basePath = `/${app}`;
903
+ const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
904
+ const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
905
+ workspaceApps,
906
+ app,
907
+ );
908
+ const sweepPath = `${basePath}${INTEGRATION_RETRY_SWEEP_PATH}`;
909
+ const entry = `import { createHmac } from "node:crypto";
910
+
911
+ const basePath = ${JSON.stringify(basePath)};
912
+ const SWEEP_PATH = ${JSON.stringify(sweepPath)};
913
+ const SWEEP_SUBJECT = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT)};
914
+
915
+ function setBasePathEnv() {
916
+ const processRef = globalThis.process ??= { env: {} };
917
+ processRef.env ??= {};
918
+ Object.assign(processRef.env, {
919
+ AGENT_NATIVE_WORKSPACE: "1",
920
+ AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
921
+ APP_BASE_PATH: basePath,
922
+ AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
923
+ AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
924
+ AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
925
+ VITE_AGENT_NATIVE_WORKSPACE: "1",
926
+ VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
927
+ VITE_APP_BASE_PATH: basePath,
928
+ VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
929
+ VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
930
+ VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
931
+ VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
932
+ ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
933
+ });
934
+ }
935
+
936
+ function enabled() {
937
+ const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
938
+ if (!raw) return false;
939
+ return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
940
+ }
941
+
942
+ function token(secret) {
943
+ const timestamp = Date.now();
944
+ const signature = createHmac("sha256", secret)
945
+ .update(\`${INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
946
+ .digest("hex");
947
+ return \`\${timestamp}.\${signature}\`;
948
+ }
949
+
950
+ setBasePathEnv();
951
+ let cachedHandler;
952
+
953
+ export default async function handler(request, context) {
954
+ setBasePathEnv();
955
+ if (!enabled()) return new Response(null, { status: 204 });
956
+ const secret = process.env.A2A_SECRET;
957
+ if (!secret) {
958
+ console.error("[integration-recovery] A2A_SECRET is required; sweep skipped");
959
+ return new Response(null, { status: 204 });
960
+ }
961
+ cachedHandler ??= (await import("./main.mjs")).default;
962
+ const url = new URL(request.url);
963
+ url.pathname = SWEEP_PATH;
964
+ const rewritten = new Request(url.toString(), {
965
+ method: "POST",
966
+ headers: {
967
+ Authorization: \`Bearer \${token(secret)}\`,
968
+ "Content-Type": "application/json",
969
+ },
970
+ body: JSON.stringify({ taskId: SWEEP_SUBJECT }),
971
+ });
972
+ return cachedHandler(rewritten, context);
973
+ }
974
+
975
+ export const config = {
976
+ name: ${JSON.stringify(`${app} integration pending-task recovery`)},
977
+ generator: "agent-native workspace deploy",
978
+ schedule: "* * * * *",
979
+ nodeBundler: "none",
980
+ includedFiles: ["**"],
981
+ preferStatic: false,
982
+ };
983
+ `;
984
+ fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
985
+ }
986
+
865
987
  function patchNetlifyFunctionEntry(
866
988
  functionDir: string,
867
989
  app: string,
@@ -0,0 +1,22 @@
1
+ export const INTEGRATION_DURABLE_DISPATCH_ENV =
2
+ "AGENT_INTEGRATION_DURABLE_DISPATCH";
3
+ export const INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV =
4
+ "AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES";
5
+ export const INTEGRATION_PROCESS_TASK_PATH =
6
+ "/_agent-native/integrations/process-task";
7
+ export const INTEGRATION_RETRY_SWEEP_PATH =
8
+ "/_agent-native/integrations/retry-stuck-tasks";
9
+ export const INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT =
10
+ "integration-pending-tasks-sweep";
11
+
12
+ export function isIntegrationDurableDispatchConfigured(): boolean {
13
+ const value = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
14
+ if (!value) return false;
15
+ const normalized = value.trim().toLowerCase();
16
+ return (
17
+ normalized === "1" ||
18
+ normalized === "true" ||
19
+ normalized === "yes" ||
20
+ normalized === "on"
21
+ );
22
+ }