@k2b/cloud 0.7.0 → 0.9.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 +5 -4
- package/scripts/README.md +10 -0
- package/scripts/build.ts +14 -1
- package/scripts/preload.ts +7 -0
- package/scripts/sync-recovery-smoke.ts +21 -6
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +35 -12
- package/src/_internal/define-app.ts +28 -11
- package/src/_internal/page-responses.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/runtime-context.ts +1 -1
- package/src/access/PermissionEditor.tsx +15 -9
- package/src/access/PrincipalPicker.tsx +9 -3
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -2
- package/src/ai/admin.ts +2 -2
- package/src/ai/assistant-models.ts +1 -1
- package/src/ai/audio-tool.ts +1 -1
- package/src/ai/browser-code-contracts.ts +174 -52
- package/src/ai/browser.ts +4 -2
- package/src/ai/capabilities.ts +3 -3
- package/src/ai/chat/blocks.tsx +127 -50
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-table.tsx +1 -1
- package/src/ai/chat/composer-adapter.ts +48 -13
- package/src/ai/chat/message-utils.ts +18 -5
- package/src/ai/chat/presentation.tsx +24 -11
- package/src/ai/chat/tool-disclosure.tsx +6 -2
- package/src/ai/chat/tool-groups.ts +8 -2
- package/src/ai/chat-quotas.ts +2 -2
- package/src/ai/chat-task-contracts.ts +9 -13
- package/src/ai/chat-tasks.ts +39 -18
- package/src/ai/client/controller.ts +87 -38
- package/src/ai/client/projection.ts +13 -6
- package/src/ai/client/transport.ts +4 -2
- package/src/ai/code-capability-routes.ts +22 -27
- package/src/ai/code-capability-transport.ts +2 -1
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +9 -5
- package/src/ai/code-runtime-tools.ts +16 -3
- package/src/ai/code-source-contracts.ts +100 -32
- package/src/ai/code-source-tools.ts +49 -6
- package/src/ai/compaction.ts +3 -3
- package/src/ai/default-tools.ts +87 -14
- package/src/ai/dictation-runtime.ts +1 -1
- package/src/ai/draft-content.ts +27 -12
- package/src/ai/enrich.ts +1 -1
- package/src/ai/executor.ts +8 -12
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +6 -1
- package/src/ai/file-tools.ts +8 -2
- package/src/ai/files-store.ts +55 -17
- package/src/ai/grids-skill.ts +2 -0
- package/src/ai/http.ts +18 -7
- package/src/ai/index.ts +24 -25
- package/src/ai/inference-calls.ts +3 -4
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +5 -5
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +23 -12
- package/src/ai/migrate.ts +6 -4
- package/src/ai/model-pricing.ts +4 -4
- package/src/ai/pdf-render-worker.ts +3 -2
- package/src/ai/projects-routes.ts +15 -40
- package/src/ai/projects.ts +18 -13
- package/src/ai/quota-provider.ts +6 -5
- package/src/ai/quota-report.ts +5 -5
- package/src/ai/quotas.ts +3 -3
- package/src/ai/routes.ts +60 -34
- package/src/ai/run-timeout.ts +3 -1
- package/src/ai/runtime-tools.ts +6 -7
- package/src/ai/runtime.ts +38 -20
- package/src/ai/settings.ts +1 -1
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-search.ts +21 -10
- package/src/ai/skill-seeds.ts +16 -7
- package/src/ai/skill-tool.ts +6 -1
- package/src/ai/skills-routes.ts +32 -15
- package/src/ai/skills.ts +54 -28
- package/src/ai/store.ts +80 -47
- package/src/ai/structured.ts +6 -2
- package/src/ai/system-prompt.ts +7 -5
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/todo-contracts.ts +24 -12
- package/src/ai/todo-tool.ts +16 -13
- package/src/ai/tools.ts +1 -1
- package/src/ai/transcription.ts +4 -4
- package/src/ai/turn-timing.ts +1 -1
- package/src/ai/ui.tsx +2 -1
- package/src/ai/usage.ts +1 -1
- package/src/ai/vision-tool.ts +38 -17
- package/src/api/admin-ai-quotas.ts +14 -8
- package/src/api/admin-ai-skills.ts +3 -3
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +2 -2
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth/schemas.ts +7 -1
- package/src/api/auth.ts +4 -3
- package/src/api/capabilities.ts +45 -13
- package/src/api/index.ts +5 -5
- package/src/api/mcp.ts +5 -12
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-bridge.ts +1 -1
- package/src/browser/command-shortcuts.ts +1 -1
- package/src/browser/commands.ts +8 -7
- package/src/browser/testing.ts +2 -1
- package/src/capabilities/client.ts +1 -1
- package/src/capabilities/command-link.ts +1 -1
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/ai-quotas.ts +5 -5
- package/src/cli/admin/app-credentials.ts +7 -9
- package/src/cli/admin/index.ts +3 -3
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -29
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -54
- package/src/config/index.ts +3 -1
- package/src/contracts/capabilities.ts +8 -1
- package/src/contracts/index.ts +5 -7
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +12 -3
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -2
- package/src/server/app-assets.ts +27 -0
- package/src/server/index.ts +3 -2
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +3 -3
- package/src/server/services/access.ts +3 -1
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/announcements/index.ts +4 -4
- package/src/services/app-approval.ts +11 -10
- package/src/services/auth-flows/magic-link.ts +13 -1
- package/src/services/identity/key-config.ts +6 -4
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/logging/index.ts +1 -1
- package/src/services/logging/trace.ts +1 -1
- package/src/services/mandates/index.ts +5 -2
- package/src/services/mandates/policy.ts +106 -6
- package/src/services/notifications/batches.ts +1 -0
- package/src/services/pdf/gotenberg.ts +50 -14
- package/src/services/pdf/index.ts +11 -11
- package/src/services/public-http.ts +1 -1
- package/src/services/rail-shortcuts.ts +1 -1
- package/src/services/rail-snapshot.ts +1 -1
- package/src/services/request-cache-redis.ts +2 -1
- package/src/services/session/user.ts +1 -1
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +6 -8
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +3 -3
- package/src/services/webauthn.ts +1 -1
- package/src/shared/index.ts +3 -6
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminSidebar.tsx +2 -2
- package/src/ssr/AppLaunchpad.island.tsx +12 -2
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/GlobalSearchDialog.tsx +6 -6
- package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
- package/src/ssr/Layout.tsx +4 -3
- package/src/ssr/LayoutHeader.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +4 -6
- package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
- package/src/ssr/LayoutRail.tsx +3 -2
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/ProfilePreferences.island.tsx +1 -1
- package/src/ssr/RailApps.island.tsx +7 -7
- package/src/ssr/admin-navigation.ts +6 -2
- package/src/ssr/index.ts +1 -0
- package/src/ssr/islands/index.ts +2 -2
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/rail-navigation.ts +3 -2
- package/src/ssr/request-path.ts +12 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +16 -91
- package/src/workflows/ai/store.ts +1 -1
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/store/index.ts +2 -2
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/session/test-fixture.ts +0 -18
|
@@ -108,10 +108,10 @@ export const aiComposerAttachmentRecords = (attachments: readonly ChatAttachment
|
|
|
108
108
|
|
|
109
109
|
export const aiComposerSendInput = (input: ChatSubmitInput): AiComposerSendInput => {
|
|
110
110
|
const attachments = aiComposerAttachmentRecords(input.attachments);
|
|
111
|
-
if (input.mentions?.length || attachments.some(attachment => attachment.kind === "project-file")) {
|
|
111
|
+
if (input.mentions?.length || attachments.some((attachment) => attachment.kind === "project-file")) {
|
|
112
112
|
const draftContent: AiDraftContentPart[] = [];
|
|
113
113
|
let offset = 0;
|
|
114
|
-
for (const mention of [...(input.mentions ?? [])].sort((a,b) => a.start - b.start)) {
|
|
114
|
+
for (const mention of [...(input.mentions ?? [])].sort((a, b) => a.start - b.start)) {
|
|
115
115
|
if (mention.start < offset || mention.end > input.text.length || mention.start >= mention.end) continue;
|
|
116
116
|
const attachment = aiComposerAttachmentRecords([mention.attachment])[0];
|
|
117
117
|
if (!attachment || attachment.kind === "file" || attachment.kind === "image") continue;
|
|
@@ -123,7 +123,7 @@ export const aiComposerSendInput = (input: ChatSubmitInput): AiComposerSendInput
|
|
|
123
123
|
for (const attachment of attachments) {
|
|
124
124
|
if (attachment.kind !== "file" && attachment.kind !== "image") draftContent.push(composerAttachmentPart(attachment));
|
|
125
125
|
}
|
|
126
|
-
const files = attachments.flatMap(attachment => attachment.kind === "file" || attachment.kind === "image" ? [attachment.file] : []);
|
|
126
|
+
const files = attachments.flatMap((attachment) => (attachment.kind === "file" || attachment.kind === "image" ? [attachment.file] : []));
|
|
127
127
|
return { draftContent, files: files.length ? files : undefined };
|
|
128
128
|
}
|
|
129
129
|
const files = attachments.filter(
|
|
@@ -187,24 +187,59 @@ export const readAiComposerFiles = async (
|
|
|
187
187
|
};
|
|
188
188
|
};
|
|
189
189
|
|
|
190
|
-
const composerAttachmentPart = (
|
|
191
|
-
|
|
190
|
+
const composerAttachmentPart = (
|
|
191
|
+
attachment: Exclude<AiComposerAttachment, { kind: "image" | "file" }>,
|
|
192
|
+
inline = false,
|
|
193
|
+
): AiDraftContentPart => {
|
|
194
|
+
if (attachment.kind === "resource")
|
|
195
|
+
return { type: "resource", ref: attachment.ref, title: attachment.name, icon: attachment.icon, href: attachment.href, inline };
|
|
192
196
|
if (attachment.kind === "project-file") return { type: "project-file", path: attachment.path, inline };
|
|
193
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
type: "file",
|
|
199
|
+
path: attachment.path,
|
|
200
|
+
version: attachment.version,
|
|
201
|
+
mediaType: attachment.mediaType,
|
|
202
|
+
size: attachment.size,
|
|
203
|
+
inline,
|
|
204
|
+
};
|
|
194
205
|
};
|
|
195
206
|
|
|
196
207
|
/** Hydrate the same ordered content used by the server draft and queued messages. */
|
|
197
|
-
export function aiComposerDraft(content: readonly AiDraftContentPart[]): {
|
|
208
|
+
export function aiComposerDraft(content: readonly AiDraftContentPart[]): {
|
|
209
|
+
text: string;
|
|
210
|
+
mentions: ChatMention[];
|
|
211
|
+
attachments: AiComposerAttachment[];
|
|
212
|
+
} {
|
|
198
213
|
let text = "";
|
|
199
214
|
const mentions: ChatMention[] = [];
|
|
200
215
|
const attachments: AiComposerAttachment[] = [];
|
|
201
216
|
for (const part of content) {
|
|
202
|
-
if (part.type === "text") {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
217
|
+
if (part.type === "text") {
|
|
218
|
+
text += part.text;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const attachment: AiComposerAttachment =
|
|
222
|
+
part.type === "resource"
|
|
223
|
+
? {
|
|
224
|
+
kind: "resource",
|
|
225
|
+
id: `resource:${part.ref.type}:${part.ref.id}`,
|
|
226
|
+
ref: part.ref,
|
|
227
|
+
name: part.title ?? part.ref.id,
|
|
228
|
+
icon: part.icon ?? "ti ti-link",
|
|
229
|
+
href: part.href,
|
|
230
|
+
}
|
|
231
|
+
: part.type === "project-file"
|
|
232
|
+
? { kind: "project-file", id: part.path, path: part.path, name: part.path, icon: "ti ti-file" }
|
|
233
|
+
: {
|
|
234
|
+
kind: "stored-file",
|
|
235
|
+
id: `file:${part.path}:${part.version}`,
|
|
236
|
+
path: part.path,
|
|
237
|
+
name: part.path.split("/").at(-1) || part.path,
|
|
238
|
+
version: part.version,
|
|
239
|
+
mediaType: part.mediaType,
|
|
240
|
+
size: part.size,
|
|
241
|
+
icon: "ti ti-file",
|
|
242
|
+
};
|
|
208
243
|
if (part.inline) {
|
|
209
244
|
mentions.push({ start: text.length, end: text.length + attachment.name.length, attachment: aiChatAttachments([attachment])[0]! });
|
|
210
245
|
text += attachment.name;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { AI_AUDIO_EXTENSIONS } from "../audio-format";
|
|
2
1
|
import type { Message, Usage } from "@k2b/nessi";
|
|
3
2
|
import { fileIcons } from "@k2b/stdlib";
|
|
4
3
|
import { formatBytes as sharedFormatBytes } from "../../shared/format";
|
|
5
4
|
import { type AiAttachmentRef, parseAiAttachmentMarkers } from "../attachments";
|
|
5
|
+
import { AI_AUDIO_EXTENSIONS } from "../audio-format";
|
|
6
6
|
import { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS } from "../limits";
|
|
7
7
|
import { type AiResourceMarker, parseAiResourceMarker } from "../resource-markers";
|
|
8
8
|
import { assistantVisibleTextFromMessage } from "../timeline";
|
|
@@ -237,10 +237,23 @@ export const latestUsageSnapshot = (messages: AiStoredMessage[]): AiLatestUsageS
|
|
|
237
237
|
if (request) {
|
|
238
238
|
return {
|
|
239
239
|
request,
|
|
240
|
-
loop:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
loop:
|
|
241
|
+
entry?.loopAggregate?.usage ??
|
|
242
|
+
(entry?.loopId
|
|
243
|
+
? messages
|
|
244
|
+
.filter((message) => message.loopId === entry.loopId && message.message.role === "assistant")
|
|
245
|
+
.reduce<Usage>(
|
|
246
|
+
(sum, message) => ({
|
|
247
|
+
input: sum.input + (message.usage?.input ?? 0),
|
|
248
|
+
output: sum.output + (message.usage?.output ?? 0),
|
|
249
|
+
total: sum.total + (message.usage?.total ?? 0),
|
|
250
|
+
...(sum.cacheRead !== undefined || message.usage?.cacheRead !== undefined
|
|
251
|
+
? { cacheRead: (sum.cacheRead ?? 0) + (message.usage?.cacheRead ?? 0) }
|
|
252
|
+
: {}),
|
|
253
|
+
}),
|
|
254
|
+
{ input: 0, output: 0, total: 0 },
|
|
255
|
+
)
|
|
256
|
+
: request),
|
|
244
257
|
modelProfileId: entry?.modelProfileId ?? null,
|
|
245
258
|
};
|
|
246
259
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Button, Chat, type ChatTimelineItem, useLocale } from "@k2b/ui";
|
|
2
2
|
import { type Accessor, createEffect, createMemo, createSignal, type JSX, onCleanup, Show } from "solid-js";
|
|
3
3
|
import type { AiActiveTurn } from "../client/projection";
|
|
4
|
-
import { type AiActiveTurnSegment,
|
|
4
|
+
import { type AiActiveTurnSegment, splitActiveTurnBlocks } from "../protocol";
|
|
5
5
|
import { type AiAssistantTimelineItem, buildAiMessageTimeline, copyTextFromAssistantEntries } from "../timeline";
|
|
6
6
|
import type { AiConversationTimelineEntry, AiStoredMessage } from "../types";
|
|
7
7
|
import { AiTurnBlockList } from "./blocks";
|
|
8
8
|
import { type AiChatActions, AiChatActionsProvider, createAssistantMessageActions, useAiChatActions } from "./message-actions";
|
|
9
9
|
import { formatWorkedDuration, isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
|
|
10
|
-
import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
|
|
11
10
|
import { aiChatMessages } from "./messages";
|
|
12
|
-
import {
|
|
11
|
+
import { type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
|
|
13
12
|
import { TurnNavigator } from "./turn-navigator";
|
|
14
13
|
import { activeTimelineSeq } from "./turn-navigator-utils";
|
|
15
14
|
import {
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
createAiSteerMessageActions,
|
|
20
19
|
createAiUserMessageActions,
|
|
21
20
|
} from "./user-message";
|
|
21
|
+
import { CloudSurveyResultBlock } from "./visual-tools";
|
|
22
22
|
|
|
23
23
|
export type AiChatTimelineSession = {
|
|
24
24
|
messages: readonly AiStoredMessage[];
|
|
@@ -70,10 +70,15 @@ export function AiAssistantContent(props: {
|
|
|
70
70
|
}): JSX.Element {
|
|
71
71
|
const locale = useLocale();
|
|
72
72
|
const turnId = () => props.item.loopId ?? props.item.id;
|
|
73
|
-
return
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
return (
|
|
74
|
+
<div class="flex flex-col gap-2">
|
|
75
|
+
<Chat.Activity
|
|
76
|
+
icon="ti ti-route"
|
|
77
|
+
label={`${locale().startsWith("de") ? "Gearbeitet für" : "Worked for"} ${formatWorkedDuration(props.item.workedMs)}`}
|
|
78
|
+
/>
|
|
79
|
+
<AiTurnBlockList blocks={props.item.blocks} turnId={turnId()} disclosureState={props.disclosureState} />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
const storedItems = (
|
|
@@ -171,10 +176,18 @@ const storedItems = (
|
|
|
171
176
|
class: segment.blocks.some(isWideBlock) ? "ai-chat-message-wide" : undefined,
|
|
172
177
|
content: (
|
|
173
178
|
<>
|
|
174
|
-
<Show when={index === 0 && item.entries.find(entry => entry.meta?.scheduledTask)?.meta?.scheduledTask}>
|
|
175
|
-
{task
|
|
176
|
-
<
|
|
177
|
-
|
|
179
|
+
<Show when={index === 0 && item.entries.find((entry) => entry.meta?.scheduledTask)?.meta?.scheduledTask}>
|
|
180
|
+
{(task) => (
|
|
181
|
+
<Button
|
|
182
|
+
variant="ghost"
|
|
183
|
+
size="xs"
|
|
184
|
+
class="mb-2"
|
|
185
|
+
onClick={() => actions.onOpenScheduledTaskRun?.(task().taskId, task().occurrenceId)}
|
|
186
|
+
disabled={!actions.onOpenScheduledTaskRun}
|
|
187
|
+
>
|
|
188
|
+
<i class="ti ti-calendar-time" aria-hidden="true" /> {aiChatMessages(locale).backgroundRun}
|
|
189
|
+
</Button>
|
|
190
|
+
)}
|
|
178
191
|
</Show>
|
|
179
192
|
<AiAssistantContent
|
|
180
193
|
item={{ ...item, blocks: segment.blocks }}
|
|
@@ -15,7 +15,9 @@ export const createAiToolDisclosureState = (): AiToolDisclosureState => {
|
|
|
15
15
|
try {
|
|
16
16
|
const saved = window.sessionStorage.getItem(storageKey(blockId));
|
|
17
17
|
if (saved === "open") openByBlockId.set(blockId, true);
|
|
18
|
-
} catch {
|
|
18
|
+
} catch {
|
|
19
|
+
/* SSR and disabled storage keep the in-memory state. */
|
|
20
|
+
}
|
|
19
21
|
}
|
|
20
22
|
return openByBlockId.get(blockId);
|
|
21
23
|
},
|
|
@@ -24,7 +26,9 @@ export const createAiToolDisclosureState = (): AiToolDisclosureState => {
|
|
|
24
26
|
try {
|
|
25
27
|
if (open) window.sessionStorage.setItem(storageKey(blockId), "open");
|
|
26
28
|
else window.sessionStorage.removeItem(storageKey(blockId));
|
|
27
|
-
} catch {
|
|
29
|
+
} catch {
|
|
30
|
+
/* Storage limits must not prevent expanding a tool. */
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
};
|
|
30
34
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { hasCapabilityTable } from "./capability-result";
|
|
2
1
|
import type { AiTurnBlock } from "../protocol";
|
|
2
|
+
import { hasCapabilityTable } from "./capability-result";
|
|
3
3
|
import { isCardToolName, isSurveyToolName, isTextEditorToolName } from "./message-utils";
|
|
4
4
|
|
|
5
5
|
type Tool = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
@@ -8,7 +8,13 @@ type Group = { kind: "tools"; blocks: Tool[] } | { kind: "block"; block: AiTurnB
|
|
|
8
8
|
export function isFailedTool(tool: Tool): boolean {
|
|
9
9
|
if (tool.isError || tool.status === "failed" || tool.status === "rejected") return true;
|
|
10
10
|
const result = tool.result;
|
|
11
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
["code_run", "code_inspect", "code_interact"].includes(tool.name) &&
|
|
13
|
+
result !== null &&
|
|
14
|
+
typeof result === "object" &&
|
|
15
|
+
"status" in result &&
|
|
16
|
+
result.status === "error"
|
|
17
|
+
);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
/** Visible content and interactive decisions are boundaries; model request boundaries are not. */
|
package/src/ai/chat-quotas.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { hasBillableAiPricing } from "../shared/ai-costs";
|
|
2
|
-
import { readAiSettingsState } from "./settings";
|
|
3
1
|
import type { AccessSubject } from "../server/services/access";
|
|
2
|
+
import { hasBillableAiPricing } from "../shared/ai-costs";
|
|
4
3
|
import type { AiChatQuotaSnapshot } from "../shared/ai-quotas";
|
|
5
4
|
import { listAssistantAiModels } from "./assistant-models";
|
|
6
5
|
import { aiQuotas } from "./quotas";
|
|
6
|
+
import { readAiSettingsState } from "./settings";
|
|
7
7
|
|
|
8
8
|
/** Own allowances only. Grant identities and unavailable model profiles stay private. */
|
|
9
9
|
export async function getAiChatQuotas(subject: AccessSubject): Promise<AiChatQuotaSnapshot> {
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { type TaskGrant, TaskGrantsSchema } from "../services/mandates";
|
|
2
|
+
|
|
3
|
+
export { TaskGrantsSchema as ChatTaskGrantsSchema };
|
|
4
|
+
|
|
3
5
|
import { dates } from "@k2b/stdlib";
|
|
4
|
-
import {
|
|
5
|
-
type AiChatTask,
|
|
6
|
-
type AiChatTaskOccurrence,
|
|
7
|
-
type AiChatTaskOccurrenceState,
|
|
8
|
-
type AiChatTaskSchedule,
|
|
9
|
-
type AiChatTaskState,
|
|
10
|
-
} from "./chat-tasks";
|
|
11
|
-
import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
6
|
+
import { z } from "zod";
|
|
12
7
|
import { coreSettings } from "../services";
|
|
13
8
|
import { normalizeWorkflowSchedule } from "../workflows/runtime";
|
|
14
|
-
import {
|
|
9
|
+
import type { AiChatTask, AiChatTaskOccurrence, AiChatTaskOccurrenceState, AiChatTaskSchedule, AiChatTaskState } from "./chat-tasks";
|
|
10
|
+
import { AI_SHORT_ID_PATTERN } from "./short-id";
|
|
15
11
|
|
|
16
12
|
export const ChatTaskIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character scheduled task ID.");
|
|
17
13
|
export const ChatTaskOccurrenceIdSchema = z.string().regex(AI_SHORT_ID_PATTERN).describe("Readable six-character task occurrence ID.");
|
|
@@ -22,7 +18,7 @@ export type AiChatTaskView = {
|
|
|
22
18
|
id: string;
|
|
23
19
|
chatId: string;
|
|
24
20
|
chatTitle: string;
|
|
25
|
-
grants:
|
|
21
|
+
grants: TaskGrant[];
|
|
26
22
|
prompt: string;
|
|
27
23
|
schedule: AiChatTaskSchedule;
|
|
28
24
|
timezone: string;
|
|
@@ -99,7 +95,7 @@ export const chatTaskCreateFingerprint = (input: {
|
|
|
99
95
|
prompt: string;
|
|
100
96
|
schedule: z.infer<typeof ChatTaskScheduleInputSchema>;
|
|
101
97
|
timezone?: string;
|
|
102
|
-
grants?:
|
|
98
|
+
grants?: TaskGrant[];
|
|
103
99
|
}): string =>
|
|
104
100
|
new Bun.CryptoHasher("sha256")
|
|
105
101
|
.update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone ?? null, input.grants ?? []]))
|
package/src/ai/chat-tasks.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { aiConversations } from "./store";
|
|
2
1
|
import type { Message } from "@k2b/nessi";
|
|
3
2
|
import { type SQL, sql } from "bun";
|
|
3
|
+
import { getCapability } from "../_internal/registry";
|
|
4
4
|
import { logger } from "../services/logging";
|
|
5
5
|
import {
|
|
6
6
|
createMandate,
|
|
7
|
-
CapabilityGrantsSchema,
|
|
8
|
-
type CapabilityGrant,
|
|
9
|
-
updateMandatePolicy,
|
|
10
|
-
validateMandateIssueAuthority,
|
|
11
7
|
type Mandate,
|
|
12
8
|
type MandatePolicyV1,
|
|
13
9
|
MandatePolicyV1Schema,
|
|
14
10
|
pauseMandate,
|
|
15
11
|
resumeMandate,
|
|
16
12
|
revokeMandate,
|
|
13
|
+
type TaskGrant,
|
|
14
|
+
TaskGrantsSchema,
|
|
15
|
+
updateMandatePolicy,
|
|
16
|
+
validateMandateIssueAuthority,
|
|
17
17
|
} from "../services/mandates";
|
|
18
|
-
import { getCapability } from "../_internal/registry";
|
|
19
18
|
import { parsePgJsonValue } from "../services/postgres";
|
|
20
19
|
import { withAiShortIdForDb } from "./short-id";
|
|
20
|
+
import { aiConversations } from "./store";
|
|
21
21
|
import type { AiChatTurnRunConfig, AiStoredMessage, AiTurnStatus } from "./types";
|
|
22
22
|
|
|
23
23
|
export type AiChatTaskState = "active" | "paused" | "completed" | "needs_attention";
|
|
@@ -31,7 +31,7 @@ export type AiChatTask = {
|
|
|
31
31
|
sponsorUserId: string;
|
|
32
32
|
mandateId: string | null;
|
|
33
33
|
mandateRevision: number | null;
|
|
34
|
-
grants:
|
|
34
|
+
grants: TaskGrant[];
|
|
35
35
|
prompt: string;
|
|
36
36
|
schedule: AiChatTaskSchedule;
|
|
37
37
|
timezone: string;
|
|
@@ -110,24 +110,27 @@ type OccurrenceRow = {
|
|
|
110
110
|
const iso = (value: Date | string): string => new Date(value).toISOString();
|
|
111
111
|
const nullableIso = (value: Date | string | null): string | null => (value === null ? null : iso(value));
|
|
112
112
|
const taskIdempotencyKey = (key: string): string => `task.create:${key}`;
|
|
113
|
-
const chatTaskMandatePolicy = (grants:
|
|
113
|
+
const chatTaskMandatePolicy = (grants: TaskGrant[]): MandatePolicyV1 => ({
|
|
114
114
|
version: 1,
|
|
115
|
-
apps: [...new Set(grants.map((grant) => grant.appId))],
|
|
115
|
+
apps: [...new Set(grants.map((grant) => ("appId" in grant ? grant.appId : "assistant")))],
|
|
116
116
|
operations: [
|
|
117
117
|
...new Set(
|
|
118
118
|
grants.flatMap((grant) =>
|
|
119
|
-
|
|
120
|
-
? [`
|
|
121
|
-
:
|
|
119
|
+
!("appId" in grant)
|
|
120
|
+
? [`runtime.${grant.kind}`]
|
|
121
|
+
: grant.kind === "query"
|
|
122
|
+
? [`capability.query:${grant.capabilityId}`]
|
|
123
|
+
: [`capability.action.review:${grant.capabilityId}`, `capability.action.run:${grant.capabilityId}`],
|
|
122
124
|
),
|
|
123
125
|
),
|
|
124
126
|
],
|
|
125
127
|
actions: grants.length ? "preapproved" : "deny",
|
|
126
128
|
grants,
|
|
127
129
|
});
|
|
128
|
-
const validateTaskGrants = async (input: unknown): Promise<
|
|
129
|
-
const grants =
|
|
130
|
+
const validateTaskGrants = async (input: unknown): Promise<TaskGrant[]> => {
|
|
131
|
+
const grants = TaskGrantsSchema.parse(input);
|
|
130
132
|
for (const grant of grants) {
|
|
133
|
+
if (!("appId" in grant)) continue;
|
|
131
134
|
const entry = await getCapability(grant.appId);
|
|
132
135
|
const operation = (grant.kind === "query" ? entry?.manifest.queries : entry?.manifest.actions)?.find(
|
|
133
136
|
(item) => item.localId === grant.capabilityId,
|
|
@@ -290,7 +293,7 @@ const taskFingerprint = (input: {
|
|
|
290
293
|
prompt: string;
|
|
291
294
|
schedule: AiChatTaskSchedule;
|
|
292
295
|
timezone: string;
|
|
293
|
-
grants?:
|
|
296
|
+
grants?: TaskGrant[];
|
|
294
297
|
}): string =>
|
|
295
298
|
new Bun.CryptoHasher("sha256")
|
|
296
299
|
.update(JSON.stringify([input.chatId, input.prompt.trim(), input.schedule, input.timezone, input.grants ?? []]))
|
|
@@ -301,7 +304,7 @@ const taskSelect = () => sql`
|
|
|
301
304
|
JOIN ai.conversations conversation ON conversation.id = task.conversation_id
|
|
302
305
|
`;
|
|
303
306
|
|
|
304
|
-
const taskGrants = (row: TaskRow):
|
|
307
|
+
const taskGrants = (row: TaskRow): TaskGrant[] => {
|
|
305
308
|
try {
|
|
306
309
|
const policy = MandatePolicyV1Schema.safeParse(parsePgJsonValue(row.mandate_policy));
|
|
307
310
|
return policy.success ? (policy.data.grants ?? []) : [];
|
|
@@ -459,6 +462,24 @@ export const aiChatTasks = {
|
|
|
459
462
|
},
|
|
460
463
|
|
|
461
464
|
validateGrants: validateTaskGrants,
|
|
465
|
+
authorizeRuntime: async (input: {
|
|
466
|
+
mandate: { id: string; revision: number };
|
|
467
|
+
kind: "http" | "database";
|
|
468
|
+
input: unknown;
|
|
469
|
+
}): Promise<void> => {
|
|
470
|
+
const result = await validateMandateIssueAuthority({
|
|
471
|
+
mandateId: input.mandate.id,
|
|
472
|
+
expectedRevision: input.mandate.revision,
|
|
473
|
+
ownerAppId: "core",
|
|
474
|
+
targetAppId: "assistant",
|
|
475
|
+
operation: `runtime.${input.kind}`,
|
|
476
|
+
input: input.input,
|
|
477
|
+
});
|
|
478
|
+
if (!result.ok)
|
|
479
|
+
throw new AiChatTaskAuthorityError(
|
|
480
|
+
`Background ${input.kind} access denied: ${result.error.message}. Update this task's grants in the normal chat.`,
|
|
481
|
+
);
|
|
482
|
+
},
|
|
462
483
|
authorizeCapability: async (input: {
|
|
463
484
|
mandate: { id: string; revision: number };
|
|
464
485
|
appId: string;
|
|
@@ -516,7 +537,7 @@ export const aiChatTasks = {
|
|
|
516
537
|
prompt: string;
|
|
517
538
|
schedule: AiChatTaskSchedule;
|
|
518
539
|
timezone: string;
|
|
519
|
-
grants?:
|
|
540
|
+
grants?: TaskGrant[];
|
|
520
541
|
idempotencyKey?: string;
|
|
521
542
|
idempotencyFingerprint?: string;
|
|
522
543
|
}): Promise<AiChatTask | null> => {
|
|
@@ -620,7 +641,7 @@ export const aiChatTasks = {
|
|
|
620
641
|
userId: string;
|
|
621
642
|
taskId: string;
|
|
622
643
|
prompt?: string;
|
|
623
|
-
grants?:
|
|
644
|
+
grants?: TaskGrant[];
|
|
624
645
|
schedule?: AiChatTaskSchedule;
|
|
625
646
|
timezone?: string;
|
|
626
647
|
}): Promise<AiChatTask | null> =>
|
|
@@ -152,11 +152,12 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
152
152
|
const [state, setState] = createStore<AiChatProjection>(initialProjection);
|
|
153
153
|
const completedDuringSend = new Map<string, string>();
|
|
154
154
|
const [pendingSends, setPendingSends] = createSignal<Record<string, AiStoredMessage | undefined>>({});
|
|
155
|
-
const clearPendingSend = (conversationId: string) =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
const clearPendingSend = (conversationId: string) =>
|
|
156
|
+
setPendingSends((current) => {
|
|
157
|
+
const next = { ...current };
|
|
158
|
+
delete next[conversationId];
|
|
159
|
+
return next;
|
|
160
|
+
});
|
|
160
161
|
|
|
161
162
|
// Cache of projections for conversations opened this session (fast switching).
|
|
162
163
|
const cache = new Map<string, AiChatProjection>();
|
|
@@ -258,7 +259,8 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
258
259
|
setProjection(detailToProjection(detail), session.conversationId);
|
|
259
260
|
setHasMore(session.conversationId, detail.hasMoreMessages ?? false);
|
|
260
261
|
if (detail.timeline) setTimeline(session.conversationId, detail.timeline);
|
|
261
|
-
if (event.type === "turn_finished" && pendingSends()[session.conversationId]?.loopId === event.turnId)
|
|
262
|
+
if (event.type === "turn_finished" && pendingSends()[session.conversationId]?.loopId === event.turnId)
|
|
263
|
+
clearPendingSend(session.conversationId);
|
|
262
264
|
} else reduceEvent(session.conversationId, event);
|
|
263
265
|
setRunStatusRaw(null);
|
|
264
266
|
void markConversationViewed(session.conversationId);
|
|
@@ -727,31 +729,51 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
727
729
|
return queueDraftOperation(async () => persistComposerDraft(input, await targetConversation));
|
|
728
730
|
};
|
|
729
731
|
|
|
730
|
-
const queuedAttempts = new Map<string, {key:string;files?:File[];id:string;saved:{revision:number}}>();
|
|
732
|
+
const queuedAttempts = new Map<string, { key: string; files?: File[]; id: string; saved: { revision: number } }>();
|
|
731
733
|
const queueMessage = (input: ComposerDraftInput & { modelProfileId?: string }): Promise<boolean> => {
|
|
732
734
|
if (!input.conversationId || !isComposerDraftSendable(input)) return Promise.resolve(false);
|
|
733
735
|
const conversationId = input.conversationId;
|
|
734
|
-
const key = JSON.stringify({
|
|
736
|
+
const key = JSON.stringify({
|
|
737
|
+
message: input.message,
|
|
738
|
+
content: input.content,
|
|
739
|
+
draftContent: input.draftContent,
|
|
740
|
+
resources: input.resources,
|
|
741
|
+
storedFiles: input.storedFiles,
|
|
742
|
+
model: input.modelProfileId,
|
|
743
|
+
});
|
|
735
744
|
return queueDraftOperation(async () => {
|
|
736
745
|
try {
|
|
737
746
|
const previous = queuedAttempts.get(conversationId);
|
|
738
|
-
const same =
|
|
747
|
+
const same =
|
|
748
|
+
previous?.key === key &&
|
|
749
|
+
(previous.files ?? []).length === (input.files ?? []).length &&
|
|
750
|
+
(previous.files ?? []).every((file, index) => file === input.files?.[index]);
|
|
739
751
|
const saved = same ? previous.saved : await persistComposerDraft(input, conversationId);
|
|
740
752
|
if (!saved) return false;
|
|
741
753
|
const queueId = same ? previous.id : crypto.randomUUID();
|
|
742
|
-
queuedAttempts.set(conversationId,{key,files:input.files,id:queueId,saved});
|
|
743
|
-
await request(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
754
|
+
queuedAttempts.set(conversationId, { key, files: input.files, id: queueId, saved });
|
|
755
|
+
await request(
|
|
756
|
+
`/conversations/${conversationId}/turns`,
|
|
757
|
+
{
|
|
758
|
+
method: "POST",
|
|
759
|
+
body: JSON.stringify({
|
|
760
|
+
draftRevision: saved.revision,
|
|
761
|
+
queueId,
|
|
762
|
+
modelProfileId: input.modelProfileId,
|
|
763
|
+
clientToolIds: options.clientToolIds?.filter((id) => options.frontendTools?.[id]),
|
|
764
|
+
}),
|
|
765
|
+
},
|
|
766
|
+
"Failed to queue message",
|
|
767
|
+
);
|
|
747
768
|
if (isActiveConversation(conversationId)) {
|
|
748
|
-
if ((state.conversation?.draft.revision ?? 0) <= saved.revision)
|
|
769
|
+
if ((state.conversation?.draft.revision ?? 0) <= saved.revision)
|
|
770
|
+
setState("conversation", "draft", { content: [], revision: saved.revision + 1, updatedAt: new Date().toISOString() });
|
|
749
771
|
} else invalidateInactiveCache(conversationId);
|
|
750
772
|
queuedAttempts.delete(conversationId);
|
|
751
773
|
if (isActiveConversation(conversationId)) setGlobalError(null);
|
|
752
774
|
return true;
|
|
753
775
|
} catch (error) {
|
|
754
|
-
setConversationError(conversationId,error instanceof Error ? error.message : "Failed to queue message");
|
|
776
|
+
setConversationError(conversationId, error instanceof Error ? error.message : "Failed to queue message");
|
|
755
777
|
return false;
|
|
756
778
|
}
|
|
757
779
|
});
|
|
@@ -770,9 +792,12 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
770
792
|
const optimisticContent: AiUserContentPart[] = [
|
|
771
793
|
...(input.content ?? []),
|
|
772
794
|
...(!input.content?.length && input.message ? [{ type: "text" as const, text: input.message }] : []),
|
|
773
|
-
...(input.resources ?? []).map(ref => ({ type: "text" as const, text: aiResourceMarker(ref) })),
|
|
774
|
-
...(input.storedFiles ?? []).map(file => ({ type: "text" as const, text: aiAttachmentMarker(file) })),
|
|
775
|
-
...(input.files ?? []).map(file => ({
|
|
795
|
+
...(input.resources ?? []).map((ref) => ({ type: "text" as const, text: aiResourceMarker(ref) })),
|
|
796
|
+
...(input.storedFiles ?? []).map((file) => ({ type: "text" as const, text: aiAttachmentMarker(file) })),
|
|
797
|
+
...(input.files ?? []).map((file) => ({
|
|
798
|
+
type: "text" as const,
|
|
799
|
+
text: aiAttachmentMarker({ path: `/${file.name}`, mediaType: file.type, size: file.size }),
|
|
800
|
+
})),
|
|
776
801
|
];
|
|
777
802
|
|
|
778
803
|
// Optimistic: show the user message immediately.
|
|
@@ -794,7 +819,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
794
819
|
meta: null,
|
|
795
820
|
createdAt: new Date().toISOString(),
|
|
796
821
|
};
|
|
797
|
-
setPendingSends(current => ({ ...current, [conversationId]: optimistic }));
|
|
822
|
+
setPendingSends((current) => ({ ...current, [conversationId]: optimistic }));
|
|
798
823
|
if (isActiveConversation(conversationId)) {
|
|
799
824
|
setRunStatusRaw("streaming");
|
|
800
825
|
}
|
|
@@ -806,13 +831,27 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
806
831
|
if (isActiveConversation(conversationId)) setRunStatusRaw("failed");
|
|
807
832
|
return false;
|
|
808
833
|
}
|
|
809
|
-
setPendingSends(current => ({
|
|
810
|
-
...
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
834
|
+
setPendingSends((current) => ({
|
|
835
|
+
...current,
|
|
836
|
+
[conversationId]: {
|
|
837
|
+
...optimistic,
|
|
838
|
+
meta: { submittedDraftRevision: savedDraft.revision },
|
|
839
|
+
message: {
|
|
840
|
+
role: "user",
|
|
841
|
+
content: savedDraft.content.map((part) => ({
|
|
842
|
+
type: "text",
|
|
843
|
+
text:
|
|
844
|
+
part.type === "text"
|
|
845
|
+
? part.text
|
|
846
|
+
: part.type === "file"
|
|
847
|
+
? aiAttachmentMarker(part)
|
|
848
|
+
: part.type === "project-file"
|
|
849
|
+
? `Project file: ${part.path}`
|
|
850
|
+
: aiResourceMarker({ ref: part.ref, title: part.title, icon: part.icon, href: part.href }),
|
|
851
|
+
})),
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
}));
|
|
816
855
|
const result = await request<SubmitTurnResult>(
|
|
817
856
|
`/conversations/${conversationId}/turns`,
|
|
818
857
|
{
|
|
@@ -828,21 +867,24 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
828
867
|
const alreadyFinished = completedDuringSend.get(conversationId) === result.turn.id;
|
|
829
868
|
completedDuringSend.delete(conversationId);
|
|
830
869
|
if (alreadyFinished) clearPendingSend(conversationId);
|
|
831
|
-
else setPendingSends(current => ({ ...current, [conversationId]: result.message }));
|
|
870
|
+
else setPendingSends((current) => ({ ...current, [conversationId]: result.message }));
|
|
832
871
|
if (invalidateInactiveCache(conversationId)) return true;
|
|
833
872
|
// Replace the optimistic message with the persisted one.
|
|
834
|
-
setState("messages", (prev) => [...prev.filter(message => message.id !== result.message.id), result.message]);
|
|
873
|
+
setState("messages", (prev) => [...prev.filter((message) => message.id !== result.message.id), result.message]);
|
|
835
874
|
setState(
|
|
836
875
|
"activeTurn",
|
|
837
876
|
(current) =>
|
|
838
|
-
current ??
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
877
|
+
current ??
|
|
878
|
+
(alreadyFinished
|
|
879
|
+
? null
|
|
880
|
+
: {
|
|
881
|
+
turnId: result.turn.id,
|
|
882
|
+
attempt: 0,
|
|
883
|
+
seq: 0,
|
|
884
|
+
status: "running",
|
|
885
|
+
blocks: [],
|
|
886
|
+
modelProfileId: result.turn.modelProfileId,
|
|
887
|
+
}),
|
|
846
888
|
);
|
|
847
889
|
if (state.conversation) {
|
|
848
890
|
setState("conversation", "draft", {
|
|
@@ -1271,7 +1313,14 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1271
1313
|
export type AiChatController = ReturnType<typeof createAiChatController>;
|
|
1272
1314
|
|
|
1273
1315
|
const isComposerDraftSendable = (input: ComposerDraftInput): boolean =>
|
|
1274
|
-
Boolean(
|
|
1316
|
+
Boolean(
|
|
1317
|
+
input.draftContent?.length ||
|
|
1318
|
+
input.message?.trim() ||
|
|
1319
|
+
input.content?.length ||
|
|
1320
|
+
input.files?.length ||
|
|
1321
|
+
input.resources?.length ||
|
|
1322
|
+
input.storedFiles?.length,
|
|
1323
|
+
);
|
|
1275
1324
|
|
|
1276
1325
|
export const __aiControllerTest = {
|
|
1277
1326
|
claimFrontendCall,
|