@agent-native/core 0.119.3 → 0.119.5

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 (277) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/engine/index.ts +2 -0
  5. package/corpus/core/src/agent/engine/registry.ts +95 -2
  6. package/corpus/core/src/agent/production-agent.ts +107 -125
  7. package/corpus/core/src/agent/run-manager.ts +45 -2
  8. package/corpus/core/src/agent/thread-data-builder.ts +85 -8
  9. package/corpus/core/src/client/AssistantChat.tsx +37 -9
  10. package/corpus/core/src/client/chat/message-components.tsx +9 -1
  11. package/corpus/core/src/client/chat/repo-helpers.ts +77 -0
  12. package/corpus/core/src/client/chat/tool-call-display.tsx +51 -15
  13. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +8 -0
  14. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
  15. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +9 -1
  16. package/corpus/core/src/server/agent-chat/framework-prompts.ts +3 -1
  17. package/corpus/core/src/server/agent-chat/plugin-options.ts +9 -0
  18. package/corpus/core/src/server/agent-chat-plugin.ts +12 -3
  19. package/corpus/core/src/templates/chat/.agents/skills/actions/SKILL.md +512 -0
  20. package/corpus/core/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +189 -0
  21. package/corpus/core/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +115 -0
  22. package/corpus/core/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  23. package/corpus/core/src/templates/chat/.agents/skills/capture-learnings/SKILL.md +89 -0
  24. package/corpus/core/src/templates/chat/.agents/skills/create-skill/SKILL.md +221 -0
  25. package/corpus/core/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  26. package/corpus/core/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +263 -0
  27. package/corpus/core/src/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  28. package/corpus/core/src/templates/chat/.agents/skills/frontend-design/SKILL.md +160 -0
  29. package/corpus/core/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +232 -0
  30. package/corpus/core/src/templates/chat/.agents/skills/security/SKILL.md +280 -0
  31. package/corpus/core/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +118 -0
  32. package/corpus/core/src/templates/chat/.agents/skills/shadcn-ui/SKILL.md +119 -0
  33. package/corpus/core/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +100 -0
  34. package/corpus/core/src/templates/chat/.env.example +5 -0
  35. package/corpus/core/src/templates/chat/.ignore +0 -0
  36. package/corpus/core/src/templates/chat/.oxfmtrc.json +8 -0
  37. package/corpus/core/src/templates/chat/AGENTS.md +69 -0
  38. package/corpus/core/src/templates/chat/CHANGELOG.md +10 -0
  39. package/corpus/core/src/templates/chat/DEVELOPING.md +185 -0
  40. package/corpus/core/src/templates/chat/README.md +32 -0
  41. package/corpus/core/src/templates/chat/_gitignore +43 -0
  42. package/corpus/core/src/templates/chat/actions/hello.ts +13 -0
  43. package/corpus/core/src/templates/chat/actions/navigate.ts +41 -0
  44. package/corpus/core/src/templates/chat/actions/run.ts +2 -0
  45. package/corpus/core/src/templates/chat/actions/view-screen.ts +31 -0
  46. package/corpus/core/src/templates/chat/app/components/layout/Header.tsx +60 -0
  47. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +185 -0
  48. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +450 -0
  49. package/corpus/core/src/templates/chat/app/components/ui/button.tsx +1 -0
  50. package/corpus/core/src/templates/chat/app/components/ui/card.tsx +1 -0
  51. package/corpus/core/src/templates/chat/app/components/ui/dropdown-menu.tsx +1 -0
  52. package/corpus/core/src/templates/chat/app/components/ui/input.tsx +1 -0
  53. package/corpus/core/src/templates/chat/app/components/ui/label.tsx +1 -0
  54. package/corpus/core/src/templates/chat/app/components/ui/sheet.tsx +1 -0
  55. package/corpus/core/src/templates/chat/app/components/ui/toolkit-provider.tsx +17 -0
  56. package/corpus/core/src/templates/chat/app/components/ui/tooltip.tsx +1 -0
  57. package/corpus/core/src/templates/chat/app/design-system.ts +3 -0
  58. package/corpus/core/src/templates/chat/app/entry.client.tsx +19 -0
  59. package/corpus/core/src/templates/chat/app/entry.server.tsx +10 -0
  60. package/corpus/core/src/templates/chat/app/global.css +93 -0
  61. package/corpus/core/src/templates/chat/app/hooks/use-navigation-state.ts +93 -0
  62. package/corpus/core/src/templates/chat/app/i18n/ar-SA.ts +68 -0
  63. package/corpus/core/src/templates/chat/app/i18n/de-DE.ts +71 -0
  64. package/corpus/core/src/templates/chat/app/i18n/en-US.ts +70 -0
  65. package/corpus/core/src/templates/chat/app/i18n/es-ES.ts +71 -0
  66. package/corpus/core/src/templates/chat/app/i18n/fr-FR.ts +71 -0
  67. package/corpus/core/src/templates/chat/app/i18n/hi-IN.ts +69 -0
  68. package/corpus/core/src/templates/chat/app/i18n/index.ts +34 -0
  69. package/corpus/core/src/templates/chat/app/i18n/ja-JP.ts +70 -0
  70. package/corpus/core/src/templates/chat/app/i18n/ko-KR.ts +70 -0
  71. package/corpus/core/src/templates/chat/app/i18n/pt-BR.ts +71 -0
  72. package/corpus/core/src/templates/chat/app/i18n/zh-CN.ts +66 -0
  73. package/corpus/core/src/templates/chat/app/i18n/zh-TW.ts +66 -0
  74. package/corpus/core/src/templates/chat/app/i18n-data.ts +556 -0
  75. package/corpus/core/src/templates/chat/app/lib/agent-page.tsx +49 -0
  76. package/corpus/core/src/templates/chat/app/lib/app-config.ts +11 -0
  77. package/corpus/core/src/templates/chat/app/lib/tab-id.ts +1 -0
  78. package/corpus/core/src/templates/chat/app/lib/utils.ts +1 -0
  79. package/corpus/core/src/templates/chat/app/root.tsx +173 -0
  80. package/corpus/core/src/templates/chat/app/routes/_index.tsx +94 -0
  81. package/corpus/core/src/templates/chat/app/routes/agent.tsx +24 -0
  82. package/corpus/core/src/templates/chat/app/routes/chat.$threadId.tsx +1 -0
  83. package/corpus/core/src/templates/chat/app/routes/database.tsx +17 -0
  84. package/corpus/core/src/templates/chat/app/routes/extensions.$id.$slug.tsx +2 -0
  85. package/corpus/core/src/templates/chat/app/routes/extensions.$id.tsx +11 -0
  86. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +11 -0
  87. package/corpus/core/src/templates/chat/app/routes/extensions.tsx +5 -0
  88. package/corpus/core/src/templates/chat/app/routes/observability.tsx +19 -0
  89. package/corpus/core/src/templates/chat/app/routes/settings.tsx +89 -0
  90. package/corpus/core/src/templates/chat/app/routes/team.tsx +11 -0
  91. package/corpus/core/src/templates/chat/app/routes.ts +4 -0
  92. package/corpus/core/src/templates/chat/app/vite-env.d.ts +6 -0
  93. package/corpus/core/src/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +6 -0
  94. package/corpus/core/src/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +6 -0
  95. package/corpus/core/src/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
  96. package/corpus/core/src/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  97. package/corpus/core/src/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  98. package/corpus/core/src/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +5 -0
  99. package/corpus/core/src/templates/chat/changelog/2026-07-10-chat-now-makes-ai-connection-setup-clear-without-shifting-th.md +6 -0
  100. package/corpus/core/src/templates/chat/changelog/2026-07-13-a-full-agent-page-now-brings-context-files-connections-jobs-.md +6 -0
  101. package/corpus/core/src/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  102. package/corpus/core/src/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
  103. package/corpus/core/src/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +6 -0
  104. 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
  105. package/corpus/core/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
  106. package/corpus/core/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
  107. package/corpus/core/src/templates/chat/changelog/2026-07-22-manage-agent-navigation-now-uses-the-connected-nodes-icon.md +6 -0
  108. package/corpus/core/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
  109. package/corpus/core/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
  110. package/corpus/core/src/templates/chat/components.json +20 -0
  111. package/corpus/core/src/templates/chat/learnings.defaults.md +5 -0
  112. package/corpus/core/src/templates/chat/netlify.toml +10 -0
  113. package/corpus/core/src/templates/chat/package.json +92 -0
  114. package/corpus/core/src/templates/chat/public/agent-native-icon-dark.svg +10 -0
  115. package/corpus/core/src/templates/chat/public/agent-native-icon-light.svg +10 -0
  116. package/corpus/core/src/templates/chat/public/agent-native-logo-dark.svg +21 -0
  117. package/corpus/core/src/templates/chat/public/agent-native-logo-light.svg +21 -0
  118. package/corpus/core/src/templates/chat/public/favicon.svg +1 -0
  119. package/corpus/core/src/templates/chat/public/icon-180.svg +1 -0
  120. package/corpus/core/src/templates/chat/public/icon-192.svg +1 -0
  121. package/corpus/core/src/templates/chat/public/icon-512.svg +1 -0
  122. package/corpus/core/src/templates/chat/public/manifest.json +21 -0
  123. package/corpus/core/src/templates/chat/react-router.config.ts +7 -0
  124. package/corpus/core/src/templates/chat/server/middleware/auth.ts +15 -0
  125. package/corpus/core/src/templates/chat/server/plugins/agent-chat.ts +21 -0
  126. package/corpus/core/src/templates/chat/server/plugins/auth.ts +17 -0
  127. package/corpus/core/src/templates/chat/server/routes/[...page].get.ts +5 -0
  128. package/corpus/core/src/templates/chat/ssr-entry.ts +15 -0
  129. package/corpus/core/src/templates/chat/tsconfig.json +21 -0
  130. package/corpus/core/src/templates/chat/vite.config.ts +19 -0
  131. package/corpus/templates/analytics/AGENTS.md +19 -11
  132. package/corpus/templates/analytics/actions/list-data-dictionary.ts +3 -1
  133. package/corpus/templates/analytics/actions/save-data-dictionary-entry.ts +14 -0
  134. package/corpus/templates/analytics/actions/search-analytics-query-catalog.ts +41 -0
  135. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +13 -0
  136. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +12 -11
  137. package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +3 -3
  138. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-chart-loading-placeholders-are-easier-to-see-in-da.md +6 -0
  139. package/corpus/templates/analytics/changelog/2026-07-23-routine-analytics-lookups-reuse-existing-query-definitions.md +6 -0
  140. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +1 -12
  141. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +405 -0
  142. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -16
  143. package/corpus/templates/content/changelog/2026-07-23-database-toolbar-sort-and-filter-controls-open-reliably.md +6 -0
  144. package/corpus/templates/forms/app/lib/public-form-link.ts +14 -0
  145. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +6 -14
  146. package/corpus/templates/forms/changelog/2026-07-23-published-form-links-can-be-copied-without-being-asked-to-re.md +6 -0
  147. package/corpus/templates/mail/changelog/2026-07-23-scheduled-emails-now-send-only-once-when-multiple-requests-r.md +6 -0
  148. package/corpus/templates/mail/server/lib/jobs.ts +13 -4
  149. package/corpus/templates/mail/server/plugins/mail-jobs.ts +1 -1
  150. package/corpus/templates/slides/actions/patch-deck.ts +2 -1
  151. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +7 -18
  152. package/corpus/templates/slides/app/lib/agent-visible-message.ts +28 -0
  153. package/corpus/templates/slides/app/pages/Index.tsx +6 -34
  154. package/corpus/templates/slides/changelog/2026-07-23-full-prompts-stay-visible-in-chat-when-creating-decks-or-add.md +6 -0
  155. package/corpus/templates/slides/changelog/2026-07-23-opening-or-refining-a-deck-no-longer-crashes-with-a-blank-er.md +6 -0
  156. package/corpus/templates/slides/shared/aspect-ratios.ts +4 -1
  157. package/corpus/templates/tasks/.oxfmtrc.json +8 -0
  158. package/corpus/templates/tasks/AGENTS.md +34 -41
  159. package/corpus/templates/tasks/README.md +13 -6
  160. package/corpus/templates/tasks/actions/bulk-delete-inbox-items.ts +10 -8
  161. package/corpus/templates/tasks/actions/bulk-delete-tasks.ts +10 -8
  162. package/corpus/templates/tasks/actions/bulk-mark-inbox-items-ready.ts +1 -1
  163. package/corpus/templates/tasks/actions/bulk-update-tasks.ts +12 -10
  164. package/corpus/templates/tasks/actions/create-inbox-item.ts +7 -4
  165. package/corpus/templates/tasks/actions/create-task.ts +2 -1
  166. package/corpus/templates/tasks/actions/delete-inbox-item.ts +6 -4
  167. package/corpus/templates/tasks/actions/delete-task.ts +1 -1
  168. package/corpus/templates/tasks/actions/list-inbox-items.ts +4 -2
  169. package/corpus/templates/tasks/actions/list-visible-task-fields.ts +3 -1
  170. package/corpus/templates/tasks/actions/mark-inbox-item-ready.ts +6 -4
  171. package/corpus/templates/tasks/actions/navigate.ts +16 -14
  172. package/corpus/templates/tasks/actions/reorder-inbox-items.ts +9 -7
  173. package/corpus/templates/tasks/actions/update-inbox-item.ts +6 -4
  174. package/corpus/templates/tasks/actions/update-task.ts +12 -9
  175. package/corpus/templates/tasks/actions/update-visible-task-fields.ts +10 -8
  176. package/corpus/templates/tasks/actions/view-screen.ts +1 -1
  177. package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +15 -0
  178. package/corpus/templates/tasks/changelog/2026-07-23-task-detail-extensions.md +6 -0
  179. package/corpus/templates/tasks/drizzle.config.ts +3 -0
  180. package/corpus/templates/tasks/package.json +4 -1
  181. package/corpus/templates/tasks/server/db/schema.ts +0 -10
  182. package/corpus/templates/tasks/server/db/test-tasks-table.ts +0 -5
  183. package/corpus/templates/tasks/server/plugins/agent-chat.ts +1 -1
  184. package/corpus/templates/tasks/server/plugins/db.ts +43 -10
  185. package/corpus/templates/tasks/server/user-config/store.ts +42 -81
  186. package/corpus/templates/tasks/tsconfig.json +1 -2
  187. package/corpus/templates/tasks/vitest.config.ts +10 -1
  188. package/corpus/toolkit/CHANGELOG.md +13 -0
  189. package/corpus/toolkit/package.json +1 -1
  190. package/corpus/toolkit/src/editor/SharedRichEditor.tsx +23 -2
  191. package/corpus/toolkit/src/editor/SlashCommandMenu.tsx +68 -2
  192. package/corpus/toolkit/src/editor/index.ts +1 -0
  193. package/corpus/toolkit/src/editor.css +75 -0
  194. package/corpus/toolkit/src/ui/button.tsx +3 -2
  195. package/dist/agent/engine/index.d.ts +1 -1
  196. package/dist/agent/engine/index.d.ts.map +1 -1
  197. package/dist/agent/engine/index.js +1 -1
  198. package/dist/agent/engine/index.js.map +1 -1
  199. package/dist/agent/engine/registry.d.ts +32 -1
  200. package/dist/agent/engine/registry.d.ts.map +1 -1
  201. package/dist/agent/engine/registry.js +75 -3
  202. package/dist/agent/engine/registry.js.map +1 -1
  203. package/dist/agent/production-agent.d.ts.map +1 -1
  204. package/dist/agent/production-agent.js +96 -115
  205. package/dist/agent/production-agent.js.map +1 -1
  206. package/dist/agent/run-manager.d.ts +9 -1
  207. package/dist/agent/run-manager.d.ts.map +1 -1
  208. package/dist/agent/run-manager.js +26 -0
  209. package/dist/agent/run-manager.js.map +1 -1
  210. package/dist/agent/thread-data-builder.d.ts +2 -0
  211. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  212. package/dist/agent/thread-data-builder.js +69 -9
  213. package/dist/agent/thread-data-builder.js.map +1 -1
  214. package/dist/client/AssistantChat.d.ts.map +1 -1
  215. package/dist/client/AssistantChat.js +32 -10
  216. package/dist/client/AssistantChat.js.map +1 -1
  217. package/dist/client/chat/message-components.d.ts +2 -1
  218. package/dist/client/chat/message-components.d.ts.map +1 -1
  219. package/dist/client/chat/message-components.js +9 -3
  220. package/dist/client/chat/message-components.js.map +1 -1
  221. package/dist/client/chat/repo-helpers.d.ts +4 -0
  222. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  223. package/dist/client/chat/repo-helpers.js +61 -0
  224. package/dist/client/chat/repo-helpers.js.map +1 -1
  225. package/dist/client/chat/tool-call-display.d.ts +2 -1
  226. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  227. package/dist/client/chat/tool-call-display.js +10 -9
  228. package/dist/client/chat/tool-call-display.js.map +1 -1
  229. package/dist/collab/struct-routes.d.ts +1 -1
  230. package/dist/observability/routes.d.ts +5 -5
  231. package/dist/progress/routes.d.ts +1 -1
  232. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  233. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  234. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  235. package/dist/resources/handlers.d.ts +1 -1
  236. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  237. package/dist/scripts/agent-engines/list-agent-engines.js +8 -2
  238. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  239. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  240. package/dist/scripts/agent-engines/manage-agent-engine.js +5 -2
  241. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  242. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  243. package/dist/scripts/agent-engines/set-agent-engine.js +9 -2
  244. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  245. package/dist/secrets/routes.d.ts +9 -9
  246. package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
  247. package/dist/server/agent-chat/framework-prompts.js +3 -1
  248. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  249. package/dist/server/agent-chat/plugin-options.d.ts +9 -0
  250. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  251. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  252. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  253. package/dist/server/agent-chat-plugin.js +11 -2
  254. package/dist/server/agent-chat-plugin.js.map +1 -1
  255. package/dist/server/realtime-token.d.ts +1 -1
  256. package/dist/server/transcribe-voice.d.ts +1 -1
  257. package/package.json +4 -4
  258. package/src/agent/engine/index.ts +2 -0
  259. package/src/agent/engine/registry.ts +95 -2
  260. package/src/agent/production-agent.ts +107 -125
  261. package/src/agent/run-manager.ts +45 -2
  262. package/src/agent/thread-data-builder.ts +85 -8
  263. package/src/client/AssistantChat.tsx +37 -9
  264. package/src/client/chat/message-components.tsx +9 -1
  265. package/src/client/chat/repo-helpers.ts +77 -0
  266. package/src/client/chat/tool-call-display.tsx +51 -15
  267. package/src/scripts/agent-engines/list-agent-engines.ts +8 -0
  268. package/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
  269. package/src/scripts/agent-engines/set-agent-engine.ts +9 -1
  270. package/src/server/agent-chat/framework-prompts.ts +3 -1
  271. package/src/server/agent-chat/plugin-options.ts +9 -0
  272. package/src/server/agent-chat-plugin.ts +12 -3
  273. package/corpus/templates/tasks/.prettierrc +0 -5
  274. package/src/templates/chat/actions/hello.spec.ts +0 -23
  275. package/src/templates/chat/actions/navigate.spec.ts +0 -68
  276. package/src/templates/chat/actions/view-screen.spec.ts +0 -40
  277. package/src/templates/chat/app/lib/agent-page.spec.tsx +0 -61
