@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
package/src/ai/store.ts CHANGED
@@ -279,6 +279,8 @@ type TurnSteerRow = {
279
279
  };
280
280
 
281
281
  type ConversationResourceRefRow = {
282
+ context: string | null;
283
+ status: string | null;
282
284
  resource_type: string;
283
285
  resource_id: string;
284
286
  title: string | null;
@@ -298,6 +300,8 @@ type ConversationResourceOccurrenceRow = ConversationResourceRefRow & {
298
300
  };
299
301
 
300
302
  type ConversationSourceRow = {
303
+ context: string | null;
304
+ status: string | null;
301
305
  source_kind: AiConversationSource["kind"];
302
306
  source_key: string;
303
307
  title: string;
@@ -340,6 +344,7 @@ type InterChatMessageRow = {
340
344
  };
341
345
 
342
346
  type TimelineRow = {
347
+ model_replacement: { kind: "model_replaced"; previousLabel: string; nextLabel: string } | null;
343
348
  id: string;
344
349
  seq: number;
345
350
  loop_id: string | null;
@@ -356,6 +361,8 @@ const iso = (value: Date | string): string => (value instanceof Date ? value.toI
356
361
 
357
362
  const rowToConversationResourceRef = (row: ConversationResourceRefRow): AiConversationResourceRef => ({
358
363
  ref: { type: row.resource_type, id: row.resource_id },
364
+ context: row.context,
365
+ status: row.status,
359
366
  title: row.title,
360
367
  preview: row.preview,
361
368
  icon: row.icon,
@@ -397,6 +404,8 @@ const rowToConversationResourceOccurrence = (row: ConversationResourceOccurrence
397
404
 
398
405
  const rowToConversationSource = (row: ConversationSourceRow): AiConversationSource => ({
399
406
  kind: row.source_kind,
407
+ context: row.context,
408
+ status: row.status,
400
409
  key: row.source_key,
401
410
  title: row.title,
402
411
  preview: row.preview,
@@ -649,7 +658,7 @@ const rowToEnrichmentRun = (row: EnrichmentRunRow): AiEnrichmentRun => ({
649
658
  conversationId: row.conversation_id,
650
659
  status: row.status,
651
660
  trigger: row.trigger,
652
- modelProfileId: row.model_profile_id,
661
+ modelId: row.model_profile_id,
653
662
  mode: row.mode,
654
663
  durationMs: row.duration_ms === null ? null : Number(row.duration_ms),
655
664
  titleUpdated: row.title_updated,
@@ -689,7 +698,7 @@ const rowToMessage = (row: MessageRow): AiStoredMessage => {
689
698
  kind: row.kind,
690
699
  message,
691
700
  loopId: row.loop_id,
692
- modelProfileId: row.model_profile_id,
701
+ modelId: row.model_profile_id,
693
702
  providerModel: row.provider_model,
694
703
  usage: row.usage ? parseJsonValue<Usage>(row.usage) : null,
695
704
  stopReason: row.stop_reason,
@@ -719,7 +728,7 @@ const rowToTurn = (row: TurnRow): AiTurn => ({
719
728
  conversationId: row.conversation_id,
720
729
  status: row.status,
721
730
  attempt: Number(row.attempt ?? 0),
722
- modelProfileId: row.model_profile_id,
731
+ modelId: row.model_profile_id,
723
732
  createdAt: iso(row.created_at),
724
733
  completedAt: row.completed_at ? iso(row.completed_at) : null,
725
734
  error: row.error,
@@ -1140,7 +1149,7 @@ const insertMessageLocked = async (
1140
1149
  kind?: "message" | "summary";
1141
1150
  seq?: number;
1142
1151
  loopId?: string | null;
1143
- modelProfileId?: string | null;
1152
+ modelId?: string | null;
1144
1153
  meta?: AiStoredMessage["meta"];
1145
1154
  },
1146
1155
  db: typeof sql = sql,
@@ -1181,7 +1190,7 @@ const insertMessageLocked = async (
1181
1190
  ${JSON.stringify(input.message)}::text::jsonb,
1182
1191
  ${messageSearchText(input.message)},
1183
1192
  ${input.loopId ?? null},
1184
- ${input.modelProfileId ?? null},
1193
+ ${input.modelId ?? null},
1185
1194
  ${providerModel},
1186
1195
  ${usage ? JSON.stringify(usage) : null}::text::jsonb,
1187
1196
  ${stopReason},
@@ -1217,7 +1226,7 @@ const appendTurnOwnedMessage = async (input: {
1217
1226
  kind?: "message" | "summary";
1218
1227
  seq?: number;
1219
1228
  loopId: string | null;
1220
- modelProfileId?: string | null;
1229
+ modelId?: string | null;
1221
1230
  meta?: AiStoredMessage["meta"];
1222
1231
  }): Promise<AiStoredMessage | null> => {
1223
1232
  return sql.begin(async (tx) => {
@@ -1252,7 +1261,7 @@ const appendTurnOwnedMessage = async (input: {
1252
1261
  ${input.message.role},
1253
1262
  ${JSON.stringify(input.message)}::text::jsonb,
1254
1263
  ${input.loopId},
1255
- ${input.modelProfileId ?? null},
1264
+ ${input.modelId ?? null},
1256
1265
  ${providerModel},
1257
1266
  ${usage ? JSON.stringify(usage) : null}::text::jsonb,
1258
1267
  ${stopReason},
@@ -1777,15 +1786,17 @@ export const aiConversations: AiConversationService = {
1777
1786
  for (const resource of resources) {
1778
1787
  await tx`
1779
1788
  INSERT INTO ai.conversation_resource_refs (
1780
- conversation_id, resource_type, resource_id, title, preview, icon, href, source_turn_id, source_call_id
1789
+ conversation_id, resource_type, resource_id, title, preview, icon, href, context, status, source_turn_id, source_call_id
1781
1790
  ) VALUES (
1782
1791
  ${input.conversationId}, ${resource.ref.type}, ${resource.ref.id}, ${resource.title ?? null}, ${resource.preview ?? null},
1783
- ${resource.icon ?? null}, ${resource.href ?? null}, ${input.turnId ?? null}, ${input.callId ?? null}
1792
+ ${resource.icon ?? null}, ${resource.href ?? null}, ${resource.context ?? null}, ${resource.status ?? null}, ${input.turnId ?? null}, ${input.callId ?? null}
1784
1793
  )
1785
1794
  ON CONFLICT (conversation_id, resource_type, resource_id)
1786
1795
  DO UPDATE SET
1787
1796
  title = COALESCE(EXCLUDED.title, ai.conversation_resource_refs.title),
1788
1797
  preview = COALESCE(EXCLUDED.preview, ai.conversation_resource_refs.preview),
1798
+ context = COALESCE(EXCLUDED.context, ai.conversation_resource_refs.context),
1799
+ status = COALESCE(EXCLUDED.status, ai.conversation_resource_refs.status),
1789
1800
  icon = COALESCE(EXCLUDED.icon, ai.conversation_resource_refs.icon),
1790
1801
  href = COALESCE(EXCLUDED.href, ai.conversation_resource_refs.href),
1791
1802
  -- Project context is indexed on every turn without a call. It must not move a resource a tool call
@@ -1811,7 +1822,7 @@ export const aiConversations: AiConversationService = {
1811
1822
  const pattern = searchPattern(input.search);
1812
1823
  const cursor = decodeResourceCursor(input.before);
1813
1824
  const rows = await sql<(ConversationResourceRefRow & { cursor_at: string })[]>`
1814
- SELECT indexed.resource_type, indexed.resource_id, indexed.title, indexed.preview, indexed.icon, indexed.href,
1825
+ SELECT indexed.resource_type, indexed.resource_id, indexed.title, indexed.preview, indexed.icon, indexed.href, indexed.context, indexed.status,
1815
1826
  source_turn.short_id AS source_turn_id, indexed.source_call_id, indexed.first_seen_at, indexed.last_seen_at,
1816
1827
  to_char(indexed.last_seen_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS cursor_at
1817
1828
  FROM ai.conversation_resource_refs indexed
@@ -1847,7 +1858,7 @@ export const aiConversations: AiConversationService = {
1847
1858
  const pattern = searchPattern(input.search);
1848
1859
  const cursor = decodeResourceOccurrenceCursor(input.before);
1849
1860
  const rows = await sql<(ConversationResourceOccurrenceRow & { cursor_at: string })[]>`
1850
- SELECT indexed.resource_type, indexed.resource_id, indexed.title, indexed.preview, indexed.icon, indexed.href,
1861
+ SELECT indexed.resource_type, indexed.resource_id, indexed.title, indexed.preview, indexed.icon, indexed.href, indexed.context, indexed.status,
1851
1862
  source_turn.short_id AS source_turn_id, indexed.source_call_id, indexed.first_seen_at, indexed.last_seen_at,
1852
1863
  conversation.short_id AS conversation_short_id, conversation.title AS conversation_title,
1853
1864
  conversation.updated_at AS conversation_updated_at,
@@ -1908,6 +1919,7 @@ export const aiConversations: AiConversationService = {
1908
1919
  SELECT *
1909
1920
  FROM (
1910
1921
  SELECT source.kind AS source_kind, source.source_key, source.title, source.preview,
1922
+ NULL::text AS context, NULL::text AS status,
1911
1923
  source.icon, source.href, file.path, file.media_type, file.size,
1912
1924
  source.resource_type, source.resource_id, source.occurrences,
1913
1925
  turn.short_id AS source_turn_id, source.source_turn_id AS turn_uuid, source.source_call_id,
@@ -1922,6 +1934,7 @@ export const aiConversations: AiConversationService = {
1922
1934
 
1923
1935
  SELECT 'file' AS source_kind, file.path AS source_key,
1924
1936
  regexp_replace(file.path, '^.*/', '') AS title, 'Attached to the conversation' AS preview,
1937
+ NULL::text AS context, NULL::text AS status,
1925
1938
  CASE WHEN file.media_type LIKE 'image/%' THEN 'ti ti-photo' ELSE 'ti ti-file' END AS icon,
1926
1939
  NULL::text AS href, file.path, file.media_type, file.size,
1927
1940
  NULL::text AS resource_type, NULL::text AS resource_id, 1 AS occurrences,
@@ -1934,7 +1947,7 @@ export const aiConversations: AiConversationService = {
1934
1947
 
1935
1948
  SELECT 'resource' AS source_kind, indexed.resource_type || ':' || indexed.resource_id AS source_key,
1936
1949
  COALESCE(indexed.title, indexed.resource_type || ' ' || indexed.resource_id) AS title,
1937
- indexed.preview, indexed.icon, indexed.href, NULL::text AS path, NULL::text AS media_type, NULL::bigint AS size,
1950
+ indexed.preview, indexed.context, indexed.status, indexed.icon, indexed.href, NULL::text AS path, NULL::text AS media_type, NULL::bigint AS size,
1938
1951
  indexed.resource_type, indexed.resource_id, 1 AS occurrences,
1939
1952
  turn.short_id AS source_turn_id, indexed.source_turn_id AS turn_uuid, indexed.source_call_id,
1940
1953
  indexed.first_seen_at, indexed.last_seen_at
@@ -2135,7 +2148,7 @@ export const aiConversations: AiConversationService = {
2135
2148
  "idx_ai_turns_conversation_short_id",
2136
2149
  (attempt, shortId) => attempt<TurnRow[]>`
2137
2150
  INSERT INTO ai.turns (short_id, conversation_id, model_profile_id, status, run_config)
2138
- VALUES (${shortId}, ${message.target_conversation_id}, ${input.modelProfileId}, 'queued', (${JSON.stringify(input.runConfig)}::text)::jsonb)
2151
+ VALUES (${shortId}, ${message.target_conversation_id}, ${input.modelId}, 'queued', (${JSON.stringify(input.runConfig)}::text)::jsonb)
2139
2152
  RETURNING *
2140
2153
  `,
2141
2154
  );
@@ -2434,7 +2447,7 @@ export const aiConversations: AiConversationService = {
2434
2447
  ${input.conversationId},
2435
2448
  ${input.status},
2436
2449
  ${input.trigger},
2437
- ${input.modelProfileId ?? null},
2450
+ ${input.modelId ?? null},
2438
2451
  ${input.mode ?? null},
2439
2452
  ${input.durationMs ?? null},
2440
2453
  ${input.titleUpdated ?? false},
@@ -2691,6 +2704,7 @@ export const aiConversations: AiConversationService = {
2691
2704
  SELECT
2692
2705
  users.short_id AS id,
2693
2706
  users.seq,
2707
+ users.meta_payload->'modelReplacement' AS model_replacement,
2694
2708
  COALESCE(timeline_turn.short_id, users.loop_id) AS loop_id,
2695
2709
  left(
2696
2710
  regexp_replace(
@@ -2722,6 +2736,7 @@ export const aiConversations: AiConversationService = {
2722
2736
  id: row.id,
2723
2737
  seq: Number(row.seq),
2724
2738
  loopId: row.loop_id,
2739
+ ...(row.model_replacement ? { modelReplacement: row.model_replacement } : {}),
2725
2740
  userPreview: row.user_preview?.trim() || "Message",
2726
2741
  assistantPreview: row.assistant_preview?.trim() || "",
2727
2742
  isSteer: Boolean(row.is_steer),
@@ -2939,7 +2954,7 @@ export const aiConversations: AiConversationService = {
2939
2954
  kind: "summary",
2940
2955
  seq: checkpointSeq,
2941
2956
  loopId: null,
2942
- modelProfileId: input.modelProfileId ?? null,
2957
+ modelId: input.modelId ?? null,
2943
2958
  meta: { compactedCount: archived[0]?.count ?? 0, ...(todoPlan ? { todoPlan } : {}) },
2944
2959
  },
2945
2960
  tx,
@@ -2958,7 +2973,7 @@ export const aiConversations: AiConversationService = {
2958
2973
  "idx_ai_turns_conversation_short_id",
2959
2974
  (attempt, shortId) => attempt<TurnRow[]>`
2960
2975
  INSERT INTO ai.turns (short_id, conversation_id, model_profile_id, status, run_config)
2961
- VALUES (${shortId}, ${input.conversationId}, ${input.modelProfileId}, 'queued',
2976
+ VALUES (${shortId}, ${input.conversationId}, ${input.modelId}, 'queued',
2962
2977
  (${input.runConfig ? JSON.stringify(input.runConfig) : null}::text)::jsonb)
2963
2978
  RETURNING *
2964
2979
  `,
@@ -3015,7 +3030,7 @@ export const aiConversations: AiConversationService = {
3015
3030
  VALUES (
3016
3031
  ${shortId},
3017
3032
  ${input.conversationId},
3018
- ${input.modelProfileId},
3033
+ ${input.modelId},
3019
3034
  'queued',
3020
3035
  (${JSON.stringify(input.runConfig)}::text)::jsonb
3021
3036
  )
@@ -3064,7 +3079,12 @@ export const aiConversations: AiConversationService = {
3064
3079
  conversationId: input.conversationId,
3065
3080
  message: input.userMessage,
3066
3081
  loopId: turn.id,
3067
- meta: input.expectedDraftRevision === undefined ? undefined : { submittedDraftRevision: input.expectedDraftRevision },
3082
+ meta: {
3083
+ ...(input.expectedDraftRevision === undefined ? {} : { submittedDraftRevision: input.expectedDraftRevision }),
3084
+ modelRequest: input.runConfig.model,
3085
+ modelResolved: input.runConfig.modelResolved,
3086
+ ...(input.runConfig.modelReplacement ? { modelReplacement: input.runConfig.modelReplacement } : {}),
3087
+ },
3068
3088
  },
3069
3089
  tx,
3070
3090
  );
@@ -3074,15 +3094,17 @@ export const aiConversations: AiConversationService = {
3074
3094
  for (const resource of resources) {
3075
3095
  await tx`
3076
3096
  INSERT INTO ai.conversation_resource_refs (
3077
- conversation_id, resource_type, resource_id, title, preview, icon, href, source_turn_id
3097
+ conversation_id, resource_type, resource_id, title, preview, icon, href, context, status, source_turn_id
3078
3098
  ) VALUES (
3079
3099
  ${input.conversationId}, ${resource.ref.type}, ${resource.ref.id}, ${resource.title ?? null},
3080
- ${resource.preview ?? null}, ${resource.icon ?? null}, ${resource.href ?? null}, ${turn.id}
3100
+ ${resource.preview ?? null}, ${resource.icon ?? null}, ${resource.href ?? null}, ${resource.context ?? null}, ${resource.status ?? null}, ${turn.id}
3081
3101
  )
3082
3102
  ON CONFLICT (conversation_id, resource_type, resource_id)
3083
3103
  DO UPDATE SET
3084
3104
  title = COALESCE(EXCLUDED.title, ai.conversation_resource_refs.title),
3085
3105
  preview = COALESCE(EXCLUDED.preview, ai.conversation_resource_refs.preview),
3106
+ context = COALESCE(EXCLUDED.context, ai.conversation_resource_refs.context),
3107
+ status = COALESCE(EXCLUDED.status, ai.conversation_resource_refs.status),
3086
3108
  icon = COALESCE(EXCLUDED.icon, ai.conversation_resource_refs.icon),
3087
3109
  href = COALESCE(EXCLUDED.href, ai.conversation_resource_refs.href),
3088
3110
  source_turn_id = EXCLUDED.source_turn_id,
@@ -3120,7 +3142,7 @@ export const aiConversations: AiConversationService = {
3120
3142
  getLatestTurn: async (input) => {
3121
3143
  const [row] = await sql<
3122
3144
  TurnRow[]
3123
- >`SELECT * FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid AND NOT COALESCE(run_config ? 'background', false)
3145
+ >`SELECT * FROM ai.turns WHERE conversation_id = ${input.conversationId}::uuid AND NOT COALESCE(run_config ? 'background', false) AND (${input.kind ?? null}::text IS NULL OR run_config->>'kind'=${input.kind ?? null})
3124
3146
  ORDER BY created_at DESC, id DESC LIMIT 1`;
3125
3147
  return row ? rowToTurn(row) : null;
3126
3148
  },
@@ -3978,7 +4000,7 @@ export const aiConversations: AiConversationService = {
3978
4000
  kind: opts?.kind,
3979
4001
  seq: opts?.seq,
3980
4002
  loopId: opts?.kind === "summary" && !input.background ? null : input.turnId,
3981
- modelProfileId: input.modelProfileId,
4003
+ modelId: input.modelId,
3982
4004
  meta,
3983
4005
  });
3984
4006
  if (!appended) {
@@ -3997,7 +4019,7 @@ export const aiConversations: AiConversationService = {
3997
4019
  kind: opts?.kind,
3998
4020
  seq: opts?.seq,
3999
4021
  loopId: input.turnId && opts?.kind !== "summary" ? input.turnId : null,
4000
- modelProfileId: input.modelProfileId,
4022
+ modelId: input.modelId,
4001
4023
  meta,
4002
4024
  },
4003
4025
  tx,
package/src/ai/stream.ts CHANGED
@@ -134,7 +134,7 @@ const turnSnapshotFromActive = (active: NonNullable<Awaited<ReturnType<typeof ai
134
134
  status: active.turn.status,
135
135
  seq: active.liveSeq,
136
136
  blocks: [...active.liveBlocks],
137
- modelProfileId: active.turn.modelProfileId,
137
+ modelId: active.turn.modelId,
138
138
  createdAt: active.turn.createdAt,
139
139
  actionWaitMs: active.actionWaitMs,
140
140
  waitingSince: active.waitingSince,
@@ -15,7 +15,7 @@ export type AiStructuredRunRecord = {
15
15
  traceId?: string;
16
16
  task: string;
17
17
  appId?: string;
18
- modelProfileId?: string;
18
+ modelId?: string;
19
19
  providerModel?: string;
20
20
  status: "ok" | "failed";
21
21
  durationMs: number;
@@ -40,7 +40,7 @@ export const recordAiStructuredRun = async (record: AiStructuredRunRecord): Prom
40
40
  input_tokens, output_tokens, total_tokens,
41
41
  mode, repaired, attempts, error_code, error, user_id, conversation_id, turn_id, workflow_run_id, trace_id
42
42
  ) VALUES (
43
- ${record.task}, ${record.appId ?? null}, ${record.modelProfileId ?? null}, ${record.providerModel ?? null},
43
+ ${record.task}, ${record.appId ?? null}, ${record.modelId ?? null}, ${record.providerModel ?? null},
44
44
  ${record.status}, ${Math.max(0, Math.round(record.durationMs))},
45
45
  ${usageNumber(record.usage, "input")}, ${usageNumber(record.usage, "output")},
46
46
  ${usageNumber(record.usage, "total")},
@@ -2,9 +2,9 @@ import type { Input, LoopAggregate, StructuredMeta, Usage } from "@k2b/nessi";
2
2
  import { nessi, StructuredOutputError } from "@k2b/nessi";
3
3
  import type { z } from "zod";
4
4
  import type { AccessSubject } from "../server";
5
- import { coreSettings } from "../services";
6
5
  import type { TraceContext } from "../services/logging";
7
6
  import { trace } from "../services/logging";
7
+ import type { AiModelRequest, AiResolvedModelRef } from "../shared/ai-models";
8
8
  import { taskReasoningEffort } from "./provider";
9
9
  import { AiProviderError } from "./provider-errors";
10
10
  import { inferenceProvider } from "./quota-provider";
@@ -12,40 +12,10 @@ import { resolveAiModel } from "./settings";
12
12
  import { type AiUsageAttribution, safelyRecordStructuredRun } from "./structured-runs";
13
13
  import type { AiResolvedModel } from "./types";
14
14
 
15
- export const AI_BACKGROUND_MODEL_SETTING_KEY = "ai.background_model_id";
16
- export const AI_WORKFLOW_MODEL_SETTING_KEY = "ai.workflow_model_id";
17
-
18
- export const selectAiWorkflowModelId = (input: {
19
- requestedModelId?: string;
20
- workflowModelId?: string;
21
- backgroundModelId?: string;
22
- }): string | undefined => input.requestedModelId?.trim() || input.workflowModelId?.trim() || input.backgroundModelId?.trim() || undefined;
23
-
24
- /**
25
- * Resolve the model for background inference: explicit request →
26
- * `ai.background_model_id` setting → platform default. Throws when AI is
27
- * disabled or the model is unavailable — callers skip their work then.
28
- */
29
- export const resolveAiBackgroundModel = async (requestedModelId?: string): Promise<AiResolvedModel> => {
30
- const backgroundModelId = String((await coreSettings.get<string>(AI_BACKGROUND_MODEL_SETTING_KEY)) ?? "").trim();
31
- return resolveAiModel({ kind: "selectable" }, requestedModelId?.trim() || backgroundModelId || undefined);
32
- };
33
-
34
- /** Resolve a workflow model: action override → workflow setting → background setting → platform default. */
35
- export const resolveAiWorkflowModel = async (requestedModelId?: string): Promise<AiResolvedModel> => {
36
- const [workflowModelId, backgroundModelId] = await Promise.all([
37
- coreSettings.get<string>(AI_WORKFLOW_MODEL_SETTING_KEY),
38
- coreSettings.get<string>(AI_BACKGROUND_MODEL_SETTING_KEY),
39
- ]);
40
- return resolveAiModel(
41
- { kind: "selectable" },
42
- selectAiWorkflowModelId({
43
- requestedModelId,
44
- workflowModelId: String(workflowModelId ?? ""),
45
- backgroundModelId: String(backgroundModelId ?? ""),
46
- }),
47
- );
48
- };
15
+ export const resolveAiBackgroundModel = (model: AiModelRequest = { class: "fast" }): Promise<AiResolvedModel> =>
16
+ resolveAiModel({ kind: "selectable" }, model);
17
+ export const resolveAiWorkflowModel = (model: AiModelRequest = { class: "balanced" }): Promise<AiResolvedModel> =>
18
+ resolveAiModel({ kind: "selectable" }, model);
49
19
 
50
20
  export type RunAiStructuredInput<TOutput extends z.ZodType> = {
51
21
  /** Short machine name for tracing, e.g. "chat-enrich". */
@@ -54,7 +24,7 @@ export type RunAiStructuredInput<TOutput extends z.ZodType> = {
54
24
  output: TOutput;
55
25
  outputName?: string;
56
26
  systemPrompt?: string;
57
- requestedModelId?: string;
27
+ model?: AiModelRequest;
58
28
  temperature?: number;
59
29
  maxOutputTokens?: number;
60
30
  signal?: AbortSignal;
@@ -66,12 +36,12 @@ export type RunAiStructuredInput<TOutput extends z.ZodType> = {
66
36
  /** Metadata-only attribution, supplied by the authorized caller. */
67
37
  attribution?: AiUsageAttribution;
68
38
  /** Model resolution seam — tests inject a fake so they never touch shared settings. */
69
- resolveModel?: (requestedModelId?: string) => Promise<AiResolvedModel>;
39
+ resolveModel?: (model?: AiModelRequest) => Promise<AiResolvedModel>;
70
40
  };
71
41
 
72
42
  export type RunAiStructuredResult<TOutput extends z.ZodType> = {
73
43
  output: z.infer<TOutput>;
74
- modelProfileId: string;
44
+ model: AiResolvedModelRef;
75
45
  usage?: Usage;
76
46
  structuredMeta: StructuredMeta;
77
47
  };
@@ -110,11 +80,17 @@ export const runAiStructured = async <TOutput extends z.ZodType>(
110
80
  const startedAt = Date.now();
111
81
  let resolved: AiResolvedModel | undefined;
112
82
  try {
113
- resolved = await (input.resolveModel ?? resolveAiBackgroundModel)(input.requestedModelId);
83
+ resolved = await (input.resolveModel ?? resolveAiBackgroundModel)(input.model ?? { class: "balanced" });
114
84
  await trace.record({
115
85
  context: span,
116
86
  event: "model.resolved",
117
- attributes: { model: resolved.profile.id, providerModel: resolved.profile.model, provider: resolved.profile.provider },
87
+ attributes: {
88
+ model: resolved.profile.id,
89
+ providerModel: resolved.profile.model,
90
+ provider: resolved.profile.provider,
91
+ requestedClass: resolved.model.requestedClass,
92
+ resolvedClass: resolved.model.class,
93
+ },
118
94
  });
119
95
 
120
96
  const result = await nessi.structured({
@@ -123,6 +99,7 @@ export const runAiStructured = async <TOutput extends z.ZodType>(
123
99
  kind: input.usageSubject ? "chat" : "background",
124
100
  subject: input.usageSubject,
125
101
  task: input.task,
102
+ modelRef: resolved.model,
126
103
  appId: input.appId,
127
104
  traceId: span.traceId,
128
105
  ...input.attribution,
@@ -156,7 +133,7 @@ export const runAiStructured = async <TOutput extends z.ZodType>(
156
133
  attribution: input.attribution,
157
134
  traceId: span.traceId,
158
135
  appId: input.appId,
159
- modelProfileId: resolved.profile.id,
136
+ modelId: resolved.profile.id,
160
137
  providerModel: resolved.profile.model,
161
138
  status: "ok",
162
139
  durationMs,
@@ -168,7 +145,7 @@ export const runAiStructured = async <TOutput extends z.ZodType>(
168
145
 
169
146
  return {
170
147
  output: result.output,
171
- modelProfileId: resolved.profile.id,
148
+ model: resolved.model,
172
149
  usage: result.usage,
173
150
  structuredMeta: result.structuredMeta,
174
151
  };
@@ -183,7 +160,7 @@ export const runAiStructured = async <TOutput extends z.ZodType>(
183
160
  attribution: input.attribution,
184
161
  traceId: span.traceId,
185
162
  appId: input.appId,
186
- modelProfileId: resolved?.profile.id,
163
+ modelId: resolved?.profile.id,
187
164
  providerModel: resolved?.profile.model,
188
165
  status: "failed",
189
166
  durationMs: Date.now() - startedAt,
@@ -201,7 +178,7 @@ export const runAiStructured = async <TOutput extends z.ZodType>(
201
178
  }
202
179
  },
203
180
  {
204
- summarize: (result) => ({ model: result.modelProfileId, mode: result.structuredMeta.mode, repaired: result.structuredMeta.repaired }),
181
+ summarize: (result) => ({ model: result.model.id, mode: result.structuredMeta.mode, repaired: result.structuredMeta.repaired }),
205
182
  onError: (error) => (error instanceof StructuredOutputError ? structuredFailureSummary(error) : undefined),
206
183
  },
207
184
  );
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
+ import { AiModelRequestSchema } from "../shared/ai-models";
2
3
 
3
4
  const prompt = z.string().trim().min(1).max(20_000);
4
- const modelProfileId = z.string().trim().min(1).max(120).optional();
5
+ const model = AiModelRequestSchema.optional();
5
6
  const choice = z.string().trim().min(1).max(200);
6
7
  const choices = z
7
8
  .array(choice)
@@ -20,6 +21,7 @@ const structuredField = z
20
21
  required: z.boolean().default(true),
21
22
  choices: z.array(choice).min(1).max(50).optional(),
22
23
  maxLength: z.number().int().min(1).max(20_000).optional(),
24
+ minLength: z.number().int().min(1).max(20_000).optional(),
23
25
  })
24
26
  .strict()
25
27
  .superRefine((field, ctx) => {
@@ -27,6 +29,8 @@ const structuredField = z
27
29
  ctx.addIssue({ code: "custom", path: ["choices"], message: "Enum fields require choices." });
28
30
  if (field.type !== "enum" && field.choices)
29
31
  ctx.addIssue({ code: "custom", path: ["choices"], message: "Choices are only valid for enum fields." });
32
+ if (field.minLength !== undefined && (field.type !== "text" || field.minLength > (field.maxLength ?? 20_000)))
33
+ ctx.addIssue({ code: "custom", path: ["minLength"], message: "minLength requires a text field and cannot exceed maxLength." });
30
34
  if (field.type !== "text" && field.maxLength)
31
35
  ctx.addIssue({ code: "custom", path: ["maxLength"], message: "maxLength is only valid for text fields." });
32
36
  });
@@ -42,7 +46,7 @@ export const AiTaskRequestSchema = z.discriminatedUnion("kind", [
42
46
  kind: z.literal("generate_text"),
43
47
  prompt,
44
48
  input: z.unknown().optional(),
45
- modelProfileId,
49
+ model,
46
50
  maxOutputChars: z.number().int().min(1).max(20_000).default(4_000),
47
51
  }),
48
52
  z.object({
@@ -50,7 +54,7 @@ export const AiTaskRequestSchema = z.discriminatedUnion("kind", [
50
54
  prompt,
51
55
  input: z.unknown(),
52
56
  choices,
53
- modelProfileId,
57
+ model,
54
58
  }),
55
59
  z
56
60
  .object({
@@ -60,7 +64,7 @@ export const AiTaskRequestSchema = z.discriminatedUnion("kind", [
60
64
  choices,
61
65
  minChoices: z.number().int().min(0).max(50).default(0),
62
66
  maxChoices: z.number().int().min(0).max(50).optional(),
63
- modelProfileId,
67
+ model,
64
68
  })
65
69
  .superRefine((value, ctx) => {
66
70
  const maximum = value.maxChoices ?? value.choices.length;
@@ -76,9 +80,25 @@ export const AiTaskRequestSchema = z.discriminatedUnion("kind", [
76
80
  prompt,
77
81
  input: z.unknown(),
78
82
  fields: structuredFields,
79
- modelProfileId,
83
+ model,
80
84
  }),
81
85
  ]);
82
86
 
83
87
  export type AiTaskRequest = z.infer<typeof AiTaskRequestSchema>;
84
88
  export type AiTaskRequestInput = z.input<typeof AiTaskRequestSchema>;
89
+
90
+ /** Existing durable task storage contract; measured in UTF-8 bytes, not characters. */
91
+ export const AI_TASK_REQUEST_MAX_BYTES = 100_000;
92
+ export class AiTaskInputTooLargeError extends Error {
93
+ readonly code = "WORKFLOW_AI_INPUT_TOO_LARGE";
94
+ readonly retryable = false;
95
+ constructor() {
96
+ super(
97
+ "AI input exceeds the 100,000-byte task budget. Narrow the supplied content or inspect the original source manually; nothing was truncated.",
98
+ );
99
+ }
100
+ }
101
+
102
+ export const assertAiTaskRequestSize = (request: unknown): void => {
103
+ if (Buffer.byteLength(JSON.stringify(request), "utf8") > AI_TASK_REQUEST_MAX_BYTES) throw new AiTaskInputTooLargeError();
104
+ };
@@ -11,7 +11,13 @@ const structuredOutputSchema = (fields: Extract<AiTaskRequest, { kind: "extract_
11
11
  for (const field of fields) {
12
12
  let schema: z.ZodType =
13
13
  field.type === "text"
14
- ? z.string().max(field.maxLength ?? 20_000)
14
+ ? field.minLength === undefined
15
+ ? z.string().max(field.maxLength ?? 20_000)
16
+ : z
17
+ .string()
18
+ .trim()
19
+ .min(field.minLength)
20
+ .max(field.maxLength ?? 20_000)
15
21
  : field.type === "number"
16
22
  ? z.number()
17
23
  : field.type === "boolean"
@@ -28,13 +34,16 @@ const structuredOutputSchema = (fields: Extract<AiTaskRequest, { kind: "extract_
28
34
  /** Schema-validated AI calculations shared by workflows and interactive code. */
29
35
  export const executeAiTask = async (
30
36
  request: AiTaskRequest,
31
- options: Pick<
32
- RunAiStructuredInput<z.ZodType>,
33
- "appId" | "attribution" | "signal" | "requestedModelId" | "resolveModel" | "usageSubject"
34
- > & { taskPrefix: string },
37
+ options: Pick<RunAiStructuredInput<z.ZodType>, "appId" | "attribution" | "signal" | "model" | "resolveModel" | "usageSubject"> & {
38
+ taskPrefix: string;
39
+ },
35
40
  runStructured: typeof runAiStructured = runAiStructured,
36
41
  ) => {
37
- const { taskPrefix, ...common } = options;
42
+ const { taskPrefix, ...provided } = options;
43
+ const common = {
44
+ ...provided,
45
+ model: provided.model ?? request.model ?? ({ class: "balanced" } satisfies import("../shared/ai-models").AiModelRequest),
46
+ };
38
47
  if (request.kind === "generate_text") {
39
48
  const result = await runStructured({
40
49
  ...common,
@@ -0,0 +1,9 @@
1
+ import type { AccessSubject } from "../server/services/access";
2
+ import { selectAssistantAiModel } from "./assistant-models";
3
+ import { personalAiModelPolicy } from "./personal-agent";
4
+ import { resolveAiModel } from "./settings";
5
+ import { resolveAiWorkflowModel } from "./structured";
6
+
7
+ /** Interactive application tasks retain Assistant grants and data-boundary policy. */
8
+ export const resolveApplicationAiTaskModel = async (subject?: AccessSubject, model?: import("../shared/ai-models").AiModelRequest) =>
9
+ subject ? resolveAiModel(personalAiModelPolicy, await selectAssistantAiModel(subject, model)) : resolveAiWorkflowModel(model);