@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
@@ -32,19 +32,32 @@ import type {
32
32
 
33
33
  export const DEFAULT_MAX_LOOP_ITEMS = 100;
34
34
 
35
- const EXACT_EXPRESSION = /^\$\{\{\s*([^{}]+?)\s*\}\}$/;
35
+ // Callers trim the captured text; no `\s*` around the group keeps matching linear.
36
+ const EXACT_EXPRESSION = /^\$\{\{([^{}]+)\}\}$/;
36
37
 
37
38
  class RuntimeVariableScope implements WorkflowVariableScope {
38
39
  readonly #values = new Map<string, WorkflowJsonValue>();
40
+ readonly #unknown = new Set<string>();
39
41
 
40
42
  constructor(
41
43
  initial: Record<string, WorkflowJsonValue> = {},
42
44
  readonly parent?: RuntimeVariableScope,
45
+ public dependsOnUnknown: boolean = parent?.dependsOnUnknown ?? false,
43
46
  ) {
44
47
  for (const [name, value] of Object.entries(initial)) this.#values.set(name, value);
45
48
  }
46
49
 
50
+ markUnknown(name: string): void {
51
+ this.#values.delete(name);
52
+ this.#unknown.add(name);
53
+ }
54
+
55
+ isUnknown(name: string): boolean {
56
+ return this.#unknown.has(name) || (!this.#values.has(name) && this.parent?.isUnknown(name) === true);
57
+ }
58
+
47
59
  get(name: string): WorkflowJsonValue | undefined {
60
+ if (this.#unknown.has(name)) return undefined;
48
61
  return this.#values.has(name) ? this.#values.get(name) : this.parent?.get(name);
49
62
  }
50
63
 
@@ -53,18 +66,18 @@ class RuntimeVariableScope implements WorkflowVariableScope {
53
66
  }
54
67
 
55
68
  set(name: string, value: WorkflowJsonValue): void {
69
+ this.#unknown.delete(name);
56
70
  this.#values.set(name, value);
57
71
  }
58
72
 
59
73
  snapshot(): Record<string, WorkflowJsonValue> {
60
- return {
61
- ...(this.parent?.snapshot() ?? {}),
62
- ...Object.fromEntries(this.#values),
63
- };
74
+ const values = { ...(this.parent?.snapshot() ?? {}), ...Object.fromEntries(this.#values) };
75
+ for (const name of this.#unknown) delete values[name];
76
+ return values;
64
77
  }
65
78
 
66
- child(initial: Record<string, WorkflowJsonValue> = {}): RuntimeVariableScope {
67
- return new RuntimeVariableScope(initial, this);
79
+ child(initial: Record<string, WorkflowJsonValue> = {}, unknown = this.dependsOnUnknown): RuntimeVariableScope {
80
+ return new RuntimeVariableScope(initial, this, unknown);
68
81
  }
69
82
  }
70
83
 
@@ -93,11 +106,18 @@ export class WorkflowRetryableStepError extends Error {
93
106
  }
94
107
  }
95
108
 
96
- class WorkflowValueError extends Error {}
109
+ export class WorkflowValueError extends Error {
110
+ constructor(
111
+ message: string,
112
+ readonly code = "WORKFLOW_VALUE_UNAVAILABLE",
113
+ ) {
114
+ super(message);
115
+ }
116
+ }
97
117
 
98
- class WorkflowValueWaiting extends Error {
118
+ export class WorkflowValueWaiting extends Error {
99
119
  constructor(
100
- reference: string,
120
+ readonly reference: string,
101
121
  readonly dependency: WorkflowDependency,
102
122
  ) {
103
123
  super(`workflow reference "${reference}" is waiting`);
@@ -189,7 +209,10 @@ const resolveLocalReference = (state: RuntimeState, scope: RuntimeVariableScope,
189
209
  if (rootName === "inputs") root = state.options.invocation.inputs;
190
210
  else if (rootName === "bindings") root = state.options.plan.bindings;
191
211
  else if (rootName === "context") root = state.options.invocation.context ?? {};
192
- else root = scope.get(rootName);
212
+ else {
213
+ if (scope.isUnknown(rootName)) return { state: "waiting", dependency: { kind: "workflow.dry-run-value", key: rootName } };
214
+ root = scope.get(rootName);
215
+ }
193
216
  const value = root === undefined || segments.length === 0 ? root : readWorkflowValuePath(root, segments);
194
217
  return value === undefined ? { state: "missing" } : { state: "resolved", value };
195
218
  };
@@ -201,7 +224,8 @@ const resolveReference = async (
201
224
  path: Array<string | number> = [],
202
225
  ): Promise<WorkflowValueResolution> => {
203
226
  const local = () => resolveLocalReference(state, scope, reference);
204
- if (!state.options.values) return local();
227
+ const localValue = local();
228
+ if (localValue.state === "waiting" || !state.options.values) return localValue;
205
229
  const resolved = await state.options.values.resolve({
206
230
  reference,
207
231
  path,
@@ -297,12 +321,43 @@ const evaluateCondition = async (
297
321
  if (waiting) throw waiting;
298
322
  return false;
299
323
  }
300
- if (condition.operator === "not") return !(await evaluateCondition(state, scope, condition.condition, [...path, "not"]));
324
+ if (condition.operator === "not") {
325
+ const matched = !(await evaluateCondition(state, scope, condition.condition, [...path, "not"]));
326
+ if (state.options.mode === "dryRun") await emit(state, { type: "condition.evaluated", run: state.run, path, matched });
327
+ return matched;
328
+ }
301
329
  if (condition.operator === "exists") {
302
330
  const resolved = await resolveReference(state, scope, condition.reference, [...path, "exists"]);
303
331
  if (resolved.state === "waiting") throw new WorkflowValueWaiting(condition.reference, resolved.dependency);
304
332
  return resolved.state === "resolved" && resolved.value !== null && resolved.value !== "";
305
333
  }
334
+ if (
335
+ condition.operator === "greaterThan" ||
336
+ condition.operator === "lessThan" ||
337
+ condition.operator === "atLeast" ||
338
+ condition.operator === "atMost"
339
+ ) {
340
+ const numbers: number[] = [];
341
+ for (const [index, operand] of condition.operands.entries()) {
342
+ const expression = typeof operand === "string" ? EXACT_EXPRESSION.exec(operand)?.[1]?.trim() : undefined;
343
+ let value: WorkflowJsonValue;
344
+ if (expression && expression !== "now()") {
345
+ const resolved = await resolveReference(state, scope, expression, [...path, condition.operator, index]);
346
+ if (resolved.state === "waiting") throw new WorkflowValueWaiting(expression, resolved.dependency);
347
+ if (resolved.state === "missing") return false;
348
+ value = resolved.value;
349
+ } else {
350
+ value = await evaluateValue(state, scope, operand, [...path, condition.operator, index]);
351
+ }
352
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
353
+ numbers.push(value);
354
+ }
355
+ const [left, right] = numbers;
356
+ if (condition.operator === "greaterThan") return left! > right!;
357
+ if (condition.operator === "lessThan") return left! < right!;
358
+ if (condition.operator === "atLeast") return left! >= right!;
359
+ return left! <= right!;
360
+ }
306
361
  const left = await evaluateValue(state, scope, condition.operands[0], [...path, condition.operator, 0]);
307
362
  const right = await evaluateValue(state, scope, condition.operands[1], [...path, condition.operator, 1]);
308
363
  if (condition.operator === "equals") return jsonEqual(left, right);
@@ -363,7 +418,7 @@ const actionContext = (
363
418
  };
364
419
  return state.options.mode === "execute"
365
420
  ? ({ ...common, mode: "execute" } as WorkflowExecuteActionContext)
366
- : ({ ...common, mode: "dryRun" } as WorkflowDryRunActionContext);
421
+ : ({ ...common, mode: "dryRun", dependsOnUnknown: scope.dependsOnUnknown } as WorkflowDryRunActionContext);
367
422
  };
368
423
 
369
424
  const invalidRestoration = (state: RuntimeState, step: WorkflowRuntimeStepIdentity, message: string): Flow =>
@@ -449,11 +504,18 @@ const restoreCompletedIfDescendants = async (
449
504
  irStep: Extract<WorkflowIrStep, { kind: "if" }>,
450
505
  iterationPath: number[],
451
506
  persistedTraversal: unknown,
507
+ unknown: boolean,
452
508
  ): Promise<Flow> => {
453
509
  const traversal = persistedIfTraversal(persistedTraversal);
454
510
  for (const branch of traversal.branches) {
455
- const flow = await restoreCompletedSteps(state, scope.child(), branch === "then" ? irStep.then : irStep.else, iterationPath);
456
- if (flow.state !== "continue") return flow;
511
+ const flow = await restoreCompletedSteps(
512
+ state,
513
+ scope.child({}, scope.dependsOnUnknown || unknown),
514
+ branch === "then" ? irStep.then : irStep.else,
515
+ iterationPath,
516
+ );
517
+ if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
518
+ else if (flow.state !== "continue") return flow;
457
519
  }
458
520
  return { state: "continue" };
459
521
  };
@@ -464,13 +526,15 @@ const restoreCompletedSwitchDescendants = async (
464
526
  irStep: Extract<WorkflowIrStep, { kind: "switch" }>,
465
527
  iterationPath: number[],
466
528
  persistedTraversal: unknown,
529
+ unknown: boolean,
467
530
  ): Promise<Flow> => {
468
531
  const traversal = persistedSwitchTraversal(irStep, persistedTraversal);
469
532
  for (const branch of traversal.branches) {
470
533
  const steps = branch === "default" ? irStep.default : irStep.cases[branch]?.steps;
471
534
  if (!steps) throw new WorkflowValueError(`completed switch step references unavailable branch "${branch}"`);
472
- const flow = await restoreCompletedSteps(state, scope.child(), steps, iterationPath);
473
- if (flow.state !== "continue") return flow;
535
+ const flow = await restoreCompletedSteps(state, scope.child({}, scope.dependsOnUnknown || unknown), steps, iterationPath);
536
+ if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
537
+ else if (flow.state !== "continue") return flow;
474
538
  }
475
539
  return { state: "continue" };
476
540
  };
@@ -481,6 +545,7 @@ const restoreCompletedForEachDescendants = async (
481
545
  irStep: Extract<WorkflowIrStep, { kind: "forEach" }>,
482
546
  iterationPath: number[],
483
547
  persistedTraversal: unknown,
548
+ unknown: boolean,
484
549
  ): Promise<Flow> => {
485
550
  const traversal = persistedForEachTraversal(persistedTraversal);
486
551
  const limit = loopItemLimit(state);
@@ -488,11 +553,11 @@ const restoreCompletedForEachDescendants = async (
488
553
  throw new WorkflowValueError(`completed forEach traversal has ${traversal.items.length} items; limit is ${limit}`);
489
554
  }
490
555
  for (let index = 0; index < traversal.items.length; index += 1) {
491
- const flow = await restoreCompletedSteps(state, scope.child({ [irStep.alias]: traversal.items[index]! }), irStep.steps, [
492
- ...iterationPath,
493
- index,
494
- ]);
495
- if (flow.state !== "continue") return flow;
556
+ const child = scope.child({ [irStep.alias]: traversal.items[index]! }, scope.dependsOnUnknown || unknown);
557
+ if (unknown) child.markUnknown(irStep.alias);
558
+ const flow = await restoreCompletedSteps(state, child, irStep.steps, [...iterationPath, index]);
559
+ if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
560
+ else if (flow.state !== "continue") return flow;
496
561
  }
497
562
  return { state: "continue" };
498
563
  };
@@ -503,13 +568,14 @@ const restoreCompletedControlDescendants = (
503
568
  irStep: Exclude<WorkflowIrStep, { kind: "action" }>,
504
569
  iterationPath: number[],
505
570
  persistedTraversal: WorkflowControlTraversal | undefined,
571
+ unknown = false,
506
572
  ): Promise<Flow> => {
507
573
  if (!persistedTraversal) {
508
574
  throw new WorkflowValueError(`completed ${irStep.kind} step is missing its persisted control traversal`);
509
575
  }
510
- if (irStep.kind === "if") return restoreCompletedIfDescendants(state, scope, irStep, iterationPath, persistedTraversal);
511
- if (irStep.kind === "switch") return restoreCompletedSwitchDescendants(state, scope, irStep, iterationPath, persistedTraversal);
512
- return restoreCompletedForEachDescendants(state, scope, irStep, iterationPath, persistedTraversal);
576
+ if (irStep.kind === "if") return restoreCompletedIfDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
577
+ if (irStep.kind === "switch") return restoreCompletedSwitchDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
578
+ return restoreCompletedForEachDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
513
579
  };
514
580
 
515
581
  const restoreCompletedState = async (
@@ -524,7 +590,14 @@ const restoreCompletedState = async (
524
590
  return restoreCompletedControlDescendants(state, scope, irStep, step.iterationPath, restored.outcome.control);
525
591
  }
526
592
  if (restored.mode === "dryRun" && restored.outcome.state === "planned") {
527
- return restoreCompletedControlDescendants(state, scope, irStep, step.iterationPath, restored.outcome.control);
593
+ return restoreCompletedControlDescendants(
594
+ state,
595
+ scope,
596
+ irStep,
597
+ step.iterationPath,
598
+ restored.outcome.control,
599
+ restored.outcome.issues?.some((issue) => issue.step.key === step.key) ?? false,
600
+ );
528
601
  }
529
602
  return { state: "continue" };
530
603
  }
@@ -539,6 +612,16 @@ const restoreCompletedState = async (
539
612
  return invalidRestoration(state, step, `planner for action "${irStep.action}" is unavailable while restoring step "${step.key}"`);
540
613
  await handler.restoreCompleted?.(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep, restored.outcome);
541
614
  }
615
+ if (restored.mode === "dryRun") {
616
+ const savedName = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
617
+ if (
618
+ typeof savedName === "string" &&
619
+ (restored.outcome.state === "indeterminate" ||
620
+ restored.outcome.state === "unsupported" ||
621
+ (restored.outcome.state === "planned" && restored.outcome.output === undefined && restored.outcome.issues?.length))
622
+ )
623
+ scope.markUnknown(savedName);
624
+ }
542
625
  return { state: "continue" };
543
626
  };
544
627
 
@@ -639,6 +722,15 @@ const evaluateAction = async (
639
722
  const outcome: WorkflowPlanningOutcome = handler
640
723
  ? await handler.plan(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep)
641
724
  : { state: "unsupported", reason: `action "${irStep.action}" has no dry-run handler` };
725
+ const savedName = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
726
+ if (
727
+ typeof savedName === "string" &&
728
+ (outcome.state === "indeterminate" ||
729
+ outcome.state === "unsupported" ||
730
+ (outcome.state === "planned" && outcome.output === undefined && outcome.issues?.length))
731
+ ) {
732
+ scope.markUnknown(savedName);
733
+ }
642
734
  if (outcome.state === "planned" || outcome.state === "terminal") {
643
735
  state.effects.push(...outcome.effects);
644
736
  state.issues.push(...restoredIssues(state, outcome.issues));
@@ -729,8 +821,9 @@ const analyzeUnknownBranches = async (
729
821
  const branches =
730
822
  irStep.kind === "if" ? [irStep.then, irStep.else] : [...irStep.cases.map((candidate) => candidate.steps), irStep.default];
731
823
  for (const branch of branches) {
732
- const flow = await runSteps(state, branch, scope.child(), iterationPath);
824
+ const flow = await runSteps(state, branch, scope.child({}, true), iterationPath);
733
825
  if (flow.state === "canceled") return flow;
826
+ if (flow.state === "terminal_planned") scope.dependsOnUnknown = true;
734
827
  }
735
828
  return { state: "continue" };
736
829
  };
@@ -786,10 +879,13 @@ const evaluateControl = async (
786
879
  if (state.options.mode !== "dryRun") throw new WorkflowValueError(reason);
787
880
  state.issues.push({ state: "indeterminate", reason, step: stepIdentity(state, irStep, iterationPath) });
788
881
  control = { kind: "forEach", items: [null] };
789
- flow = await runSteps(state, irStep.steps, scope.child({ [irStep.alias]: null }), [...iterationPath, 0]);
882
+ const unknownScope = scope.child({}, true);
883
+ unknownScope.markUnknown(irStep.alias);
884
+ flow = await runSteps(state, irStep.steps, unknownScope, [...iterationPath, 0]);
790
885
  if (flow.state === "canceled") {
791
886
  return { flow, result: resultForHaltedControl(state, flow, effectsStart, issuesStart) };
792
887
  }
888
+ if (flow.state === "terminal_planned") scope.dependsOnUnknown = true;
793
889
  flow = { state: "continue" };
794
890
  return { flow, result: resultForCompletedControl(state, undefined, control, effectsStart, issuesStart) };
795
891
  }
@@ -842,6 +938,10 @@ const evaluateStep = async (
842
938
  };
843
939
  }
844
940
  if (error instanceof WorkflowValueWaiting) {
941
+ if (state.options.mode === "dryRun" && irStep.kind === "action") {
942
+ const name = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
943
+ if (typeof name === "string") scope.markUnknown(name);
944
+ }
845
945
  if (state.options.mode === "execute") {
846
946
  const outcome: WorkflowStepOutcome = { state: "waiting", dependency: error.dependency };
847
947
  return { flow: { state: "waiting", dependency: error.dependency, step }, result: { mode: "execute", outcome } };
@@ -850,16 +950,16 @@ const evaluateStep = async (
850
950
  return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
851
951
  }
852
952
  if (state.options.mode === "dryRun") {
953
+ if (irStep.kind === "action") {
954
+ const name = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
955
+ if (typeof name === "string") scope.markUnknown(name);
956
+ }
853
957
  const outcome: WorkflowPlanningOutcome = { state: "indeterminate", reason: errorMessage(error) };
854
958
  return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
855
959
  }
856
960
  const outcome: WorkflowStepOutcome = {
857
961
  state: "failed",
858
- error: executionError(
859
- error instanceof WorkflowValueError ? "WORKFLOW_VALUE_UNAVAILABLE" : "WORKFLOW_ACTION_ERROR",
860
- errorMessage(error),
861
- false,
862
- ),
962
+ error: executionError(error instanceof WorkflowValueError ? error.code : "WORKFLOW_ACTION_ERROR", errorMessage(error), false),
863
963
  };
864
964
  return { flow: { state: "failed", error: outcome.error, step }, result: { mode: "execute", outcome } };
865
965
  }
@@ -14,6 +14,8 @@ export {
14
14
  executeWorkflowPlan,
15
15
  WorkflowLeaseLostError,
16
16
  WorkflowRetryableStepError,
17
+ WorkflowValueError,
18
+ WorkflowValueWaiting,
17
19
  } from "./executor";
18
20
  export type {
19
21
  WorkflowActionStep,
@@ -78,7 +78,10 @@ type WorkflowActionContextBase<Mode extends WorkflowInvocationMode> = {
78
78
  };
79
79
 
80
80
  export type WorkflowExecuteActionContext = WorkflowActionContextBase<"execute">;
81
- export type WorkflowDryRunActionContext = WorkflowActionContextBase<"dryRun">;
81
+ export type WorkflowDryRunActionContext = WorkflowActionContextBase<"dryRun"> & {
82
+ /** The containing branch cannot be selected without an unavailable value. */
83
+ dependsOnUnknown?: boolean;
84
+ };
82
85
 
83
86
  export type WorkflowExecuteActionHandler = {
84
87
  execute(context: WorkflowExecuteActionContext, step: WorkflowActionStep): Promise<WorkflowStepOutcome>;
@@ -120,6 +123,7 @@ export interface WorkflowDryRunActionPort {
120
123
  * carried a copy of it would be one more thing to keep in step.
121
124
  */
122
125
  export type WorkflowTraceEvent =
126
+ | { type: "condition.evaluated"; run: WorkflowRuntimeRunIdentity; path: Array<string | number>; matched: boolean }
123
127
  | { type: "run.started"; run: WorkflowRuntimeRunIdentity }
124
128
  | { type: "step.started"; step: WorkflowRuntimeStepIdentity }
125
129
  | { type: "step.restored"; step: WorkflowRuntimeStepIdentity; restored: WorkflowRestoredStep }
@@ -434,7 +434,7 @@ export const createWorkflowDryRunPort = (module: DefinedWorkflowModule): Workflo
434
434
  applySaveAs(ctx, step, (planned.output ?? undefined) as WorkflowJsonValue | undefined);
435
435
  return {
436
436
  state: "planned" as const,
437
- output: (planned.output ?? null) as WorkflowJsonValue,
437
+ ...(planned.outputUnavailable ? {} : { output: planned.output ?? null }),
438
438
  effects: [{ action: name, summary: planned.summary, ...(consumes ? { consumes } : {}) } as WorkflowJsonValue],
439
439
  // An issue names the step it belongs to, so the dry-run view can point
440
440
  // at what could not be determined rather than listing loose strings.
@@ -145,6 +145,11 @@ export const deleteWorkflowScope = async (scope: { appId: string; scopeId: strin
145
145
  DELETE FROM workflows.workflow WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId} RETURNING id
146
146
  `;
147
147
  await tx`DELETE FROM workflows.event WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId}`;
148
+ const [ai] = await tx<{ present: boolean }[]>`SELECT to_regclass('ai.workflow_scope_budget') IS NOT NULL AS present`;
149
+ if (ai?.present) {
150
+ await tx`DELETE FROM ai.workflow_scope_budget WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId}`;
151
+ await tx`DELETE FROM ai.workflow_scope_admission WHERE app_id = ${scope.appId} AND scope_id = ${scope.scopeId}`;
152
+ }
148
153
  return rows.length;
149
154
  });
150
155
  };
@@ -24,7 +24,7 @@ import type {
24
24
  WorkflowCoordinatorPort,
25
25
  WorkflowCoordinatorReleaseState,
26
26
  } from "../runtime/coordinator";
27
- import { WorkflowLeaseLostError } from "../runtime/executor";
27
+ import { WorkflowLeaseLostError, WorkflowRetryableStepError } from "../runtime/executor";
28
28
  import type {
29
29
  WorkflowHeartbeatOutcome,
30
30
  WorkflowRestoredStep,
@@ -401,7 +401,7 @@ export const finishWorkflowRun = async (
401
401
  */
402
402
  export const releaseWorkflowRun = async (
403
403
  claim: { runId: string; executionGeneration: number; consecutiveFailures?: number },
404
- options: { backoffMs?: number; db?: SQL } = {},
404
+ options: { backoffMs?: number; db?: SQL; error?: WorkflowJsonValue } = {},
405
405
  ): Promise<WorkflowCoordinatorReleaseState> => {
406
406
  const db = options.db ?? sql;
407
407
  const backoffMs =
@@ -413,6 +413,7 @@ export const releaseWorkflowRun = async (
413
413
  lease_expires_at = NULL,
414
414
  retry_after = now() + ${`${backoffMs} milliseconds`}::interval,
415
415
  consecutive_failures = consecutive_failures + 1,
416
+ error = ${options.error ?? null},
416
417
  updated_at = now()
417
418
  WHERE id = ${claim.runId}
418
419
  AND execution_generation = ${claim.executionGeneration}
@@ -545,7 +546,8 @@ export const createWorkflowCoordinatorPort = (options: {
545
546
  claim: () => claimWorkflowRun(options),
546
547
  renew: (claim) => renewWorkflowRunLease(claim, { leaseMs: options.leaseMs }),
547
548
  finish: (claim, result) => finishWorkflowRun(claim, result),
548
- release: (claim) => releaseWorkflowRun(claim),
549
+ release: (claim, error) =>
550
+ releaseWorkflowRun(claim, { ...(error instanceof WorkflowRetryableStepError ? { error: error.executionError } : {}) }),
549
551
  });
550
552
 
551
553
  // ─── Scans ───────────────────────────────────────────────────────────────────
@@ -184,10 +184,15 @@ export const runOneWorkflow = async (options: WorkflowWorkerOptions & { runId?:
184
184
  const incompatible = await validateCurrentModule(claim, options.module);
185
185
  if (incompatible) return incompatible;
186
186
  if (claim.consecutiveFailures >= WORKFLOW_RUN_MAX_CONSECUTIVE_FAILURES) {
187
+ const [prior] = await db<{ code: string | null; message: string | null }[]>`
188
+ SELECT error ->> 'code' AS code, error ->> 'message' AS message FROM workflows.run WHERE id = ${claim.runId}::uuid
189
+ `;
190
+ const aiCode = prior?.code?.startsWith("WORKFLOW_AI_") ? prior.code : null;
187
191
  const error = {
188
- code: "WORKFLOW_RETRY_EXHAUSTED",
189
- message: `workflow failed ${claim.consecutiveFailures} consecutive attempts`,
192
+ code: aiCode ?? "WORKFLOW_RETRY_EXHAUSTED",
193
+ message: `${aiCode && prior?.message ? `${prior.message} ` : ""}Workflow failed ${claim.consecutiveFailures} consecutive attempts. Inspect the source or retry after capacity recovers.`,
190
194
  retryable: false,
195
+ ...(aiCode ? { details: { retryExhausted: true } } : {}),
191
196
  };
192
197
  await db`
193
198
  UPDATE workflows.step_outcome AS s
@@ -1,43 +0,0 @@
1
- import { sql } from "bun";
2
- import { coreSettings } from "../services";
3
- import { audit } from "../services/audit";
4
- import { invalidateSettingsCache, set as setSetting } from "../services/settings";
5
- import { decryptValue } from "../services/settings/crypto";
6
- import { type AiModelPricing, AiModelPricingSchema } from "../shared/ai-costs";
7
- import { AiQuotaError } from "./quotas";
8
- import { parseAiModelProfiles } from "./settings";
9
-
10
- /** Change prices only, preserving credentials and model access in their own stores. */
11
- export async function setAiModelPricing(id: string, pricing: AiModelPricing | null, expected: AiModelPricing | null, actorId: string) {
12
- if (pricing) AiModelPricingSchema.parse(pricing);
13
- const key = "ai.model_profiles_json";
14
- const fallback = await coreSettings.get<string>(key);
15
- const result = await sql.begin(async (db) => {
16
- const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key=${key} FOR UPDATE`;
17
- const raw = row ? await decryptValue(row.value) : fallback;
18
- if (typeof raw !== "string") throw new Error("Invalid model configuration.");
19
- const parsed = parseAiModelProfiles(raw);
20
- if (parsed.error) throw new Error(parsed.error.message);
21
- const profile = parsed.profiles.find((profile) => profile.id === id);
22
- if (!profile) throw new AiQuotaError("quota_conflict", "Model no longer exists.");
23
- if (profile.capabilities.includes("transcription")) throw new AiQuotaError("quota_conflict", "Audio pricing is not supported.");
24
- if (JSON.stringify(profile.pricing ?? null) !== JSON.stringify(expected))
25
- throw new AiQuotaError("quota_conflict", "Model prices changed. Read them again before updating.");
26
- if (pricing) profile.pricing = pricing;
27
- else delete profile.pricing;
28
- await setSetting(key, JSON.stringify(parsed.profiles), db);
29
- await audit.record(
30
- {
31
- action: "ai.model.pricing",
32
- outcome: "allowed",
33
- actor: { userId: actorId },
34
- target: { type: "ai-model", id },
35
- metadata: { pricing },
36
- },
37
- db,
38
- );
39
- return { id, pricing: profile.pricing ?? null };
40
- });
41
- await invalidateSettingsCache([key]);
42
- return result;
43
- }
@@ -1,98 +0,0 @@
1
- import { sql } from "bun";
2
- import { coreSettings } from "../services";
3
- import { audit } from "../services/audit";
4
- import { invalidateSettingsCache, set as setSetting } from "../services/settings";
5
- import { decryptValue } from "../services/settings/crypto";
6
- import {
7
- type AiModelRequestSettings,
8
- type AiModelRequestSettingsUpdate,
9
- AiModelRequestSettingsUpdateSchema,
10
- } from "../shared/ai-model-request-settings";
11
- import { AiQuotaError } from "./quotas";
12
- import { getAiRequestHeaders, planAiProfileRequestHeaders, storeAiRequestHeaderPlan } from "./request-headers";
13
- import { parseAiModelProfiles } from "./settings";
14
- import type { AiModelProfile } from "./types";
15
-
16
- const key = "ai.model_profiles_json";
17
- export class AiModelRequestSettingsInvalid extends Error {}
18
-
19
- const readSnapshot = async (id: string, fallback: string, db: typeof sql, lock = false) => {
20
- const rows = lock
21
- ? await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key=${key} FOR UPDATE`
22
- : await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key=${key}`;
23
- const raw = rows[0] ? await decryptValue(rows[0].value) : fallback;
24
- if (typeof raw !== "string") throw new AiModelRequestSettingsInvalid("Invalid model configuration.");
25
- const parsed = parseAiModelProfiles(raw);
26
- if (parsed.error) throw new AiModelRequestSettingsInvalid(parsed.error.fields?.[key] ?? parsed.error.message);
27
- const profile = parsed.profiles.find((profile) => profile.id === id);
28
- if (!profile) throw new AiQuotaError("quota_conflict", "Model no longer exists.");
29
- const [headerRow] = await db<{ secret: string }[]>`SELECT secret FROM ai.model_request_headers WHERE profile_id=${id}`;
30
- // Hash ciphertext, never secret values. Any settings/header change invalidates
31
- // the snapshot, including replacement of a value under an unchanged name.
32
- const revision = new Bun.CryptoHasher("sha256").update(JSON.stringify([rows[0]?.value ?? raw, headerRow?.secret ?? null])).digest("hex");
33
- const requestHeaderNames = Object.keys(await getAiRequestHeaders(id, db)).sort();
34
- return { profiles: parsed.profiles, profile, settings: present(profile, requestHeaderNames, revision) };
35
- };
36
- const present = (profile: AiModelProfile, requestHeaderNames: string[], revision: string): AiModelRequestSettings => ({
37
- id: profile.id,
38
- reasoningEffort: profile.reasoningEffort ?? null,
39
- extraBody: profile.extraBody ?? null,
40
- requestHeaderNames,
41
- revision,
42
- });
43
-
44
- export const getAiModelRequestSettings = async (id: string): Promise<AiModelRequestSettings> =>
45
- (await readSnapshot(id, await coreSettings.get<string>(key), sql)).settings;
46
-
47
- /** One transaction changes the non-secret profile and encrypted headers; audit contains names only. */
48
- export const setAiModelRequestSettings = async (
49
- id: string,
50
- input: AiModelRequestSettingsUpdate,
51
- actorId: string,
52
- ): Promise<AiModelRequestSettings> => {
53
- const data = AiModelRequestSettingsUpdateSchema.parse(input);
54
- const fallback = await coreSettings.get<string>(key);
55
- const result = await sql.begin(async (db) => {
56
- const current = await readSnapshot(id, fallback, db, true);
57
- if (current.settings.revision !== data.expected)
58
- throw new AiQuotaError("quota_conflict", "Model request settings changed. Read them again before updating.");
59
- const nextProfile = { ...current.profile };
60
- if (Object.hasOwn(data, "reasoningEffort")) {
61
- if (data.reasoningEffort) nextProfile.reasoningEffort = data.reasoningEffort;
62
- else delete nextProfile.reasoningEffort;
63
- }
64
- if (Object.hasOwn(data, "extraBody")) {
65
- if (data.extraBody) nextProfile.extraBody = data.extraBody;
66
- else delete nextProfile.extraBody;
67
- }
68
- const validated = parseAiModelProfiles(JSON.stringify([nextProfile]));
69
- if (validated.error) throw new AiModelRequestSettingsInvalid(validated.error.fields?.[key] ?? validated.error.message);
70
- const headers = data.clearRequestHeaders ? {} : await getAiRequestHeaders(id, db);
71
- const plan = planAiProfileRequestHeaders({
72
- currentProfiles: [current.profile],
73
- nextProfiles: [nextProfile],
74
- existingNames: { [id]: Object.keys(headers) },
75
- submitted: data.requestHeaders === undefined ? [] : [{ profileId: id, patch: data.requestHeaders }],
76
- });
77
- if (plan.error) throw new AiModelRequestSettingsInvalid(plan.error);
78
- // This endpoint owns one profile only. Never prune the other profiles.
79
- const keepOthers = current.profiles.filter((profile) => profile.id !== id).map((profile) => profile.id);
80
- plan.keepHeaderProfileIds = [...keepOthers, ...(data.clearRequestHeaders ? [] : plan.keepHeaderProfileIds)];
81
- await storeAiRequestHeaderPlan(plan, db);
82
- await setSetting(key, JSON.stringify(current.profiles.map((profile) => (profile.id === id ? validated.profiles[0] : profile))), db);
83
- const saved = (await readSnapshot(id, fallback, db)).settings;
84
- await audit.record(
85
- {
86
- action: "ai.model.request-settings",
87
- outcome: "allowed",
88
- actor: { userId: actorId },
89
- target: { type: "ai-model", id },
90
- metadata: { reasoningEffort: saved.reasoningEffort, extraBody: saved.extraBody, requestHeaderNames: saved.requestHeaderNames },
91
- },
92
- db,
93
- );
94
- return saved;
95
- });
96
- await invalidateSettingsCache([key]);
97
- return result;
98
- };