@@ -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
 
@@ -110,6 +110,7 @@ import {
110
110
  shouldImportServerThreadData,
111
111
  dedupeRepoMessagesById,
112
112
  dropEmptyAssistantMessages,
113
+ withLastAssistantRunDuration,
113
114
  } from "./chat/repo-helpers.js";
114
115
  import {
115
116
  BuilderSetupContent,
@@ -2842,6 +2843,26 @@ const AssistantChatInner = forwardRef<
2842
2843
  ensureMessageMetadata(normalizeThreadRepository(threadRuntime.export())),
2843
2844
  [threadRuntime],
2844
2845
  );
2846
+ const exportPersistableThreadRepo = useCallback(
2847
+ () =>
2848
+ withLastAssistantRunDuration(
2849
+ exportCleanThreadRepo(),
2850
+ showRunningInUI ? null : lastChatRunDurationMs,
2851
+ ),
2852
+ [exportCleanThreadRepo, lastChatRunDurationMs, showRunningInUI],
2853
+ );
2854
+ useEffect(() => {
2855
+ if (showRunningInUI || lastChatRunDurationMs == null) return;
2856
+ const repo = exportCleanThreadRepo();
2857
+ const persisted = withLastAssistantRunDuration(repo, lastChatRunDurationMs);
2858
+ if (persisted === repo) return;
2859
+ threadRuntime.import(ensureMessageMetadata(persisted));
2860
+ }, [
2861
+ exportCleanThreadRepo,
2862
+ lastChatRunDurationMs,
2863
+ showRunningInUI,
2864
+ threadRuntime,
2865
+ ]);
2845
2866
 
