@agent-native/core 0.119.3 → 0.119.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 (217) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/production-agent.ts +107 -125
  5. package/corpus/core/src/agent/run-manager.ts +45 -2
  6. package/corpus/core/src/agent/thread-data-builder.ts +85 -8
  7. package/corpus/core/src/client/AssistantChat.tsx +37 -9
  8. package/corpus/core/src/client/chat/message-components.tsx +9 -1
  9. package/corpus/core/src/client/chat/repo-helpers.ts +77 -0
  10. package/corpus/core/src/client/chat/tool-call-display.tsx +51 -15
  11. package/corpus/core/src/server/agent-chat/framework-prompts.ts +3 -1
  12. package/corpus/core/src/server/agent-chat/plugin-options.ts +9 -0
  13. package/corpus/core/src/server/agent-chat-plugin.ts +12 -3
  14. package/corpus/core/src/templates/chat/.agents/skills/actions/SKILL.md +512 -0
  15. package/corpus/core/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +189 -0
  16. package/corpus/core/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +115 -0
  17. package/corpus/core/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  18. package/corpus/core/src/templates/chat/.agents/skills/capture-learnings/SKILL.md +89 -0
  19. package/corpus/core/src/templates/chat/.agents/skills/create-skill/SKILL.md +221 -0
  20. package/corpus/core/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  21. package/corpus/core/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +263 -0
  22. package/corpus/core/src/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  23. package/corpus/core/src/templates/chat/.agents/skills/frontend-design/SKILL.md +160 -0
  24. package/corpus/core/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +232 -0
  25. package/corpus/core/src/templates/chat/.agents/skills/security/SKILL.md +280 -0
  26. package/corpus/core/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +118 -0
  27. package/corpus/core/src/templates/chat/.agents/skills/shadcn-ui/SKILL.md +119 -0
  28. package/corpus/core/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +100 -0
  29. package/corpus/core/src/templates/chat/.env.example +5 -0
  30. package/corpus/core/src/templates/chat/.ignore +0 -0
  31. package/corpus/core/src/templates/chat/.oxfmtrc.json +8 -0
  32. package/corpus/core/src/templates/chat/AGENTS.md +69 -0
  33. package/corpus/core/src/templates/chat/CHANGELOG.md +10 -0
  34. package/corpus/core/src/templates/chat/DEVELOPING.md +185 -0
  35. package/corpus/core/src/templates/chat/README.md +32 -0
  36. package/corpus/core/src/templates/chat/_gitignore +43 -0
  37. package/corpus/core/src/templates/chat/actions/hello.ts +13 -0
  38. package/corpus/core/src/templates/chat/actions/navigate.ts +41 -0
  39. package/corpus/core/src/templates/chat/actions/run.ts +2 -0
  40. package/corpus/core/src/templates/chat/actions/view-screen.ts +31 -0
  41. package/corpus/core/src/templates/chat/app/components/layout/Header.tsx +60 -0
  42. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +185 -0
  43. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +450 -0
  44. package/corpus/core/src/templates/chat/app/components/ui/button.tsx +1 -0
  45. package/corpus/core/src/templates/chat/app/components/ui/card.tsx +1 -0
  46. package/corpus/core/src/templates/chat/app/components/ui/dropdown-menu.tsx +1 -0
  47. package/corpus/core/src/templates/chat/app/components/ui/input.tsx +1 -0
  48. package/corpus/core/src/templates/chat/app/components/ui/label.tsx +1 -0
  49. package/corpus/core/src/templates/chat/app/components/ui/sheet.tsx +1 -0
  50. package/corpus/core/src/templates/chat/app/components/ui/toolkit-provider.tsx +17 -0
  51. package/corpus/core/src/templates/chat/app/components/ui/tooltip.tsx +1 -0
  52. package/corpus/core/src/templates/chat/app/design-system.ts +3 -0
  53. package/corpus/core/src/templates/chat/app/entry.client.tsx +19 -0
  54. package/corpus/core/src/templates/chat/app/entry.server.tsx +10 -0
  55. package/corpus/core/src/templates/chat/app/global.css +93 -0
  56. package/corpus/core/src/templates/chat/app/hooks/use-navigation-state.ts +93 -0
  57. package/corpus/core/src/templates/chat/app/i18n/ar-SA.ts +68 -0
  58. package/corpus/core/src/templates/chat/app/i18n/de-DE.ts +71 -0
  59. package/corpus/core/src/templates/chat/app/i18n/en-US.ts +70 -0
  60. package/corpus/core/src/templates/chat/app/i18n/es-ES.ts +71 -0
  61. package/corpus/core/src/templates/chat/app/i18n/fr-FR.ts +71 -0
  62. package/corpus/core/src/templates/chat/app/i18n/hi-IN.ts +69 -0
  63. package/corpus/core/src/templates/chat/app/i18n/index.ts +34 -0
  64. package/corpus/core/src/templates/chat/app/i18n/ja-JP.ts +70 -0
  65. package/corpus/core/src/templates/chat/app/i18n/ko-KR.ts +70 -0
  66. package/corpus/core/src/templates/chat/app/i18n/pt-BR.ts +71 -0
  67. package/corpus/core/src/templates/chat/app/i18n/zh-CN.ts +66 -0
  68. package/corpus/core/src/templates/chat/app/i18n/zh-TW.ts +66 -0
  69. package/corpus/core/src/templates/chat/app/i18n-data.ts +556 -0
  70. package/corpus/core/src/templates/chat/app/lib/agent-page.tsx +49 -0
  71. package/corpus/core/src/templates/chat/app/lib/app-config.ts +11 -0
  72. package/corpus/core/src/templates/chat/app/lib/tab-id.ts +1 -0
  73. package/corpus/core/src/templates/chat/app/lib/utils.ts +1 -0
  74. package/corpus/core/src/templates/chat/app/root.tsx +173 -0
  75. package/corpus/core/src/templates/chat/app/routes/_index.tsx +94 -0
  76. package/corpus/core/src/templates/chat/app/routes/agent.tsx +24 -0
  77. package/corpus/core/src/templates/chat/app/routes/chat.$threadId.tsx +1 -0
  78. package/corpus/core/src/templates/chat/app/routes/database.tsx +17 -0
  79. package/corpus/core/src/templates/chat/app/routes/extensions.$id.$slug.tsx +2 -0
  80. package/corpus/core/src/templates/chat/app/routes/extensions.$id.tsx +11 -0
  81. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +11 -0
  82. package/corpus/core/src/templates/chat/app/routes/extensions.tsx +5 -0
  83. package/corpus/core/src/templates/chat/app/routes/observability.tsx +19 -0
  84. package/corpus/core/src/templates/chat/app/routes/settings.tsx +89 -0
  85. package/corpus/core/src/templates/chat/app/routes/team.tsx +11 -0
  86. package/corpus/core/src/templates/chat/app/routes.ts +4 -0
  87. package/corpus/core/src/templates/chat/app/vite-env.d.ts +6 -0
  88. package/corpus/core/src/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  89. package/corpus/core/src/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  90. package/corpus/core/src/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  91. package/corpus/core/src/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  92. package/corpus/core/src/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  93. package/corpus/core/src/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  94. package/corpus/core/src/templates/chat/changelog/2026-07-10-chat-now-makes-ai-connection-setup-clear-without-shifting-th.md +6 -0
  95. package/corpus/core/src/templates/chat/changelog/2026-07-13-a-full-agent-page-now-brings-context-files-connections-jobs-.md +6 -0
  96. package/corpus/core/src/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  97. package/corpus/core/src/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
  98. package/corpus/core/src/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +6 -0
  99. package/corpus/core/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +6 -0
  100. package/corpus/core/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  101. package/corpus/core/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  102. package/corpus/core/src/templates/chat/changelog/2026-07-22-manage-agent-navigation-now-uses-the-connected-nodes-icon.md +6 -0
  103. package/corpus/core/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
  104. package/corpus/core/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  105. package/corpus/core/src/templates/chat/components.json +20 -0
  106. package/corpus/core/src/templates/chat/learnings.defaults.md +5 -0
  107. package/corpus/core/src/templates/chat/netlify.toml +10 -0
  108. package/corpus/core/src/templates/chat/package.json +92 -0
  109. package/corpus/core/src/templates/chat/public/agent-native-icon-dark.svg +10 -0
  110. package/corpus/core/src/templates/chat/public/agent-native-icon-light.svg +10 -0
  111. package/corpus/core/src/templates/chat/public/agent-native-logo-dark.svg +21 -0
  112. package/corpus/core/src/templates/chat/public/agent-native-logo-light.svg +21 -0
  113. package/corpus/core/src/templates/chat/public/favicon.svg +1 -0
  114. package/corpus/core/src/templates/chat/public/icon-180.svg +1 -0
  115. package/corpus/core/src/templates/chat/public/icon-192.svg +1 -0
  116. package/corpus/core/src/templates/chat/public/icon-512.svg +1 -0
  117. package/corpus/core/src/templates/chat/public/manifest.json +21 -0
  118. package/corpus/core/src/templates/chat/react-router.config.ts +7 -0
  119. package/corpus/core/src/templates/chat/server/middleware/auth.ts +15 -0
  120. package/corpus/core/src/templates/chat/server/plugins/agent-chat.ts +21 -0
  121. package/corpus/core/src/templates/chat/server/plugins/auth.ts +17 -0
  122. package/corpus/core/src/templates/chat/server/routes/[...page].get.ts +5 -0
  123. package/corpus/core/src/templates/chat/ssr-entry.ts +15 -0
  124. package/corpus/core/src/templates/chat/tsconfig.json +21 -0
  125. package/corpus/core/src/templates/chat/vite.config.ts +19 -0
  126. package/corpus/templates/analytics/AGENTS.md +19 -11
  127. package/corpus/templates/analytics/actions/list-data-dictionary.ts +3 -1
  128. package/corpus/templates/analytics/actions/save-data-dictionary-entry.ts +14 -0
  129. package/corpus/templates/analytics/actions/search-analytics-query-catalog.ts +41 -0
  130. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +13 -0
  131. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +12 -11
  132. package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +3 -3
  133. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-chart-loading-placeholders-are-easier-to-see-in-da.md +6 -0
  134. package/corpus/templates/analytics/changelog/2026-07-23-routine-analytics-lookups-reuse-existing-query-definitions.md +6 -0
  135. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +1 -12
  136. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +405 -0
  137. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -16
  138. package/corpus/templates/content/changelog/2026-07-23-database-toolbar-sort-and-filter-controls-open-reliably.md +6 -0
  139. package/corpus/templates/forms/app/lib/public-form-link.ts +14 -0
  140. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +6 -14
  141. package/corpus/templates/forms/changelog/2026-07-23-published-form-links-can-be-copied-without-being-asked-to-re.md +6 -0
  142. package/corpus/templates/mail/changelog/2026-07-23-scheduled-emails-now-send-only-once-when-multiple-requests-r.md +6 -0
  143. package/corpus/templates/mail/server/lib/jobs.ts +13 -4
  144. package/corpus/templates/mail/server/plugins/mail-jobs.ts +1 -1
  145. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +7 -18
  146. package/corpus/templates/slides/app/lib/agent-visible-message.ts +28 -0
  147. package/corpus/templates/slides/app/pages/Index.tsx +6 -34
  148. package/corpus/templates/slides/changelog/2026-07-23-full-prompts-stay-visible-in-chat-when-creating-decks-or-add.md +6 -0
  149. package/corpus/toolkit/CHANGELOG.md +13 -0
  150. package/corpus/toolkit/package.json +1 -1
  151. package/corpus/toolkit/src/editor/SharedRichEditor.tsx +23 -2
  152. package/corpus/toolkit/src/editor/SlashCommandMenu.tsx +68 -2
  153. package/corpus/toolkit/src/editor/index.ts +1 -0
  154. package/corpus/toolkit/src/editor.css +75 -0
  155. package/corpus/toolkit/src/ui/button.tsx +3 -2
  156. package/dist/agent/production-agent.d.ts.map +1 -1
  157. package/dist/agent/production-agent.js +96 -115
  158. package/dist/agent/production-agent.js.map +1 -1
  159. package/dist/agent/run-manager.d.ts +9 -1
  160. package/dist/agent/run-manager.d.ts.map +1 -1
  161. package/dist/agent/run-manager.js +26 -0
  162. package/dist/agent/run-manager.js.map +1 -1
  163. package/dist/agent/thread-data-builder.d.ts +2 -0
  164. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  165. package/dist/agent/thread-data-builder.js +69 -9
  166. package/dist/agent/thread-data-builder.js.map +1 -1
  167. package/dist/client/AssistantChat.d.ts.map +1 -1
  168. package/dist/client/AssistantChat.js +32 -10
  169. package/dist/client/AssistantChat.js.map +1 -1
  170. package/dist/client/chat/message-components.d.ts +2 -1
  171. package/dist/client/chat/message-components.d.ts.map +1 -1
  172. package/dist/client/chat/message-components.js +9 -3
  173. package/dist/client/chat/message-components.js.map +1 -1
  174. package/dist/client/chat/repo-helpers.d.ts +4 -0
  175. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  176. package/dist/client/chat/repo-helpers.js +61 -0
  177. package/dist/client/chat/repo-helpers.js.map +1 -1
  178. package/dist/client/chat/tool-call-display.d.ts +2 -1
  179. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  180. package/dist/client/chat/tool-call-display.js +10 -9
  181. package/dist/client/chat/tool-call-display.js.map +1 -1
  182. package/dist/collab/routes.d.ts +1 -1
  183. package/dist/notifications/routes.d.ts +1 -1
  184. package/dist/observability/routes.d.ts +5 -5
  185. package/dist/progress/routes.d.ts +1 -1
  186. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  187. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  188. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  189. package/dist/resources/handlers.d.ts +1 -1
  190. package/dist/secrets/routes.d.ts +9 -9
  191. package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
  192. package/dist/server/agent-chat/framework-prompts.js +3 -1
  193. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  194. package/dist/server/agent-chat/plugin-options.d.ts +9 -0
  195. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  196. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  197. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  198. package/dist/server/agent-chat-plugin.js +11 -2
  199. package/dist/server/agent-chat-plugin.js.map +1 -1
  200. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  201. package/dist/server/realtime-token.d.ts +1 -1
  202. package/dist/server/transcribe-voice.d.ts +1 -1
  203. package/package.json +4 -4
  204. package/src/agent/production-agent.ts +107 -125
  205. package/src/agent/run-manager.ts +45 -2
  206. package/src/agent/thread-data-builder.ts +85 -8
  207. package/src/client/AssistantChat.tsx +37 -9
  208. package/src/client/chat/message-components.tsx +9 -1
  209. package/src/client/chat/repo-helpers.ts +77 -0
  210. package/src/client/chat/tool-call-display.tsx +51 -15
  211. package/src/server/agent-chat/framework-prompts.ts +3 -1
  212. package/src/server/agent-chat/plugin-options.ts +9 -0
  213. package/src/server/agent-chat-plugin.ts +12 -3
  214. package/src/templates/chat/actions/hello.spec.ts +0 -23
  215. package/src/templates/chat/actions/navigate.spec.ts +0 -68
  216. package/src/templates/chat/actions/view-screen.spec.ts +0 -40
  217. package/src/templates/chat/app/lib/agent-page.spec.tsx +0 -61
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
37
37
  //# sourceMappingURL=agent-engine-api-key-route.d.ts.map
