@k2b/cloud 0.43.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 +7 -2
- 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 +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- 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/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/PermissionEditor.tsx +9 -3
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- 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/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/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 +31 -22
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +5 -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.ts +26 -9
- 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 +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- 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 +21 -44
- 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 +27 -22
- 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 +16 -0
- 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/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 +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- 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/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- 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 +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -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 +1 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +6 -0
- package/src/services/accounts/entities.ts +14 -3
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +62 -10
- package/src/services/accounts/users.ts +7 -6
- package/src/services/app-access.ts +25 -7
- 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/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -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/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +71 -43
- 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 +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +8 -6
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +25 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +2 -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
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { type SQL, sql } from "bun";
|
|
2
|
+
import { AI_TASK_REQUEST_MAX_BYTES, AiTaskInputTooLargeError, assertAiTaskRequestSize } from "../../ai/task-contracts";
|
|
3
|
+
import type { AccessSubject } from "../../server/services/access";
|
|
4
|
+
import { type AiResolvedModelRef, AiResolvedModelRefSchema } from "../../shared/ai-models";
|
|
2
5
|
import type { WorkflowJsonValue } from "../contracts";
|
|
3
6
|
import { wakeWorkflowRunsWaitingOn } from "../store/runs";
|
|
7
|
+
import { withTransaction } from "../store/transaction";
|
|
4
8
|
import { notifyWorkflowWorker } from "../store/worker-runtime";
|
|
9
|
+
import { WorkflowAiAdmissionError, type WorkflowAiScope, workflowAiHash } from "./policy";
|
|
5
10
|
import {
|
|
6
11
|
type WorkflowAiRequest,
|
|
7
12
|
type WorkflowAiRequestInput,
|
|
@@ -12,17 +17,23 @@ import {
|
|
|
12
17
|
|
|
13
18
|
export const WORKFLOW_AI_DEPENDENCY_KIND = "ai.workflow-task";
|
|
14
19
|
|
|
20
|
+
// Released application results use the same 24-hour horizon as admission history.
|
|
21
|
+
const APPLICATION_AI_TASK_RETENTION_HOURS = 24;
|
|
22
|
+
|
|
15
23
|
type WorkflowAiTaskRow = {
|
|
16
24
|
usage_user_id: string | null;
|
|
17
25
|
id: string;
|
|
18
26
|
app_id: string;
|
|
19
|
-
run_id: string;
|
|
27
|
+
run_id: string | null;
|
|
28
|
+
usage_subject: AccessSubject | null;
|
|
20
29
|
step_key: string;
|
|
21
30
|
effect_key: string;
|
|
22
31
|
kind: WorkflowAiRequest["kind"];
|
|
23
32
|
request: unknown;
|
|
24
33
|
input_hash: string;
|
|
25
34
|
model_profile_id: string;
|
|
35
|
+
model_ref: unknown;
|
|
36
|
+
model_config_hash: string | null;
|
|
26
37
|
status: WorkflowAiTaskStatus;
|
|
27
38
|
output: unknown;
|
|
28
39
|
usage: unknown;
|
|
@@ -51,13 +62,16 @@ const taskFromRow = (row: WorkflowAiTaskRow): WorkflowAiTask => ({
|
|
|
51
62
|
id: row.id,
|
|
52
63
|
appId: row.app_id,
|
|
53
64
|
runId: row.run_id,
|
|
65
|
+
usageSubject: row.usage_subject ?? undefined,
|
|
54
66
|
usageUserId: row.usage_user_id ?? undefined,
|
|
55
67
|
stepKey: row.step_key,
|
|
56
68
|
effectKey: row.effect_key,
|
|
57
69
|
kind: row.kind,
|
|
58
70
|
request: workflowAiRequestSchema.parse(json(row.request)) as WorkflowAiRequest,
|
|
59
71
|
inputHash: row.input_hash,
|
|
60
|
-
|
|
72
|
+
modelId: row.model_profile_id,
|
|
73
|
+
modelRef: row.model_ref ? AiResolvedModelRefSchema.parse(row.model_ref) : undefined,
|
|
74
|
+
modelConfigHash: row.model_config_hash ?? undefined,
|
|
61
75
|
status: row.status,
|
|
62
76
|
output: json(row.output) as WorkflowJsonValue | null,
|
|
63
77
|
usage: json(row.usage) as WorkflowJsonValue | null,
|
|
@@ -76,12 +90,15 @@ const TASK_COLUMNS = `
|
|
|
76
90
|
task.id::text,
|
|
77
91
|
task.app_id,
|
|
78
92
|
task.run_id::text,
|
|
93
|
+
task.usage_subject,
|
|
79
94
|
task.step_key,
|
|
80
95
|
task.effect_key,
|
|
81
96
|
task.kind,
|
|
82
97
|
task.request,
|
|
83
98
|
task.input_hash,
|
|
84
99
|
task.model_profile_id,
|
|
100
|
+
task.model_ref,
|
|
101
|
+
task.model_config_hash,
|
|
85
102
|
task.status,
|
|
86
103
|
task.output,
|
|
87
104
|
task.usage,
|
|
@@ -129,6 +146,32 @@ export const migrateWorkflowAi = async (db: SQL = sql): Promise<void> => {
|
|
|
129
146
|
ALTER TABLE ai.workflow_task ADD CONSTRAINT workflow_task_kind_check
|
|
130
147
|
CHECK (kind IN ('generate_text', 'classify', 'classify_many', 'extract_data'))
|
|
131
148
|
`.simple();
|
|
149
|
+
await db`
|
|
150
|
+
ALTER TABLE ai.workflow_task ALTER COLUMN run_id DROP NOT NULL;
|
|
151
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS usage_subject JSONB;
|
|
152
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS scope_id TEXT;
|
|
153
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS source_id TEXT;
|
|
154
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS cache_key TEXT;
|
|
155
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS model_config_hash TEXT;
|
|
156
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS model_ref JSONB;
|
|
157
|
+
ALTER TABLE ai.workflow_task ADD COLUMN IF NOT EXISTS released_at TIMESTAMPTZ;
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_ai_application_task_sources ON ai.workflow_task(app_id, scope_id, source_id)
|
|
159
|
+
WHERE run_id IS NULL AND released_at IS NULL;
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_ai_application_task_retention ON ai.workflow_task(released_at, id)
|
|
161
|
+
WHERE run_id IS NULL AND released_at IS NOT NULL AND status IN ('succeeded', 'failed', 'canceled');
|
|
162
|
+
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_ai_workflow_task_cache ON ai.workflow_task(app_id, scope_id, cache_key, completed_at DESC) WHERE status = 'succeeded';
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_ai_workflow_task_active_scope ON ai.workflow_task(app_id, scope_id) WHERE status IN ('queued', 'running');
|
|
165
|
+
CREATE TABLE IF NOT EXISTS ai.workflow_scope_budget (
|
|
166
|
+
app_id TEXT NOT NULL, scope_id TEXT NOT NULL, tokens DOUBLE PRECISION NOT NULL,
|
|
167
|
+
refilled_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY(app_id, scope_id)
|
|
168
|
+
);
|
|
169
|
+
CREATE TABLE IF NOT EXISTS ai.workflow_scope_admission (
|
|
170
|
+
app_id TEXT NOT NULL, scope_id TEXT NOT NULL, source_id TEXT NOT NULL,
|
|
171
|
+
task_id UUID NOT NULL PRIMARY KEY, logical_key TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
172
|
+
);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_ai_workflow_scope_admission ON ai.workflow_scope_admission(app_id, scope_id, created_at, source_id)
|
|
174
|
+
`.simple();
|
|
132
175
|
await db`
|
|
133
176
|
CREATE INDEX IF NOT EXISTS idx_ai_workflow_task_recovery
|
|
134
177
|
ON ai.workflow_task(status, updated_at, id)
|
|
@@ -136,7 +179,7 @@ export const migrateWorkflowAi = async (db: SQL = sql): Promise<void> => {
|
|
|
136
179
|
`.simple();
|
|
137
180
|
};
|
|
138
181
|
|
|
139
|
-
const requestHash = (request: WorkflowAiRequest): string =>
|
|
182
|
+
const requestHash = (request: WorkflowAiRequest): string => workflowAiHash(request);
|
|
140
183
|
|
|
141
184
|
export const getWorkflowAiTask = async (id: string, db: SQL = sql): Promise<WorkflowAiTask | null> => {
|
|
142
185
|
const rows = await db<WorkflowAiTaskRow[]>`
|
|
@@ -163,23 +206,127 @@ export const workflowAiTaskExists = async (effectKey: string, db: SQL = sql): Pr
|
|
|
163
206
|
return Boolean(row?.exists);
|
|
164
207
|
};
|
|
165
208
|
|
|
209
|
+
/** The application has durably settled or discarded the request; replay is no longer needed. */
|
|
210
|
+
export const releaseApplicationAiTaskByEffectKey = async (
|
|
211
|
+
input: { appId: string; scopeId: string; effectKey: string },
|
|
212
|
+
db: SQL = sql,
|
|
213
|
+
): Promise<boolean> => {
|
|
214
|
+
const rows = await db<{ id: string }[]>`
|
|
215
|
+
UPDATE ai.workflow_task SET released_at = COALESCE(released_at, now())
|
|
216
|
+
WHERE app_id = ${input.appId} AND scope_id = ${input.scopeId} AND effect_key = ${input.effectKey} AND run_id IS NULL
|
|
217
|
+
RETURNING id::text
|
|
218
|
+
`;
|
|
219
|
+
return rows.length > 0;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Bounded owner identities for application reconciliation after a crash during deletion. */
|
|
223
|
+
export const listUnreleasedApplicationAiTaskSources = async (
|
|
224
|
+
input: { appId: string; task?: string; after?: { scopeId: string; sourceId: string } },
|
|
225
|
+
db: SQL = sql,
|
|
226
|
+
): Promise<Array<{ scopeId: string; sourceId: string }>> =>
|
|
227
|
+
db`SELECT DISTINCT scope_id AS "scopeId", source_id AS "sourceId" FROM ai.workflow_task
|
|
228
|
+
WHERE app_id = ${input.appId} AND run_id IS NULL AND released_at IS NULL
|
|
229
|
+
AND (${input.task ?? null}::text IS NULL OR step_key = ${input.task ?? null})
|
|
230
|
+
AND scope_id IS NOT NULL AND source_id IS NOT NULL
|
|
231
|
+
AND (${input.after?.scopeId ?? null}::text IS NULL
|
|
232
|
+
OR (scope_id, source_id) > (${input.after?.scopeId ?? null}, ${input.after?.sourceId ?? null}))
|
|
233
|
+
ORDER BY scope_id, source_id LIMIT 100`;
|
|
234
|
+
|
|
235
|
+
/** Remove an application's task payloads when their owning scope or source is deleted. */
|
|
236
|
+
export const deleteApplicationAiTasks = async (
|
|
237
|
+
input: { appId: string; scopeId: string; sourceId?: string },
|
|
238
|
+
db: SQL = sql,
|
|
239
|
+
): Promise<number> =>
|
|
240
|
+
withTransaction(db, async (tx) => {
|
|
241
|
+
// Serialize with task creation/cache copies. The application fences future creation.
|
|
242
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended('workflow-ai-admission', 0))`;
|
|
243
|
+
const [row] = await tx<{ count: number }[]>`
|
|
244
|
+
WITH deleted AS (
|
|
245
|
+
DELETE FROM ai.workflow_task
|
|
246
|
+
WHERE app_id = ${input.appId} AND scope_id = ${input.scopeId} AND run_id IS NULL
|
|
247
|
+
AND (${input.sourceId ?? null}::text IS NULL OR source_id = ${input.sourceId ?? null})
|
|
248
|
+
RETURNING id
|
|
249
|
+
) SELECT count(*)::int AS count FROM deleted
|
|
250
|
+
`;
|
|
251
|
+
// Keep admission history: deleting owner data must not reset rate/daily accounting.
|
|
252
|
+
return row?.count ?? 0;
|
|
253
|
+
});
|
|
254
|
+
|
|
166
255
|
export const createWorkflowAiTask = async (
|
|
167
|
-
input: {
|
|
256
|
+
input: {
|
|
257
|
+
runId?: string;
|
|
258
|
+
/** Required for a task owned by a durable application job instead of a workflow. */
|
|
259
|
+
owner?: { appId: string; scopeId: string; usageSubject?: AccessSubject };
|
|
260
|
+
stepKey: string;
|
|
261
|
+
effectKey: string;
|
|
262
|
+
request: WorkflowAiRequestInput;
|
|
263
|
+
modelId: string;
|
|
264
|
+
modelRef?: AiResolvedModelRef;
|
|
265
|
+
modelConfigHash?: string;
|
|
266
|
+
scope?: WorkflowAiScope;
|
|
267
|
+
},
|
|
168
268
|
db: SQL = sql,
|
|
169
269
|
): Promise<{ task: WorkflowAiTask; created: boolean }> => {
|
|
270
|
+
if (Boolean(input.runId) === Boolean(input.owner)) throw new Error("AI task requires exactly one workflow run or application owner.");
|
|
271
|
+
assertAiTaskRequestSize(input.request);
|
|
170
272
|
const request = workflowAiRequestSchema.parse(input.request) as WorkflowAiRequest;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
273
|
+
assertAiTaskRequestSize(request);
|
|
274
|
+
// JSONB adds separator spaces and expands numeric exponents. Measure its exact
|
|
275
|
+
// representation before INSERT, keeping the DB constraint as a final guard.
|
|
276
|
+
const [size] = await db<{ bytes: number }[]>`SELECT octet_length(${request}::jsonb::text) AS bytes`;
|
|
277
|
+
if (!size || size.bytes > AI_TASK_REQUEST_MAX_BYTES) throw new AiTaskInputTooLargeError();
|
|
278
|
+
if (input.scope && !input.modelConfigHash) throw new Error("Scoped inference requires a resolved model configuration hash.");
|
|
279
|
+
return withTransaction(db, async (db) => {
|
|
280
|
+
// This short admission transaction serializes reservations across replicas.
|
|
281
|
+
// It is never held during inference.
|
|
282
|
+
if (input.scope) await db`SELECT pg_advisory_xact_lock(hashtextextended('workflow-ai-admission', 0))`;
|
|
283
|
+
const existingTask = await getWorkflowAiTaskByEffectKey(input.effectKey, db);
|
|
284
|
+
if (existingTask) return { task: existingTask, created: false };
|
|
285
|
+
const run = input.owner
|
|
286
|
+
? { app_id: input.owner.appId, scope_id: input.owner.scopeId }
|
|
287
|
+
: (
|
|
288
|
+
await db<{ app_id: string; scope_id: string }[]>`
|
|
289
|
+
SELECT app_id, scope_id FROM workflows.run WHERE id = ${input.runId ?? null}::uuid
|
|
290
|
+
AND cancel_requested_at IS NULL AND state <> 'canceled'
|
|
291
|
+
`
|
|
292
|
+
)[0];
|
|
293
|
+
if (!run) throw new Error("Workflow AI run is unavailable.");
|
|
294
|
+
const cacheKey = input.scope
|
|
295
|
+
? workflowAiHash({
|
|
296
|
+
source: input.scope.sourceId,
|
|
297
|
+
revision: input.scope.sourceRevision,
|
|
298
|
+
request,
|
|
299
|
+
model: input.modelConfigHash,
|
|
300
|
+
...(input.owner?.usageSubject ? { owner: input.owner.usageSubject } : {}),
|
|
301
|
+
})
|
|
302
|
+
: null;
|
|
303
|
+
let cached: WorkflowAiTask | null = null;
|
|
304
|
+
if (input.scope && !input.scope.recompute) {
|
|
305
|
+
const [hit] = await db<{ id: string }[]>`
|
|
306
|
+
SELECT id::text FROM ai.workflow_task
|
|
307
|
+
WHERE app_id = ${run.app_id} AND scope_id = ${run.scope_id} AND cache_key = ${cacheKey} AND status = 'succeeded'
|
|
308
|
+
ORDER BY completed_at DESC, id LIMIT 1
|
|
309
|
+
`;
|
|
310
|
+
if (hit) cached = await getWorkflowAiTask(hit.id, db);
|
|
311
|
+
}
|
|
312
|
+
if (input.scope && !cached) await reserveWorkflowAiBudget(db, run, input.scope);
|
|
313
|
+
const rows = await db<WorkflowAiTaskRow[]>`
|
|
314
|
+
INSERT INTO ai.workflow_task (app_id, run_id, usage_subject, step_key, effect_key, kind, request, input_hash, model_profile_id, model_ref, scope_id, source_id, cache_key, model_config_hash, status, output, completed_at)
|
|
315
|
+
VALUES (${run.app_id}, ${input.runId ?? null}::uuid, ${input.owner?.usageSubject ?? null}, ${input.stepKey}, ${input.effectKey}, ${request.kind}, ${request}, ${requestHash(request)}, ${input.modelId}, ${input.modelRef ? JSON.stringify(AiResolvedModelRefSchema.parse(input.modelRef)) : null}::text::jsonb,
|
|
316
|
+
${run.scope_id}, ${input.scope?.sourceId ?? null}, ${cacheKey}, ${input.modelConfigHash ?? null},
|
|
317
|
+
${cached ? "succeeded" : "queued"}, ${cached?.output ?? null}, ${cached ? new Date() : null})
|
|
176
318
|
ON CONFLICT (effect_key) DO NOTHING
|
|
177
319
|
RETURNING ${db.unsafe(TASK_COLUMNS.replaceAll("task.", ""))}
|
|
178
320
|
`;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
321
|
+
if (rows[0]) {
|
|
322
|
+
if (input.scope && !cached)
|
|
323
|
+
await db`INSERT INTO ai.workflow_scope_admission(app_id, scope_id, source_id, task_id, logical_key) VALUES (${run.app_id}, ${run.scope_id}, ${input.scope.sourceId}, ${rows[0].id}::uuid, ${input.scope.recompute ? rows[0].id : cacheKey})`;
|
|
324
|
+
return { task: taskFromRow(rows[0]), created: true };
|
|
325
|
+
}
|
|
326
|
+
const existing = await getWorkflowAiTaskByEffectKey(input.effectKey, db);
|
|
327
|
+
if (!existing) throw new Error("Workflow AI task could not be created because its run is unavailable.");
|
|
328
|
+
return { task: existing, created: false };
|
|
329
|
+
});
|
|
183
330
|
};
|
|
184
331
|
|
|
185
332
|
export const listRecoverableWorkflowAiTaskIds = async (db: SQL = sql): Promise<string[]> => {
|
|
@@ -188,6 +335,7 @@ export const listRecoverableWorkflowAiTaskIds = async (db: SQL = sql): Promise<s
|
|
|
188
335
|
FROM ai.workflow_task
|
|
189
336
|
WHERE status IN ('queued', 'running')
|
|
190
337
|
ORDER BY updated_at, id
|
|
338
|
+
LIMIT 100
|
|
191
339
|
`;
|
|
192
340
|
return rows.map((row) => row.id);
|
|
193
341
|
};
|
|
@@ -203,12 +351,13 @@ export const claimWorkflowAiTask = async (id: string, db: SQL = sql): Promise<Wo
|
|
|
203
351
|
error_code = NULL,
|
|
204
352
|
error_message = NULL,
|
|
205
353
|
updated_at = now()
|
|
206
|
-
FROM workflows.run AS run
|
|
207
354
|
WHERE task.id = ${id}::uuid
|
|
208
|
-
AND
|
|
209
|
-
AND
|
|
210
|
-
|
|
211
|
-
|
|
355
|
+
AND task.cancel_requested_at IS NULL
|
|
356
|
+
AND (task.run_id IS NULL OR EXISTS (
|
|
357
|
+
SELECT 1 FROM workflows.run run WHERE run.id = task.run_id
|
|
358
|
+
AND run.cancel_requested_at IS NULL AND run.state <> 'canceled'
|
|
359
|
+
))
|
|
360
|
+
AND task.status = 'queued'
|
|
212
361
|
RETURNING ${db.unsafe(TASK_COLUMNS)}
|
|
213
362
|
`;
|
|
214
363
|
return rows[0] ? taskFromRow(rows[0]) : markWorkflowAiTaskCanceledIfRequested(id, db);
|
|
@@ -216,21 +365,24 @@ export const claimWorkflowAiTask = async (id: string, db: SQL = sql): Promise<Wo
|
|
|
216
365
|
|
|
217
366
|
export const workflowAiTaskCancellationRequested = async (id: string, db: SQL = sql): Promise<boolean> => {
|
|
218
367
|
const [row] = await db<{ canceled: boolean }[]>`
|
|
219
|
-
SELECT run.cancel_requested_at IS NOT NULL OR run.state = 'canceled' AS canceled
|
|
368
|
+
SELECT task.cancel_requested_at IS NOT NULL OR run.cancel_requested_at IS NOT NULL OR COALESCE(run.state = 'canceled', false) AS canceled
|
|
220
369
|
FROM ai.workflow_task AS task
|
|
221
|
-
JOIN workflows.run AS run ON run.id = task.run_id
|
|
370
|
+
LEFT JOIN workflows.run AS run ON run.id = task.run_id
|
|
222
371
|
WHERE task.id = ${id}::uuid
|
|
223
372
|
`;
|
|
224
|
-
|
|
373
|
+
// Owner deletion also revokes an in-flight task; do not finish a removed request.
|
|
374
|
+
return !row || row.canceled;
|
|
225
375
|
};
|
|
226
376
|
|
|
227
|
-
export const wakeWorkflowAiTask = async (task: Pick<WorkflowAiTask, "appId" | "id">): Promise<void> => {
|
|
377
|
+
export const wakeWorkflowAiTask = async (task: Pick<WorkflowAiTask, "appId" | "id"> & { runId?: string | null }): Promise<void> => {
|
|
378
|
+
if (task.runId === null) return;
|
|
228
379
|
await wakeWorkflowRunsWaitingOn({ appId: task.appId, kind: WORKFLOW_AI_DEPENDENCY_KIND, key: task.id });
|
|
229
380
|
notifyWorkflowWorker(task.appId);
|
|
230
381
|
};
|
|
231
382
|
|
|
232
383
|
export const markWorkflowAiTaskCanceledIfRequested = async (id: string, db: SQL = sql): Promise<WorkflowAiTask | null> => {
|
|
233
|
-
const rows = await db
|
|
384
|
+
const rows = await withTransaction(db, async (db) => {
|
|
385
|
+
const rows = await db<WorkflowAiTaskRow[]>`
|
|
234
386
|
UPDATE ai.workflow_task AS task
|
|
235
387
|
SET status = 'canceled',
|
|
236
388
|
output = NULL,
|
|
@@ -240,13 +392,17 @@ export const markWorkflowAiTaskCanceledIfRequested = async (id: string, db: SQL
|
|
|
240
392
|
cancel_requested_at = COALESCE(task.cancel_requested_at, now()),
|
|
241
393
|
completed_at = now(),
|
|
242
394
|
updated_at = now()
|
|
243
|
-
FROM workflows.run AS run
|
|
244
395
|
WHERE task.id = ${id}::uuid
|
|
245
|
-
AND
|
|
246
|
-
|
|
396
|
+
AND (task.cancel_requested_at IS NOT NULL OR EXISTS (
|
|
397
|
+
SELECT 1 FROM workflows.run run WHERE run.id = task.run_id
|
|
398
|
+
AND (run.cancel_requested_at IS NOT NULL OR run.state = 'canceled')
|
|
399
|
+
))
|
|
247
400
|
AND task.status IN ('queued', 'running')
|
|
248
401
|
RETURNING ${db.unsafe(TASK_COLUMNS)}
|
|
249
402
|
`;
|
|
403
|
+
if (rows[0]) await db`UPDATE ai.workflow_scope_admission SET logical_key = NULL WHERE task_id = ${id}::uuid`;
|
|
404
|
+
return rows;
|
|
405
|
+
});
|
|
250
406
|
const task = rows[0] ? taskFromRow(rows[0]) : null;
|
|
251
407
|
if (task) await wakeWorkflowAiTask(task);
|
|
252
408
|
return task;
|
|
@@ -269,11 +425,12 @@ export const completeWorkflowAiTask = async (
|
|
|
269
425
|
const rows = await db<WorkflowAiTaskRow[]>`
|
|
270
426
|
UPDATE ai.workflow_task AS task
|
|
271
427
|
SET status = 'succeeded', output = ${output}, usage = ${usage}, completed_at = now(), updated_at = now()
|
|
272
|
-
FROM workflows.run AS run
|
|
273
428
|
WHERE task.id = ${id}::uuid
|
|
274
|
-
AND
|
|
275
|
-
AND
|
|
276
|
-
|
|
429
|
+
AND task.cancel_requested_at IS NULL
|
|
430
|
+
AND (task.run_id IS NULL OR EXISTS (
|
|
431
|
+
SELECT 1 FROM workflows.run run WHERE run.id = task.run_id
|
|
432
|
+
AND run.cancel_requested_at IS NULL AND run.state <> 'canceled'
|
|
433
|
+
))
|
|
277
434
|
AND task.status = 'running'
|
|
278
435
|
RETURNING ${db.unsafe(TASK_COLUMNS)}
|
|
279
436
|
`;
|
|
@@ -290,7 +447,8 @@ export const failWorkflowAiTask = async (
|
|
|
290
447
|
error: { code: string; message: string },
|
|
291
448
|
db: SQL = sql,
|
|
292
449
|
): Promise<WorkflowAiTask | null> => {
|
|
293
|
-
const rows = await db
|
|
450
|
+
const rows = await withTransaction(db, async (db) => {
|
|
451
|
+
const rows = await db<WorkflowAiTaskRow[]>`
|
|
294
452
|
UPDATE ai.workflow_task AS task
|
|
295
453
|
SET status = 'failed',
|
|
296
454
|
output = NULL,
|
|
@@ -299,14 +457,18 @@ export const failWorkflowAiTask = async (
|
|
|
299
457
|
error_message = ${error.message.slice(0, 2_000)},
|
|
300
458
|
completed_at = now(),
|
|
301
459
|
updated_at = now()
|
|
302
|
-
FROM workflows.run AS run
|
|
303
460
|
WHERE task.id = ${id}::uuid
|
|
304
|
-
AND
|
|
305
|
-
AND
|
|
306
|
-
|
|
461
|
+
AND task.cancel_requested_at IS NULL
|
|
462
|
+
AND (task.run_id IS NULL OR EXISTS (
|
|
463
|
+
SELECT 1 FROM workflows.run run WHERE run.id = task.run_id
|
|
464
|
+
AND run.cancel_requested_at IS NULL AND run.state <> 'canceled'
|
|
465
|
+
))
|
|
307
466
|
AND task.status IN ('queued', 'running')
|
|
308
467
|
RETURNING ${db.unsafe(TASK_COLUMNS)}
|
|
309
468
|
`;
|
|
469
|
+
if (rows[0]) await db`UPDATE ai.workflow_scope_admission SET logical_key = NULL WHERE task_id = ${id}::uuid`;
|
|
470
|
+
return rows;
|
|
471
|
+
});
|
|
310
472
|
const task = rows[0] ? taskFromRow(rows[0]) : null;
|
|
311
473
|
if (task) {
|
|
312
474
|
await wakeWorkflowAiTask(task);
|
|
@@ -314,3 +476,89 @@ export const failWorkflowAiTask = async (
|
|
|
314
476
|
}
|
|
315
477
|
return markWorkflowAiTaskCanceledIfRequested(id, db);
|
|
316
478
|
};
|
|
479
|
+
|
|
480
|
+
/** DB liveness is separate from NATS heartbeat: lost/expired deliveries must settle. */
|
|
481
|
+
export const heartbeatWorkflowAiTask = async (id: string, db: SQL = sql): Promise<void> => {
|
|
482
|
+
await db`UPDATE ai.workflow_task SET updated_at = now() WHERE id = ${id}::uuid AND status = 'running'`;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export const expireWorkflowAiTasks = async (queueTimeoutMs: number, db: SQL = sql): Promise<void> => {
|
|
486
|
+
const rows = await withTransaction(db, async (db) => {
|
|
487
|
+
const rows = await db<{ id: string; app_id: string; run_id: string | null }[]>`
|
|
488
|
+
WITH expired AS (
|
|
489
|
+
SELECT candidate.id, run.cancel_requested_at, run.state
|
|
490
|
+
FROM ai.workflow_task candidate LEFT JOIN workflows.run run ON run.id = candidate.run_id
|
|
491
|
+
WHERE (candidate.status = 'queued' AND candidate.updated_at < now() - ${queueTimeoutMs} * interval '1 millisecond')
|
|
492
|
+
OR (candidate.status = 'running' AND candidate.updated_at < now() - interval '30 seconds')
|
|
493
|
+
ORDER BY candidate.updated_at, candidate.id LIMIT 100 FOR UPDATE OF candidate SKIP LOCKED
|
|
494
|
+
)
|
|
495
|
+
UPDATE ai.workflow_task AS task
|
|
496
|
+
SET status = CASE WHEN task.cancel_requested_at IS NOT NULL OR run.cancel_requested_at IS NOT NULL OR run.state = 'canceled' THEN 'canceled' ELSE 'failed' END,
|
|
497
|
+
completed_at = now(), updated_at = now(),
|
|
498
|
+
error_code = CASE
|
|
499
|
+
WHEN task.cancel_requested_at IS NOT NULL OR run.cancel_requested_at IS NOT NULL OR run.state = 'canceled' THEN 'WORKFLOW_AI_CANCELED'
|
|
500
|
+
WHEN task.status = 'queued' THEN 'WORKFLOW_AI_QUEUE_TIMEOUT' ELSE 'WORKFLOW_AI_INTERRUPTED' END,
|
|
501
|
+
error_message = CASE WHEN task.cancel_requested_at IS NOT NULL OR run.cancel_requested_at IS NOT NULL OR run.state = 'canceled' THEN 'Workflow AI task was canceled.'
|
|
502
|
+
ELSE 'Workflow AI delivery expired or its worker stopped. Retry the workflow after checking capacity and provider health.' END
|
|
503
|
+
FROM expired AS run WHERE run.id = task.id
|
|
504
|
+
RETURNING task.id::text, task.app_id, task.run_id::text
|
|
505
|
+
`;
|
|
506
|
+
for (const row of rows) await db`UPDATE ai.workflow_scope_admission SET logical_key = NULL WHERE task_id = ${row.id}::uuid`;
|
|
507
|
+
return rows;
|
|
508
|
+
});
|
|
509
|
+
for (const row of rows) await wakeWorkflowAiTask({ id: row.id, appId: row.app_id, runId: row.run_id });
|
|
510
|
+
// A crash between terminal persistence and the wake must also be repaired.
|
|
511
|
+
const waiting = await db<{ id: string; app_id: string }[]>`
|
|
512
|
+
SELECT task.id::text, task.app_id
|
|
513
|
+
FROM workflows.step_outcome step
|
|
514
|
+
JOIN workflows.run run ON run.id = step.run_id
|
|
515
|
+
JOIN ai.workflow_task task ON task.id = CASE
|
|
516
|
+
WHEN step.dependency ->> 'kind' = 'ai.workflow-task' THEN (step.dependency ->> 'key')::uuid END
|
|
517
|
+
AND task.run_id = run.id
|
|
518
|
+
WHERE step.state = 'waiting' AND run.state = 'waiting'
|
|
519
|
+
AND step.dependency ->> 'kind' = 'ai.workflow-task'
|
|
520
|
+
AND task.status IN ('succeeded', 'failed', 'canceled')
|
|
521
|
+
ORDER BY step.run_id, step.step_key LIMIT 100
|
|
522
|
+
`;
|
|
523
|
+
for (const row of waiting) await wakeWorkflowAiTask({ id: row.id, appId: row.app_id });
|
|
524
|
+
await db`DELETE FROM ai.workflow_scope_admission WHERE task_id IN (
|
|
525
|
+
SELECT task_id FROM ai.workflow_scope_admission WHERE created_at <= now() - interval '24 hours' ORDER BY created_at LIMIT 100
|
|
526
|
+
)`;
|
|
527
|
+
await db`
|
|
528
|
+
WITH expired AS (
|
|
529
|
+
SELECT id FROM ai.workflow_task
|
|
530
|
+
WHERE run_id IS NULL AND status IN ('succeeded', 'failed', 'canceled')
|
|
531
|
+
AND released_at < now() - ${APPLICATION_AI_TASK_RETENTION_HOURS} * interval '1 hour'
|
|
532
|
+
AND completed_at < now() - ${APPLICATION_AI_TASK_RETENTION_HOURS} * interval '1 hour'
|
|
533
|
+
ORDER BY released_at, id LIMIT 100 FOR UPDATE SKIP LOCKED
|
|
534
|
+
)
|
|
535
|
+
DELETE FROM ai.workflow_task task USING expired WHERE task.id = expired.id
|
|
536
|
+
`;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const reserveWorkflowAiBudget = async (db: SQL, run: { app_id: string; scope_id: string }, scope: WorkflowAiScope): Promise<void> => {
|
|
540
|
+
const budget = scope.budget;
|
|
541
|
+
for (const value of Object.values(budget)) if (!Number.isSafeInteger(value) || value < 1) throw new Error("Invalid workflow AI budget.");
|
|
542
|
+
await db`DELETE FROM ai.workflow_scope_admission WHERE app_id = ${run.app_id} AND scope_id = ${run.scope_id} AND created_at <= now() - interval '24 hours'`;
|
|
543
|
+
const [counts] = await db<{ source_steps: number; sources: number; seen_source: boolean }[]>`
|
|
544
|
+
SELECT count(DISTINCT logical_key) FILTER (WHERE source_id = ${scope.sourceId})::int AS source_steps,
|
|
545
|
+
count(DISTINCT source_id)::int AS sources, bool_or(source_id = ${scope.sourceId}) AS seen_source
|
|
546
|
+
FROM ai.workflow_scope_admission WHERE app_id = ${run.app_id} AND scope_id = ${run.scope_id}
|
|
547
|
+
`;
|
|
548
|
+
if (counts && counts.source_steps >= budget.maxStepsPerSource) throw new WorkflowAiAdmissionError("WORKFLOW_AI_SOURCE_BUDGET");
|
|
549
|
+
if (counts && !counts.seen_source && counts.sources >= budget.sourcesPer24Hours)
|
|
550
|
+
throw new WorkflowAiAdmissionError("WORKFLOW_AI_DAILY_BUDGET");
|
|
551
|
+
const [active] = await db<{ scope: number; global: number }[]>`
|
|
552
|
+
SELECT count(*) FILTER (WHERE app_id = ${run.app_id} AND scope_id = ${run.scope_id})::int AS scope, count(*)::int AS global
|
|
553
|
+
FROM ai.workflow_task WHERE status IN ('queued', 'running') AND cache_key IS NOT NULL
|
|
554
|
+
`;
|
|
555
|
+
if (active && (active.scope >= budget.scopeConcurrent || active.global >= budget.globalConcurrent))
|
|
556
|
+
throw new WorkflowAiAdmissionError("WORKFLOW_AI_CAPACITY");
|
|
557
|
+
await db`INSERT INTO ai.workflow_scope_budget(app_id, scope_id, tokens) VALUES (${run.app_id}, ${run.scope_id}, ${budget.burst}) ON CONFLICT DO NOTHING`;
|
|
558
|
+
const [bucket] = await db<{ available: number }[]>`
|
|
559
|
+
SELECT least(${budget.burst}::double precision, tokens + extract(epoch FROM now() - refilled_at) * ${budget.tasksPerMinute} / 60) AS available
|
|
560
|
+
FROM ai.workflow_scope_budget WHERE app_id = ${run.app_id} AND scope_id = ${run.scope_id} FOR UPDATE
|
|
561
|
+
`;
|
|
562
|
+
if (!bucket || bucket.available < 1) throw new WorkflowAiAdmissionError("WORKFLOW_AI_RATE_LIMIT");
|
|
563
|
+
await db`UPDATE ai.workflow_scope_budget SET tokens = ${bucket.available - 1}, refilled_at = now() WHERE app_id = ${run.app_id} AND scope_id = ${run.scope_id}`;
|
|
564
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { AiTaskRequest as WorkflowAiRequest } from "../../ai/task-contracts";
|
|
2
|
+
import type { AccessSubject } from "../../server/services/access";
|
|
3
|
+
import type { AiResolvedModelRef } from "../../shared/ai-models";
|
|
2
4
|
import type { WorkflowJsonValue } from "../contracts";
|
|
3
5
|
|
|
4
6
|
export {
|
|
@@ -14,13 +16,17 @@ export type WorkflowAiTask = {
|
|
|
14
16
|
usageUserId?: string;
|
|
15
17
|
id: string;
|
|
16
18
|
appId: string;
|
|
17
|
-
runId: string;
|
|
19
|
+
runId: string | null;
|
|
20
|
+
/** Authenticated owner of an interactive application task; never supplied by model input. */
|
|
21
|
+
usageSubject?: AccessSubject;
|
|
18
22
|
stepKey: string;
|
|
19
23
|
effectKey: string;
|
|
20
24
|
kind: WorkflowAiRequest["kind"];
|
|
21
25
|
request: WorkflowAiRequest;
|
|
22
26
|
inputHash: string;
|
|
23
|
-
|
|
27
|
+
modelId: string;
|
|
28
|
+
modelRef?: AiResolvedModelRef;
|
|
29
|
+
modelConfigHash?: string;
|
|
24
30
|
status: WorkflowAiTaskStatus;
|
|
25
31
|
output: WorkflowJsonValue | null;
|
|
26
32
|
usage: WorkflowJsonValue | null;
|