2846
2867
  const appendRealtimeVoiceTranscript = useCallback(
2847
2868
  (
@@ -2896,7 +2917,7 @@ const AssistantChatInner = forwardRef<
2896
2917
 
2897
2918
  const cacheCurrentThreadSnapshot = useCallback(() => {
2898
2919
  if (!threadId || messages.length === 0) return;
2899
- const repo = exportCleanThreadRepo();
2920
+ const repo = exportPersistableThreadRepo();
2900
2921
  const threadData = JSON.stringify(stripBase64FromRepo(repo));
2901
2922
  const { title, preview } = extractThreadMeta(repo);
2902
2923
  writeCachedThreadSnapshot(apiUrl, threadId, {
@@ -2905,7 +2926,7 @@ const AssistantChatInner = forwardRef<
2905
2926
  preview,
2906
2927
  messageCount: messages.length,
2907
2928
  });
2908
- }, [apiUrl, exportCleanThreadRepo, messages.length, threadId]);
2929
+ }, [apiUrl, exportPersistableThreadRepo, messages.length, threadId]);
2909
2930
 
2910
2931
  useBrowserLayoutEffect(() => {
2911
2932
  if (hasImportedInitialCachedSnapshotRef.current) return;
@@ -3669,7 +3690,7 @@ const AssistantChatInner = forwardRef<
3669
3690
  const timeSinceLastSave = now - lastSaveTimeRef.current;
3670
3691
  if (timeSinceLastSave < 5000) return;
3671
3692
 
3672
- const repo = exportCleanThreadRepo();
3693
+ const repo = exportPersistableThreadRepo();
3673
3694
  const { title, preview } = extractThreadMeta(repo);
3674
3695
  const threadData = JSON.stringify(stripBase64FromRepo(repo));
3675
3696
  const snapshot = {
@@ -3683,7 +3704,7 @@ const AssistantChatInner = forwardRef<
3683
3704
  savedTitleRef.current = title;
3684
3705
  writeCachedThreadSnapshot(apiUrl, threadId, snapshot);
3685
3706
  onSaveThreadRef.current(threadId, snapshot);
3686
- }, [apiUrl, exportCleanThreadRepo, messages, isRunning, threadId]);
3707
+ }, [apiUrl, exportPersistableThreadRepo, messages, isRunning, threadId]);
3687
3708
 