@@ -26,9 +26,9 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
34
34
  //# sourceMappingURL=realtime-token.d.ts.map
@@ -20,7 +20,7 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
26
26
  //# sourceMappingURL=transcribe-voice.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.119.3",
3
+ "version": "0.119.4",
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": {
@@ -362,7 +362,7 @@
362
362
  "highlight.js": "^11.11.1",
363
363
  "i18next": "26.3.1",
364
364
  "isbot": "^5",
365
- "jiti": "^2.6.1",
365
+ "jiti": "^2.7.0",
366
366
  "jose": "^6.2.2",
367
367
  "linkedom": "0.18.12",
368
368
  "lowlight": "^3.3.0",
@@ -370,7 +370,7 @@
370
370
  "nanoid": "^5.1.9",
371
371
  "next-themes": "^0.4.6",
372
372
  "nf3": "0.3.17",
373
- "nitro": "3.0.260415-beta",
373
+ "nitro": "3.0.260610-beta",
374
374
  "p-limit": "^7.3.0",
375
375
  "prettier": "^3.8.3",
376
376
  "react-i18next": "17.0.8",
@@ -390,7 +390,7 @@
390
390
  "yjs": "^13.6.31",
391
391
  "zod": "^4.3.6",
392
392
  "@agent-native/recap-cli": "0.5.1",
