@k2b/cloud 0.6.0-rc.0 → 0.7.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/README.md +2 -2
- package/package.json +30 -17
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +66 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +5 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +114 -34
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/solid.tsx +3 -3
- package/src/index.ts +0 -4
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +107 -84
- package/src/services/pdf/index.ts +3 -0
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +7 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +2 -0
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/scripts/app-favicon.test.ts +0 -49
- package/src/ai/chat/blocks.render.test.tsx +0 -1108
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
- package/src/ssr/Layout.render.test.tsx +0 -148
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
package/src/ai/routes.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { getAiChatQuotas } from "./chat-quotas";
|
|
2
|
+
import { enqueueChatMessage, listQueuedMessages, updateQueuedMessage, editQueuedMessage, queuedMessageAccepted, AiMessageQueueConflict } from "./message-queue";
|
|
3
|
+
import { prepareAiChatTurn, wakeAiMessageQueue } from "./runtime";
|
|
4
|
+
import { bodyLimit } from "hono/body-limit";
|
|
5
|
+
import { AI_AUDIO_MAX_BYTES } from "./audio-format";
|
|
6
|
+
import { aiDictations, AiDictationConflict, AiDictationStartSchema, AiDictationListSchema } from "./dictations";
|
|
7
|
+
import { enqueueAiDictation } from "./dictation-runtime";
|
|
8
|
+
import { resolveAiAudioModel } from "./transcription";
|
|
9
|
+
import { aiModelAccess } from "./model-access";
|
|
1
10
|
import { type Context, Hono } from "hono";
|
|
2
11
|
import { z } from "zod";
|
|
3
12
|
import { listCapabilities } from "../_internal/registry";
|
|
@@ -16,6 +25,7 @@ import {
|
|
|
16
25
|
v,
|
|
17
26
|
} from "../server";
|
|
18
27
|
import { coreSettings } from "../services/settings/api";
|
|
28
|
+
import { logger } from "../services/logging";
|
|
19
29
|
import type { AiToolApprovalContext } from "./approvals";
|
|
20
30
|
import { assistantAiSettingsState, listAssistantAiModels, selectAssistantAiModelId } from "./assistant-models";
|
|
21
31
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
@@ -102,6 +112,10 @@ const ConversationListQuerySchema = z
|
|
|
102
112
|
.enum(["true", "false"])
|
|
103
113
|
.transform((value) => value === "true")
|
|
104
114
|
.optional(),
|
|
115
|
+
done: z
|
|
116
|
+
.enum(["true", "false"])
|
|
117
|
+
.transform((value) => value === "true")
|
|
118
|
+
.optional(),
|
|
105
119
|
status: z.enum(["running", "needs_attention", "failed", "unread"]).optional(),
|
|
106
120
|
projectId: z.string().regex(AI_SHORT_ID_PATTERN).optional(),
|
|
107
121
|
unassigned: z
|
|
@@ -123,6 +137,7 @@ const ConversationMetadataInputSchema = z.object({
|
|
|
123
137
|
});
|
|
124
138
|
|
|
125
139
|
const ConversationProjectInputSchema = z.object({
|
|
140
|
+
onlyUnassigned: z.boolean().optional(),
|
|
126
141
|
projectId: z.string().regex(AI_SHORT_ID_PATTERN).nullable(),
|
|
127
142
|
});
|
|
128
143
|
|
|
@@ -212,12 +227,13 @@ const rememberLastUsedModel = (actor: RequestActor, modelProfileId: string | nul
|
|
|
212
227
|
};
|
|
213
228
|
|
|
214
229
|
const conversationDetail = async (conversation: AiConversation, subject: AccessSubject) => {
|
|
215
|
-
const [state, timeline] = await Promise.all([
|
|
230
|
+
const [state, timeline, publicView] = await Promise.all([
|
|
216
231
|
loadAiStreamState(conversation),
|
|
217
232
|
aiConversations.listConversationTimeline({ conversationId: conversation.id }),
|
|
233
|
+
publicConversationFor(conversation, subject),
|
|
218
234
|
]);
|
|
219
235
|
return {
|
|
220
|
-
conversation:
|
|
236
|
+
conversation: publicView,
|
|
221
237
|
messages: state.messages.map((message) => ({ ...message, id: message.shortId })),
|
|
222
238
|
hasMoreMessages: state.hasMoreMessages ?? false,
|
|
223
239
|
activeTurn: state.activeTurn,
|
|
@@ -284,6 +300,10 @@ export const aiRoutes = (() => {
|
|
|
284
300
|
.get("/status", async (c) => {
|
|
285
301
|
return respond(c, ok(await assistantAiSettingsState(c.get("accessSubject"))));
|
|
286
302
|
})
|
|
303
|
+
.get("/quotas", async (c) => {
|
|
304
|
+
c.header("Cache-Control", "no-store");
|
|
305
|
+
return c.json(await getAiChatQuotas(c.get("accessSubject")));
|
|
306
|
+
})
|
|
287
307
|
.get("/models", async (c) => {
|
|
288
308
|
return respond(c, ok(await listAssistantAiModels(c.get("accessSubject"))));
|
|
289
309
|
})
|
|
@@ -428,6 +448,7 @@ export const aiRoutes = (() => {
|
|
|
428
448
|
ownerUserId: ctx.ownerUserId,
|
|
429
449
|
search: query.q,
|
|
430
450
|
archived: query.archived,
|
|
451
|
+
done: query.done,
|
|
431
452
|
status: query.status,
|
|
432
453
|
projectId: project?.id,
|
|
433
454
|
unassigned: query.unassigned,
|
|
@@ -448,6 +469,7 @@ export const aiRoutes = (() => {
|
|
|
448
469
|
ownerUserId: ctx.ownerUserId,
|
|
449
470
|
search: query.q,
|
|
450
471
|
archived: query.archived,
|
|
472
|
+
done: query.done,
|
|
451
473
|
status: query.status,
|
|
452
474
|
projectId: project?.id,
|
|
453
475
|
unassigned: query.unassigned,
|
|
@@ -552,7 +574,7 @@ export const aiRoutes = (() => {
|
|
|
552
574
|
if (ctx instanceof Response) return ctx;
|
|
553
575
|
const conversation = await loadConversation(c, ctx);
|
|
554
576
|
if (!conversation) return notFound(c);
|
|
555
|
-
const messageId = c.req.param("messageId");
|
|
577
|
+
const messageId = (c.req.param("messageId") ?? "");
|
|
556
578
|
if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
|
|
557
579
|
const input = c.req.valid("json");
|
|
558
580
|
const feedback = await aiConversations.setMessageFeedback({
|
|
@@ -571,7 +593,7 @@ export const aiRoutes = (() => {
|
|
|
571
593
|
if (ctx instanceof Response) return ctx;
|
|
572
594
|
const conversation = await loadConversation(c, ctx);
|
|
573
595
|
if (!conversation) return notFound(c);
|
|
574
|
-
const messageId = c.req.param("messageId");
|
|
596
|
+
const messageId = (c.req.param("messageId") ?? "");
|
|
575
597
|
if (!messageId) return respond(c, fail(err.badInput("Invalid message id.")));
|
|
576
598
|
return (await aiConversations.clearMessageFeedback({
|
|
577
599
|
conversationId: conversation.id,
|
|
@@ -650,8 +672,10 @@ export const aiRoutes = (() => {
|
|
|
650
672
|
conversationId: conversation.id,
|
|
651
673
|
ownerUserId: ctx.ownerUserId,
|
|
652
674
|
projectId: project?.id ?? null,
|
|
675
|
+
onlyUnassigned: body.onlyUnassigned,
|
|
653
676
|
});
|
|
654
677
|
if (!result.ok) {
|
|
678
|
+
if (result.reason === "already_assigned") return respond(c, fail(err.conflict("This chat already belongs to a Project.")));
|
|
655
679
|
if (result.reason === "active_turn") {
|
|
656
680
|
return respond(c, fail(err.conflict("Wait for the active turn before changing the Project.")));
|
|
657
681
|
}
|
|
@@ -683,6 +707,22 @@ export const aiRoutes = (() => {
|
|
|
683
707
|
});
|
|
684
708
|
return updated ? respond(c, ok(await publicConversationFor(updated, c.get("accessSubject")))) : notFound(c);
|
|
685
709
|
})
|
|
710
|
+
.put("/conversations/:conversationId/done", v("json", z.object({ done: z.boolean().nullable() })), async (c) => {
|
|
711
|
+
const ctx = await resolveContext(c);
|
|
712
|
+
if (ctx instanceof Response) return ctx;
|
|
713
|
+
const conversation = await loadConversation(c, ctx);
|
|
714
|
+
if (!conversation) return notFound(c);
|
|
715
|
+
const result = await aiConversations.setConversationDone({
|
|
716
|
+
conversationId: conversation.id,
|
|
717
|
+
ownerUserId: ctx.ownerUserId,
|
|
718
|
+
done: c.req.valid("json").done,
|
|
719
|
+
});
|
|
720
|
+
if (!result.ok)
|
|
721
|
+
return result.reason === "active_turn"
|
|
722
|
+
? respond(c, fail(err.conflict("Stop the current response before marking this chat done.")))
|
|
723
|
+
: notFound(c);
|
|
724
|
+
return respond(c, ok(await publicConversationFor(result.conversation, c.get("accessSubject"))));
|
|
725
|
+
})
|
|
686
726
|
.post("/conversations/:conversationId/archive", async (c) => {
|
|
687
727
|
const ctx = await resolveContext(c);
|
|
688
728
|
if (ctx instanceof Response) return ctx;
|
|
@@ -720,6 +760,37 @@ export const aiRoutes = (() => {
|
|
|
720
760
|
});
|
|
721
761
|
return viewed ? respond(c, ok({ ok: true })) : notFound(c);
|
|
722
762
|
})
|
|
763
|
+
.get("/conversations/:conversationId/queue", async (c) => {
|
|
764
|
+
const ctx = await resolveContext(c);
|
|
765
|
+
if (ctx instanceof Response) return ctx;
|
|
766
|
+
const conversation = await loadConversation(c, ctx);
|
|
767
|
+
return conversation ? respond(c, ok(await listQueuedMessages(conversation.id))) : notFound(c);
|
|
768
|
+
})
|
|
769
|
+
.patch("/conversations/:conversationId/queue/:messageId", v("json", z.object({text:z.string().trim().min(1).max(20000)}).strict()), async (c) => {
|
|
770
|
+
const ctx = await resolveContext(c);
|
|
771
|
+
if (ctx instanceof Response) return ctx;
|
|
772
|
+
const conversation = await loadConversation(c, ctx);
|
|
773
|
+
if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
|
|
774
|
+
return await editQueuedMessage(conversation.id,(c.req.param("messageId") ?? ""),c.req.valid("json").text) ? respond(c,ok({ok:true})) : notFound(c);
|
|
775
|
+
})
|
|
776
|
+
.delete("/conversations/:conversationId/queue/:messageId", async (c) => {
|
|
777
|
+
const ctx = await resolveContext(c);
|
|
778
|
+
if (ctx instanceof Response) return ctx;
|
|
779
|
+
const conversation = await loadConversation(c, ctx);
|
|
780
|
+
if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
|
|
781
|
+
if (!await updateQueuedMessage(conversation.id, (c.req.param("messageId") ?? ""), "cancel")) return notFound(c);
|
|
782
|
+
wakeAiMessageQueue(conversation.id);
|
|
783
|
+
return respond(c, ok({ok:true}));
|
|
784
|
+
})
|
|
785
|
+
.post("/conversations/:conversationId/queue/:messageId/retry", async (c) => {
|
|
786
|
+
const ctx = await resolveContext(c);
|
|
787
|
+
if (ctx instanceof Response) return ctx;
|
|
788
|
+
const conversation = await loadConversation(c, ctx);
|
|
789
|
+
if (!conversation || !z.uuid().safeParse(c.req.param("messageId")).success) return notFound(c);
|
|
790
|
+
if (!await updateQueuedMessage(conversation.id, (c.req.param("messageId") ?? ""), "retry")) return notFound(c);
|
|
791
|
+
wakeAiMessageQueue(conversation.id);
|
|
792
|
+
return respond(c, ok({ok:true}));
|
|
793
|
+
})
|
|
723
794
|
.put("/conversations/:conversationId/draft", v("json", AiSaveConversationDraftInputSchema), async (c) => {
|
|
724
795
|
const ctx = await resolveContext(c);
|
|
725
796
|
if (ctx instanceof Response) return ctx;
|
|
@@ -749,6 +820,7 @@ export const aiRoutes = (() => {
|
|
|
749
820
|
const conversation = await loadConversation(c, ctx);
|
|
750
821
|
if (!conversation) return notFound(c);
|
|
751
822
|
const body = c.req.valid("json");
|
|
823
|
+
if (body.queueId && await queuedMessageAccepted(conversation.id, body.queueId)) return respond(c,ok({queued:true}));
|
|
752
824
|
if (conversation.draft.revision !== body.draftRevision) {
|
|
753
825
|
return respond(c, fail(err.conflict("The conversation draft changed in another session.")));
|
|
754
826
|
}
|
|
@@ -756,6 +828,7 @@ export const aiRoutes = (() => {
|
|
|
756
828
|
const resourceParts = conversation.draft.content.filter((part) => part.type === "resource");
|
|
757
829
|
const turnContent = conversation.draft.content.map((part) => {
|
|
758
830
|
if (part.type === "text") return { type: "text" as const, text: part.text };
|
|
831
|
+
if (part.type === "project-file") return { type: "text" as const, text: `Project file: ${part.path}` };
|
|
759
832
|
if (part.type === "file") {
|
|
760
833
|
return { type: "attachment" as const, path: part.path, mediaType: part.mediaType, size: part.size };
|
|
761
834
|
}
|
|
@@ -768,8 +841,8 @@ export const aiRoutes = (() => {
|
|
|
768
841
|
const project = conversation.projectId ? await aiProjects.snapshot(conversation.projectId, c.get("accessSubject")) : null;
|
|
769
842
|
if (conversation.projectId && !project) return respond(c, fail(err.notFound("Project")));
|
|
770
843
|
try {
|
|
771
|
-
const
|
|
772
|
-
assistantChat: true,
|
|
844
|
+
const submission = {
|
|
845
|
+
assistantChat: true as const,
|
|
773
846
|
conversationId: conversation.id,
|
|
774
847
|
chatId: conversation.shortId,
|
|
775
848
|
input,
|
|
@@ -780,7 +853,7 @@ export const aiRoutes = (() => {
|
|
|
780
853
|
modelPolicy: ctx.modelPolicy,
|
|
781
854
|
project: project ?? undefined,
|
|
782
855
|
clientToolIds: body.clientToolIds,
|
|
783
|
-
toolSource: { kind: "default", appTools: true },
|
|
856
|
+
toolSource: { kind: "default" as const, appTools: true },
|
|
784
857
|
toolApprovalContext: ctx.toolApprovalContext,
|
|
785
858
|
expectedDraftRevision: body.draftRevision,
|
|
786
859
|
expectedProjectId: conversation.projectId,
|
|
@@ -788,7 +861,13 @@ export const aiRoutes = (() => {
|
|
|
788
861
|
expectedFiles: conversation.draft.content.flatMap((part) =>
|
|
789
862
|
part.type === "file" ? [{ path: part.path, version: part.version }] : [],
|
|
790
863
|
),
|
|
791
|
-
}
|
|
864
|
+
};
|
|
865
|
+
if (body.queueId) {
|
|
866
|
+
await enqueueChatMessage(body.queueId, await prepareAiChatTurn(submission), conversation.draft.content);
|
|
867
|
+
wakeAiMessageQueue(conversation.id);
|
|
868
|
+
return respond(c, ok({ queued: true }));
|
|
869
|
+
}
|
|
870
|
+
const result = await submitAiChatTurn(submission);
|
|
792
871
|
rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
|
|
793
872
|
return respond(
|
|
794
873
|
c,
|
|
@@ -803,6 +882,7 @@ export const aiRoutes = (() => {
|
|
|
803
882
|
201,
|
|
804
883
|
);
|
|
805
884
|
} catch (error) {
|
|
885
|
+
if (error instanceof AiMessageQueueConflict || (error instanceof Error && error.message === "Queued messages must be sent first.")) return respond(c,fail(err.conflict(error.message)));
|
|
806
886
|
return toAiErrorResponse(c, error);
|
|
807
887
|
}
|
|
808
888
|
})
|
|
@@ -832,7 +912,7 @@ export const aiRoutes = (() => {
|
|
|
832
912
|
if (ctx instanceof Response) return ctx;
|
|
833
913
|
const conversation = await loadConversation(c, ctx);
|
|
834
914
|
if (!conversation) return notFound(c);
|
|
835
|
-
const messageId = c.req.param("messageId");
|
|
915
|
+
const messageId = (c.req.param("messageId") ?? "");
|
|
836
916
|
|
|
837
917
|
const messages = await aiConversations.listMessages({ conversationId: conversation.id });
|
|
838
918
|
const target = messages.find((m) => m.shortId === messageId);
|
|
@@ -881,6 +961,7 @@ export const aiRoutes = (() => {
|
|
|
881
961
|
truncateFromSeq: target.seq,
|
|
882
962
|
fileSnapshot: originalRunConfig?.kind !== "compact" ? originalRunConfig?.files : undefined,
|
|
883
963
|
retrySourceTurnId: target.loopId ?? undefined,
|
|
964
|
+
clientToolIds: originalRunConfig?.kind !== "compact" ? originalRunConfig?.clientToolIds : undefined,
|
|
884
965
|
});
|
|
885
966
|
rememberLastUsedModel(ctx.actor, result.turn.modelProfileId);
|
|
886
967
|
return respond(
|
|
@@ -904,7 +985,7 @@ export const aiRoutes = (() => {
|
|
|
904
985
|
if (ctx instanceof Response) return ctx;
|
|
905
986
|
const conversation = await loadConversation(c, ctx);
|
|
906
987
|
if (!conversation) return notFound(c);
|
|
907
|
-
const messageId = c.req.param("messageId");
|
|
988
|
+
const messageId = (c.req.param("messageId") ?? "");
|
|
908
989
|
|
|
909
990
|
const messages = await aiConversations.listMessages({ conversationId: conversation.id });
|
|
910
991
|
const target = messages.find((m) => m.shortId === messageId);
|
|
@@ -1029,6 +1110,123 @@ export const aiRoutes = (() => {
|
|
|
1029
1110
|
const files = await aiFileStore.list({ conversationId: conversation.id, prefix: c.req.valid("query").prefix ?? "/" });
|
|
1030
1111
|
return respond(c, ok({ files, totalBytes: await aiFileStore.totalBytes(conversation.id) }));
|
|
1031
1112
|
})
|
|
1113
|
+
.post("/conversations/:conversationId/dictations", bodyLimit({ maxSize: AI_AUDIO_MAX_BYTES + 65_536 }), async (c) => {
|
|
1114
|
+
const ctx = await resolveContext(c);
|
|
1115
|
+
if (ctx instanceof Response) return ctx;
|
|
1116
|
+
const conversation = await loadConversation(c, ctx);
|
|
1117
|
+
if (!conversation) return notFound(c);
|
|
1118
|
+
try {
|
|
1119
|
+
const form = await c.req.formData();
|
|
1120
|
+
const file = form.get("file");
|
|
1121
|
+
if (!(file instanceof File)) return respond(c, fail(err.badInput("Attach one audio recording.")));
|
|
1122
|
+
const options = AiDictationStartSchema.parse({
|
|
1123
|
+
operationId: form.get("operationId"),
|
|
1124
|
+
language: form.get("language") ?? undefined,
|
|
1125
|
+
});
|
|
1126
|
+
const dictation = await aiDictations.start({
|
|
1127
|
+
...options,
|
|
1128
|
+
conversationId: conversation.id,
|
|
1129
|
+
userId: ctx.ownerUserId,
|
|
1130
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
1131
|
+
resolveModel: async () => {
|
|
1132
|
+
const model = await resolveAiAudioModel({ allowedDataBoundaries: ctx.modelPolicy.allowedDataBoundaries });
|
|
1133
|
+
await aiModelAccess.assertAllowed(model.profile.id, c.get("accessSubject"));
|
|
1134
|
+
return model;
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
await enqueueAiDictation(dictation.id).catch(() => undefined); // The DB sweep heals failed delivery.
|
|
1138
|
+
return respond(c, ok(dictation));
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
return respond(
|
|
1141
|
+
c,
|
|
1142
|
+
fail(
|
|
1143
|
+
error instanceof AiDictationConflict
|
|
1144
|
+
? err.conflict(error.message)
|
|
1145
|
+
: err.badInput(error instanceof Error ? error.message : "Recording upload failed."),
|
|
1146
|
+
),
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
})
|
|
1150
|
+
.post("/conversations/:conversationId/dictations/discard", v("json", z.object({ operationId: z.uuid() })), async (c) => {
|
|
1151
|
+
const ctx = await resolveContext(c);
|
|
1152
|
+
if (ctx instanceof Response) return ctx;
|
|
1153
|
+
const conversation = await loadConversation(c, ctx);
|
|
1154
|
+
if (!conversation) return notFound(c);
|
|
1155
|
+
return respond(c, ok(await aiDictations.discardOperation({
|
|
1156
|
+
conversationId: conversation.id, userId: ctx.ownerUserId, operationId: c.req.valid("json").operationId,
|
|
1157
|
+
})));
|
|
1158
|
+
})
|
|
1159
|
+
.get("/conversations/:conversationId/dictations", v("query", AiDictationListSchema), async (c) => {
|
|
1160
|
+
const ctx = await resolveContext(c);
|
|
1161
|
+
if (ctx instanceof Response) return ctx;
|
|
1162
|
+
const conversation = await loadConversation(c, ctx);
|
|
1163
|
+
if (!conversation) return notFound(c);
|
|
1164
|
+
return respond(
|
|
1165
|
+
c,
|
|
1166
|
+
ok(await aiDictations.list({ ...c.req.valid("query"), conversationId: conversation.id, userId: ctx.ownerUserId })),
|
|
1167
|
+
);
|
|
1168
|
+
})
|
|
1169
|
+
.get("/conversations/:conversationId/dictations/:dictationId", async (c) => {
|
|
1170
|
+
const ctx = await resolveContext(c);
|
|
1171
|
+
if (ctx instanceof Response) return ctx;
|
|
1172
|
+
const conversation = await loadConversation(c, ctx);
|
|
1173
|
+
if (!conversation) return notFound(c);
|
|
1174
|
+
const result = await aiDictations.get({
|
|
1175
|
+
conversationId: conversation.id,
|
|
1176
|
+
userId: ctx.ownerUserId,
|
|
1177
|
+
id: z.string().regex(AI_SHORT_ID_PATTERN).parse(c.req.param("dictationId")),
|
|
1178
|
+
});
|
|
1179
|
+
return result ? respond(c, ok(result)) : notFound(c);
|
|
1180
|
+
})
|
|
1181
|
+
.post("/conversations/:conversationId/dictations/:dictationId/apply", v("json", AiSaveConversationDraftInputSchema), async (c) => {
|
|
1182
|
+
const ctx = await resolveContext(c);
|
|
1183
|
+
if (ctx instanceof Response) return ctx;
|
|
1184
|
+
const conversation = await loadConversation(c, ctx);
|
|
1185
|
+
if (!conversation) return notFound(c);
|
|
1186
|
+
try {
|
|
1187
|
+
return respond(
|
|
1188
|
+
c,
|
|
1189
|
+
ok(
|
|
1190
|
+
await aiDictations.apply({
|
|
1191
|
+
...c.req.valid("json"),
|
|
1192
|
+
conversationId: conversation.id,
|
|
1193
|
+
userId: ctx.ownerUserId,
|
|
1194
|
+
id: z.string().regex(AI_SHORT_ID_PATTERN).parse(c.req.param("dictationId")),
|
|
1195
|
+
}),
|
|
1196
|
+
),
|
|
1197
|
+
);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
return respond(
|
|
1200
|
+
c,
|
|
1201
|
+
fail(
|
|
1202
|
+
error instanceof AiDictationConflict
|
|
1203
|
+
? err.conflict(error.message)
|
|
1204
|
+
: err.badInput(error instanceof Error ? error.message : "Dictation could not be inserted."),
|
|
1205
|
+
),
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
})
|
|
1209
|
+
.post(
|
|
1210
|
+
"/conversations/:conversationId/dictations/:dictationId/action",
|
|
1211
|
+
v("json", z.object({ action: z.enum(["discard", "retry"]) })),
|
|
1212
|
+
async (c) => {
|
|
1213
|
+
const ctx = await resolveContext(c);
|
|
1214
|
+
if (ctx instanceof Response) return ctx;
|
|
1215
|
+
const conversation = await loadConversation(c, ctx);
|
|
1216
|
+
if (!conversation) return notFound(c);
|
|
1217
|
+
const input = {
|
|
1218
|
+
conversationId: conversation.id,
|
|
1219
|
+
userId: ctx.ownerUserId,
|
|
1220
|
+
id: z.string().regex(AI_SHORT_ID_PATTERN).parse(c.req.param("dictationId")),
|
|
1221
|
+
};
|
|
1222
|
+
if (c.req.valid("json").action === "discard") await aiDictations.discard(input);
|
|
1223
|
+
else {
|
|
1224
|
+
await aiDictations.retry(input);
|
|
1225
|
+
await enqueueAiDictation(input.id).catch(() => undefined);
|
|
1226
|
+
}
|
|
1227
|
+
return respond(c, ok({ success: true }));
|
|
1228
|
+
},
|
|
1229
|
+
)
|
|
1032
1230
|
.post("/conversations/:conversationId/files", async (c) => {
|
|
1033
1231
|
const ctx = await resolveContext(c);
|
|
1034
1232
|
if (ctx instanceof Response) return ctx;
|
|
@@ -1042,7 +1240,9 @@ export const aiRoutes = (() => {
|
|
|
1042
1240
|
return respond(c, fail(err.badInput(`File exceeds the ${Math.floor(AI_FILES_MAX_FILE_BYTES_DEFAULT / (1024 * 1024))} MB limit`)));
|
|
1043
1241
|
}
|
|
1044
1242
|
const name = (file.name || "upload").replaceAll("/", "_").replaceAll("\\", "_").replaceAll("\0", "").slice(0, 160) || "upload";
|
|
1045
|
-
const
|
|
1243
|
+
const directory=form?.get("directory") ?? "/";
|
|
1244
|
+
if(typeof directory!=="string")return respond(c,fail(err.badInput("Invalid directory")));
|
|
1245
|
+
const path = normalizeAiFilePath(`${directory.replace(/\/$/,"")}/${name}`);
|
|
1046
1246
|
if (!path) return respond(c, fail(err.badInput("Invalid file name")));
|
|
1047
1247
|
if (aiProjectFilePathFromMount(path) !== null) return respond(c, fail(err.badInput("The /project namespace is reserved.")));
|
|
1048
1248
|
|
|
@@ -1055,6 +1255,10 @@ export const aiRoutes = (() => {
|
|
|
1055
1255
|
});
|
|
1056
1256
|
return respond(c, ok({ file: stat }));
|
|
1057
1257
|
} catch (error) {
|
|
1258
|
+
logger("ai:files").warn("Conversation upload failed", {
|
|
1259
|
+
code: "file_upload_failed", conversationId: conversation.id,
|
|
1260
|
+
errorType: error instanceof Error ? error.name : "unknown",
|
|
1261
|
+
});
|
|
1058
1262
|
return respond(c, fail(err.badInput(error instanceof Error ? error.message : "Upload failed")));
|
|
1059
1263
|
}
|
|
1060
1264
|
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Preserve why execution stopped instead of reporting an operator deadline as a user abort. */
|
|
2
|
+
export class AiRunTimeout extends Error {
|
|
3
|
+
constructor(readonly budgetMs: number | null) { super("AI_RUN_TIMEOUT"); }
|
|
4
|
+
messageFor(locale?: string): string {
|
|
5
|
+
const minutes = this.budgetMs ? this.budgetMs / 60_000 : null;
|
|
6
|
+
return locale?.startsWith("de")
|
|
7
|
+
? `Laufzeitlimit${minutes ? ` von ${minutes} Minuten` : ""} erreicht. Du kannst die Aufgabe mit einer neuen Nachricht fortsetzen.`
|
|
8
|
+
: `Run time limit${minutes ? ` of ${minutes} minutes` : ""} reached. You can continue the task with a new message.`;
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/ai/runtime-tools.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export {
|
|
2
2
|
CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES,
|
|
3
3
|
createCloudAiCardTool,
|
|
4
|
+
createCloudAiCodeTools,
|
|
4
5
|
createCloudAiLocalBashTool,
|
|
5
6
|
createCloudAiSurveyTool,
|
|
6
7
|
createCloudAiTextEditorTool,
|
|
@@ -37,3 +38,5 @@ export {
|
|
|
37
38
|
createCloudAiMarkdownToPdfTool,
|
|
38
39
|
} from "./markdown-pdf-tool";
|
|
39
40
|
export { createCloudAiViewImageTool } from "./vision-tool";
|
|
41
|
+
|
|
42
|
+
export { createCloudAiTranscribeAudioTool, CloudAiTranscribeAudioInputSchema, CloudAiTranscribeAudioOutputSchema } from "./audio-tool";
|
package/src/ai/runtime.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { parseAiResourceMarker } from "./resource-markers";
|
|
2
|
+
import { aiQuotas } from "./quotas";
|
|
3
|
+
import { drainQueuedMessages } from "./message-queue";
|
|
4
|
+
import { coreSettings } from "../services/settings/api";
|
|
5
|
+
import { startAiDictationRuntime } from "./dictation-runtime";
|
|
1
6
|
import { isDeepStrictEqual } from "node:util";
|
|
2
7
|
import type { InboundEvent, Input, Message } from "@k2b/nessi";
|
|
3
8
|
import type { QueueMessage } from "@k2b/sync";
|
|
@@ -44,7 +49,7 @@ const AI_TURN_LEASE_MS = 45_000;
|
|
|
44
49
|
const AI_TURN_HEARTBEAT_MS = 3_000;
|
|
45
50
|
const AI_TURN_WORKER_CONCURRENCY = 8;
|
|
46
51
|
const AI_TURN_MAX_ATTEMPTS = 5;
|
|
47
|
-
|
|
52
|
+
|
|
48
53
|
const AI_SWEEP_INTERVAL_MS = 15_000;
|
|
49
54
|
|
|
50
55
|
// ---------------------------------------------------------------------------
|
|
@@ -65,7 +70,14 @@ const aiTurnQueue = lazySync((sync) => {
|
|
|
65
70
|
|
|
66
71
|
// No idempotency key: the DB claim is the only gate, so re-enqueues (recovery,
|
|
67
72
|
// continuation, stale-sweep) are always allowed and never silently swallowed.
|
|
68
|
-
const enqueueAiTurn = (job: AiTurnJob): Promise<unknown> => aiTurnQueue().send({ data: job, orderingKey: job.
|
|
73
|
+
const enqueueAiTurn = (job: AiTurnJob): Promise<unknown> => aiTurnQueue().send({ data: job, orderingKey: job.turnId });
|
|
74
|
+
|
|
75
|
+
/** Wake the durable queue promptly; the periodic sweep retries a failed wake-up. */
|
|
76
|
+
export const wakeAiMessageQueue = (conversationId: string): void => {
|
|
77
|
+
void drainQueuedMessages(enqueueAiTurn, conversationId).catch(() =>
|
|
78
|
+
log.warn("AI message queue wake-up failed", { code: "queue_wakeup_failed", conversationId }),
|
|
79
|
+
);
|
|
80
|
+
};
|
|
69
81
|
|
|
70
82
|
/** Enqueue a turn created atomically by another durable AI workflow. */
|
|
71
83
|
export const enqueueExistingAiTurn = (input: AiTurnJob): Promise<unknown> => enqueueAiTurn(input);
|
|
@@ -103,7 +115,7 @@ export type SubmitAiChatTurnInput = {
|
|
|
103
115
|
retrySourceTurnId?: string;
|
|
104
116
|
};
|
|
105
117
|
|
|
106
|
-
export const
|
|
118
|
+
export const prepareAiChatTurn = async (input: SubmitAiChatTurnInput) => {
|
|
107
119
|
if (input.clientToolIds?.length && input.toolSource?.kind !== "default") {
|
|
108
120
|
throw new Error("Optional client tools require the default tool source.");
|
|
109
121
|
}
|
|
@@ -128,8 +140,14 @@ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{
|
|
|
128
140
|
modelPolicy: input.modelPolicy,
|
|
129
141
|
requestedModelId,
|
|
130
142
|
});
|
|
143
|
+
const quotaSubject = aiChatAccessSubject(input.actor);
|
|
144
|
+
if (input.assistantChat && quotaSubject) await aiQuotas.assertAllowed(quotaSubject, resolved.profile.id);
|
|
131
145
|
const runConfig: AiChatTurnRunConfig = {
|
|
132
146
|
kind: "chat",
|
|
147
|
+
selectedSkillIds: [...new Set((userMessage.role === "user" ? userMessage.content : []).flatMap(part => {
|
|
148
|
+
const marker = parseAiResourceMarker(typeof part === "string" ? part : part.type === "text" ? part.text : "");
|
|
149
|
+
return marker?.ref.type === "core.ai.skill" ? [marker.ref.id] : [];
|
|
150
|
+
}))],
|
|
133
151
|
...(input.assistantChat ? { assistantChat: true } : {}),
|
|
134
152
|
input: canonicalInput,
|
|
135
153
|
chatId: input.chatId,
|
|
@@ -146,7 +164,7 @@ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{
|
|
|
146
164
|
toolApprovalContext: input.toolApprovalContext,
|
|
147
165
|
};
|
|
148
166
|
|
|
149
|
-
|
|
167
|
+
return {
|
|
150
168
|
conversationId: input.conversationId,
|
|
151
169
|
modelProfileId: resolved.profile.id,
|
|
152
170
|
runConfig,
|
|
@@ -156,8 +174,11 @@ export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{
|
|
|
156
174
|
expectedProjectId: input.expectedProjectId,
|
|
157
175
|
resources: input.resources,
|
|
158
176
|
retrySourceTurnId: input.retrySourceTurnId,
|
|
159
|
-
}
|
|
177
|
+
};
|
|
178
|
+
};
|
|
160
179
|
|
|
180
|
+
export const submitAiChatTurn = async (input: SubmitAiChatTurnInput): Promise<{ turn: AiTurn; message: AiStoredMessage }> => {
|
|
181
|
+
const submitted = await aiConversations.submitChatTurn(await prepareAiChatTurn(input));
|
|
161
182
|
await enqueueAiTurn({ conversationId: input.conversationId, turnId: submitted.turn.id });
|
|
162
183
|
return submitted;
|
|
163
184
|
};
|
|
@@ -242,7 +263,8 @@ export const abortAiTurn = async (input: { conversationId: string; turnId: strin
|
|
|
242
263
|
|
|
243
264
|
if (request.ownerless) {
|
|
244
265
|
const finalized = await aiConversations.completeTurn({ ...input, status: "aborted", error: null });
|
|
245
|
-
|
|
266
|
+
const config = await aiConversations.getTurnRunConfig(input);
|
|
267
|
+
if (finalized === "completed" && !(config?.kind === "chat" && config.background)) {
|
|
246
268
|
const attempt = active?.turn.id === input.turnId ? active.turn.attempt : 1;
|
|
247
269
|
const seq = (active?.turn.id === input.turnId ? active.liveSeq : 0) + 1;
|
|
248
270
|
await publishAiWireEvent({
|
|
@@ -343,9 +365,7 @@ export const submitAiTurnAction = async (input: {
|
|
|
343
365
|
return { ok: false, status: 409, message: "AI action was already resolved with a different response." };
|
|
344
366
|
}
|
|
345
367
|
}
|
|
346
|
-
await aiToolAudit
|
|
347
|
-
.noteToolCompleted({ turnId: input.turnId, callId: input.callId, result: input.action.result, isError: false })
|
|
348
|
-
.catch(() => undefined);
|
|
368
|
+
await aiToolAudit.noteToolCompleted({ turnId: input.turnId, callId: input.callId, isError: false }).catch(() => undefined);
|
|
349
369
|
}
|
|
350
370
|
|
|
351
371
|
await enqueueAiTurn({ conversationId: input.conversationId, turnId: input.turnId });
|
|
@@ -362,6 +382,8 @@ const runClaimedTurn = async (
|
|
|
362
382
|
leaseOwner: string,
|
|
363
383
|
onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>,
|
|
364
384
|
): Promise<void> => {
|
|
385
|
+
const timeoutMinutes = await coreSettings.get<number>("ai.turn_timeout_minutes");
|
|
386
|
+
const runBudgetMs = Math.max(0, Number(timeoutMinutes ?? 30)) * 60_000;
|
|
365
387
|
const claim =
|
|
366
388
|
(await aiConversations.claimTurn({
|
|
367
389
|
...job,
|
|
@@ -369,7 +391,7 @@ const runClaimedTurn = async (
|
|
|
369
391
|
leaseMs: AI_TURN_LEASE_MS,
|
|
370
392
|
from: "queue",
|
|
371
393
|
maxAttempts: AI_TURN_MAX_ATTEMPTS,
|
|
372
|
-
runBudgetMs
|
|
394
|
+
runBudgetMs,
|
|
373
395
|
})) ??
|
|
374
396
|
(await aiConversations.claimTurn({
|
|
375
397
|
...job,
|
|
@@ -377,7 +399,7 @@ const runClaimedTurn = async (
|
|
|
377
399
|
leaseMs: AI_TURN_LEASE_MS,
|
|
378
400
|
from: "waiting",
|
|
379
401
|
maxAttempts: AI_TURN_MAX_ATTEMPTS,
|
|
380
|
-
runBudgetMs
|
|
402
|
+
runBudgetMs,
|
|
381
403
|
}));
|
|
382
404
|
|
|
383
405
|
if (!claim) return; // Already owned, done, cancelled, or attempt-capped.
|
|
@@ -392,12 +414,22 @@ const runClaimedTurn = async (
|
|
|
392
414
|
};
|
|
393
415
|
|
|
394
416
|
const processMessage = async (
|
|
395
|
-
message: QueueMessage<AiTurnJob>,
|
|
417
|
+
message: Pick<QueueMessage<AiTurnJob>, "messageId" | "data" | "heartbeat">,
|
|
396
418
|
signal: AbortSignal,
|
|
397
419
|
onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>,
|
|
398
420
|
): Promise<void> => {
|
|
399
421
|
const leaseOwner = `${AI_WORKER_ID}:${message.messageId}:${crypto.randomUUID()}`;
|
|
400
|
-
|
|
422
|
+
let heartbeatFailed = false;
|
|
423
|
+
const touch = setInterval(() => {
|
|
424
|
+
void message.heartbeat().then(() => { heartbeatFailed = false; }).catch(() => {
|
|
425
|
+
if (!heartbeatFailed && !signal.aborted) {
|
|
426
|
+
log.warn("AI queue heartbeat failed", {
|
|
427
|
+
code: "queue_heartbeat_failed", conversationId: message.data.conversationId, turnId: message.data.turnId,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
heartbeatFailed = true;
|
|
431
|
+
});
|
|
432
|
+
}, AI_TURN_HEARTBEAT_MS);
|
|
401
433
|
if (typeof touch === "object" && "unref" in touch) touch.unref();
|
|
402
434
|
try {
|
|
403
435
|
await runClaimedTurn(message.data, signal, leaseOwner, onTurnFinalized);
|
|
@@ -409,10 +441,13 @@ const processMessage = async (
|
|
|
409
441
|
});
|
|
410
442
|
} finally {
|
|
411
443
|
clearInterval(touch);
|
|
444
|
+
if (!signal.aborted) wakeAiMessageQueue(message.data.conversationId);
|
|
412
445
|
}
|
|
413
446
|
};
|
|
414
447
|
|
|
415
448
|
const publishSweepFinished = async (turn: AiTurnFinalizedAction & { error?: string }, status: "failed" | "aborted"): Promise<void> => {
|
|
449
|
+
const config = await aiConversations.getTurnRunConfig(turn);
|
|
450
|
+
if (config?.kind === "chat" && config.background) return;
|
|
416
451
|
await publishAiWireEvent({
|
|
417
452
|
v: 1,
|
|
418
453
|
conversationId: turn.conversationId,
|
|
@@ -422,13 +457,15 @@ const publishSweepFinished = async (turn: AiTurnFinalizedAction & { error?: stri
|
|
|
422
457
|
type: "turn_finished",
|
|
423
458
|
status,
|
|
424
459
|
error: status === "failed" ? (turn.error ?? "AI turn failed.") : null,
|
|
425
|
-
}).catch(() =>
|
|
460
|
+
}).catch(() => log.warn("AI completion notification failed", {code:"completion_publish_failed",conversationId:turn.conversationId,turnId:turn.turnId}));
|
|
426
461
|
};
|
|
427
462
|
|
|
428
463
|
export const sweepAiRuntime = async (onTurnFinalized?: (event: AiTurnFinalizedEvent) => Promise<void>): Promise<void> => {
|
|
464
|
+
await aiQuotas.prune().catch(() => log.warn("Quota retention failed", { code: "quota_retention_failed" }));
|
|
465
|
+
await drainQueuedMessages(enqueueAiTurn);
|
|
429
466
|
const sweep = await aiConversations.sweepTurns({ maxAttempts: AI_TURN_MAX_ATTEMPTS });
|
|
430
467
|
await Promise.all([
|
|
431
|
-
...sweep.requeued.map((job) => enqueueAiTurn(job).catch(() =>
|
|
468
|
+
...sweep.requeued.map((job) => enqueueAiTurn(job).catch(() => log.warn("AI recovery enqueue failed", {code:"recovery_enqueue_failed",...job}))),
|
|
432
469
|
...sweep.failed.map((turn) => publishSweepFinished(turn, "failed")),
|
|
433
470
|
...sweep.aborted.map((turn) => publishSweepFinished(turn, "aborted")),
|
|
434
471
|
]);
|
|
@@ -508,11 +545,13 @@ export const startAiRuntime = (
|
|
|
508
545
|
const sweepTimer = setInterval(runSweep, AI_SWEEP_INTERVAL_MS);
|
|
509
546
|
if (typeof sweepTimer === "object" && "unref" in sweepTimer) sweepTimer.unref();
|
|
510
547
|
|
|
548
|
+
const stopDictations = startAiDictationRuntime();
|
|
511
549
|
const state: AiRuntimeState = {
|
|
512
550
|
listeners,
|
|
513
551
|
stop: () => {
|
|
514
552
|
if (running !== state) return;
|
|
515
553
|
controller.abort();
|
|
554
|
+
stopDictations();
|
|
516
555
|
clearInterval(sweepTimer);
|
|
517
556
|
listeners.clear();
|
|
518
557
|
running = null;
|
|
@@ -541,4 +580,4 @@ const releaseAiRuntime = (listener: ((event: AiTurnFinalizedEvent) => Promise<vo
|
|
|
541
580
|
};
|
|
542
581
|
};
|
|
543
582
|
|
|
544
|
-
export const __aiRuntimeTest = { actionMatchesResolvedEvent };
|
|
583
|
+
export const __aiRuntimeTest = { actionMatchesResolvedEvent, processMessage, publishSweepFinished };
|