3688
3709
  // Persist full thread data after each completed response
3689
3710
  useEffect(() => {
@@ -3691,7 +3712,7 @@ const AssistantChatInner = forwardRef<
3691
3712
  if (isRunning) return;
3692
3713
  if (messages.length === 0) return;
3693
3714
 
3694
- const repo = exportCleanThreadRepo();
3715
+ const repo = exportPersistableThreadRepo();
3695
3716
 
3696
3717
  if (threadId && onSaveThreadRef.current) {
3697
3718
  // Save to server via the hook callback
@@ -3713,7 +3734,14 @@ const AssistantChatInner = forwardRef<
3713
3734
  sessionStorage.setItem(storageKey, JSON.stringify(repo));
3714
3735
  } catch {}
3715
3736
  }
3716
- }, [apiUrl, exportCleanThreadRepo, messages, isRunning, threadId, tabId]);
3737
+ }, [
3738
+ apiUrl,
3739
+ exportPersistableThreadRepo,
3740
+ messages,
3741
+ isRunning,
3742
+ threadId,
3743
+ tabId,
3744
+ ]);
3717
3745
 
3718
3746
  useEffect(() => {
3719
3747
  onMessageCountChange?.(messages.length);
@@ -4368,8 +4396,8 @@ const AssistantChatInner = forwardRef<
4368
4396
  }
4369
4397
  reconnectTailOnlyRef.current = false;
4370
4398
  }
