@k2b/cloud 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -295,7 +295,8 @@ export const searchAiTools = (
295
295
  .flatMap((entry) => {
296
296
  const name = normalizeSearchText(entry.name);
297
297
  const title = normalizeSearchText(entry.title);
298
- const identity = `${name} ${title}`;
298
+ const localizedTitle = normalizeSearchText(entry.capability?.display.title ?? "");
299
+ const identity = `${name} ${title} ${localizedTitle}`;
299
300
  const app = normalizeSearchText(`${entry.appId ?? ""} ${entry.searchText}`);
300
301
  const description = normalizeSearchText(entry.description);
301
302
  const identityWords = searchWordForms(identity);
@@ -303,7 +304,7 @@ export const searchAiTools = (
303
304
  const descriptionWords = searchWordForms(description);
304
305
  let matchedTerms = 0;
305
306
  let score = 0;
306
- if (name === phrase || title === phrase) score += 100;
307
+ if (name === phrase || title === phrase || localizedTitle === phrase) score += 100;
307
308
  else if (includesSearchPhrase(identity, phrase)) score += 50;
308
309
  if (!input.appId && app === phrase) score += 40;
309
310
  else if (!input.appId && includesSearchPhrase(app, phrase)) score += 20;
@@ -324,6 +325,15 @@ export const searchAiTools = (
324
325
  right.matchedTerms - left.matchedTerms || right.score - left.score || left.entry.name.localeCompare(right.entry.name),
325
326
  )
326
327
  .slice(0, DEFAULT_SEARCH_LIMIT);
328
+ // appId is a discovery hint, not the conversation's authorization scope. The
329
+ // resolver has already removed inaccessible, unoffered and disallowed tools.
330
+ // Keep successful scoped searches precise; recover a mistaken owner once.
331
+ if (matches.length === 0 && input.appId) {
332
+ return searchAiTools(
333
+ catalog.filter((entry) => entry.appId !== input.appId),
334
+ { query: input.query },
335
+ );
336
+ }
327
337
  return {
328
338
  tools: matches.map(({ entry }) => ({
329
339
  name: entry.name,
@@ -669,11 +679,16 @@ export const createAiToolMetaTools = (input: {
669
679
  : "";
670
680
  const search = defineAiTool({
671
681
  name: "search_tools",
672
- description: `Search available Cloud tools and installed app operations by concise task terms.${liveAppDirectory} When the app is known, set its exact appId on the first attempt. Use list_apps only when the owning app is unclear. This only discovers tools; call load_tools with the exact returned names before using deferred tools.${unavailableLoadedNotice}`,
682
+ description: `Search available Cloud tools and installed app operations by concise task terms.${liveAppDirectory} When the app is known, set its exact appId. If that app has no matches, search also returns matching available tools from other apps or built-ins. If the owner is unclear, omit appId or use list_apps. This only discovers tools; call load_tools with the exact returned names before using deferred tools.${unavailableLoadedNotice}`,
673
683
  inputSchema: z
674
684
  .object({
675
685
  query: z.string().trim().min(1).max(200).describe("What the tool should do."),
676
- appId: z.string().trim().min(1).optional().describe("Optional exact Cloud app id."),
686
+ appId: z
687
+ .string()
688
+ .trim()
689
+ .min(1)
690
+ .optional()
691
+ .describe("Preferred exact Cloud app id; an empty scoped search falls back to other available tools."),
677
692
  })
678
693
  .strict(),
679
694
  outputSchema: z.object({ tools: z.array(ToolCatalogItemSchema).max(DEFAULT_SEARCH_LIMIT) }).strict(),
@@ -98,6 +98,8 @@ type AiCapabilityCall = {
98
98
  mandate?: { id: string; revision: number };
99
99
  /** Set only by the capability tool after its explicit approval request resolved true. */
100
100
  actionApproval?: "approved";
101
+ /** Outcome Queries retain turn authority without replacing the Action tool-call link. */
102
+ traceToolCall?: boolean;
101
103
  dependencies?: CapabilityDispatchDependencies & {
102
104
  dispatch?: typeof dispatchCapability;
103
105
  };
@@ -123,7 +125,7 @@ const dispatchAiCapability = async (
123
125
  ? idempotencyKey(input.conversationId, input.context.callId)
124
126
  : null;
125
127
  if (key) headers.set("idempotency-key", key);
126
- if (!review && input.turnId && input.context.callId) {
128
+ if (!review && input.traceToolCall !== false && input.turnId && input.context.callId) {
127
129
  await aiToolAudit.noteCapabilityDispatch({
128
130
  conversationId: input.conversationId,
129
131
  turnId: input.turnId,
@@ -143,6 +145,7 @@ const dispatchAiCapability = async (
143
145
  request,
144
146
  kind: input.entry.kind === "query" ? "queries" : "actions",
145
147
  origin: "assistant",
148
+ ...(input.turnId ? { assistantTurn: { conversationId: input.conversationId, turnId: input.turnId } } : {}),
146
149
  review,
147
150
  appId: input.entry.appId,
148
151
  capabilityId: input.entry.operation.localId,
@@ -187,7 +190,7 @@ export const checkAiCapability = async (
187
190
  if (!query || "idempotency" in query.operation || !capabilityQueryChecksActionOutcome(query.operation))
188
191
  throw new Error("Action outcome Query is unavailable.");
189
192
  // The Action's audit keeps its own dispatch identity; the Query has its separate platform execution record.
190
- const result = await dispatchAiCapability({ ...input, turnId: undefined, entry: query, args: input.check.input }, false);
193
+ const result = await dispatchAiCapability({ ...input, traceToolCall: false, entry: query, args: input.check.input }, false);
191
194
  if (!result || typeof result !== "object" || !("data" in result)) throw new Error("Invalid outcome check envelope.");
192
195
  const checked = CapabilityActionCheckSchema.parse(result.data);
193
196
  if (checked.receipt && capabilityActionReceiptIssues(checked.receipt, "links" in result ? result.links : undefined).length)
@@ -1,7 +1,6 @@
1
1
  // Named renderers, not the `charts` namespace, so bundles leave out the map and state timeline renderers.
2
2
  import {
3
3
  autoBin,
4
- bar,
5
4
  barGauge,
6
5
  boxplot,
7
6
  type ChartDatum,
@@ -21,11 +20,14 @@ import {
21
20
  sparkline,
22
21
  stat,
23
22
  } from "@k2b/stdlib";
23
+ import { type BarChartOptions, barChartLayout, renderBarChartSvg } from "@k2b/ui/charts";
24
24
  import { z } from "zod";
25
25
  import type { CloudAiChartInput, CloudAiFormat, CloudAiNumberFormat } from "./default-tool-contracts";
26
26
 
27
27
  type RenderOptions = {
28
- [K in CloudAiChartInput["kind"]]: { kind: K } & Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">;
28
+ [K in CloudAiChartInput["kind"]]: { kind: K } & (K extends "bar"
29
+ ? BarChartOptions
30
+ : Omit<Parameters<(typeof charts)[K]>[0], "width" | "height" | "inspect">);
29
31
  }[CloudAiChartInput["kind"]];
30
32
 
31
33
  /**
@@ -205,18 +207,26 @@ type Formats = {
205
207
  x: { tick: (value: number) => string; value: (value: number) => string };
206
208
  };
207
209
 
208
- /** Value precision and tick precision share the same style and units; ticks must remain distinct. */
209
- export const cloudAiNumberFormatters = (format: CloudAiNumberFormat | undefined, locale: string) => {
210
+ /**
211
+ * Value precision and tick precision share the same style and units; ticks must remain distinct. A currency without
212
+ * `decimals` or compact notation writes cents only when one of the formatted `values` has them: 418,000 € rather than
213
+ * 418,000.00 €.
214
+ */
215
+ export const cloudAiNumberFormatters = (format: CloudAiNumberFormat | undefined, locale: string, values?: readonly number[]) => {
210
216
  const common: Intl.NumberFormatOptions = {
211
217
  style: format?.style === "number" ? "decimal" : format?.style,
212
218
  currency: format?.currency,
213
219
  };
220
+ // Compact notation keeps its fraction: 1.2K and 1.4K must not both read 1K.
221
+ const decimals =
222
+ format?.decimals ??
223
+ (format?.style === "currency" && !format.compact && values?.length && values.every(Number.isInteger) ? 0 : undefined);
214
224
  const value = new Intl.NumberFormat(
215
225
  locale,
216
226
  format
217
227
  ? {
218
228
  ...common,
219
- ...(format.decimals === undefined ? {} : { minimumFractionDigits: format.decimals, maximumFractionDigits: format.decimals }),
229
+ ...(decimals === undefined ? {} : { minimumFractionDigits: decimals, maximumFractionDigits: decimals }),
220
230
  ...(format.compact ? { notation: "compact" } : {}),
221
231
  }
222
232
  : { maximumSignificantDigits: 15 },
@@ -234,10 +244,37 @@ export const cloudAiNumberFormatters = (format: CloudAiNumberFormat | undefined,
234
244
  return { value: (v: number) => `${value.format(v)}${unit}`, tick: (v: number) => `${tickValue(v)}${unit}` };
235
245
  };
236
246
  const numberFormat = (format: CloudAiFormat | undefined) => (format && !("date" in format) ? format : undefined);
247
+ /** The values a chart writes with its value format, for whole-number currency. */
248
+ const formattedValues = (input: CloudAiChartInput): number[] => {
249
+ switch (input.kind) {
250
+ case "line":
251
+ case "scatter":
252
+ return [...input.series.flatMap((series) => series.data.map((point) => point.y)), ...referencesOf(input, "y")];
253
+ case "bar":
254
+ return [...input.data.map((item) => item.value), ...referencesOf(input, "y")];
255
+ case "pie":
256
+ case "donut":
257
+ case "heatmap":
258
+ return input.data.map((item) => item.value);
259
+ case "barGauge":
260
+ return [...input.data.flatMap((item) => [item.value, item.min, item.max]), input.min, input.max].filter(
261
+ (value) => value !== undefined,
262
+ );
263
+ case "stat":
264
+ return [input.value, ...(input.delta === undefined ? [] : [input.delta]), ...(input.sparkline ?? [])];
265
+ case "gauge":
266
+ return [input.value, input.min, input.max].filter((value) => value !== undefined);
267
+ case "sparkline":
268
+ return input.data;
269
+ // Box plots write quartiles they compute, and histograms bin edges: those keep their cents.
270
+ default:
271
+ return [];
272
+ }
273
+ };
237
274
  const formats = (input: CloudAiChartInput, locale: string): Formats => {
238
275
  const yFormat = "yAxis" in input ? numberFormat(input.yAxis?.format) : "format" in input ? input.format : undefined;
239
- const y = cloudAiNumberFormatters(yFormat, locale);
240
- const xFormat = "xAxis" in input ? input.xAxis?.format : undefined;
276
+ const y = cloudAiNumberFormatters(yFormat, locale, formattedValues(input));
277
+ const xFormat = "xAxis" in input && input.kind !== "bar" ? input.xAxis?.format : undefined;
241
278
  const x = cloudAiNumberFormatters(numberFormat(xFormat), locale);
242
279
  // An unformatted x axis often counts years or steps, which compact notation would turn into 2.02K.
243
280
  const xTick = new Intl.NumberFormat(locale, { maximumSignificantDigits: 15, useGrouping: "min2" });
@@ -268,7 +305,7 @@ const leftPadding = (values: number[], value: ChartAxisInput | undefined, tick:
268
305
  };
269
306
 
270
307
  /**
271
- * Category labels never shorten: when they would overlap in the narrowest column, every n-th label shows in full and
308
+ * Box plot labels never shorten: when they would overlap in the narrowest column, every n-th label shows in full and
272
309
  * the others stay empty, as in `cloud.chart()`. Table and tooltips keep every label.
273
310
  */
274
311
  const thinned = <T extends { label: string }>(data: T[], left: number, width = WIDTH, factor = NARROW): T[] => {
@@ -327,31 +364,27 @@ export function cloudAiChartRenderOptions(
327
364
  switch (input.kind) {
328
365
  case "bar":
329
366
  case "boxplot": {
330
- const values = input.kind === "bar" ? input.data.map((item) => item.value) : input.data.flatMap((item) => item.values);
331
367
  const yAxis = referencedAxis(input, "y");
332
- // The renderer prints bar values with the axis format, so printed values take the value precision for both.
333
- const yFormat = options.formats?.y ?? (input.kind === "bar" && input.showValues ? format.value : format.tick);
334
- const left = leftPadding(
335
- [...values, ...referencesOf(input, "y"), ...(input.kind === "bar" && input.yAxis?.scale !== "log" ? [0] : [])],
336
- yAxis,
337
- yFormat,
338
- factor,
339
- );
340
- if (input.kind === "boxplot")
368
+ // The bar renderer places every category label and printed value itself, sized for the narrowest column.
369
+ if (input.kind === "bar")
341
370
  return {
342
- kind: "boxplot",
343
- groups: thinned(input.data, left, width, factor),
344
- yAxis: axis(yAxis, format.tick),
371
+ kind: "bar",
372
+ data: input.data,
373
+ xAxis: input.xAxis,
374
+ yAxis: axis(yAxis, options.formats?.y ?? format.tick),
375
+ // A host's own function format writes printed values like its ticks and table cells.
376
+ valueFormat: options.formats?.value ?? options.formats?.y ?? format.value,
377
+ colorByBar: input.colorByBar,
378
+ showValues: input.showValues,
379
+ legend: options.legend === "outside" ? false : input.legend,
345
380
  references,
346
- padding: { left },
381
+ ...(options.width === undefined ? {} : { displayWidth: options.width }),
347
382
  };
383
+ const left = leftPadding([...input.data.flatMap((item) => item.values), ...referencesOf(input, "y")], yAxis, format.tick, factor);
348
384
  return {
349
- kind: "bar",
350
- data: input.colorByBar && input.legend && options.legend !== "outside" ? input.data : thinned(input.data, left, width, factor),
351
- yAxis: axis(yAxis, yFormat),
352
- colorByBar: input.colorByBar,
353
- showValues: input.showValues,
354
- legend: options.legend === "outside" ? false : input.legend,
385
+ kind: "boxplot",
386
+ groups: thinned(input.data, left, width, factor),
387
+ yAxis: axis(yAxis, format.tick),
355
388
  references,
356
389
  padding: { left },
357
390
  };
@@ -478,7 +511,7 @@ const draw = (options: RenderOptions): string => {
478
511
  const common = { width: WIDTH, height: HEIGHT, inspect: true };
479
512
  switch (options.kind) {
480
513
  case "bar":
481
- return bar({ ...options, ...common });
514
+ return renderBarChartSvg({ ...options, ...common, height: barChartLayout(options).height });
482
515
  case "line":
483
516
  return line({ ...options, ...common });
484
517
  case "scatter":
@@ -554,7 +587,7 @@ export function cloudAiChartRows(
554
587
  const t = labels.resolve([locale]).t;
555
588
  const format = formats(input, locale);
556
589
  const valueFormat = input.tooltip?.format
557
- ? cloudAiNumberFormatters(input.tooltip.format, locale).value
590
+ ? cloudAiNumberFormatters(input.tooltip.format, locale, formattedValues(input)).value
558
591
  : (options.formats?.value ?? (input.kind === "histogram" ? undefined : options.formats?.y) ?? format.value);
559
592
  const xFormat = options.formats?.x ?? format.x.value;
560
593
  const series = seriesOf(input);
@@ -50,6 +50,9 @@ const drawChart = (input: CloudAiChartInput, locale: string): DrawnChart | null
50
50
  * 280-unit drawing; at 12rem a row is still taller than its text.
51
51
  */
52
52
  const chartHeight = (kind: Kind) => (kind === "sparkline" ? "8rem" : kind === "stat" ? "10rem" : kind === "barGauge" ? "12rem" : "18rem");
53
+ /** A drawing taller than the usual 280 units, such as many bar rows, grows its frame up to 40rem and scrolls inside. */
54
+ const grownHeight = (kind: Kind, chart: DrawnChart | null) =>
55
+ chart && chart.snapshot.height > 280 ? `min(40rem, max(${chartHeight(kind)}, ${chart.snapshot.height}px))` : chartHeight(kind);
53
56
 
54
57
  function ChartFrame(props: {
55
58
  title: string;
@@ -60,6 +63,11 @@ function ChartFrame(props: {
60
63
  cursor?: ReturnType<typeof createChartCursor>;
61
64
  /** A chart of a titled group: its title sits one heading level and one size below the group's. */
62
65
  nested?: boolean;
66
+ /**
67
+ * The frame takes the drawing's height (`grownHeight`). Only for a drawing present with the first frame: a frame
68
+ * that waits for its data keeps the kind's height, so the data never moves the chat.
69
+ */
70
+ grow?: boolean;
63
71
  }) {
64
72
  const locale = useLocale();
65
73
  const columns = () =>
@@ -80,7 +88,7 @@ function ChartFrame(props: {
80
88
  title={props.title}
81
89
  headingLevel={props.nested ? 4 : 3}
82
90
  description={props.subtitle ? <span class="text-sm text-secondary">{props.subtitle}</span> : undefined}
83
- height={chartHeight(props.kind)}
91
+ height={props.grow ? grownHeight(props.kind, props.chart) : chartHeight(props.kind)}
84
92
  data={props.chart ? { chart: props.chart.snapshot, rows: props.chart.table.rows } : { chart: emptySnapshot(props.kind), rows: [] }}
85
93
  columns={columns()}
86
94
  legend={props.chart?.legend.length ? props.chart.legend : undefined}
@@ -166,7 +174,8 @@ const groupSummary = (group: CloudAiGroupView): GroupSummary => ({
166
174
 
167
175
  /**
168
176
  * One `chart` tool call: the shared chart with its data table and Copy data, or a group of them. The block takes its
169
- * full size with its first frame and keeps it; switching to the table scrolls inside the same height. A call whose
177
+ * full size with its first frame and keeps it, up to 40rem for a tall single chart; switching to the table scrolls
178
+ * inside the same height. A call whose
170
179
  * arguments are not a valid chart shows nothing while it runs, because the model gets the validation error and the
171
180
  * call fails.
172
181
  */
@@ -198,7 +207,7 @@ export function CloudChartBlock(props: { args: unknown; completed: boolean }) {
198
207
  }
199
208
  >
200
209
  {(value) => (
201
- <ChartFrame title={value().input.title} subtitle={value().input.subtitle} kind={value().input.kind} chart={value()} />
210
+ <ChartFrame title={value().input.title} subtitle={value().input.subtitle} kind={value().input.kind} chart={value()} grow />
202
211
  )}
203
212
  </Show>
204
213
  }
@@ -1,7 +1,7 @@
1
1
  import type { ChatAttachment, ChatMention, ChatModelOption, ChatSubmitInput } from "@k2b/ui";
2
2
  import { AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "../limits";
3
3
  import type { AiResourceMarker } from "../resource-markers";
4
- import type { AiDraftContentPart, AiPublicModelProfile, AiUserContentPart } from "../types";
4
+ import type { AiDraftContentPart, AiUserContentPart } from "../types";
5
5
  import {
6
6
  type AiComposerAttachment,
7
7
  FILE_INPUT_ACCEPT,
@@ -45,14 +45,15 @@ export const aiComposerFileAccept = FILE_INPUT_ACCEPT;
45
45
  */
46
46
  export const aiComposerChooseMaxBytes = (attached: number): number => Math.max(0, MAX_ATTACHMENTS - attached) * VFS_FILE_MAX_BYTES;
47
47
 
48
- export const aiChatModelOptions = (profiles: readonly AiPublicModelProfile[]): ChatModelOption[] =>
48
+ export const aiChatModelOptions = (
49
+ profiles: readonly import("../../shared/ai-models").AiPeopleModels["options"][number][],
50
+ ): ChatModelOption[] =>
49
51
  profiles.map((profile) => ({
50
52
  id: profile.id,
51
- label: profile.label,
52
- description: profile.model,
53
- image: profile.image,
54
- icon: profile.capabilities.includes("vision") ? "ti ti-photo-spark" : "ti ti-message",
55
- capabilities: profile.capabilities,
53
+ label: [profile.label, profile.variantLabel].filter(Boolean).join(" · "),
54
+ description: profile.description,
55
+ image: profile.icon.kind === "image" ? profile.icon.src : undefined,
56
+ icon: "ti ti-sparkles",
56
57
  }));
57
58
 
58
59
  const isTextAttachment = (
@@ -109,7 +109,7 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
109
109
 
110
110
  const meta = [
111
111
  { label: t.infoProviderModel, value: entry.providerModel ?? t.infoUnknown },
112
- { label: t.infoModelProfile, value: entry.modelProfileId ?? t.infoUnknown },
112
+ { label: t.infoModelProfile, value: entry.modelId ?? t.infoUnknown },
113
113
  { label: t.infoLoopId, value: entry.loopId ?? t.infoLegacy },
114
114
  { label: t.infoFinished, value: describeResponseEnd(entry, locale) },
115
115
  { label: t.infoCreated, value: formatDateTime(entry.createdAt, locale) },
@@ -259,7 +259,7 @@ export const textAttachmentSummariesFromMessage = (message: Message) => {
259
259
  export type AiLatestUsageSnapshot = {
260
260
  request: Usage;
261
261
  loop: Usage;
262
- modelProfileId: string | null;
262
+ modelId: string | null;
263
263
  };
264
264
 
265
265
  export const latestUsageSnapshot = (messages: AiStoredMessage[]): AiLatestUsageSnapshot | null => {
@@ -287,7 +287,7 @@ export const latestUsageSnapshot = (messages: AiStoredMessage[]): AiLatestUsageS
287
287
  { input: 0, output: 0, total: 0 },
288
288
  )
289
289
  : request),
290
- modelProfileId: entry?.modelProfileId ?? null,
290
+ modelId: entry?.modelId ?? null,
291
291
  };
292
292
  }
293
293
  // A provider without usage must not inherit a previous request's numbers.
@@ -13,6 +13,7 @@ import {
13
13
  Show,
14
14
  } from "solid-js";
15
15
  import type { AiActiveTurn } from "../client/projection";
16
+ import { aiModelChooserMessages } from "../model-chooser-messages";
16
17
  import { type AiActiveTurnSegment, type AiTurnBlock, splitActiveTurnBlocks } from "../protocol";
17
18
  import { type AiAssistantTimelineItem, buildAiMessageTimeline } from "../timeline";
18
19
  import type { AiConversationTimelineEntry, AiStoredMessage, AiTurnError } from "../types";
@@ -257,7 +258,27 @@ const storedItems = (
257
258
  actionDisplay: "menu",
258
259
  anchorId: item.entry.seq,
259
260
  };
260
- return [userItem, ...unansweredNotice(item.entry, itemIndex)];
261
+ // A removed model: the chat says once which model answers from now on, right before the message it took over.
262
+ const replacement = item.entry.meta?.modelReplacement;
263
+ const replacementItem: ChatTimelineItem[] = replacement
264
+ ? [
265
+ {
266
+ kind: "message",
267
+ id: `${item.id}:model-replaced`,
268
+ role: "system",
269
+ createdAt: item.entry.createdAt,
270
+ content: (
271
+ <p class="text-xs text-dimmed">
272
+ <i class="ti ti-replace mr-1" aria-hidden="true" />
273
+ {aiModelChooserMessages
274
+ .resolve([locale])
275
+ .t.noteRemoved({ removed: replacement.previousLabel, next: replacement.nextLabel })}
276
+ </p>
277
+ ),
278
+ },
279
+ ]
280
+ : [];
281
+ return [...replacementItem, userItem, ...unansweredNotice(item.entry, itemIndex)];
261
282
  }
262
283
 
263
284
  if (item.type === "summary") {
@@ -16,14 +16,15 @@ const usage = (b: AiQuotaBalance): AiChatQuotaBalance => {
16
16
  export async function getAiChatQuotas(subject: AccessSubject): Promise<AiChatQuotaSnapshot> {
17
17
  const config = await aiQuotas.config();
18
18
  if (!config.enabled) return { enabled: false, balances: [] };
19
- const models = await listAssistantAiModels(subject);
20
- const snapshot = await aiQuotas.snapshot(subject);
21
- const profiles = (await readAiSettingsState()).profiles;
19
+ const state = await readAiSettingsState();
20
+ const models = await listAssistantAiModels(subject, undefined, state);
21
+ const snapshot = await aiQuotas.snapshot(subject, undefined, undefined, undefined, { state, config });
22
+ const profiles = state.profiles;
22
23
  return {
23
24
  enabled: snapshot.enabled,
24
25
  unlimitedModels: models
25
26
  .filter((model) => !hasBillableAiPricing(profiles.find((profile) => profile.id === model.id)?.pricing))
26
27
  .map((model) => model.id),
27
- balances: snapshot.balances.filter((b) => b.scope === "*" || models.some((m) => m.id === b.scope)).map(usage),
28
+ balances: snapshot.balances.filter((b) => b.scope === "*" || models.some((m) => m.modelId === b.scope)).map(usage),
28
29
  };
29
30
  }
@@ -1394,7 +1394,7 @@ export const aiChatTasks = {
1394
1394
 
1395
1395
  deliverOccurrence: async (input: {
1396
1396
  occurrenceId: string;
1397
- modelProfileId: string;
1397
+ modelId: string;
1398
1398
  runConfig: AiChatTurnRunConfig;
1399
1399
  userMessage: Message;
1400
1400
  expectedRevision: number;
@@ -1464,7 +1464,7 @@ export const aiChatTasks = {
1464
1464
  "idx_ai_turns_conversation_short_id",
1465
1465
  (attempt, shortId) => attempt<{ id: string }[]>`
1466
1466
  INSERT INTO ai.turns (short_id, conversation_id, model_profile_id, status, run_config)
1467
- VALUES (${shortId}, ${occurrence.conversation_id}::uuid, ${input.modelProfileId}, 'queued', (${JSON.stringify(runConfig)}::text)::jsonb)
1467
+ VALUES (${shortId}, ${occurrence.conversation_id}::uuid, ${input.modelId}, 'queued', (${JSON.stringify(runConfig)}::text)::jsonb)
1468
1468
  RETURNING id
1469
1469
  `,
1470
1470
  );
@@ -2,6 +2,7 @@ import { type Accessor, createMemo, createSignal, onCleanup } from "solid-js";
2
2
  import { createStore, reconcile } from "solid-js/store";
3
3
  import { z } from "zod";
4
4
  import { CapabilityActionReceiptSchema } from "../../contracts/capabilities";
5
+ import type { AiModelRequest } from "../../shared/ai-models";
5
6
  import { type AiActionCheck, preferredActionCheck } from "../action-review";
6
7
  import { type AiAttachmentRef, aiAttachmentMarker } from "../attachments";
7
8
  import { aiChatMessages } from "../chat/messages";
@@ -857,7 +858,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
857
858
  };
858
859
 
859
860
  const queuedAttempts = new Map<string, { key: string; files?: File[]; id: string }>();
860
- const queueMessage = (input: ComposerDraftInput & { modelProfileId?: string }): Promise<boolean> => {
861
+ const queueMessage = (input: ComposerDraftInput & { model?: AiModelRequest }): Promise<boolean> => {
861
862
  if (!input.conversationId || !isComposerDraftSendable(input)) return Promise.resolve(false);
862
863
  const conversationId = input.conversationId;
863
864
  const key = JSON.stringify({
@@ -866,7 +867,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
866
867
  draftContent: input.draftContent,
867
868
  resources: input.resources,
868
869
  storedFiles: input.storedFiles,
869
- model: input.modelProfileId,
870
+ model: input.model,
870
871
  });
871
872
  const previous = queuedAttempts.get(conversationId);
872
873
  if (uncertainSubmissions()[conversationId] !== undefined) {
@@ -886,7 +887,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
886
887
  const body = JSON.stringify({
887
888
  draftRevision: saved.revision,
888
889
  queueId,
889
- modelProfileId: input.modelProfileId,
890
+ model: input.model,
890
891
  clientToolIds: options.clientToolIds?.filter((id) => options.frontendTools?.[id]),
891
892
  });
892
893
  queuedAttempts.set(conversationId, { key, files: input.files, id: queueId });
@@ -914,7 +915,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
914
915
  });
915
916
  };
916
917
 
917
- const send = (input: ComposerDraftInput & { modelProfileId?: string }): Promise<boolean> => {
918
+ const send = (input: ComposerDraftInput & { model?: AiModelRequest }): Promise<boolean> => {
918
919
  if (!isComposerDraftSendable(input)) return Promise.resolve(false);
919
920
  const targetConversation = input.conversationId ? Promise.resolve(input.conversationId) : ensureConversation();
920
921
  return queueDraftOperation(async () => {
@@ -945,7 +946,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
945
946
  kind: "message",
946
947
  message: { role: "user", content: optimisticContent },
947
948
  loopId: null,
948
- modelProfileId: null,
949
+ modelId: null,
949
950
  providerModel: null,
950
951
  usage: null,
951
952
  stopReason: null,
@@ -993,7 +994,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
993
994
  const submissionBody = JSON.stringify({
994
995
  draftRevision: savedDraft.revision,
995
996
  clientToolIds: options.clientToolIds?.filter((id) => options.frontendTools?.[id]),
996
- modelProfileId: input.modelProfileId,
997
+ model: input.model,
997
998
  });
998
999
  submissionBodies.set(conversationId, submissionBody);
999
1000
  const result = await request<SubmitTurnResult>(
@@ -1024,7 +1025,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
1024
1025
  seq: 0,
1025
1026
  status: "running",
1026
1027
  blocks: [],
1027
- modelProfileId: result.turn.modelProfileId,
1028
+ modelId: result.turn.modelId,
1028
1029
  }),
1029
1030
  );
1030
1031
  if (state.conversation) {
@@ -1244,7 +1245,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
1244
1245
  return requestPromise;
1245
1246
  };
1246
1247
 
1247
- const compactConversation = async (input: { modelProfileId?: string } = {}) => {
1248
+ const compactConversation = async (input: { model?: AiModelRequest } = {}) => {
1248
1249
  const conversationId = activeConversationId();
1249
1250
  if (!conversationId) {
1250
1251
  setGlobalError("Open a chat before compacting context.");
@@ -1272,7 +1273,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
1272
1273
  input: {
1273
1274
  content?: AiUserContentPart[];
1274
1275
  mode?: "retry" | "details" | "concise";
1275
- modelProfileId?: string;
1276
+ model?: AiModelRequest;
1276
1277
  } = {},
1277
1278
  ) => {
1278
1279
  const conversationId = activeConversationId();
@@ -1292,7 +1293,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
1292
1293
  body: JSON.stringify({
1293
1294
  mode: input.mode ?? "retry",
1294
1295
  content: input.content,
1295
- modelProfileId: input.modelProfileId,
1296
+ model: input.model,
1296
1297
  }),
1297
1298
  },
1298
1299
  "AI retry failed",
@@ -8,7 +8,7 @@ export type AiActiveTurn = {
8
8
  seq: number;
9
9
  status: "running" | "waiting_for_action";
10
10
  blocks: AiTurnBlock[];
11
- modelProfileId: string | null;
11
+ modelId: string | null;
12
12
  /**
13
13
  * A model call failed transiently and waits for its retry. Live only: the next turn event ends it, and so does a
14
14
  * snapshot that has moved past the retry.
@@ -124,7 +124,7 @@ export const activeTurnFromSnapshot = (snapshot: AiTurnSnapshot | null): AiActiv
124
124
  seq: snapshot.seq,
125
125
  status: snapshot.status === "waiting_for_action" ? "waiting_for_action" : "running",
126
126
  blocks: normalizeCustomApprovalBlocks(snapshot.blocks),
127
- modelProfileId: snapshot.modelProfileId,
127
+ modelId: snapshot.modelId,
128
128
  createdAt: snapshot.createdAt,
129
129
  ...(snapshot.actionWaitMs !== undefined ? { actionWaitMs: snapshot.actionWaitMs } : {}),
130
130
  ...(snapshot.waitingSince !== undefined ? { waitingSince: snapshot.waitingSince } : {}),
@@ -191,7 +191,7 @@ export const reduceWireEvent = (state: AiChatProjection, event: AiWireEvent): Ai
191
191
  seq: event.seq,
192
192
  status: deriveStatus(blocks),
193
193
  blocks,
194
- modelProfileId: event.modelProfileId,
194
+ modelId: event.modelId,
195
195
  ...turnTiming(active?.turnId === event.turnId ? active : null),
196
196
  },
197
197
  };
@@ -53,6 +53,7 @@ export function createCodeCapabilityRoutes(
53
53
  request: c.req.raw,
54
54
  authority: turnAuthority(c),
55
55
  origin: "assistant",
56
+ assistantTurn: { conversationId: c.req.param("conversationId")!, turnId: c.req.param("turnId")! },
56
57
  continuation: codeCapabilityOperation(c.req.param("conversationId")!, c.req.param("turnId")!),
57
58
  mandate: config.mandate
58
59
  ? { mandateId: config.mandate.id, mandateRevision: config.mandate.revision, ownerAppId: "core" }
@@ -120,7 +120,7 @@ const buildCompactionPrompt = (additionalInstructions: string, source: string) =
120
120
  export const createCloudCompactFn = (input: {
121
121
  conversationId: string;
122
122
  turnId?: string;
123
- modelProfileId: string;
123
+ modelId: string;
124
124
  additionalInstructions: string;
125
125
  maxOutputTokens?: number;
126
126
  signal: AbortSignal;
@@ -157,7 +157,7 @@ export const createCloudCompactFn = (input: {
157
157
  task: "chat-compaction",
158
158
  attribution: { conversationId: input.conversationId, turnId: input.turnId },
159
159
  appId: "core",
160
- modelProfileId: input.modelProfileId,
160
+ modelId: input.modelId,
161
161
  providerModel: ctx.provider.model,
162
162
  };
163
163
  let usage: Usage | undefined;
@@ -203,7 +203,7 @@ export const createCloudCompactFn = (input: {
203
203
  conversationId: input.conversationId,
204
204
  turnId: input.turnId,
205
205
  checkpointSeq: checkpoint.seq,
206
- modelProfileId: input.modelProfileId,
206
+ modelId: input.modelId,
207
207
  summary: {
208
208
  role: "assistant",
209
209
  content: [{ type: "text", text: `Conversation summary:\n${summaryText}` }],