@k2b/cloud 0.26.0 → 0.28.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 +3 -3
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/browser-code-contracts.ts +46 -64
- package/src/ai/browser.ts +9 -1
- package/src/ai/capabilities.ts +82 -22
- package/src/ai/chat/blocks.tsx +16 -7
- package/src/ai/chat/builtin-tools.tsx +31 -23
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +15 -5
- package/src/ai/chat/message-actions.tsx +123 -101
- package/src/ai/chat/message-utils.ts +30 -1
- package/src/ai/chat/messages.ts +206 -2
- package/src/ai/chat/presentation.tsx +94 -28
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-view.tsx +102 -16
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/client/controller.ts +83 -37
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +7 -2
- package/src/ai/code-mode-skill.ts +32 -36
- package/src/ai/code-runtime-tools.ts +14 -1
- package/src/ai/code-source-contracts.ts +56 -6
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +15 -16
- package/src/ai/executor.ts +281 -141
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +9 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/protocol.ts +6 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +2 -2
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +40 -6
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +9 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +35 -7
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +155 -71
- package/src/ai/stream.ts +180 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +8 -0
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-failure.ts +100 -0
- package/src/ai/turn-policy.ts +248 -0
- package/src/ai/types.ts +52 -4
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +242 -0
- package/src/api/index.ts +2 -0
- package/src/browser/FileChooser.tsx +11 -0
- package/src/browser/file-chooser-messages.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +8 -0
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +215 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +265 -0
- package/src/contracts/registry.ts +2 -0
- package/src/services/help/store.ts +2 -1
- package/src/services/index.ts +13 -0
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +57 -5
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -45
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +112 -0
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +397 -0
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +17 -0
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/core-settings.ts +19 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +49 -5
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +3 -1
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +15 -17
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
"@simplewebauthn/server": "14.0.3",
|
|
99
99
|
"@tabler/icons": "3.48.0",
|
|
100
100
|
"@tailwindcss/typography": "0.5.20",
|
|
101
|
-
"@k2b/nessi": "0.
|
|
101
|
+
"@k2b/nessi": "0.15.0",
|
|
102
102
|
"@k2b/ssr": "0.14.0",
|
|
103
|
-
"@k2b/ui": "0.
|
|
103
|
+
"@k2b/ui": "0.18.0",
|
|
104
104
|
"@k2b/stdlib": "0.27.0",
|
|
105
105
|
"@k2b/sync": "7.0.0",
|
|
106
106
|
"@nats-io/jetstream": "3.4.0",
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
CapabilityOriginSchema,
|
|
41
41
|
} from "../contracts/capabilities";
|
|
42
42
|
import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
|
|
43
|
+
import { type PlatformPermission, PlatformPermissionSchema } from "../contracts/outgoing-mail";
|
|
43
44
|
import { isPwaPartId, PWA_AUTH_PATH, PWA_CANVAS_COLORS, PWA_MANIFEST_PATH, PWA_SHELL_APP_ID } from "../contracts/pwa";
|
|
44
45
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
45
46
|
import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
|
|
@@ -201,6 +202,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
201
202
|
* prefix-trie from these strings.
|
|
202
203
|
*/
|
|
203
204
|
routes: readonly string[];
|
|
205
|
+
/** Platform services requested by this application, visible to administrators. */
|
|
206
|
+
platformPermissions?: readonly PlatformPermission[];
|
|
204
207
|
/**
|
|
205
208
|
* Gateway-relative URL at which this app's OpenAPI 3.x JSON spec is
|
|
206
209
|
* served, e.g. `"/api/notebooks/openapi.json"`. Opt-in: only set this
|
|
@@ -471,6 +474,9 @@ export const defineApp = <
|
|
|
471
474
|
// ── 2. Meta ───────────────────────────────────────────────────────────
|
|
472
475
|
const baseMeta: AppMeta = {
|
|
473
476
|
id: opts.id,
|
|
477
|
+
platformPermissions: opts.platformPermissions
|
|
478
|
+
? opts.platformPermissions.map((permission) => PlatformPermissionSchema.parse(permission))
|
|
479
|
+
: undefined,
|
|
474
480
|
name: opts.name,
|
|
475
481
|
icon: opts.icon,
|
|
476
482
|
description: opts.description,
|
|
@@ -528,7 +534,7 @@ export const defineApp = <
|
|
|
528
534
|
}
|
|
529
535
|
})());
|
|
530
536
|
try {
|
|
531
|
-
bindProcessApplicationId(meta.id);
|
|
537
|
+
bindProcessApplicationId(meta.id, meta.platformPermissions);
|
|
532
538
|
cleanup.unshift(clearProcessApplicationId);
|
|
533
539
|
const startedAt = Date.now();
|
|
534
540
|
|
|
@@ -587,6 +593,7 @@ export const defineApp = <
|
|
|
587
593
|
? { href: meta.pwa.href, requiresRoles: meta.pwa.requiresRoles ? [...meta.pwa.requiresRoles] : undefined }
|
|
588
594
|
: undefined,
|
|
589
595
|
widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
|
|
596
|
+
platformPermissions: meta.platformPermissions ? [...meta.platformPermissions] : undefined,
|
|
590
597
|
settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
|
|
591
598
|
openapi: advertiseOpenapi ? opts.openapi : undefined,
|
|
592
599
|
cliModules: cliModules.length > 0 ? cliModules : undefined,
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import type { PlatformPermission } from "../contracts/outgoing-mail";
|
|
2
|
+
|
|
1
3
|
/** Identity of the application started in this process, derived from its declaration. */
|
|
2
4
|
let applicationId: string | undefined;
|
|
5
|
+
let platformPermissions: readonly PlatformPermission[] = [];
|
|
6
|
+
export const getProcessPlatformPermissions = (): readonly PlatformPermission[] => platformPermissions;
|
|
3
7
|
|
|
4
8
|
export const getProcessApplicationId = (): string | undefined => applicationId;
|
|
5
9
|
|
|
6
|
-
export const bindProcessApplicationId = (id: string): void => {
|
|
10
|
+
export const bindProcessApplicationId = (id: string, permissions: readonly PlatformPermission[] = []): void => {
|
|
7
11
|
if (applicationId !== undefined && applicationId !== id) {
|
|
8
12
|
throw new Error(`This process already hosts application "${applicationId}"`);
|
|
9
13
|
}
|
|
10
14
|
applicationId = id;
|
|
15
|
+
platformPermissions = [...permissions];
|
|
11
16
|
};
|
|
12
17
|
|
|
13
18
|
export const clearProcessApplicationId = (): void => {
|
|
14
19
|
applicationId = undefined;
|
|
20
|
+
platformPermissions = [];
|
|
15
21
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLOUD_CLI_MODULE_NAME } from "../cli/plugin";
|
|
2
|
+
import { PlatformPermissionSchema } from "../contracts/outgoing-mail";
|
|
2
3
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
3
4
|
|
|
4
5
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -118,6 +119,8 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
|
|
|
118
119
|
return invalid("widgets", "an array of valid widgets");
|
|
119
120
|
}
|
|
120
121
|
}
|
|
122
|
+
if (value.platformPermissions !== undefined && !PlatformPermissionSchema.array().safeParse(value.platformPermissions).success)
|
|
123
|
+
return invalid("platformPermissions", "an array of platform permissions");
|
|
121
124
|
if (value.settingKeys !== undefined && !isStringArray(value.settingKeys)) return invalid("settingKeys", "an array of strings");
|
|
122
125
|
if (
|
|
123
126
|
value.cliModules !== undefined &&
|
|
@@ -15,6 +15,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
|
|
|
15
15
|
const searchTags = searchQueries?.flatMap((query) => query.universalSearch!.tags.flatMap((tag) => [tag.tag, ...(tag.aliases ?? [])]));
|
|
16
16
|
return {
|
|
17
17
|
id: e.id,
|
|
18
|
+
platformPermissions: e.platformPermissions ? [...e.platformPermissions] : undefined,
|
|
18
19
|
name: e.name,
|
|
19
20
|
icon: e.icon,
|
|
20
21
|
description: e.description,
|
package/src/ai/admin.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { quotaAdminConfig, quotaReport } from "./quota-report";
|
|
2
2
|
export { AiQuotaError, aiQuotas } from "./quotas";
|
|
3
|
+
export { listAiRequestHeaderNames } from "./request-headers";
|
|
3
4
|
/** Server-only AI administration and usage accounting. */
|
|
4
5
|
|
|
5
6
|
export { type AiModelAccessDraft, type AiModelAccessMap, type AiModelAccessState, aiModelAccess } from "./model-access";
|
|
@@ -10,20 +10,20 @@ export const CODE_RUNTIME_TOOL_NAMES = [
|
|
|
10
10
|
"code_run",
|
|
11
11
|
"code_action",
|
|
12
12
|
"code_inspect",
|
|
13
|
-
"code_interact",
|
|
14
13
|
"code_stop",
|
|
15
14
|
"code_open",
|
|
16
15
|
"code_export",
|
|
17
16
|
"code_present",
|
|
17
|
+
"code_check",
|
|
18
18
|
"code_secret",
|
|
19
19
|
] as const;
|
|
20
20
|
export const CodeRunInput = z
|
|
21
21
|
.object({
|
|
22
|
-
id: id.optional(),
|
|
22
|
+
id: id.optional().describe("Saved script resource to run; an app with an index.html interface opens with code_open instead."),
|
|
23
23
|
resourceId: id
|
|
24
24
|
.optional()
|
|
25
25
|
.describe(
|
|
26
|
-
"Optional data context for one-off code. Requires Manage; uses this resource database and shared files/KV without changing its source.
|
|
26
|
+
"Optional data context for one-off code. Requires Manage; uses this resource database and shared files/KV without changing its source. Runs use real shared and personal data; writes and capability effects keep normal permissions and approvals.",
|
|
27
27
|
),
|
|
28
28
|
version: z
|
|
29
29
|
.number()
|
|
@@ -36,14 +36,12 @@ export const CodeRunInput = z
|
|
|
36
36
|
.min(1)
|
|
37
37
|
.max(1024 * 1024)
|
|
38
38
|
.optional()
|
|
39
|
-
.describe("One-off JavaScript/TypeScript
|
|
39
|
+
.describe("One-off JavaScript/TypeScript script exporting one function. Use code OR a saved resource id; no title or icon needed."),
|
|
40
40
|
inputPaths: z
|
|
41
41
|
.array(z.string().min(1).max(500))
|
|
42
42
|
.max(64)
|
|
43
43
|
.default([])
|
|
44
|
-
.describe(
|
|
45
|
-
"Explicit current-chat input files. Scripts can read them; app test runs expose them only through the simulated picker. User apps never receive chat files.",
|
|
46
|
-
),
|
|
44
|
+
.describe("Explicit current-chat input files; the script reads them from its context files. Apps never receive chat files."),
|
|
47
45
|
})
|
|
48
46
|
.strict()
|
|
49
47
|
.refine((input) => Number(input.id !== undefined) + Number(input.code !== undefined) === 1, "Provide exactly one of id or code")
|
|
@@ -71,7 +69,10 @@ export const CodeActionInput = z
|
|
|
71
69
|
.positive()
|
|
72
70
|
.optional()
|
|
73
71
|
.describe("Exact draft revision returned by code_actions({draft:true}); Manage required. Supply this OR publishedVersion."),
|
|
74
|
-
input: z
|
|
72
|
+
input: z
|
|
73
|
+
.record(z.string(), z.json())
|
|
74
|
+
.default({})
|
|
75
|
+
.describe("Action input object matching the discovered inputSchema. Pass the object itself, never JSON text."),
|
|
75
76
|
})
|
|
76
77
|
.strict()
|
|
77
78
|
.refine(
|
|
@@ -88,66 +89,38 @@ export const CodeInspectInput = z
|
|
|
88
89
|
.max(30000)
|
|
89
90
|
.default(0)
|
|
90
91
|
.describe("Wait up to this duration for background work to finish before returning its real state; never restarts work."),
|
|
91
|
-
nodeId: z.string().min(1).max(80).optional(),
|
|
92
|
-
offset: z.number().int().min(0).default(0),
|
|
93
|
-
limit: z.number().int().min(1).max(100).default(20),
|
|
94
92
|
})
|
|
95
93
|
.strict();
|
|
96
|
-
const
|
|
94
|
+
export const CodeStopInput = z.object({ runId }).strict();
|
|
95
|
+
export const CodeOpenInput = z.object({ id }).strict();
|
|
96
|
+
const AppFilePath = z
|
|
97
|
+
.string()
|
|
98
|
+
.min(1)
|
|
99
|
+
.max(180)
|
|
100
|
+
.regex(/^[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*$/)
|
|
101
|
+
.refine((path) => path.split("/").every((part) => part !== "." && part !== ".."), "Expected a relative path without . or ..");
|
|
102
|
+
export const CodePresentInput = z
|
|
97
103
|
.object({
|
|
98
|
-
id:
|
|
99
|
-
event: z
|
|
100
|
-
.discriminatedUnion("type", [
|
|
101
|
-
z
|
|
102
|
-
.object({
|
|
103
|
-
type: z.literal("change"),
|
|
104
|
-
value: z.union([
|
|
105
|
-
z.string(),
|
|
106
|
-
z.number().finite(),
|
|
107
|
-
z.boolean(),
|
|
108
|
-
z.null(),
|
|
109
|
-
z.array(z.string()),
|
|
110
|
-
z.object({ start: z.iso.date().nullable(), end: z.iso.date().nullable() }).strict(),
|
|
111
|
-
]),
|
|
112
|
-
})
|
|
113
|
-
.strict(),
|
|
114
|
-
z.object({ type: z.literal("select"), key: z.string().nullable() }).strict(),
|
|
115
|
-
z.object({ type: z.literal("view"), value: z.enum(["chart", "table"]) }).strict(),
|
|
116
|
-
z.object({ type: z.literal("refresh") }).strict(),
|
|
117
|
-
z.object({ type: z.literal("request"), request: z.record(z.string(), z.json()) }).strict(),
|
|
118
|
-
])
|
|
104
|
+
id: id
|
|
119
105
|
.optional()
|
|
120
|
-
.describe(
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
answer: z.json().optional().describe("Only for the pending modal ID: the modal answer, or null to cancel. Do not use for controls."),
|
|
124
|
-
})
|
|
125
|
-
.strict();
|
|
126
|
-
const validInteraction = (input: z.infer<typeof CodeInteraction>) => input.event === undefined || input.answer === undefined;
|
|
127
|
-
export const CodeInteractInput = CodeInteraction.partial({ id: true })
|
|
128
|
-
.extend({
|
|
129
|
-
runId,
|
|
130
|
-
// Three normal 15-second callback budgets share the existing 45-second call budget.
|
|
131
|
-
steps: z
|
|
132
|
-
.array(CodeInteraction.refine(validInteraction, "Supply event OR answer"))
|
|
106
|
+
.describe("Saved app with an index.html interface; the card runs it live with its data. Omit title to use the app title."),
|
|
107
|
+
files: z
|
|
108
|
+
.array(z.object({ path: AppFilePath, content: z.string().max(1024 * 1024) }).strict())
|
|
133
109
|
.min(1)
|
|
134
|
-
.max(
|
|
110
|
+
.max(64)
|
|
135
111
|
.optional()
|
|
136
|
-
.describe(
|
|
137
|
-
|
|
138
|
-
),
|
|
112
|
+
.describe("One-off app for this chat: index.html plus optional style.css, app.js and other modules. It has no saved data."),
|
|
113
|
+
title: z.string().trim().min(1).max(120).optional().describe("Card title; required with files."),
|
|
139
114
|
})
|
|
140
115
|
.strict()
|
|
141
|
-
.refine(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
export const CodeOpenInput = z.object({ id }).strict();
|
|
150
|
-
export const CodePresentInput = z.object({ runId, title: z.string().trim().min(1).max(120) }).strict();
|
|
116
|
+
.refine((input) => Number(input.id !== undefined) + Number(input.files !== undefined) === 1, "Provide exactly one of id or files")
|
|
117
|
+
.refine((input) => input.id !== undefined || input.title !== undefined, "A one-off app needs a title")
|
|
118
|
+
.refine((input) => !input.files || input.files.some((file) => file.path === "index.html"), "files must contain index.html");
|
|
119
|
+
export const CodeCheckInput = z
|
|
120
|
+
.object({ id: id.optional(), files: CodePresentInput.shape.files })
|
|
121
|
+
.strict()
|
|
122
|
+
.refine((input) => Number(input.id !== undefined) + Number(input.files !== undefined) === 1, "Provide exactly one of id or files")
|
|
123
|
+
.refine((input) => !input.files || input.files.some((file) => file.path === "index.html"), "files must contain index.html");
|
|
151
124
|
export const CodeExportInput = z
|
|
152
125
|
.object({ runId, name: z.string().min(1).max(180).describe("Captured output filename returned by the snapshot.") })
|
|
153
126
|
.strict();
|
|
@@ -162,16 +135,25 @@ export const CodeSecretInput = z
|
|
|
162
135
|
})
|
|
163
136
|
.strict();
|
|
164
137
|
|
|
138
|
+
/**
|
|
139
|
+
* A runtime call that did not complete: rejected input, timeout, lost run or host failure.
|
|
140
|
+
* Hosts return it as data because a client tool result carries no error flag; the managed
|
|
141
|
+
* server tools report it to the model as a tool error. A run snapshot whose code failed is a
|
|
142
|
+
* completed call and keeps its own `status` and `error`.
|
|
143
|
+
*/
|
|
144
|
+
export const CodeToolFailure = z.object({ failed: z.literal(true), error: z.string(), guidance: z.string().optional() });
|
|
145
|
+
export type CodeToolFailure = z.infer<typeof CodeToolFailure>;
|
|
146
|
+
|
|
165
147
|
// Internal bridge envelope. Each model-facing tool receives only its own flat schema.
|
|
166
148
|
export const CodeRuntimeInput = z.discriminatedUnion("operation", [
|
|
167
149
|
CodeRunInput.safeExtend({ operation: z.literal("run") }),
|
|
168
150
|
CodeActionInput.safeExtend({ operation: z.literal("action") }),
|
|
169
151
|
CodeInspectInput.extend({ operation: z.literal("inspect") }),
|
|
170
|
-
CodeInteractInput.safeExtend({ operation: z.literal("interact") }),
|
|
171
152
|
CodeStopInput.extend({ operation: z.literal("stop") }),
|
|
172
153
|
CodeOpenInput.extend({ operation: z.literal("open") }),
|
|
173
154
|
CodeSecretInput.extend({ operation: z.literal("secret") }),
|
|
174
|
-
CodePresentInput.
|
|
155
|
+
CodePresentInput.safeExtend({ operation: z.literal("present") }),
|
|
156
|
+
CodeCheckInput.safeExtend({ operation: z.literal("check") }),
|
|
175
157
|
CodeExportInput.extend({ operation: z.literal("export") }),
|
|
176
158
|
]);
|
|
177
159
|
export type CodeRuntimeInput = z.infer<typeof CodeRuntimeInput>;
|
|
@@ -185,12 +167,12 @@ export function parseCodeToolInput(name: string, args: unknown): CodeRuntimeInpu
|
|
|
185
167
|
return { operation: "run", ...CodeRunInput.parse(args) };
|
|
186
168
|
case "code_inspect":
|
|
187
169
|
return { operation: "inspect", ...CodeInspectInput.parse(args) };
|
|
188
|
-
case "code_interact":
|
|
189
|
-
return { operation: "interact", ...CodeInteractInput.parse(args) };
|
|
190
170
|
case "code_stop":
|
|
191
171
|
return { operation: "stop", ...CodeStopInput.parse(args) };
|
|
192
172
|
case "code_open":
|
|
193
173
|
return { operation: "open", ...CodeOpenInput.parse(args) };
|
|
174
|
+
case "code_check":
|
|
175
|
+
return { operation: "check", ...CodeCheckInput.parse(args) };
|
|
194
176
|
case "code_present":
|
|
195
177
|
return { operation: "present", ...CodePresentInput.parse(args) };
|
|
196
178
|
case "code_export":
|
package/src/ai/browser.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
CODE_RUNTIME_TOOL_NAMES,
|
|
3
|
+
CodeActionInput,
|
|
4
|
+
CodeCheckInput,
|
|
5
|
+
CodeResourceId,
|
|
6
|
+
CodeRuntimeInput,
|
|
7
|
+
CodeToolFailure,
|
|
8
|
+
parseCodeToolInput,
|
|
9
|
+
} from "./browser-code-contracts";
|
|
2
10
|
export { type AiTaskRequest, type AiTaskRequestInput, AiTaskRequestSchema } from "./task-contracts";
|
|
3
11
|
export { type AiTodoPlan, parseAiTodoPlan } from "./todo-contracts";
|
|
4
12
|
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -72,6 +72,16 @@ type AiToolCatalogEntry = AiToolCatalogItem & {
|
|
|
72
72
|
|
|
73
73
|
export type AiRememberableCapabilityApprovals = ReadonlyMap<string, string>;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Why `load_tools` could not make a requested name callable:
|
|
77
|
+
* - `unknown`: no tool has this exact name;
|
|
78
|
+
* - `not_offered_in_turn`: the tool exists, but this turn's client or task does not provide it;
|
|
79
|
+
* - `not_allowed`: the conversation's fixed tool scope excludes it;
|
|
80
|
+
* - `app_offline`: the app operation is not in the live registry now.
|
|
81
|
+
*/
|
|
82
|
+
export const AI_TOOL_UNAVAILABLE_REASONS = ["unknown", "not_offered_in_turn", "not_allowed", "app_offline"] as const;
|
|
83
|
+
export type AiToolUnavailableReason = (typeof AI_TOOL_UNAVAILABLE_REASONS)[number];
|
|
84
|
+
|
|
75
85
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
76
86
|
const DEFAULT_APP_LIST_LIMIT = 20;
|
|
77
87
|
const MAX_APP_LIST_LIMIT = 25;
|
|
@@ -367,15 +377,16 @@ export const createAiHelpTools = (help: HelpReaderFactory = createHelpReader, lo
|
|
|
367
377
|
return [search, read];
|
|
368
378
|
};
|
|
369
379
|
|
|
380
|
+
/** `null` when the registry could not be read. */
|
|
370
381
|
const resolveCapabilityRegistry = async (
|
|
371
382
|
listRegistry: () => Promise<CapabilityRegistryEntry[]>,
|
|
372
383
|
onError?: (error: unknown) => void,
|
|
373
|
-
): Promise<CapabilityRegistryEntry[]> => {
|
|
384
|
+
): Promise<CapabilityRegistryEntry[] | null> => {
|
|
374
385
|
try {
|
|
375
386
|
return await listRegistry();
|
|
376
387
|
} catch (error) {
|
|
377
388
|
onError?.(error);
|
|
378
|
-
return
|
|
389
|
+
return null;
|
|
379
390
|
}
|
|
380
391
|
};
|
|
381
392
|
|
|
@@ -453,6 +464,9 @@ const ToolCatalogItemSchema = z
|
|
|
453
464
|
|
|
454
465
|
type ToolStateStore = Pick<AiConversationService, "loadTools">;
|
|
455
466
|
|
|
467
|
+
/** `call` is the name to call the tool by; app operations get a provider-safe name. */
|
|
468
|
+
const LoadedToolSchema = z.object({ name: z.string(), call: z.string() }).strict();
|
|
469
|
+
|
|
456
470
|
export const createAiToolMetaTools = (input: {
|
|
457
471
|
apps: readonly CapabilityRegistryEntry[];
|
|
458
472
|
catalog: readonly AiToolCatalogEntry[];
|
|
@@ -461,6 +475,10 @@ export const createAiToolMetaTools = (input: {
|
|
|
461
475
|
store: ToolStateStore;
|
|
462
476
|
maxLoadedTools?: number;
|
|
463
477
|
unavailableLoadedNames?: readonly string[];
|
|
478
|
+
/** False when this turn offers no app operations at all. */
|
|
479
|
+
appOperationsOffered?: boolean;
|
|
480
|
+
/** Why a requested name that is not in `catalog` cannot be loaded; defaults to `unknown`. */
|
|
481
|
+
unavailableReason?: (name: string) => AiToolUnavailableReason;
|
|
464
482
|
}): AiRuntimeTool[] => {
|
|
465
483
|
const apps = buildAiCapabilityAppCatalog(input.apps);
|
|
466
484
|
const directoryEntries: string[] = [];
|
|
@@ -476,17 +494,17 @@ export const createAiToolMetaTools = (input: {
|
|
|
476
494
|
const liveAppDirectory =
|
|
477
495
|
directoryEntries.length > 0
|
|
478
496
|
? ` Live capability apps: ${directoryEntries.join(", ")}${hiddenAppCount > 0 ? `, and ${hiddenAppCount} more` : ""}.`
|
|
479
|
-
:
|
|
497
|
+
: input.appOperationsOffered === false
|
|
498
|
+
? " App operations are not offered in this turn."
|
|
499
|
+
: " No Cloud app publishes operations right now. Do not search for app operations again in this turn; tell the user which app is not reachable instead of claiming a permanent product limitation.";
|
|
480
500
|
const unavailableLoadedNames = input.unavailableLoadedNames ?? [];
|
|
481
501
|
const unavailableLoadedNotice =
|
|
482
502
|
unavailableLoadedNames.length > 0
|
|
483
|
-
? ` Previously loaded tools
|
|
484
|
-
.slice(-MAX_UNAVAILABLE_LOADED_NAMES)
|
|
485
|
-
.join(", ")}${
|
|
503
|
+
? ` Previously loaded tools that are not available now: ${unavailableLoadedNames.slice(-MAX_UNAVAILABLE_LOADED_NAMES).join(", ")}${
|
|
486
504
|
unavailableLoadedNames.length > MAX_UNAVAILABLE_LOADED_NAMES
|
|
487
505
|
? `, and ${unavailableLoadedNames.length - MAX_UNAVAILABLE_LOADED_NAMES} more`
|
|
488
506
|
: ""
|
|
489
|
-
}.
|
|
507
|
+
}. Do not search for them again in this turn; continue without them or tell the user what is missing.`
|
|
490
508
|
: "";
|
|
491
509
|
const search = defineAiTool({
|
|
492
510
|
name: "search_tools",
|
|
@@ -524,38 +542,52 @@ export const createAiToolMetaTools = (input: {
|
|
|
524
542
|
const load = defineAiTool({
|
|
525
543
|
name: "load_tools",
|
|
526
544
|
description:
|
|
527
|
-
"Load stable capability ids such as mail.conversation.list, or exact built-in names returned by search_tools, as ordinary tools for the next model turn. Skills may name capability ids directly, so load them without searching first.",
|
|
545
|
+
"Load stable capability ids such as mail.conversation.list, or exact built-in names returned by search_tools, as ordinary tools for the next model turn. Skills may name capability ids directly, so load them without searching first. Call each loaded tool by the `call` name returned for it. `unavailable` gives a reason per name: unknown means no tool has this exact name, so look it up once with search_tools instead of guessing; not_offered_in_turn means this client or task does not provide the tool; not_allowed means this chat's fixed tool scope excludes it; app_offline means its app is not reachable now. For the last three, do not search or load the name again in this turn.",
|
|
528
546
|
inputSchema: z.object({ names: z.array(z.string().trim().min(1)).min(1).max(25) }).strict(),
|
|
529
547
|
outputSchema: z
|
|
530
548
|
.object({
|
|
531
|
-
loaded: z.array(
|
|
532
|
-
alreadyLoaded: z.array(
|
|
533
|
-
|
|
549
|
+
loaded: z.array(LoadedToolSchema),
|
|
550
|
+
alreadyLoaded: z.array(LoadedToolSchema),
|
|
551
|
+
unavailable: z.array(z.object({ name: z.string(), reason: z.enum(AI_TOOL_UNAVAILABLE_REASONS) }).strict()),
|
|
534
552
|
evicted: z.array(z.string()),
|
|
535
553
|
titles: z.record(z.string(), z.string()),
|
|
536
554
|
})
|
|
537
555
|
.strict(),
|
|
538
556
|
approval: "never",
|
|
539
557
|
}).server(async ({ names }) => {
|
|
540
|
-
const
|
|
558
|
+
const catalogByName = new Map(input.catalog.map((entry) => [entry.name, entry]));
|
|
559
|
+
// A provider name the model saw in an earlier call loads its operation, too.
|
|
560
|
+
const byProviderName = new Map(input.catalog.flatMap((entry) => (entry.capability ? [[entry.capability.providerName, entry]] : [])));
|
|
541
561
|
const requested = [...new Set(names)];
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
562
|
+
const found = requested.flatMap((name) => {
|
|
563
|
+
const entry = catalogByName.get(name) ?? byProviderName.get(name);
|
|
564
|
+
return entry ? [entry.name] : [];
|
|
565
|
+
});
|
|
566
|
+
const eager = [...new Set(found.filter((name) => input.eagerNames.has(name)))];
|
|
567
|
+
const valid = [...new Set(found.filter((name) => !input.eagerNames.has(name)))];
|
|
568
|
+
const unavailable = requested
|
|
569
|
+
.filter((name) => !catalogByName.has(name) && !byProviderName.has(name))
|
|
570
|
+
.map((name) => ({ name, reason: input.unavailableReason?.(name) ?? ("unknown" as const) }));
|
|
545
571
|
const updated = await input.store.loadTools({
|
|
546
572
|
conversationId: input.conversationId,
|
|
547
573
|
names: valid,
|
|
548
574
|
maxLoadedTools: input.maxLoadedTools,
|
|
549
575
|
});
|
|
576
|
+
const callable = (name: string) => ({ name, call: catalogByName.get(name)?.capability?.providerName ?? name });
|
|
550
577
|
const alreadyLoaded = [...new Set([...eager, ...updated.alreadyLoaded])];
|
|
551
|
-
const catalogByName = new Map(input.catalog.map((entry) => [entry.name, entry]));
|
|
552
578
|
const titles = Object.fromEntries(
|
|
553
579
|
[...new Set([...updated.loaded, ...alreadyLoaded, ...updated.evicted])].flatMap((name) => {
|
|
554
580
|
const entry = catalogByName.get(name);
|
|
555
581
|
return entry ? [[name, entry.title]] : [];
|
|
556
582
|
}),
|
|
557
583
|
);
|
|
558
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
loaded: updated.loaded.map(callable),
|
|
586
|
+
alreadyLoaded: alreadyLoaded.map(callable),
|
|
587
|
+
unavailable,
|
|
588
|
+
evicted: updated.evicted,
|
|
589
|
+
titles,
|
|
590
|
+
};
|
|
559
591
|
});
|
|
560
592
|
|
|
561
593
|
return [search, load, listApps];
|
|
@@ -657,6 +689,8 @@ export const createAiToolResolver =
|
|
|
657
689
|
actor: RequestActor;
|
|
658
690
|
staticTools: AiRuntimeTool[];
|
|
659
691
|
allowedTools?: readonly string[] | null;
|
|
692
|
+
/** Built-in tools that exist but that this turn does not offer, such as client tools its client did not declare or tools outside `allowedTools`. */
|
|
693
|
+
unofferedTools?: readonly string[];
|
|
660
694
|
runtimeContext?: Omit<AiToolPreparationContext, "actor" | "conversationId">;
|
|
661
695
|
store: Pick<AiConversationService, "getLoadedTools" | "loadTools">;
|
|
662
696
|
listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
|
|
@@ -675,10 +709,11 @@ export const createAiToolResolver =
|
|
|
675
709
|
}) => void;
|
|
676
710
|
}): ToolResolver =>
|
|
677
711
|
async (): Promise<Tool[]> => {
|
|
678
|
-
const [
|
|
712
|
+
const [liveRegistry, persistedLoadedNames] = await Promise.all([
|
|
679
713
|
input.listRegistry ? resolveCapabilityRegistry(input.listRegistry, input.onCapabilityRegistryError) : [],
|
|
680
714
|
input.store.getLoadedTools({ conversationId: input.conversationId }),
|
|
681
715
|
]);
|
|
716
|
+
const registry = liveRegistry ?? [];
|
|
682
717
|
const configuredLimit = Math.floor(input.maxLoadedTools ?? 0);
|
|
683
718
|
const loadedNames = configuredLimit > 0 ? persistedLoadedNames.slice(-configuredLimit) : persistedLoadedNames;
|
|
684
719
|
if (loadedNames.length !== persistedLoadedNames.length) {
|
|
@@ -689,18 +724,41 @@ export const createAiToolResolver =
|
|
|
689
724
|
});
|
|
690
725
|
}
|
|
691
726
|
const allowed = input.allowedTools == null ? null : new Set(input.allowedTools);
|
|
692
|
-
const
|
|
727
|
+
const fullCapabilityCatalog = buildAiCapabilityCatalog(registry, input.locale);
|
|
728
|
+
const capabilityCatalog = fullCapabilityCatalog.filter((entry) => !allowed || allowed.has(entry.name));
|
|
693
729
|
const helpTools = input.help ? createAiHelpTools(input.help, input.locale) : [];
|
|
694
730
|
const resourceTool =
|
|
695
731
|
input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
|
|
696
732
|
? createAiResourceReaderTool({ apps: registry, catalog: capabilityCatalog, execute: input.execute })
|
|
697
733
|
: null;
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
);
|
|
734
|
+
const allBuiltIns = [...input.staticTools, ...helpTools, ...(resourceTool ? [resourceTool] : [])];
|
|
735
|
+
const builtIns = allBuiltIns.filter((tool) => !allowed || allowed.has(tool.def.name));
|
|
701
736
|
const catalog = buildAiToolCatalog(builtIns, capabilityCatalog);
|
|
702
737
|
const catalogNames = new Set(catalog.map((entry) => entry.name));
|
|
703
738
|
const unavailableLoadedNames = loadedNames.filter((name) => !catalogNames.has(name));
|
|
739
|
+
const outOfScope = new Set([
|
|
740
|
+
...allBuiltIns.map((tool) => tool.def.name),
|
|
741
|
+
...fullCapabilityCatalog.flatMap((entry) => [entry.name, entry.providerName]),
|
|
742
|
+
...(input.unofferedTools ?? []),
|
|
743
|
+
]);
|
|
744
|
+
const unoffered = new Set(input.unofferedTools);
|
|
745
|
+
// A model may name a loaded operation by the provider name it called it by; app IDs contain no dot.
|
|
746
|
+
const loadedByProviderName = new Map(
|
|
747
|
+
persistedLoadedNames.flatMap((id) => {
|
|
748
|
+
const dot = id.indexOf(".");
|
|
749
|
+
if (dot < 1) return [];
|
|
750
|
+
return (["query", "action"] as const).map((kind) => [aiCapabilityToolName(id.slice(0, dot), kind, id.slice(dot + 1)), id] as const);
|
|
751
|
+
}),
|
|
752
|
+
);
|
|
753
|
+
// App operation IDs always contain a dot; built-in names never do.
|
|
754
|
+
const unavailableReason = (requested: string): AiToolUnavailableReason => {
|
|
755
|
+
const name = loadedByProviderName.get(requested) ?? requested;
|
|
756
|
+
if (allowed && outOfScope.has(name) && !allowed.has(name)) return "not_allowed";
|
|
757
|
+
if (unoffered.has(name)) return "not_offered_in_turn";
|
|
758
|
+
if (!name.includes(".")) return "unknown";
|
|
759
|
+
if (!input.listRegistry) return "not_offered_in_turn";
|
|
760
|
+
return liveRegistry === null || persistedLoadedNames.includes(name) ? "app_offline" : "unknown";
|
|
761
|
+
};
|
|
704
762
|
const eagerNames = new Set(builtIns.map((tool) => tool.def.name).filter((name) => !CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES.has(name)));
|
|
705
763
|
const activeBuiltIns = builtIns.filter((tool) => eagerNames.has(tool.def.name) || loadedNames.includes(tool.def.name));
|
|
706
764
|
const runtimeTools = [
|
|
@@ -712,6 +770,8 @@ export const createAiToolResolver =
|
|
|
712
770
|
store: input.store,
|
|
713
771
|
maxLoadedTools: input.maxLoadedTools,
|
|
714
772
|
unavailableLoadedNames,
|
|
773
|
+
appOperationsOffered: Boolean(input.listRegistry),
|
|
774
|
+
unavailableReason,
|
|
715
775
|
}),
|
|
716
776
|
...activeBuiltIns,
|
|
717
777
|
...(input.execute
|
package/src/ai/chat/blocks.tsx
CHANGED
|
@@ -283,7 +283,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
283
283
|
<div class="mt-4 flex flex-col gap-4 text-xs leading-5 text-secondary">
|
|
284
284
|
<p class="whitespace-pre-wrap">{description()}</p>
|
|
285
285
|
<Show when={inlineReviewDetails().length > 0}>
|
|
286
|
-
<dl class="grid gap-x-5 gap-y-1.5
|
|
286
|
+
<dl class="grid gap-x-5 gap-y-1.5 sm:grid-cols-[max-content_minmax(0,1fr)]">
|
|
287
287
|
<For each={inlineReviewDetails()}>
|
|
288
288
|
{(detail) => (
|
|
289
289
|
<>
|
|
@@ -313,7 +313,12 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
313
313
|
</pre>
|
|
314
314
|
}
|
|
315
315
|
>
|
|
316
|
-
<div
|
|
316
|
+
<div
|
|
317
|
+
class="-mx-2 max-h-72 overflow-auto px-2 text-xs text-secondary"
|
|
318
|
+
role="region"
|
|
319
|
+
tabIndex={0}
|
|
320
|
+
aria-label={detail.label}
|
|
321
|
+
>
|
|
317
322
|
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
318
323
|
</div>
|
|
319
324
|
</Show>
|
|
@@ -323,10 +328,12 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
323
328
|
</div>
|
|
324
329
|
</Show>
|
|
325
330
|
</div>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
331
|
+
{/* The decision continues the card below its content: spacing, not a line or a band, sets it apart. */}
|
|
332
|
+
<footer class="flex min-h-12 flex-wrap items-center gap-2 px-4 pb-3" data-ai-approval-footer>
|
|
333
|
+
{/* Present from the start, so screen readers announce progress after the decision buttons leave. */}
|
|
334
|
+
<span class="k2b-sr-only" role="status" aria-live="polite">
|
|
335
|
+
{approval.loading() ? t().submitting : submitted() ? t().submitted : ""}
|
|
336
|
+
</span>
|
|
330
337
|
<Show when={reviewLinks().length > 0}>
|
|
331
338
|
<nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
|
|
332
339
|
<For each={reviewLinks()}>
|
|
@@ -339,7 +346,9 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
339
346
|
</nav>
|
|
340
347
|
</Show>
|
|
341
348
|
<Show when={approval.error()}>
|
|
342
|
-
<p class="text-xs text-red-700 dark:text-red-300">
|
|
349
|
+
<p class="text-xs text-red-700 dark:text-red-300" role="alert">
|
|
350
|
+
{t().approvalFailed}
|
|
351
|
+
</p>
|
|
343
352
|
</Show>
|
|
344
353
|
<div class="ml-auto shrink-0">
|
|
345
354
|
<Show
|