4371
- threadRuntime.cancelRun();
4372
4399
  settleVisibleInterruptedTools();
4400
+ threadRuntime.cancelRun();
4373
4401
  if (typeof window !== "undefined") {
4374
4402
  window.dispatchEvent(
4375
4403
  new CustomEvent("agentNative.chatRunning", {
@@ -4810,7 +4838,7 @@ const AssistantChatInner = forwardRef<
4810
4838
  },
4811
4839
  exportThreadSnapshot() {
4812
4840
  if (messages.length === 0) return null;
4813
- const repo = exportCleanThreadRepo();
4841
+ const repo = exportPersistableThreadRepo();
4814
4842
  const { title, preview } = extractThreadMeta(repo);
4815
4843
  return {
4816
4844
  threadData: JSON.stringify(repo),
@@ -4822,7 +4850,7 @@ const AssistantChatInner = forwardRef<
4822
4850
  }),
4823
4851
  [
4824
4852
  addToQueue,
4825
- exportCleanThreadRepo,
4853
+ exportPersistableThreadRepo,
4826
4854
  isRunning,
4827
4855
  messages.length,
4828
4856
  stageComposerContextItem,
@@ -73,6 +73,7 @@ import {
73
73
  MarkdownText,
74
74
  renderMarkdownToClipboardHtml,
75
75
  } from "./markdown-renderer.js";
76
+ import { getAssistantRunDurationMs } from "./repo-helpers.js";
76
77
  import {
77
78
  ToolCallFallback,
78
79
  ToolActivityPresentation,
@@ -989,17 +990,22 @@ export function shouldShowAssistantMessageFooter({
989
990
  }
990
991
 
991
992
  export function shouldShowMissingFinalResponse({
993
+ isCurrentTurnRunning,
992
994
  statusIsTerminal,
993
995
  hasAssistantText,
994
996
  hasUnresolvedTool,
995
997
  hasCompletedCustomUi,
996
998
  }: {
999
+ isCurrentTurnRunning: boolean;
997
1000
  statusIsTerminal: boolean;
998
1001
  hasAssistantText: boolean;
999
1002
  hasUnresolvedTool: boolean;
1000
1003
  hasCompletedCustomUi?: boolean;
1001
1004
  }): boolean {
1005
+ // A completed tool can make the latest message look terminal before the
1006
+ // active turn attaches its follow-up text.
1002
1007
  return (
1008
+ !isCurrentTurnRunning &&
1003
1009
  statusIsTerminal &&
1004
1010
  !hasAssistantText &&
1005
1011
  !hasUnresolvedTool &&
@@ -1202,6 +1208,7 @@ export function AssistantMessage() {
1202
1208
  const chatRunning = React.useContext(ChatRunningContext);
1203
1209
  const lastRunDurationMs = React.useContext(ChatRunDurationContext);
1204
1210
  const msg = messageRuntime.getState();
1211
+ const persistedDurationMs = getAssistantRunDurationMs(msg);
1205
1212
  const timestamp = formatMessageTimestamp(msg.createdAt);
1206
1213
  const isLast =
1207
1214
  thread.messages.length > 0 &&
@@ -1215,6 +1222,7 @@ export function AssistantMessage() {
1215
1222
  );
1216
1223
  const hasCustomUi = assistantMessageHasCustomUi(msg.content);
1217
1224
  const showMissingFinalResponse = shouldShowMissingFinalResponse({
1225
+ isCurrentTurnRunning: isLast && chatRunning,
1218
1226
  statusIsTerminal,
1219
1227
  hasAssistantText: responseConnectionText.trim().length > 0,
1220
1228
  hasUnresolvedTool,
@@ -1370,7 +1378,7 @@ export function AssistantMessage() {
1370
1378
  if (!showSummary) return <>{children}</>;
1371
1379
  return (
1372
1380
  <WorkedForSummary
1373
- durationMs={capturedDurationMs}
1381
+ durationMs={capturedDurationMs ?? persistedDurationMs}
1374
1382
  defaultOpen={hasCustomUi}
1375
1383
  autoCollapse={animateCollapse && !hasCustomUi}
1376
1384
  >
@@ -5,6 +5,8 @@
5
5
  // interface covering the shapes both `threadRuntime.export()` and
6
6
  // `normalizeThreadRepository()` produce, and use it everywhere instead of `any`.
7
7
 
8
+ import { ASSISTANT_RUN_DURATION_METADATA_KEY } from "../../agent/thread-data-builder.js";
9
+
8
10
  export interface RepoMessageStatus {
9
11
  type?: string;
10
12
  reason?: string;
@@ -56,6 +58,81 @@ export function getRepoMessage(entry: RepoEntry): RepoMessage | null {
56
58
  return (entry?.message ?? entry) as RepoMessage | null;
57
59
  }
58
60
 
61
+ export function getAssistantRunDurationMs(
62
+ message:
63
+ | {
64
+ metadata?: unknown;
65
+ }
66
+ | null
67
+ | undefined,
68
+ ): number | null {
69
+ const metadata =
70
+ message?.metadata && typeof message.metadata === "object"
71
+ ? (message.metadata as Record<string, unknown>)
72
+ : null;
73
+ const custom =
74
+ metadata?.custom && typeof metadata.custom === "object"
75
+ ? (metadata.custom as Record<string, unknown>)
76
+ : null;
77
+ const durationMs = custom?.[ASSISTANT_RUN_DURATION_METADATA_KEY];
78
+ return typeof durationMs === "number" &&
79
+ Number.isFinite(durationMs) &&
80
+ durationMs >= 0
81
+ ? durationMs
82
+ : null;
83
+ }
84
+
85
+ export function withLastAssistantRunDuration<T extends NormalizedRepo>(
86
+ repo: T,
87
+ durationMs: number | null | undefined,
88
+ ): T {
89
+ if (
90
+ !Array.isArray(repo.messages) ||
91
+ typeof durationMs !== "number" ||
92
+ !Number.isFinite(durationMs) ||
93
+ durationMs < 0
94
+ ) {
95
+ return repo;
96
+ }
97
+
98
+ let messageIndex = -1;
99
+ for (let index = repo.messages.length - 1; index >= 0; index -= 1) {
100
+ if (getRepoMessage(repo.messages[index]!)?.role === "assistant") {
101
+ messageIndex = index;
102
+ break;
103
+ }
104
+ }
105
+ if (messageIndex < 0) return repo;
106
+
107
+ const entry = repo.messages[messageIndex]!;
108
+ const message = getRepoMessage(entry);
109
+ if (!message || getAssistantRunDurationMs(message) === durationMs) {
110
+ return repo;
111
+ }
112
+
113
+ const metadata = message.metadata ?? {};
114
+ const custom =
115
+ metadata.custom && typeof metadata.custom === "object"
116
+ ? (metadata.custom as Record<string, unknown>)
117
+ : {};
118
+ const nextMessage: RepoMessage = {
119
+ ...message,
120
+ metadata: {
121
+ ...metadata,
122
+ custom: {
123
+ ...custom,
124
+ [ASSISTANT_RUN_DURATION_METADATA_KEY]: durationMs,
125
+ },
126
+ },
127
+ };
128
+ const messages = repo.messages.slice();
129
+ messages[messageIndex] =
130
+ entry.message === undefined
131
+ ? (nextMessage as RepoEntry)
132
+ : { ...entry, message: nextMessage };
133
+ return { ...repo, messages };
134
+ }
135
+
59
136
  /**
60
137
  * Collapse duplicate message ids before a repository is handed to
61
138
  * `threadRuntime.import()`. assistant-ui's `MessageRepository` throws
@@ -25,7 +25,10 @@ import React, {
25
25
  useRef,
26
26
  } from "react";
27
27
 
28
- import type { A2AAgentActivitySnapshot } from "../../a2a/activity.js";
28
+ import type {
29
+ A2AAgentActivitySnapshot,
30
+ A2AAgentActivityToolCall,
31
+ } from "../../a2a/activity.js";
29
32
  import type { ActionChatUIConfig } from "../../action-ui.js";
30
33
  import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
31
34
  import { AgentTaskCard } from "../AgentTaskCard.js";
@@ -100,11 +103,13 @@ export function ToolActivityPresentation({
100
103
  toolName,
101
104
  isRunning,
102
105
  isActiveTail,
106
+ suppressLongRunningHint = false,
103
107
  children,
104
108
  }: {
105
109
  toolName: string;
106
110
  isRunning: boolean;
107
111
  isActiveTail: boolean;
112
+ suppressLongRunningHint?: boolean;
108
113
  children: React.ReactNode;
109
114
  }) {
110
115
  const [showLongRunningHint, setShowLongRunningHint] = useState(false);
@@ -114,7 +119,7 @@ export function ToolActivityPresentation({
114
119
  const [animateEntry] = useState(isActiveTail);
115
120
 
116
121
  useEffect(() => {
117
- if (!isRunning) {
122
+ if (!isRunning || suppressLongRunningHint) {
118
123
  setShowLongRunningHint(false);
119
124
  return;
120
125
  }
@@ -123,7 +128,7 @@ export function ToolActivityPresentation({
123
128
  setShowLongRunningHint(true);
124
129
  }, TOOL_LONG_RUNNING_HINT_DELAY_MS);
125
130
  return () => window.clearTimeout(timeout);
126
- }, [isRunning, toolName]);
131
+ }, [isRunning, suppressLongRunningHint, toolName]);
127
132
 
128
133
  return (
129
134
  <div
@@ -584,6 +589,9 @@ export function ToolCallDisplay({
584
589
  toolName={toolName}
585
590
  isRunning={isRunning}
586
591
  isActiveTail={showActiveTail}
592
+ suppressLongRunningHint={
593
+ toolName === "call-agent" || toolName.startsWith("agent:")
594
+ }
587
595
  >
588
596
  {children}
589
597
  </ToolActivityPresentation>
@@ -955,18 +963,8 @@ function AgentCallCell({
955
963
  />
956
964
  )}
957
965
  {tool && (
958
- <ToolCallDisplay
959
- toolName={tool.name}
960
- args={{}}
961
- argsText=""
962
- isRunning={tool.status === "running"}
963
- result={
964
- tool.status === "failed"
965
- ? "Failed"
966
- : tool.status === "completed"
967
- ? "Done"
968
- : undefined
969
- }
966
+ <AgentActivityToolCallRow
967
+ tool={tool}
970
968
  isActiveTail={
971
969
  isRunning && index === activity.toolCalls.length - 1
972
970
  }
@@ -1051,6 +1049,44 @@ function AgentCallCell({
1051
1049
  );
1052
1050
  }
1053
1051
 
1052
+ function AgentActivityToolCallRow({
1053
+ tool,
1054
+ isActiveTail,
1055
+ }: {
1056
+ tool: A2AAgentActivityToolCall;
1057
+ isActiveTail: boolean;
1058
+ }) {
1059
+ const isRunning = tool.status === "running";
1060
+ const ToolIcon = resolveToolIcon(tool.name);
1061
+
1062
+ return (
1063
+ <ToolActivityPresentation
1064
+ toolName={tool.name}
1065
+ isRunning={isRunning}
1066
+ isActiveTail={isActiveTail}
1067
+ suppressLongRunningHint
1068
+ >
1069
+ <div className="my-0.5 flex w-full items-center gap-1.5 rounded-md py-0.5 text-left text-[13px] text-muted-foreground">
1070
+ <span className="flex size-4 shrink-0 items-center justify-center">
1071
+ {isRunning ? (
1072
+ <IconLoader2 className="size-3.5 animate-spin" />
1073
+ ) : (
1074
+ <ToolIcon className="size-3.5" />
1075
+ )}
1076
+ </span>
1077
+ <span
1078
+ className={cn(
1079
+ "min-w-0 truncate font-normal",
1080
+ isActiveTail && "agent-running-shimmer",
1081
+ )}
1082
+ >
1083
+ {humanizeToolName(tool.name)}
1084
+ </span>
1085
+ </div>
1086
+ </ToolActivityPresentation>
1087
+ );
1088
+ }
1089
+
1054
1090
  // ─── ToolCallFallback ──────────────────────────────────────────────────────────
1055
1091
 
1056
1092
  export function ToolCallFallback({
@@ -13,6 +13,7 @@ import {
13
13
  isAgentEnginePackageInstalled,
14
14
  isStoredEngineUsableForRequest,
15
15
  normalizeModelForEngine,
16
+ resolveEnginePreservesCustomModels,
16
17
  } from "../../agent/engine/index.js";
17
18
  import type { ActionTool } from "../../agent/types.js";
18
19
  import { getSetting } from "../../settings/index.js";
@@ -82,11 +83,18 @@ export async function run(args: Record<string, string> = {}): Promise<string> {
82
83
  ? current?.model
83
84
  : undefined;
84
85
  const currentEngineName = currentEntry?.name ?? "anthropic";
86
+ // Resolve the OpenAI-compatible-endpoint capability so a custom gateway model
87
+ // is reported as-is instead of being normalized to the engine default — the
88
+ // read-side counterpart of the same fix in set-/manage-agent-engine.
89
+ const preserveCustomModels = currentEntry
90
+ ? await resolveEnginePreservesCustomModels(currentEntry)
91
+ : false;
85
92
  const currentModel =
86
93
  currentEntry && !envUnavailable
87
94
  ? normalizeModelForEngine(
88
95
  currentEntry,
89
96
  currentModelCandidate ?? currentEntry.defaultModel,
97
+ { preserveCustomModels },
90
98
  )
91
99
  : (currentModelCandidate ?? DEFAULT_MODEL);
92
100
  const result = {
@@ -16,6 +16,7 @@ import {
16
16
  getAgentEngineEntry,
17
17
  isAgentEnginePackageInstalled,
18
18
  normalizeModelForEngine,
19
+ resolveEnginePreservesCustomModels,
19
20
  registerBuiltinEngines,
20
21
  } from "../../agent/engine/index.js";
21
22
  import type { ActionTool } from "../../agent/types.js";
@@ -113,7 +114,10 @@ async function runSetAppDefault(args: Record<string, string>): Promise<string> {
113
114
  if (!isAgentEnginePackageInstalled(entry)) {
114
115
  return `Error: Engine "${engine}" requires optional packages that are not installed in this app. Run: pnpm add ${entry.installPackage}`;
115
116
  }
116
- const normalizedModel = normalizeModelForEngine(entry, model);
117
+ const preserveCustomModels = await resolveEnginePreservesCustomModels(entry);
118
+ const normalizedModel = normalizeModelForEngine(entry, model, {
119
+ preserveCustomModels,
120
+ });
117
121
 
118
122
  const ctx = currentContext();
119
123
  const canUpdate = await canUpdateAgentAppModelDefaultSettings(
@@ -8,6 +8,7 @@ import {
8
8
  isAgentEnginePackageInstalled,
9
9
  isStoredEngineUsableForRequest,
10
10
  normalizeModelForEngine,
11
+ resolveEnginePreservesCustomModels,
11
12
  registerBuiltinEngines,
12
13
  } from "../../agent/engine/index.js";
13
14
  import type { ActionTool } from "../../agent/types.js";
@@ -54,7 +55,14 @@ export async function run(args: Record<string, string>): Promise<string> {
54
55
  }
55
56
 
56
57
  const requestedModel = model ?? entry.defaultModel;
57
- const resolvedModel = normalizeModelForEngine(entry, requestedModel);
58
+ // A static registry entry can't carry the runtime `preserveCustomModels`
59
+ // flag, so resolve the OpenAI-compatible-endpoint capability here and pass it
60
+ // through — otherwise a gateway model (e.g. an Ollama id) is rewritten to the
61
+ // engine default on save.
62
+ const preserveCustomModels = await resolveEnginePreservesCustomModels(entry);
63
+ const resolvedModel = normalizeModelForEngine(entry, requestedModel, {
64
+ preserveCustomModels,
65
+ });
58
66
 
59
67
  const usable = await isStoredEngineUsableForRequest(
60
68
  { engine: engineName },
@@ -413,7 +413,9 @@ export function generateCorpusToolsPrompt(
413
413
 
414
414
  Available corpus-capable tools: ${available.join(", ")}.
415
415
 
416
- For broad provider searches, raw API access, multi-page cohorts, cross-source joins, classification/counting over records, or absence-sensitive answers, do not stop at a bounded shortcut action. Use the provider's broad API/search/list surface, fetch every relevant page or an explicit bounded cohort, stage/save large responses when needed, and reduce the corpus with durable corpus jobs, staged-dataset queries, or code execution.
416
+ This workflow does not apply to ordinary structured lookups, bounded aggregates, or counts grouped over one known source. For those requests, use the single most directly authoritative source, run one bounded query, and answer as soon as it succeeds. Do not cross-check or expand into a corpus workflow unless the user asks for multiple sources, exhaustive unstructured-record coverage, or an absence claim.
417
+
418
+ For broad provider searches, raw API access, multi-page cohorts, cross-source joins, classification over unstructured records, or absence-sensitive answers, do not stop at a bounded shortcut action. Use the provider's broad API/search/list surface, fetch every relevant page or an explicit bounded cohort, stage/save large responses when needed, and reduce the corpus with durable corpus jobs, staged-dataset queries, or code execution.
417
419
 
418
420
  When \`provider-corpus-job\` is available, prefer it for transcript/message/ticket/issue/document scans that may exceed one turn, need provider-side backoff, or need a defensible "not found" conclusion. Use operation="start" with mode="paginated-search" for any paginated provider endpoint, or mode="batch-search" when a prior cohort of ids/records must feed a second provider endpoint. Continue paused jobs with operation="continue" until status is completed or quota_wait, then read operation="results". In run-code, prefer providerFetchAll() for short cursor/page/offset pagination and providerRequest() when response status, headers, or truncation metadata matters. Report source, filters, row counts, pagination/truncation, failed pages, quota_wait times, and remaining gaps.`;
419
421
  }
@@ -216,6 +216,15 @@ export interface AgentChatPluginOptions {
216
216
  * tool-search unless the app explicitly includes them in this list.
217
217
  */
218
218
  initialToolNames?: string[];
219
+ /**
220
+ * Controls whether broad provider/corpus tools and their workflow prompt are
221
+ * loaded into the first model request. Use `"lazy"` for apps that can answer
222
+ * ordinary lookups from a compact curated tool set; corpus tools remain
223
+ * discoverable through tool-search and the full retry surface.
224
+ *
225
+ * @default "initial"
226
+ */
227
+ corpusTools?: "initial" | "lazy";
219
228
  /**
220
229
  * Use a compact system prompt with on-demand context loading. The system
221
230
  * prompt includes essential behavioral rules and action signatures, but
@@ -1179,8 +1179,10 @@ export function createAgentChatPlugin(
1179
1179
  // that never emit the corpus prompt (no provider/run-code tools
1180
1180
  // registered), so this never silently expands the initial set for
1181
1181
  // apps that don't teach these tools by name.
1182
- const corpusToolNames =
1183
- corpusToolNamesTaughtByPrompt(corpusPromptRegistry);
1182
+ const loadCorpusToolsInitially = options?.corpusTools !== "lazy";
1183
+ const corpusToolNames = loadCorpusToolsInitially
1184
+ ? corpusToolNamesTaughtByPrompt(corpusPromptRegistry)
1185
+ : [];
1184
1186
  const effectiveInitialToolNames =
1185
1187
  corpusToolNames.length > 0
1186
1188
  ? [...new Set([...templateInitialToolNames, ...corpusToolNames])]
@@ -1907,7 +1909,9 @@ export function createAgentChatPlugin(
1907
1909
  // Dev: actions are invoked via bash — emit `pnpm action name --arg <type>`
1908
1910
  // and include discoveredActions too, since those are also missing
1909
1911
  // from the dev tool registry.
1910
- const corpusToolsPrompt = generateCorpusToolsPrompt(corpusPromptRegistry);
1912
+ const corpusToolsPrompt = loadCorpusToolsInitially
1913
+ ? generateCorpusToolsPrompt(corpusPromptRegistry)
1914
+ : "";
1911
1915
  const prodActionsPrompt =
1912
1916
  generateActionsPrompt(
1913
1917
  templateScripts,
@@ -2250,6 +2254,11 @@ export function createAgentChatPlugin(
2250
2254
  typeof run.turnId === "string" && run.turnId
2251
2255
  ? run.turnId
2252
2256
  : undefined,
2257
+ runDurationMs:
2258
+ typeof run.startedAt === "number" &&
2259
+ Number.isFinite(run.startedAt)
2260
+ ? Math.max(0, Date.now() - run.startedAt)
2261
+ : undefined,
2253
2262
  },
2254
2263
  );
2255
2264
  if (!assistantMsg) {