393
- "@agent-native/toolkit": "^0.10.2"
393
+ "@agent-native/toolkit": "^0.10.4"
394
394
  },
395
395
  "devDependencies": {
396
396
  "@ai-sdk/anthropic": "^3.0.71",
@@ -7746,18 +7746,6 @@ export function createProductionAgentHandler(
7746
7746
  }).catch(() => {});
7747
7747
  }
7748
7748
 
7749
- // The background worker must AWAIT the run to completion before returning,
7750
- // or Netlify freezes/kills the function the instant the handler returns and
7751
- // the detached run dies mid-turn (mirrors the agent-teams processor, which
7752
- // wraps startRun in `await new Promise(resolve => startRun(..., onComplete:
7753
- // () => resolve()))`). We resolve this when the run's onComplete fires.
7754
- let resolveBackgroundRunDone: (() => void) | null = null;
7755
- const backgroundRunDone = isBackgroundWorker
7756
- ? new Promise<void>((resolve) => {
7757
- resolveBackgroundRunDone = resolve;
7758
- })
7759
- : null;
7760
-
7761
7749
  const baseHandleRunComplete =
7762
7750
  options.onRunComplete || trackedProgressRunId
7763
7751
  ? async (run: ActiveRun) => {
@@ -7771,119 +7759,114 @@ export function createProductionAgentHandler(
7771
7759
  }
7772
7760
  : undefined;
7773
7761
 
7774
- // Wrap so the background worker is unblocked even when there is no app
7775
- // onRunComplete / tracked-progress callback configured. Also installed for
7776
- // a foreground run eligible for self-chain that path needs this same
7777
- // wrapper to fire the continuation dispatch below.
7762
+ // Install the completion callback for background continuation even when
7763
+ // there is no app onRunComplete / tracked-progress callback configured.
7764
+ // The foreground self-chain path uses the same continuation dispatch.
7778
7765
  const handleRunComplete =
7779
7766
  isBackgroundWorker || foregroundSelfChainEligible || baseHandleRunComplete
7780
7767
  ? async (run: ActiveRun) => {
7781
- try {
7782
- // DIAGNOSTIC: a background worker that completed in an errored
7783
- // state threw inside the loop. Record it (with the last error
7784
- // event's message when available) so the failure cause is
7785
- // readable from the client. Skipped for clean completions and for
7786
- // recoverable soft-timeout boundaries (those chain a continuation
7787
- // below, they did not "throw").
7788
- if (
7789
- isBackgroundWorker &&
7790
- run.status === "errored" &&
7791
- !willChainBackgroundContinuation(run)
7792
- ) {
7793
- const errEvent = [...run.events]
7794
- .reverse()
7795
- .find((e) => e.event.type === "error")?.event as
7796
- | { error?: string; errorCode?: string }
7797
- | undefined;
7798
- await recordRunDiagnostic(
7799
- run.runId,
7800
- RUN_DIAG_STAGE.workerThrew,
7801
- errEvent?.errorCode || errEvent?.error
7802
- ? `${errEvent.errorCode ?? ""} ${errEvent.error ?? ""}`.trim()
7803
- : "run ended in errored state",
7804
- ).catch(() => {});
7805
- }
7806
- // Persist the (partial) assistant turn to thread_data FIRST the
7807
- // server-driven continuation below rebuilds from it, so it must be
7808
- // committed before we re-fire.
7809
- await baseHandleRunComplete?.(run);
7810
-
7811
- // Server-driven background→background continuation. If this chunk
7812
- // hit its soft-timeout still unfinished (ended at an auto_continue
7813
- // / loop_limit / recoverable boundary), chain the next chunk by
7814
- // re-firing the `_process-run` self-dispatch with mode "continue"
7815
- // (carried as internalContinuation + an incremented count),
7816
- // instead of relying on the client to re-POST. Mirrors the
7817
- // agent-teams `fireInternalDispatch({ body: { mode: "continue" }})`
7818
- // chain. Bounded by MAX_BACKGROUND_RUN_CONTINUATIONS. Aborted /
7819
- // user-stopped runs do NOT chain.
7820
- // Self-chain server-side for EVERY durable worker, not only the
7821
- // ones inside a `-background` function. Server-driven
7822
- // continuation is the whole point of durable background: the run
7823
- // must survive the client disconnecting (closed tab), so it
7824
- // cannot depend on the browser re-POSTing `auto_continue`. A
7825
- // worker on the regular ~60s function — a Netlify routing miss,
7826
- // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
7827
- // never emits a `-background` function checkpoints at the 40s
7828
- // soft-timeout and self-dispatches the next 40s chunk; a worker
7829
- // in a real `-background` function chains ~13-min chunks. Only
7830
- // the per-chunk BUDGET differs by function type (gated by
7831
- // `runsInBackgroundFunction` at the startRun call below); the
7832
- // continuation itself must stay server-driven on both. (The
7833
- // self-chain is only reachable when the initial dispatch already
7834
- // succeeded a dispatch fast-fail degrades to the inline
7835
- // foreground fallback, which is not a worker and rides the
7836
- // connected client's auto_continue instead.)
7837
- if (willChainBackgroundContinuation(run)) {
7838
- // Full handoff discipline lives in
7839
- // `chainServerDrivenContinuation` (exported + unit-tested):
7840
- // per-turn SQL run budget, successor row PRE-INSERTED before
7841
- // the dispatch, fully awaited dispatch with retries, loud
7842
- // diag/terminal marking on failure. Never throws.
7843
- await chainServerDrivenContinuation({
7844
- event,
7845
- run,
7846
- effectiveThreadId,
7847
- effectiveTurnId,
7848
- requestBody: body as unknown as Record<string, unknown>,
7849
- backgroundContinuationCount,
7850
- // Re-evaluate the durable gate rather than keying off
7851
- // isBackgroundWorker: a successor chunk of a FOREGROUND
7852
- // self-chain re-enters as a worker too, and must keep
7853
- // chaining via the regular function the Netlify
7854
- // `-background` function is only emitted into the deploy
7855
- // output when the durable flag is on.
7856
- //
7857
- // `&& !runsInBackgroundFunction`: a worker PROVEN to already
7858
- // be executing inside the real `-background` function must
7859
- // never dispatch back to that same function's own URL — a
7860
- // background function invoking itself by URL from inside a
7861
- // live invocation is a documented Netlify platform
7862
- // limitation (404), unlike the initial foreground→background
7863
- // dispatch or a worker that landed on the regular function
7864
- // (a genuinely different function calling the background
7865
- // one, which works). This only changes the dispatch TARGET
7866
- // for that one proven-in-bg-function case by this point
7867
- // (with the in-process resumable-error resume above) it is
7868
- // reached only when the chunk genuinely exhausted its
7869
- // budget, so falling back to the regular `_process-run`
7870
- // function (40s clamp) here is the correct, safe target.
7871
- chainViaDurableBackground:
7872
- isAgentChatDurableBackgroundEnabled({
7873
- appOptIn: options.durableBackgroundRuns,
7874
- }) && !runsInBackgroundFunction,
7875
- // Only changes the retry BUDGET, never the dispatch target
7876
- // above: a worker proven in a real background function has
7877
- // minutes of remaining wall clock and no connected-client
7878
- // fallback, so it must not be demoted to the foreground's
7879
- // 2-attempt budget just because it was forced onto the
7880
- // regular-function dispatch target. See
7881
- // `resolveContinuationDispatchBudget`.
7882
- workerProvenInBackgroundFunction: runsInBackgroundFunction,
7883
- });
7884
- }
7885
- } finally {
7886
- resolveBackgroundRunDone?.();
7768
+ // DIAGNOSTIC: a background worker that completed in an errored
7769
+ // state threw inside the loop. Record it (with the last error
7770
+ // event's message when available) so the failure cause is
7771
+ // readable from the client. Skipped for clean completions and for
7772
+ // recoverable soft-timeout boundaries (those chain a continuation
7773
+ // below, they did not "throw").
7774
+ if (
7775
+ isBackgroundWorker &&
7776
+ run.status === "errored" &&
7777
+ !willChainBackgroundContinuation(run)
7778
+ ) {
7779
+ const errEvent = [...run.events]
7780
+ .reverse()
7781
+ .find((e) => e.event.type === "error")?.event as
7782
+ | { error?: string; errorCode?: string }
7783
+ | undefined;
7784
+ await recordRunDiagnostic(
7785
+ run.runId,
7786
+ RUN_DIAG_STAGE.workerThrew,
7787
+ errEvent?.errorCode || errEvent?.error
7788
+ ? `${errEvent.errorCode ?? ""} ${errEvent.error ?? ""}`.trim()
7789
+ : "run ended in errored state",
7790
+ ).catch(() => {});
7791
+ }
7792
+ // Persist the (partial) assistant turn to thread_data FIRST — the
7793
+ // server-driven continuation below rebuilds from it, so it must be
7794
+ // committed before we re-fire.
7795
+ await baseHandleRunComplete?.(run);
7796
+
7797
+ // Server-driven background→background continuation. If this chunk
7798
+ // hit its soft-timeout still unfinished (ended at an auto_continue
7799
+ // / loop_limit / recoverable boundary), chain the next chunk by
7800
+ // re-firing the `_process-run` self-dispatch with mode "continue"
7801
+ // (carried as internalContinuation + an incremented count),
7802
+ // instead of relying on the client to re-POST. Mirrors the
7803
+ // agent-teams `fireInternalDispatch({ body: { mode: "continue" }})`
7804
+ // chain. Bounded by MAX_BACKGROUND_RUN_CONTINUATIONS. Aborted /
7805
+ // user-stopped runs do NOT chain.
7806
+ // Self-chain server-side for EVERY durable worker, not only the
7807
+ // ones inside a `-background` function. Server-driven
7808
+ // continuation is the whole point of durable background: the run
7809
+ // must survive the client disconnecting (closed tab), so it
7810
+ // cannot depend on the browser re-POSTing `auto_continue`. A
7811
+ // worker on the regular ~60s function — a Netlify routing miss,
7812
+ // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
7813
+ // never emits a `-background` function checkpoints at the 40s
7814
+ // soft-timeout and self-dispatches the next 40s chunk; a worker
7815
+ // in a real `-background` function chains ~13-min chunks. Only
7816
+ // the per-chunk BUDGET differs by function type (gated by
7817
+ // `runsInBackgroundFunction` at the startRun call below); the
7818
+ // continuation itself must stay server-driven on both. (The
7819
+ // self-chain is only reachable when the initial dispatch already
7820
+ // succeeded a dispatch fast-fail degrades to the inline
7821
+ // foreground fallback, which is not a worker and rides the
7822
+ // connected client's auto_continue instead.)
7823
+ if (willChainBackgroundContinuation(run)) {
7824
+ // Full handoff discipline lives in
7825
+ // `chainServerDrivenContinuation` (exported + unit-tested):
7826
+ // per-turn SQL run budget, successor row PRE-INSERTED before
7827
+ // the dispatch, fully awaited dispatch with retries, loud
7828
+ // diag/terminal marking on failure. Never throws.
7829
+ await chainServerDrivenContinuation({
7830
+ event,
7831
+ run,
7832
+ effectiveThreadId,
7833
+ effectiveTurnId,
7834
+ requestBody: body as unknown as Record<string, unknown>,
7835
+ backgroundContinuationCount,
7836
+ // Re-evaluate the durable gate rather than keying off
7837
+ // isBackgroundWorker: a successor chunk of a FOREGROUND
7838
+ // self-chain re-enters as a worker too, and must keep
7839
+ // chaining via the regular function the Netlify
7840
+ // `-background` function is only emitted into the deploy
7841
+ // output when the durable flag is on.
7842
+ //
7843
+ // `&& !runsInBackgroundFunction`: a worker PROVEN to already
7844
+ // be executing inside the real `-background` function must
7845
+ // never dispatch back to that same function's own URL — a
7846
+ // background function invoking itself by URL from inside a
7847
+ // live invocation is a documented Netlify platform
7848
+ // limitation (404), unlike the initial foreground→background
7849
+ // dispatch or a worker that landed on the regular function
7850
+ // (a genuinely different function calling the background
7851
+ // one, which works). This only changes the dispatch TARGET
7852
+ // for that one proven-in-bg-function case by this point
7853
+ // (with the in-process resumable-error resume above) it is
7854
+ // reached only when the chunk genuinely exhausted its
7855
+ // budget, so falling back to the regular `_process-run`
7856
+ // function (40s clamp) here is the correct, safe target.
7857
+ chainViaDurableBackground:
7858
+ isAgentChatDurableBackgroundEnabled({
7859
+ appOptIn: options.durableBackgroundRuns,
7860
+ }) && !runsInBackgroundFunction,
7861
+ // Only changes the retry BUDGET, never the dispatch target
7862
+ // above: a worker proven in a real background function has
7863
+ // minutes of remaining wall clock and no connected-client
7864
+ // fallback, so it must not be demoted to the foreground's
7865
+ // 2-attempt budget just because it was forced onto the
7866
+ // regular-function dispatch target. See
7867
+ // `resolveContinuationDispatchBudget`.
7868
+ workerProvenInBackgroundFunction: runsInBackgroundFunction,
7869
+ });
7887
7870
  }
7888
7871
  }
7889
7872
  : undefined;
@@ -7970,7 +7953,7 @@ export function createProductionAgentHandler(
7970
7953
  )
7971
7954
  : null;
7972
7955
 
7973
- startRun(
7956
+ const startedRun = startRun(
7974
7957
  runId,
7975
7958
  effectiveThreadId,
7976
7959
  async (rawSend, signal) => {
@@ -8414,9 +8397,8 @@ export function createProductionAgentHandler(
8414
8397
  // Background worker: await the run to completion so Netlify keeps the
8415
8398
  // background function alive for the whole turn (the client is streaming the
8416
8399
  // same events via the foreground POST's cross-isolate SQL subscription).
8417
- // The onComplete wrapper above resolves `backgroundRunDone`.
8418
8400
  if (isBackgroundWorker) {
8419
- if (backgroundRunDone) await backgroundRunDone;
8401
+ await startedRun.finalized;
8420
8402
  return { ok: true, runId };
8421
8403
  }
8422
8404
 
@@ -44,6 +44,15 @@ export interface ActiveRun {
44
44
  startedAt: number;
45
45
  }
46
46
 
47
+ export interface StartedRun extends ActiveRun {
48
+ /**
49
+ * Resolves after the terminal event and final SQL status have been persisted.
50
+ * Serverless workers must await this before returning or the runtime can
51
+ * freeze the isolate between onComplete and terminalization.
52
+ */
53
+ finalized: Promise<void>;
54
+ }
55
+
47
56
  const activeRuns = new Map<string, ActiveRun>();
48
57
  const threadToRun = new Map<string, string>();
49
58
 
@@ -447,7 +456,7 @@ export function startRun(
447
456
  ) => Promise<void>,
448
457
  onComplete?: (run: ActiveRun) => void | Promise<void>,
449
458
  options?: StartRunOptions,
450
- ): ActiveRun {
459
+ ): StartedRun {
451
460
  // If there's already a run for this thread, abort it
452
461
  const existingRunId = threadToRun.get(threadId);
453
462
  if (existingRunId) {
@@ -456,7 +465,17 @@ export function startRun(
456
465
 
457
466
  const abort = new AbortController();
458
467
  let softTimedOut = false;
459
- const run: ActiveRun = {
468
+ let resolveFinalized: () => void = () => {};
469
+ let rejectFinalized: (reason?: unknown) => void = () => {};
470
+ const finalized = new Promise<void>((resolve, reject) => {
471
+ resolveFinalized = resolve;
472
+ rejectFinalized = reject;
473
+ });
474
+ // Foreground callers do not await this promise, but terminal persistence
475
+ // failures must still be observable to background workers without creating
476
+ // an unhandled rejection in the foreground path.
477
+ void finalized.catch(() => {});
478
+ const run: StartedRun = {
460
479
  runId,
461
480
  threadId,
462
481
  turnId: options?.turnId ?? runId,
@@ -465,6 +484,7 @@ export function startRun(
465
484
  subscribers: new Set(),
466
485
  abort,
467
486
  startedAt: Date.now(),
487
+ finalized,
468
488
  };
469
489
 
470
490
  activeRuns.set(runId, run);
@@ -1039,6 +1059,21 @@ export function startRun(
1039
1059
  "[run-manager] terminal event persistence error:",
1040
1060
  err instanceof Error ? err.message : err,
1041
1061
  );
1062
+ try {
1063
+ await insertRunEvent(
1064
+ runId,
1065
+ terminal.seq,
1066
+ JSON.stringify(terminal.event),
1067
+ );
1068
+ terminalPersistenceError = null;
1069
+ } catch (retryError) {
1070
+ terminalPersistenceError = retryError;
1071
+ captureRunError(retryError, "completion");
1072
+ console.error(
1073
+ "[run-manager] terminal event retry persistence error:",
1074
+ retryError instanceof Error ? retryError.message : retryError,
1075
+ );
1076
+ }
1042
1077
  }
1043
1078
  }
1044
1079
  }
@@ -1112,6 +1147,13 @@ export function startRun(
1112
1147
  await setRunError(runId, errorCode ?? "unknown", errorDetail);
1113
1148
  }
1114
1149
 
1150
+ if (terminalPersistenceError) {
1151
+ const reconciled = await reconcileTerminalRunFromEvents(runId).catch(
1152
+ () => false,
1153
+ );
1154
+ if (!reconciled) throw terminalPersistenceError;
1155
+ }
1156
+
1115
1157
  // 6. Schedule in-memory cleanup + opportunistic old-run pruning.
1116
1158
  setTimeout(() => {
1117
1159
  activeRuns.delete(runId);
@@ -1124,6 +1166,7 @@ export function startRun(
1124
1166
  resolveErroredRunRetentionMs(),
1125
1167
  ).catch(() => {});
1126
1168
  });
1169
+ runPromise.then(resolveFinalized, rejectFinalized);
1127
1170
 
1128
1171
  // On Cloudflare Workers, keep the isolate alive for this run
1129
1172
  try {
@@ -35,6 +35,7 @@ interface BuildAssistantMessageOptions {
35
35
  * overwriting the others.
36
36
  */
37
37
  turnId?: string;
38
+ runDurationMs?: number;
38
39
  }
39
40
 
40
41
  type AssistantMessage = NonNullable<ReturnType<typeof buildAssistantMessage>>;
@@ -43,6 +44,8 @@ type UserMessage = ReturnType<typeof buildUserMessage>;
43
44
  const INTERRUPTED_TOOL_RESULT =
44
45
  "Interrupted before this tool returned a result.";
45
46
 
47
+ export const ASSISTANT_RUN_DURATION_METADATA_KEY = "agentNativeRunDurationMs";
48
+
46
49
  const MAX_STORED_ATTACHMENT_CHARS = 60_000;
47
50
  /**
48
51
  * When no file-upload provider is configured we fall back to storing base64
@@ -267,6 +270,13 @@ export function buildAssistantMessage(
267
270
  const custom: Record<string, unknown> = {};
268
271
  if (options.turnId) custom.turnId = options.turnId;
269
272
  if (runId) custom.foldedRunIds = [runId];
273
+ if (
274
+ typeof options.runDurationMs === "number" &&
275
+ Number.isFinite(options.runDurationMs) &&
276
+ options.runDurationMs >= 0
277
+ ) {
278
+ custom[ASSISTANT_RUN_DURATION_METADATA_KEY] = options.runDurationMs;
279
+ }
270
280
  if (continued) custom.continued = true;
271
281
  if (runError) {
272
282
  custom.runError = {
@@ -462,6 +472,41 @@ function messagesMatch(a: any, b: any): boolean {
462
472
  return messageIdentityKeys(a).some((key) => bKeys.has(key));
463
473
  }
464
474
 
475
+ function preserveAssistantRunDuration(chosenEntry: any, otherEntry: any): any {
476
+ const chosen = getStoredMessage(chosenEntry);
477
+ const other = getStoredMessage(otherEntry);
478
+ if (chosen?.role !== "assistant" || other?.role !== "assistant") {
479
+ return chosenEntry;
480
+ }
481
+
482
+ const chosenCustom =
483
+ chosen.metadata?.custom && typeof chosen.metadata.custom === "object"
484
+ ? (chosen.metadata.custom as Record<string, unknown>)
485
+ : {};
486
+ if (assistantRunDurationMs(chosenCustom) != null) return chosenEntry;
487
+
488
+ const otherCustom =
489
+ other.metadata?.custom && typeof other.metadata.custom === "object"
490
+ ? (other.metadata.custom as Record<string, unknown>)
491
+ : {};
492
+ const durationMs = assistantRunDurationMs(otherCustom);
493
+ if (durationMs == null) return chosenEntry;
494
+
495
+ const nextMessage = {
496
+ ...chosen,
497
+ metadata: {
498
+ ...chosen.metadata,
499
+ custom: {
500
+ ...chosenCustom,
501
+ [ASSISTANT_RUN_DURATION_METADATA_KEY]: durationMs,
502
+ },
503
+ },
504
+ };
505
+ return chosenEntry?.message === undefined
506
+ ? nextMessage
507
+ : { ...chosenEntry, message: nextMessage };
508
+ }
509
+
465
510
  function chooseMergedMessageEntry(existingEntry: any, incomingEntry: any): any {
466
511
  const existing = getStoredMessage(existingEntry);
467
512
  const incoming = getStoredMessage(incomingEntry);
@@ -479,12 +524,19 @@ function chooseMergedMessageEntry(existingEntry: any, incomingEntry: any): any {
479
524
  ) {
480
525
  const existingWeight = assistantContentWeight(existing.content);
481
526
  const incomingWeight = assistantContentWeight(incoming.content);
482
- if (existingWeight > incomingWeight) return existingEntry;
483
- if (incomingWeight > existingWeight) return incomingEntry;
484
- return isTerminalAssistantStatus(existing?.status) &&
485
- !isTerminalAssistantStatus(incoming?.status)
486
- ? existingEntry
487
- : incomingEntry;
527
+ const chosen =
528
+ existingWeight > incomingWeight
529
+ ? existingEntry
530
+ : incomingWeight > existingWeight
531
+ ? incomingEntry
532
+ : isTerminalAssistantStatus(existing?.status) &&
533
+ !isTerminalAssistantStatus(incoming?.status)
534
+ ? existingEntry
535
+ : incomingEntry;
536
+ return preserveAssistantRunDuration(
537
+ chosen,
538
+ chosen === existingEntry ? incomingEntry : existingEntry,
539
+ );
488
540
  }
489
541
  if (
490
542
  existing?.role === "assistant" &&
@@ -492,9 +544,9 @@ function chooseMergedMessageEntry(existingEntry: any, incomingEntry: any): any {
492
544
  isTerminalAssistantStatus(existing?.status) &&
493
545
  !isTerminalAssistantStatus(incoming?.status)
494
546
  ) {
495
- return existingEntry;
547
+ return preserveAssistantRunDuration(existingEntry, incomingEntry);
496
548
  }
497
- return incomingEntry;
549
+ return preserveAssistantRunDuration(incomingEntry, existingEntry);
498
550
  }
499
551
 
500
552
  function normalizeMessageEntry(
@@ -1397,6 +1449,17 @@ function foldedRunIdsOf(message: any): string[] {
1397
1449
  : [];
1398
1450
  }
1399
1451
 
1452
+ function assistantRunDurationMs(
1453
+ custom: Record<string, unknown>,
1454
+ ): number | null {
1455
+ const durationMs = custom[ASSISTANT_RUN_DURATION_METADATA_KEY];
1456
+ return typeof durationMs === "number" &&
1457
+ Number.isFinite(durationMs) &&
1458
+ durationMs >= 0
1459
+ ? durationMs
1460
+ : null;
1461
+ }
1462
+
1400
1463
  /** Rough size of an assistant message's content, used only to pick the larger
1401
1464
  * of two representations of the same chunk so a fold can never shrink. */
1402
1465
  function assistantContentWeight(content: unknown): number {
@@ -1519,6 +1582,20 @@ export function foldAssistantTurn(
1519
1582
  turnId,
1520
1583
  foldedRunIds: mergedFolded,
1521
1584
  };
1585
+ const existingDurationMs = assistantRunDurationMs(existingCustom);
1586
+ const incomingDurationMs = assistantRunDurationMs(incomingCustom);
1587
+ const mergedDurationMs = runAlreadyFolded
1588
+ ? existingDurationMs == null
1589
+ ? incomingDurationMs
1590
+ : incomingDurationMs == null
1591
+ ? existingDurationMs
1592
+ : Math.max(existingDurationMs, incomingDurationMs)
1593
+ : existingDurationMs == null && incomingDurationMs == null
1594
+ ? null
1595
+ : (existingDurationMs ?? 0) + (incomingDurationMs ?? 0);
1596
+ if (mergedDurationMs != null) {
1597
+ mergedCustom[ASSISTANT_RUN_DURATION_METADATA_KEY] = mergedDurationMs;
1598
+ }
1522
1599
  // Only the freshest chunk decides whether the turn is still continuing.
1523
1600
  if (incomingCustom.continued !== true) delete mergedCustom.continued;
1524
1601