@k2b/cloud 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -36,15 +36,27 @@ const EXACT_EXPRESSION = /^\$\{\{\s*([^{}]+?)\s*\}\}$/;
|
|
|
36
36
|
|
|
37
37
|
class RuntimeVariableScope implements WorkflowVariableScope {
|
|
38
38
|
readonly #values = new Map<string, WorkflowJsonValue>();
|
|
39
|
+
readonly #unknown = new Set<string>();
|
|
39
40
|
|
|
40
41
|
constructor(
|
|
41
42
|
initial: Record<string, WorkflowJsonValue> = {},
|
|
42
43
|
readonly parent?: RuntimeVariableScope,
|
|
44
|
+
public dependsOnUnknown: boolean = parent?.dependsOnUnknown ?? false,
|
|
43
45
|
) {
|
|
44
46
|
for (const [name, value] of Object.entries(initial)) this.#values.set(name, value);
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
markUnknown(name: string): void {
|
|
50
|
+
this.#values.delete(name);
|
|
51
|
+
this.#unknown.add(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isUnknown(name: string): boolean {
|
|
55
|
+
return this.#unknown.has(name) || (!this.#values.has(name) && this.parent?.isUnknown(name) === true);
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
get(name: string): WorkflowJsonValue | undefined {
|
|
59
|
+
if (this.#unknown.has(name)) return undefined;
|
|
48
60
|
return this.#values.has(name) ? this.#values.get(name) : this.parent?.get(name);
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -53,18 +65,18 @@ class RuntimeVariableScope implements WorkflowVariableScope {
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
set(name: string, value: WorkflowJsonValue): void {
|
|
68
|
+
this.#unknown.delete(name);
|
|
56
69
|
this.#values.set(name, value);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
snapshot(): Record<string, WorkflowJsonValue> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
73
|
+
const values = { ...(this.parent?.snapshot() ?? {}), ...Object.fromEntries(this.#values) };
|
|
74
|
+
for (const name of this.#unknown) delete values[name];
|
|
75
|
+
return values;
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
child(initial: Record<string, WorkflowJsonValue> = {}): RuntimeVariableScope {
|
|
67
|
-
return new RuntimeVariableScope(initial, this);
|
|
78
|
+
child(initial: Record<string, WorkflowJsonValue> = {}, unknown = this.dependsOnUnknown): RuntimeVariableScope {
|
|
79
|
+
return new RuntimeVariableScope(initial, this, unknown);
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
@@ -93,11 +105,18 @@ export class WorkflowRetryableStepError extends Error {
|
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
107
|
|
|
96
|
-
class WorkflowValueError extends Error {
|
|
108
|
+
export class WorkflowValueError extends Error {
|
|
109
|
+
constructor(
|
|
110
|
+
message: string,
|
|
111
|
+
readonly code = "WORKFLOW_VALUE_UNAVAILABLE",
|
|
112
|
+
) {
|
|
113
|
+
super(message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
97
116
|
|
|
98
|
-
class WorkflowValueWaiting extends Error {
|
|
117
|
+
export class WorkflowValueWaiting extends Error {
|
|
99
118
|
constructor(
|
|
100
|
-
reference: string,
|
|
119
|
+
readonly reference: string,
|
|
101
120
|
readonly dependency: WorkflowDependency,
|
|
102
121
|
) {
|
|
103
122
|
super(`workflow reference "${reference}" is waiting`);
|
|
@@ -189,7 +208,10 @@ const resolveLocalReference = (state: RuntimeState, scope: RuntimeVariableScope,
|
|
|
189
208
|
if (rootName === "inputs") root = state.options.invocation.inputs;
|
|
190
209
|
else if (rootName === "bindings") root = state.options.plan.bindings;
|
|
191
210
|
else if (rootName === "context") root = state.options.invocation.context ?? {};
|
|
192
|
-
else
|
|
211
|
+
else {
|
|
212
|
+
if (scope.isUnknown(rootName)) return { state: "waiting", dependency: { kind: "workflow.dry-run-value", key: rootName } };
|
|
213
|
+
root = scope.get(rootName);
|
|
214
|
+
}
|
|
193
215
|
const value = root === undefined || segments.length === 0 ? root : readWorkflowValuePath(root, segments);
|
|
194
216
|
return value === undefined ? { state: "missing" } : { state: "resolved", value };
|
|
195
217
|
};
|
|
@@ -201,7 +223,8 @@ const resolveReference = async (
|
|
|
201
223
|
path: Array<string | number> = [],
|
|
202
224
|
): Promise<WorkflowValueResolution> => {
|
|
203
225
|
const local = () => resolveLocalReference(state, scope, reference);
|
|
204
|
-
|
|
226
|
+
const localValue = local();
|
|
227
|
+
if (localValue.state === "waiting" || !state.options.values) return localValue;
|
|
205
228
|
const resolved = await state.options.values.resolve({
|
|
206
229
|
reference,
|
|
207
230
|
path,
|
|
@@ -297,12 +320,43 @@ const evaluateCondition = async (
|
|
|
297
320
|
if (waiting) throw waiting;
|
|
298
321
|
return false;
|
|
299
322
|
}
|
|
300
|
-
if (condition.operator === "not")
|
|
323
|
+
if (condition.operator === "not") {
|
|
324
|
+
const matched = !(await evaluateCondition(state, scope, condition.condition, [...path, "not"]));
|
|
325
|
+
if (state.options.mode === "dryRun") await emit(state, { type: "condition.evaluated", run: state.run, path, matched });
|
|
326
|
+
return matched;
|
|
327
|
+
}
|
|
301
328
|
if (condition.operator === "exists") {
|
|
302
329
|
const resolved = await resolveReference(state, scope, condition.reference, [...path, "exists"]);
|
|
303
330
|
if (resolved.state === "waiting") throw new WorkflowValueWaiting(condition.reference, resolved.dependency);
|
|
304
331
|
return resolved.state === "resolved" && resolved.value !== null && resolved.value !== "";
|
|
305
332
|
}
|
|
333
|
+
if (
|
|
334
|
+
condition.operator === "greaterThan" ||
|
|
335
|
+
condition.operator === "lessThan" ||
|
|
336
|
+
condition.operator === "atLeast" ||
|
|
337
|
+
condition.operator === "atMost"
|
|
338
|
+
) {
|
|
339
|
+
const numbers: number[] = [];
|
|
340
|
+
for (const [index, operand] of condition.operands.entries()) {
|
|
341
|
+
const expression = typeof operand === "string" ? EXACT_EXPRESSION.exec(operand)?.[1]?.trim() : undefined;
|
|
342
|
+
let value: WorkflowJsonValue;
|
|
343
|
+
if (expression && expression !== "now()") {
|
|
344
|
+
const resolved = await resolveReference(state, scope, expression, [...path, condition.operator, index]);
|
|
345
|
+
if (resolved.state === "waiting") throw new WorkflowValueWaiting(expression, resolved.dependency);
|
|
346
|
+
if (resolved.state === "missing") return false;
|
|
347
|
+
value = resolved.value;
|
|
348
|
+
} else {
|
|
349
|
+
value = await evaluateValue(state, scope, operand, [...path, condition.operator, index]);
|
|
350
|
+
}
|
|
351
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
352
|
+
numbers.push(value);
|
|
353
|
+
}
|
|
354
|
+
const [left, right] = numbers;
|
|
355
|
+
if (condition.operator === "greaterThan") return left! > right!;
|
|
356
|
+
if (condition.operator === "lessThan") return left! < right!;
|
|
357
|
+
if (condition.operator === "atLeast") return left! >= right!;
|
|
358
|
+
return left! <= right!;
|
|
359
|
+
}
|
|
306
360
|
const left = await evaluateValue(state, scope, condition.operands[0], [...path, condition.operator, 0]);
|
|
307
361
|
const right = await evaluateValue(state, scope, condition.operands[1], [...path, condition.operator, 1]);
|
|
308
362
|
if (condition.operator === "equals") return jsonEqual(left, right);
|
|
@@ -363,7 +417,7 @@ const actionContext = (
|
|
|
363
417
|
};
|
|
364
418
|
return state.options.mode === "execute"
|
|
365
419
|
? ({ ...common, mode: "execute" } as WorkflowExecuteActionContext)
|
|
366
|
-
: ({ ...common, mode: "dryRun" } as WorkflowDryRunActionContext);
|
|
420
|
+
: ({ ...common, mode: "dryRun", dependsOnUnknown: scope.dependsOnUnknown } as WorkflowDryRunActionContext);
|
|
367
421
|
};
|
|
368
422
|
|
|
369
423
|
const invalidRestoration = (state: RuntimeState, step: WorkflowRuntimeStepIdentity, message: string): Flow =>
|
|
@@ -449,11 +503,18 @@ const restoreCompletedIfDescendants = async (
|
|
|
449
503
|
irStep: Extract<WorkflowIrStep, { kind: "if" }>,
|
|
450
504
|
iterationPath: number[],
|
|
451
505
|
persistedTraversal: unknown,
|
|
506
|
+
unknown: boolean,
|
|
452
507
|
): Promise<Flow> => {
|
|
453
508
|
const traversal = persistedIfTraversal(persistedTraversal);
|
|
454
509
|
for (const branch of traversal.branches) {
|
|
455
|
-
const flow = await restoreCompletedSteps(
|
|
456
|
-
|
|
510
|
+
const flow = await restoreCompletedSteps(
|
|
511
|
+
state,
|
|
512
|
+
scope.child({}, scope.dependsOnUnknown || unknown),
|
|
513
|
+
branch === "then" ? irStep.then : irStep.else,
|
|
514
|
+
iterationPath,
|
|
515
|
+
);
|
|
516
|
+
if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
517
|
+
else if (flow.state !== "continue") return flow;
|
|
457
518
|
}
|
|
458
519
|
return { state: "continue" };
|
|
459
520
|
};
|
|
@@ -464,13 +525,15 @@ const restoreCompletedSwitchDescendants = async (
|
|
|
464
525
|
irStep: Extract<WorkflowIrStep, { kind: "switch" }>,
|
|
465
526
|
iterationPath: number[],
|
|
466
527
|
persistedTraversal: unknown,
|
|
528
|
+
unknown: boolean,
|
|
467
529
|
): Promise<Flow> => {
|
|
468
530
|
const traversal = persistedSwitchTraversal(irStep, persistedTraversal);
|
|
469
531
|
for (const branch of traversal.branches) {
|
|
470
532
|
const steps = branch === "default" ? irStep.default : irStep.cases[branch]?.steps;
|
|
471
533
|
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
|
|
534
|
+
const flow = await restoreCompletedSteps(state, scope.child({}, scope.dependsOnUnknown || unknown), steps, iterationPath);
|
|
535
|
+
if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
536
|
+
else if (flow.state !== "continue") return flow;
|
|
474
537
|
}
|
|
475
538
|
return { state: "continue" };
|
|
476
539
|
};
|
|
@@ -481,6 +544,7 @@ const restoreCompletedForEachDescendants = async (
|
|
|
481
544
|
irStep: Extract<WorkflowIrStep, { kind: "forEach" }>,
|
|
482
545
|
iterationPath: number[],
|
|
483
546
|
persistedTraversal: unknown,
|
|
547
|
+
unknown: boolean,
|
|
484
548
|
): Promise<Flow> => {
|
|
485
549
|
const traversal = persistedForEachTraversal(persistedTraversal);
|
|
486
550
|
const limit = loopItemLimit(state);
|
|
@@ -488,11 +552,11 @@ const restoreCompletedForEachDescendants = async (
|
|
|
488
552
|
throw new WorkflowValueError(`completed forEach traversal has ${traversal.items.length} items; limit is ${limit}`);
|
|
489
553
|
}
|
|
490
554
|
for (let index = 0; index < traversal.items.length; index += 1) {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (flow.state !== "continue") return flow;
|
|
555
|
+
const child = scope.child({ [irStep.alias]: traversal.items[index]! }, scope.dependsOnUnknown || unknown);
|
|
556
|
+
if (unknown) child.markUnknown(irStep.alias);
|
|
557
|
+
const flow = await restoreCompletedSteps(state, child, irStep.steps, [...iterationPath, index]);
|
|
558
|
+
if (unknown && flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
559
|
+
else if (flow.state !== "continue") return flow;
|
|
496
560
|
}
|
|
497
561
|
return { state: "continue" };
|
|
498
562
|
};
|
|
@@ -503,13 +567,14 @@ const restoreCompletedControlDescendants = (
|
|
|
503
567
|
irStep: Exclude<WorkflowIrStep, { kind: "action" }>,
|
|
504
568
|
iterationPath: number[],
|
|
505
569
|
persistedTraversal: WorkflowControlTraversal | undefined,
|
|
570
|
+
unknown = false,
|
|
506
571
|
): Promise<Flow> => {
|
|
507
572
|
if (!persistedTraversal) {
|
|
508
573
|
throw new WorkflowValueError(`completed ${irStep.kind} step is missing its persisted control traversal`);
|
|
509
574
|
}
|
|
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);
|
|
575
|
+
if (irStep.kind === "if") return restoreCompletedIfDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
|
|
576
|
+
if (irStep.kind === "switch") return restoreCompletedSwitchDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
|
|
577
|
+
return restoreCompletedForEachDescendants(state, scope, irStep, iterationPath, persistedTraversal, unknown);
|
|
513
578
|
};
|
|
514
579
|
|
|
515
580
|
const restoreCompletedState = async (
|
|
@@ -524,7 +589,14 @@ const restoreCompletedState = async (
|
|
|
524
589
|
return restoreCompletedControlDescendants(state, scope, irStep, step.iterationPath, restored.outcome.control);
|
|
525
590
|
}
|
|
526
591
|
if (restored.mode === "dryRun" && restored.outcome.state === "planned") {
|
|
527
|
-
return restoreCompletedControlDescendants(
|
|
592
|
+
return restoreCompletedControlDescendants(
|
|
593
|
+
state,
|
|
594
|
+
scope,
|
|
595
|
+
irStep,
|
|
596
|
+
step.iterationPath,
|
|
597
|
+
restored.outcome.control,
|
|
598
|
+
restored.outcome.issues?.some((issue) => issue.step.key === step.key) ?? false,
|
|
599
|
+
);
|
|
528
600
|
}
|
|
529
601
|
return { state: "continue" };
|
|
530
602
|
}
|
|
@@ -539,6 +611,16 @@ const restoreCompletedState = async (
|
|
|
539
611
|
return invalidRestoration(state, step, `planner for action "${irStep.action}" is unavailable while restoring step "${step.key}"`);
|
|
540
612
|
await handler.restoreCompleted?.(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep, restored.outcome);
|
|
541
613
|
}
|
|
614
|
+
if (restored.mode === "dryRun") {
|
|
615
|
+
const savedName = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
616
|
+
if (
|
|
617
|
+
typeof savedName === "string" &&
|
|
618
|
+
(restored.outcome.state === "indeterminate" ||
|
|
619
|
+
restored.outcome.state === "unsupported" ||
|
|
620
|
+
(restored.outcome.state === "planned" && restored.outcome.output === undefined && restored.outcome.issues?.length))
|
|
621
|
+
)
|
|
622
|
+
scope.markUnknown(savedName);
|
|
623
|
+
}
|
|
542
624
|
return { state: "continue" };
|
|
543
625
|
};
|
|
544
626
|
|
|
@@ -639,6 +721,15 @@ const evaluateAction = async (
|
|
|
639
721
|
const outcome: WorkflowPlanningOutcome = handler
|
|
640
722
|
? await handler.plan(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep)
|
|
641
723
|
: { state: "unsupported", reason: `action "${irStep.action}" has no dry-run handler` };
|
|
724
|
+
const savedName = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
725
|
+
if (
|
|
726
|
+
typeof savedName === "string" &&
|
|
727
|
+
(outcome.state === "indeterminate" ||
|
|
728
|
+
outcome.state === "unsupported" ||
|
|
729
|
+
(outcome.state === "planned" && outcome.output === undefined && outcome.issues?.length))
|
|
730
|
+
) {
|
|
731
|
+
scope.markUnknown(savedName);
|
|
732
|
+
}
|
|
642
733
|
if (outcome.state === "planned" || outcome.state === "terminal") {
|
|
643
734
|
state.effects.push(...outcome.effects);
|
|
644
735
|
state.issues.push(...restoredIssues(state, outcome.issues));
|
|
@@ -729,8 +820,9 @@ const analyzeUnknownBranches = async (
|
|
|
729
820
|
const branches =
|
|
730
821
|
irStep.kind === "if" ? [irStep.then, irStep.else] : [...irStep.cases.map((candidate) => candidate.steps), irStep.default];
|
|
731
822
|
for (const branch of branches) {
|
|
732
|
-
const flow = await runSteps(state, branch, scope.child(), iterationPath);
|
|
823
|
+
const flow = await runSteps(state, branch, scope.child({}, true), iterationPath);
|
|
733
824
|
if (flow.state === "canceled") return flow;
|
|
825
|
+
if (flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
734
826
|
}
|
|
735
827
|
return { state: "continue" };
|
|
736
828
|
};
|
|
@@ -786,10 +878,13 @@ const evaluateControl = async (
|
|
|
786
878
|
if (state.options.mode !== "dryRun") throw new WorkflowValueError(reason);
|
|
787
879
|
state.issues.push({ state: "indeterminate", reason, step: stepIdentity(state, irStep, iterationPath) });
|
|
788
880
|
control = { kind: "forEach", items: [null] };
|
|
789
|
-
|
|
881
|
+
const unknownScope = scope.child({}, true);
|
|
882
|
+
unknownScope.markUnknown(irStep.alias);
|
|
883
|
+
flow = await runSteps(state, irStep.steps, unknownScope, [...iterationPath, 0]);
|
|
790
884
|
if (flow.state === "canceled") {
|
|
791
885
|
return { flow, result: resultForHaltedControl(state, flow, effectsStart, issuesStart) };
|
|
792
886
|
}
|
|
887
|
+
if (flow.state === "terminal_planned") scope.dependsOnUnknown = true;
|
|
793
888
|
flow = { state: "continue" };
|
|
794
889
|
return { flow, result: resultForCompletedControl(state, undefined, control, effectsStart, issuesStart) };
|
|
795
890
|
}
|
|
@@ -842,6 +937,10 @@ const evaluateStep = async (
|
|
|
842
937
|
};
|
|
843
938
|
}
|
|
844
939
|
if (error instanceof WorkflowValueWaiting) {
|
|
940
|
+
if (state.options.mode === "dryRun" && irStep.kind === "action") {
|
|
941
|
+
const name = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
942
|
+
if (typeof name === "string") scope.markUnknown(name);
|
|
943
|
+
}
|
|
845
944
|
if (state.options.mode === "execute") {
|
|
846
945
|
const outcome: WorkflowStepOutcome = { state: "waiting", dependency: error.dependency };
|
|
847
946
|
return { flow: { state: "waiting", dependency: error.dependency, step }, result: { mode: "execute", outcome } };
|
|
@@ -850,16 +949,16 @@ const evaluateStep = async (
|
|
|
850
949
|
return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
|
|
851
950
|
}
|
|
852
951
|
if (state.options.mode === "dryRun") {
|
|
952
|
+
if (irStep.kind === "action") {
|
|
953
|
+
const name = irStep.action === "setVariable" ? irStep.config.name : irStep.config.saveAs;
|
|
954
|
+
if (typeof name === "string") scope.markUnknown(name);
|
|
955
|
+
}
|
|
853
956
|
const outcome: WorkflowPlanningOutcome = { state: "indeterminate", reason: errorMessage(error) };
|
|
854
957
|
return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
|
|
855
958
|
}
|
|
856
959
|
const outcome: WorkflowStepOutcome = {
|
|
857
960
|
state: "failed",
|
|
858
|
-
error: executionError(
|
|
859
|
-
error instanceof WorkflowValueError ? "WORKFLOW_VALUE_UNAVAILABLE" : "WORKFLOW_ACTION_ERROR",
|
|
860
|
-
errorMessage(error),
|
|
861
|
-
false,
|
|
862
|
-
),
|
|
961
|
+
error: executionError(error instanceof WorkflowValueError ? error.code : "WORKFLOW_ACTION_ERROR", errorMessage(error), false),
|
|
863
962
|
};
|
|
864
963
|
return { flow: { state: "failed", error: outcome.error, step }, result: { mode: "execute", outcome } };
|
|
865
964
|
}
|
|
@@ -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:
|
|
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) =>
|
|
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: `
|
|
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
|
package/src/ai/model-pricing.ts
DELETED
|
@@ -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
|
-
};
|