@k2b/cloud 0.25.0 → 0.27.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/capabilities.ts +12 -0
- 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/registry.ts +1 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/GroupCoverage.tsx +175 -0
- package/src/access/PermissionEditor.tsx +119 -99
- package/src/access/messages.ts +30 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/approval-routes.ts +5 -5
- package/src/ai/browser-code-contracts.ts +14 -2
- package/src/ai/browser.ts +8 -1
- package/src/ai/capabilities.ts +115 -33
- package/src/ai/chat/blocks.tsx +86 -179
- package/src/ai/chat/builtin-tools.tsx +83 -48
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +44 -0
- package/src/ai/chat/message-actions.tsx +6 -2
- package/src/ai/chat/message-utils.ts +17 -14
- package/src/ai/chat/messages.ts +330 -2
- package/src/ai/chat/presentation.tsx +278 -104
- package/src/ai/chat/tool-groups.ts +55 -35
- package/src/ai/chat/turn-layout.ts +141 -0
- package/src/ai/chat/turn-view.tsx +644 -0
- package/src/ai/client/controller.ts +60 -31
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +42 -6
- package/src/ai/code-mode-skill.ts +27 -27
- package/src/ai/code-runtime-tools.ts +10 -1
- package/src/ai/code-source-contracts.ts +54 -4
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +2 -2
- package/src/ai/default-tools.ts +2 -2
- package/src/ai/executor.ts +177 -80
- 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 +7 -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/protocol.ts +26 -4
- package/src/ai/provider-fetch.ts +67 -15
- package/src/ai/provider-retry.ts +105 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +16 -7
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +34 -6
- package/src/ai/runtime.ts +1 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +31 -3
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +202 -59
- package/src/ai/stream.ts +182 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +25 -0
- package/src/ai/timeline.ts +9 -11
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-policy.ts +247 -0
- package/src/ai/turn-timing.ts +31 -3
- package/src/ai/types.ts +36 -5
- 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 +62 -0
- package/src/api/index.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +118 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +77 -0
- package/src/contracts/registry.ts +4 -0
- package/src/services/index.ts +3 -0
- package/src/services/notifications/email.ts +16 -26
- package/src/services/outgoing-mail/index.ts +19 -0
- package/src/services/outgoing-mail/store.ts +286 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +13 -0
- package/src/services/settings/core-settings.ts +1 -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 +1 -1
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/app-presentation.ts +10 -2
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +69 -0
package/src/access/messages.ts
CHANGED
|
@@ -29,6 +29,21 @@ export const accessMessages = i18n.define({
|
|
|
29
29
|
lastManager: ({ level }: { level: string }) =>
|
|
30
30
|
`The last entry with “${level}” access can't be lowered or removed. Give another person or group “${level}” access first.`,
|
|
31
31
|
lastManagerOption: ({ level }: { level: string }) => `Available once another person or group has “${level}” access`,
|
|
32
|
+
groupMembers: "Members",
|
|
33
|
+
groupMembersOf: ({ name }: { name: string }) => `Members of ${name}`,
|
|
34
|
+
groupMemberCount: ({ count }: { count: number }) =>
|
|
35
|
+
count === 0
|
|
36
|
+
? "No one receives access through this group right now."
|
|
37
|
+
: count === 1
|
|
38
|
+
? "1 person receives access through this group right now."
|
|
39
|
+
: `${count.toLocaleString("en")} people receive access through this group right now.`,
|
|
40
|
+
groupMembersLoading: "Loading members…",
|
|
41
|
+
groupMembersHidden: "Your account can't see who belongs to this group.",
|
|
42
|
+
groupMembersFailed: "Members couldn't be loaded.",
|
|
43
|
+
retry: "Try again",
|
|
44
|
+
showMoreMembers: ({ count }: { count: number }) => `Show ${count.toLocaleString("en")} more`,
|
|
45
|
+
directoryGroupHint:
|
|
46
|
+
"This group comes from the directory. Local accounts such as guests can't be members, so give them access directly.",
|
|
32
47
|
read: "Read",
|
|
33
48
|
readDescription: "Read this resource through the app API.",
|
|
34
49
|
write: "Write",
|
|
@@ -92,6 +107,21 @@ export const accessMessages = i18n.define({
|
|
|
92
107
|
lastManager: ({ level }) =>
|
|
93
108
|
`Der letzte Eintrag mit Zugriff „${level}“ kann nicht herabgestuft oder entfernt werden. Gib zuerst einer anderen Person oder Gruppe Zugriff „${level}“.`,
|
|
94
109
|
lastManagerOption: ({ level }) => `Möglich, sobald eine weitere Person oder Gruppe Zugriff „${level}“ hat`,
|
|
110
|
+
groupMembers: "Mitglieder",
|
|
111
|
+
groupMembersOf: ({ name }) => `Mitglieder von ${name}`,
|
|
112
|
+
groupMemberCount: ({ count }) =>
|
|
113
|
+
count === 0
|
|
114
|
+
? "Über diese Gruppe erhält gerade niemand Zugriff."
|
|
115
|
+
: count === 1
|
|
116
|
+
? "Über diese Gruppe erhält gerade 1 Person Zugriff."
|
|
117
|
+
: `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")} Personen Zugriff.`,
|
|
118
|
+
groupMembersLoading: "Mitglieder werden geladen…",
|
|
119
|
+
groupMembersHidden: "Dein Konto kann nicht sehen, wer zu dieser Gruppe gehört.",
|
|
120
|
+
groupMembersFailed: "Die Mitglieder konnten nicht geladen werden.",
|
|
121
|
+
retry: "Erneut versuchen",
|
|
122
|
+
showMoreMembers: ({ count }) => `${count.toLocaleString("de")} weitere anzeigen`,
|
|
123
|
+
directoryGroupHint:
|
|
124
|
+
"Diese Gruppe stammt aus dem Verzeichnis. Lokale Konten wie Gäste können nicht Mitglied sein; gib ihnen direkt Zugriff.",
|
|
95
125
|
read: "Lesen",
|
|
96
126
|
readDescription: "Diese Ressource über die App-API lesen.",
|
|
97
127
|
write: "Schreiben",
|
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";
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { listCapabilities } from "../_internal/registry";
|
|
5
5
|
import { ErrorResponseSchema } from "../contracts";
|
|
6
6
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
7
|
-
import { type AuthContext, auth, expectUserBackedActor, jsonResponse, rateLimit, requiresAuth, v } from "../server";
|
|
7
|
+
import { type AuthContext, auth, expectUserBackedActor, getLocale, jsonResponse, rateLimit, requiresAuth, v } from "../server";
|
|
8
8
|
import { type AiToolApprovalPreference, listAiToolApprovalPreferences, revokeAiToolApprovalPreference } from "./approvals";
|
|
9
9
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
10
10
|
|
|
@@ -70,7 +70,7 @@ export const createAiApprovalPreferenceRoutes = (dependencies: AiApprovalRouteDe
|
|
|
70
70
|
describeRoute({
|
|
71
71
|
tags: ["AI"],
|
|
72
72
|
summary: "List remembered AI approvals",
|
|
73
|
-
description: "Lists AI tool approvals remembered for the current user.",
|
|
73
|
+
description: "Lists AI tool approvals remembered for the current user, with titles in the request locale.",
|
|
74
74
|
...requiresAuth,
|
|
75
75
|
responses: {
|
|
76
76
|
200: jsonResponse(AiApprovalPreferenceListSchema, "Remembered approvals"),
|
|
@@ -84,16 +84,16 @@ export const createAiApprovalPreferenceRoutes = (dependencies: AiApprovalRouteDe
|
|
|
84
84
|
listPreferences(user.id),
|
|
85
85
|
registry().catch(() => [] as CapabilityRegistryEntry[]),
|
|
86
86
|
]);
|
|
87
|
-
const catalogByName = new Map(buildAiCapabilityCatalog(capabilities).map((entry) => [entry.name, entry]));
|
|
87
|
+
const catalogByName = new Map(buildAiCapabilityCatalog(capabilities, getLocale(c)).map((entry) => [entry.name, entry]));
|
|
88
88
|
const approvals: AiApprovalPreferenceView[] = preferences.map((preference) => {
|
|
89
89
|
const capability = catalogByName.get(preference.toolName);
|
|
90
90
|
return {
|
|
91
91
|
...preference,
|
|
92
|
-
title: capability?.title ?? fallbackTitle(preference.toolName),
|
|
92
|
+
title: capability?.display.title ?? fallbackTitle(preference.toolName),
|
|
93
93
|
app: capability
|
|
94
94
|
? {
|
|
95
95
|
id: capability.appId,
|
|
96
|
-
name: capability.appName,
|
|
96
|
+
name: capability.display.appName,
|
|
97
97
|
icon: capability.app.appIcon,
|
|
98
98
|
...(capability.app.appAccent ? { accent: capability.app.appAccent } : {}),
|
|
99
99
|
}
|
|
@@ -23,7 +23,7 @@ export const CodeRunInput = z
|
|
|
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. Test 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()
|
|
@@ -71,7 +71,10 @@ export const CodeActionInput = z
|
|
|
71
71
|
.positive()
|
|
72
72
|
.optional()
|
|
73
73
|
.describe("Exact draft revision returned by code_actions({draft:true}); Manage required. Supply this OR publishedVersion."),
|
|
74
|
-
input: z
|
|
74
|
+
input: z
|
|
75
|
+
.record(z.string(), z.json())
|
|
76
|
+
.default({})
|
|
77
|
+
.describe("Action input object matching the discovered inputSchema. Pass the object itself, never JSON text."),
|
|
75
78
|
})
|
|
76
79
|
.strict()
|
|
77
80
|
.refine(
|
|
@@ -162,6 +165,15 @@ export const CodeSecretInput = z
|
|
|
162
165
|
})
|
|
163
166
|
.strict();
|
|
164
167
|
|
|
168
|
+
/**
|
|
169
|
+
* A runtime call that did not complete: rejected input, timeout, lost run or host failure.
|
|
170
|
+
* Hosts return it as data because a client tool result carries no error flag; the managed
|
|
171
|
+
* server tools report it to the model as a tool error. A run snapshot whose code failed is a
|
|
172
|
+
* completed call and keeps its own `status` and `error`.
|
|
173
|
+
*/
|
|
174
|
+
export const CodeToolFailure = z.object({ failed: z.literal(true), error: z.string(), guidance: z.string().optional() });
|
|
175
|
+
export type CodeToolFailure = z.infer<typeof CodeToolFailure>;
|
|
176
|
+
|
|
165
177
|
// Internal bridge envelope. Each model-facing tool receives only its own flat schema.
|
|
166
178
|
export const CodeRuntimeInput = z.discriminatedUnion("operation", [
|
|
167
179
|
CodeRunInput.safeExtend({ operation: z.literal("run") }),
|
package/src/ai/browser.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
CODE_RUNTIME_TOOL_NAMES,
|
|
3
|
+
CodeActionInput,
|
|
4
|
+
CodeResourceId,
|
|
5
|
+
CodeRuntimeInput,
|
|
6
|
+
CodeToolFailure,
|
|
7
|
+
parseCodeToolInput,
|
|
8
|
+
} from "./browser-code-contracts";
|
|
2
9
|
export { type AiTaskRequest, type AiTaskRequestInput, AiTaskRequestSchema } from "./task-contracts";
|
|
3
10
|
export { type AiTodoPlan, parseAiTodoPlan } from "./todo-contracts";
|
|
4
11
|
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
4
5
|
import { HELP_READ_MAX_CHARS, HELP_SEARCH_MAX_LIMIT, readHelpArticle } from "../_internal/help-catalog";
|
|
5
6
|
import {
|
|
6
7
|
type CapabilityActionManifest,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
import type { CapabilityRegistryEntry } from "../contracts/registry";
|
|
14
15
|
import type { RequestActor } from "../server";
|
|
15
16
|
import { createHelpReader, type HelpReaderFactory } from "../services/help";
|
|
17
|
+
import { resolveAppIdentityPresentation } from "../shared/app-presentation";
|
|
16
18
|
import { recordRejectedAiCapability } from "./capability-execution";
|
|
17
19
|
import { CODE_SOURCE_TOOLS } from "./code-source-contracts";
|
|
18
20
|
import { createCodeSourceTool } from "./code-source-tools";
|
|
@@ -44,6 +46,12 @@ export type AiCapabilityAppCatalogItem = {
|
|
|
44
46
|
export type AiCapabilityCatalogEntry = AiCapabilityCatalogItem & {
|
|
45
47
|
app: CapabilityRegistryEntry;
|
|
46
48
|
operation: CapabilityQueryManifest | CapabilityActionManifest;
|
|
49
|
+
/**
|
|
50
|
+
* What people see in tool rows and approvals, in the locale the catalog was built for. The model keeps
|
|
51
|
+
* the base `title`, `description`, and schemas: they are prompt text, and stable prompts do not change
|
|
52
|
+
* with the reader's language.
|
|
53
|
+
*/
|
|
54
|
+
display: { appName: string; title: string };
|
|
47
55
|
};
|
|
48
56
|
|
|
49
57
|
export type AiToolKind = "builtin" | AiCapabilityKind;
|
|
@@ -64,6 +72,16 @@ type AiToolCatalogEntry = AiToolCatalogItem & {
|
|
|
64
72
|
|
|
65
73
|
export type AiRememberableCapabilityApprovals = ReadonlyMap<string, string>;
|
|
66
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
|
+
|
|
67
85
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
68
86
|
const DEFAULT_APP_LIST_LIMIT = 20;
|
|
69
87
|
const MAX_APP_LIST_LIMIT = 25;
|
|
@@ -109,8 +127,11 @@ export const buildAiCapabilityAppCatalog = (apps: readonly CapabilityRegistryEnt
|
|
|
109
127
|
});
|
|
110
128
|
};
|
|
111
129
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Build one deterministic, immutable view of the current live registry. `locale` selects the human
|
|
132
|
+
* presentation in `display`; model-facing fields always stay in the app's base presentation.
|
|
133
|
+
*/
|
|
134
|
+
export const buildAiCapabilityCatalog = (apps: CapabilityRegistryEntry[], locale?: string): AiCapabilityCatalogEntry[] => {
|
|
114
135
|
const entries = [...apps]
|
|
115
136
|
.sort(
|
|
116
137
|
(left, right) =>
|
|
@@ -119,12 +140,18 @@ export const buildAiCapabilityCatalog = (apps: CapabilityRegistryEntry[]): AiCap
|
|
|
119
140
|
left.appName.localeCompare(right.appName) ||
|
|
120
141
|
left.endpoint.localeCompare(right.endpoint),
|
|
121
142
|
)
|
|
122
|
-
.flatMap((app) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
.flatMap((app) => {
|
|
144
|
+
const appName = locale
|
|
145
|
+
? resolveAppIdentityPresentation({ name: app.appName, description: app.appDescription, presentation: app.appPresentation }, locale)
|
|
146
|
+
.name
|
|
147
|
+
: app.appName;
|
|
148
|
+
return [
|
|
149
|
+
...app.manifest.actions.map((operation) => ({ app, appName, operation, kind: "action" as const })),
|
|
150
|
+
...app.manifest.queries.map((operation) => ({ app, appName, operation, kind: "query" as const })),
|
|
151
|
+
];
|
|
152
|
+
})
|
|
126
153
|
.map(
|
|
127
|
-
({ app, operation, kind }): AiCapabilityCatalogEntry => ({
|
|
154
|
+
({ app, appName, operation, kind }): AiCapabilityCatalogEntry => ({
|
|
128
155
|
name: aiCapabilityId(app.appId, operation.localId),
|
|
129
156
|
providerName: aiCapabilityToolName(app.appId, kind, operation.localId),
|
|
130
157
|
appId: app.appId,
|
|
@@ -135,6 +162,12 @@ export const buildAiCapabilityCatalog = (apps: CapabilityRegistryEntry[]): AiCap
|
|
|
135
162
|
description: operation.description,
|
|
136
163
|
app,
|
|
137
164
|
operation,
|
|
165
|
+
display: {
|
|
166
|
+
appName,
|
|
167
|
+
title: locale
|
|
168
|
+
? resolveCapabilityOperationTitle(operation, kind === "action" ? "actions" : "queries", app.presentation, locale)
|
|
169
|
+
: operation.title,
|
|
170
|
+
},
|
|
138
171
|
}),
|
|
139
172
|
)
|
|
140
173
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -344,15 +377,16 @@ export const createAiHelpTools = (help: HelpReaderFactory = createHelpReader, lo
|
|
|
344
377
|
return [search, read];
|
|
345
378
|
};
|
|
346
379
|
|
|
380
|
+
/** `null` when the registry could not be read. */
|
|
347
381
|
const resolveCapabilityRegistry = async (
|
|
348
382
|
listRegistry: () => Promise<CapabilityRegistryEntry[]>,
|
|
349
383
|
onError?: (error: unknown) => void,
|
|
350
|
-
): Promise<CapabilityRegistryEntry[]> => {
|
|
384
|
+
): Promise<CapabilityRegistryEntry[] | null> => {
|
|
351
385
|
try {
|
|
352
386
|
return await listRegistry();
|
|
353
387
|
} catch (error) {
|
|
354
388
|
onError?.(error);
|
|
355
|
-
return
|
|
389
|
+
return null;
|
|
356
390
|
}
|
|
357
391
|
};
|
|
358
392
|
|
|
@@ -430,6 +464,9 @@ const ToolCatalogItemSchema = z
|
|
|
430
464
|
|
|
431
465
|
type ToolStateStore = Pick<AiConversationService, "loadTools">;
|
|
432
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
|
+
|
|
433
470
|
export const createAiToolMetaTools = (input: {
|
|
434
471
|
apps: readonly CapabilityRegistryEntry[];
|
|
435
472
|
catalog: readonly AiToolCatalogEntry[];
|
|
@@ -438,6 +475,10 @@ export const createAiToolMetaTools = (input: {
|
|
|
438
475
|
store: ToolStateStore;
|
|
439
476
|
maxLoadedTools?: number;
|
|
440
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;
|
|
441
482
|
}): AiRuntimeTool[] => {
|
|
442
483
|
const apps = buildAiCapabilityAppCatalog(input.apps);
|
|
443
484
|
const directoryEntries: string[] = [];
|
|
@@ -453,17 +494,17 @@ export const createAiToolMetaTools = (input: {
|
|
|
453
494
|
const liveAppDirectory =
|
|
454
495
|
directoryEntries.length > 0
|
|
455
496
|
? ` Live capability apps: ${directoryEntries.join(", ")}${hiddenAppCount > 0 ? `, and ${hiddenAppCount} more` : ""}.`
|
|
456
|
-
:
|
|
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.";
|
|
457
500
|
const unavailableLoadedNames = input.unavailableLoadedNames ?? [];
|
|
458
501
|
const unavailableLoadedNotice =
|
|
459
502
|
unavailableLoadedNames.length > 0
|
|
460
|
-
? ` Previously loaded tools
|
|
461
|
-
.slice(-MAX_UNAVAILABLE_LOADED_NAMES)
|
|
462
|
-
.join(", ")}${
|
|
503
|
+
? ` Previously loaded tools that are not available now: ${unavailableLoadedNames.slice(-MAX_UNAVAILABLE_LOADED_NAMES).join(", ")}${
|
|
463
504
|
unavailableLoadedNames.length > MAX_UNAVAILABLE_LOADED_NAMES
|
|
464
505
|
? `, and ${unavailableLoadedNames.length - MAX_UNAVAILABLE_LOADED_NAMES} more`
|
|
465
506
|
: ""
|
|
466
|
-
}.
|
|
507
|
+
}. Do not search for them again in this turn; continue without them or tell the user what is missing.`
|
|
467
508
|
: "";
|
|
468
509
|
const search = defineAiTool({
|
|
469
510
|
name: "search_tools",
|
|
@@ -501,38 +542,52 @@ export const createAiToolMetaTools = (input: {
|
|
|
501
542
|
const load = defineAiTool({
|
|
502
543
|
name: "load_tools",
|
|
503
544
|
description:
|
|
504
|
-
"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.",
|
|
505
546
|
inputSchema: z.object({ names: z.array(z.string().trim().min(1)).min(1).max(25) }).strict(),
|
|
506
547
|
outputSchema: z
|
|
507
548
|
.object({
|
|
508
|
-
loaded: z.array(
|
|
509
|
-
alreadyLoaded: z.array(
|
|
510
|
-
|
|
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()),
|
|
511
552
|
evicted: z.array(z.string()),
|
|
512
553
|
titles: z.record(z.string(), z.string()),
|
|
513
554
|
})
|
|
514
555
|
.strict(),
|
|
515
556
|
approval: "never",
|
|
516
557
|
}).server(async ({ names }) => {
|
|
517
|
-
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]] : [])));
|
|
518
561
|
const requested = [...new Set(names)];
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
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) }));
|
|
522
571
|
const updated = await input.store.loadTools({
|
|
523
572
|
conversationId: input.conversationId,
|
|
524
573
|
names: valid,
|
|
525
574
|
maxLoadedTools: input.maxLoadedTools,
|
|
526
575
|
});
|
|
576
|
+
const callable = (name: string) => ({ name, call: catalogByName.get(name)?.capability?.providerName ?? name });
|
|
527
577
|
const alreadyLoaded = [...new Set([...eager, ...updated.alreadyLoaded])];
|
|
528
|
-
const catalogByName = new Map(input.catalog.map((entry) => [entry.name, entry]));
|
|
529
578
|
const titles = Object.fromEntries(
|
|
530
579
|
[...new Set([...updated.loaded, ...alreadyLoaded, ...updated.evicted])].flatMap((name) => {
|
|
531
580
|
const entry = catalogByName.get(name);
|
|
532
581
|
return entry ? [[name, entry.title]] : [];
|
|
533
582
|
}),
|
|
534
583
|
);
|
|
535
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
loaded: updated.loaded.map(callable),
|
|
586
|
+
alreadyLoaded: alreadyLoaded.map(callable),
|
|
587
|
+
unavailable,
|
|
588
|
+
evicted: updated.evicted,
|
|
589
|
+
titles,
|
|
590
|
+
};
|
|
536
591
|
});
|
|
537
592
|
|
|
538
593
|
return [search, load, listApps];
|
|
@@ -566,8 +621,7 @@ export const createLoadedAiCapabilityTools = (input: {
|
|
|
566
621
|
if (!input.authorizeBackground && entry.kind === "action" && (entry.operation as CapabilityActionManifest).approval !== "none") {
|
|
567
622
|
const review = (await input.review?.(entry, args, context)) ?? null;
|
|
568
623
|
if (review && context.callId) input.onReview?.(context.callId, review);
|
|
569
|
-
const message =
|
|
570
|
-
review?.message ?? `${entry.appName}: ${entry.title}\nReview the validated arguments below before running this Action.`;
|
|
624
|
+
const message = review?.message ?? `${entry.display.appName}: ${entry.display.title}`;
|
|
571
625
|
if (!(await context.requestApproval(message))) {
|
|
572
626
|
if (input.actor.kind === "user") {
|
|
573
627
|
await recordRejectedAiCapability({ entry, actor: input.actor, args }).catch(() => undefined);
|
|
@@ -635,6 +689,8 @@ export const createAiToolResolver =
|
|
|
635
689
|
actor: RequestActor;
|
|
636
690
|
staticTools: AiRuntimeTool[];
|
|
637
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[];
|
|
638
694
|
runtimeContext?: Omit<AiToolPreparationContext, "actor" | "conversationId">;
|
|
639
695
|
store: Pick<AiConversationService, "getLoadedTools" | "loadTools">;
|
|
640
696
|
listRegistry?: () => Promise<CapabilityRegistryEntry[]>;
|
|
@@ -653,10 +709,11 @@ export const createAiToolResolver =
|
|
|
653
709
|
}) => void;
|
|
654
710
|
}): ToolResolver =>
|
|
655
711
|
async (): Promise<Tool[]> => {
|
|
656
|
-
const [
|
|
712
|
+
const [liveRegistry, persistedLoadedNames] = await Promise.all([
|
|
657
713
|
input.listRegistry ? resolveCapabilityRegistry(input.listRegistry, input.onCapabilityRegistryError) : [],
|
|
658
714
|
input.store.getLoadedTools({ conversationId: input.conversationId }),
|
|
659
715
|
]);
|
|
716
|
+
const registry = liveRegistry ?? [];
|
|
660
717
|
const configuredLimit = Math.floor(input.maxLoadedTools ?? 0);
|
|
661
718
|
const loadedNames = configuredLimit > 0 ? persistedLoadedNames.slice(-configuredLimit) : persistedLoadedNames;
|
|
662
719
|
if (loadedNames.length !== persistedLoadedNames.length) {
|
|
@@ -667,18 +724,41 @@ export const createAiToolResolver =
|
|
|
667
724
|
});
|
|
668
725
|
}
|
|
669
726
|
const allowed = input.allowedTools == null ? null : new Set(input.allowedTools);
|
|
670
|
-
const
|
|
727
|
+
const fullCapabilityCatalog = buildAiCapabilityCatalog(registry, input.locale);
|
|
728
|
+
const capabilityCatalog = fullCapabilityCatalog.filter((entry) => !allowed || allowed.has(entry.name));
|
|
671
729
|
const helpTools = input.help ? createAiHelpTools(input.help, input.locale) : [];
|
|
672
730
|
const resourceTool =
|
|
673
731
|
input.execute && (capabilityCatalog.length > 0 || input.staticTools.some((tool) => tool.def.name === "code_read"))
|
|
674
732
|
? createAiResourceReaderTool({ apps: registry, catalog: capabilityCatalog, execute: input.execute })
|
|
675
733
|
: null;
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
);
|
|
734
|
+
const allBuiltIns = [...input.staticTools, ...helpTools, ...(resourceTool ? [resourceTool] : [])];
|
|
735
|
+
const builtIns = allBuiltIns.filter((tool) => !allowed || allowed.has(tool.def.name));
|
|
679
736
|
const catalog = buildAiToolCatalog(builtIns, capabilityCatalog);
|
|
680
737
|
const catalogNames = new Set(catalog.map((entry) => entry.name));
|
|
681
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
|
+
};
|
|
682
762
|
const eagerNames = new Set(builtIns.map((tool) => tool.def.name).filter((name) => !CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES.has(name)));
|
|
683
763
|
const activeBuiltIns = builtIns.filter((tool) => eagerNames.has(tool.def.name) || loadedNames.includes(tool.def.name));
|
|
684
764
|
const runtimeTools = [
|
|
@@ -690,6 +770,8 @@ export const createAiToolResolver =
|
|
|
690
770
|
store: input.store,
|
|
691
771
|
maxLoadedTools: input.maxLoadedTools,
|
|
692
772
|
unavailableLoadedNames,
|
|
773
|
+
appOperationsOffered: Boolean(input.listRegistry),
|
|
774
|
+
unavailableReason,
|
|
693
775
|
}),
|
|
694
776
|
...activeBuiltIns,
|
|
695
777
|
...(input.execute
|
|
@@ -719,10 +801,10 @@ export const createAiToolResolver =
|
|
|
719
801
|
presentations.set(entry.providerName, {
|
|
720
802
|
kind: "capability",
|
|
721
803
|
appId: entry.appId,
|
|
722
|
-
appName: entry.appName,
|
|
804
|
+
appName: entry.display.appName,
|
|
723
805
|
appIcon: entry.app.appIcon,
|
|
724
806
|
appAccent: entry.app.appAccent,
|
|
725
|
-
title: entry.title,
|
|
807
|
+
title: entry.display.title,
|
|
726
808
|
capabilityKind: entry.kind,
|
|
727
809
|
});
|
|
728
810
|
// Rememberable scopes are resolved by the owning app for each concrete
|