@k2b/cloud 0.29.1 → 0.31.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 +4 -2
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +113 -5
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +24 -5
- package/src/_internal/registry-validation.ts +5 -0
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +97 -40
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +3 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +26 -2
- package/src/ai/chat/tool-groups.ts +66 -12
- package/src/ai/chat/tool-names.ts +6 -2
- package/src/ai/chat/turn-layout.ts +10 -2
- package/src/ai/chat/turn-view.tsx +123 -36
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/code-runtime-tools.ts +109 -61
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +189 -35
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/index.ts +17 -1
- package/src/ai/migrate.ts +172 -14
- package/src/ai/protocol.ts +22 -1
- package/src/ai/routes.ts +11 -0
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/runtime.ts +46 -7
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +66 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/tools.ts +91 -3
- package/src/ai/types.ts +47 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/capabilities.ts +3 -2
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +291 -101
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +30 -0
- package/src/capabilities/executions.ts +3 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/commands.ts +20 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/app.ts +34 -2
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +10 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +67 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/server/index.ts +1 -0
- package/src/server/widget-request.ts +27 -0
- package/src/services/accounts/users.ts +15 -0
- package/src/services/approvals/index.ts +225 -0
- package/src/services/approvals/migrate.ts +33 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +43 -8
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +216 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +35 -2
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
|
@@ -3,8 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
4
4
|
import { getApp } from "../_internal/registry";
|
|
5
5
|
import type { RequestActor } from "../server";
|
|
6
|
+
import { IDENTITY_REFRESH_TIMEOUT_MS } from "../services/identity/constants";
|
|
6
7
|
import { signInvocationToken } from "../services/identity/invocation-token";
|
|
7
8
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
9
|
+
import { logger } from "../services/logging";
|
|
8
10
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
9
11
|
import { TIMEZONE_COOKIE } from "../shared/time";
|
|
10
12
|
import { CodeToolFailure } from "./browser-code-contracts";
|
|
@@ -12,6 +14,10 @@ import { resolveAiCapabilityActor } from "./capability-execution";
|
|
|
12
14
|
import { CODE_CAPABILITY_TOKEN_HEADER, codeCapabilityOperation } from "./code-capability-transport";
|
|
13
15
|
import { authorizeCodeExecution } from "./code-execution";
|
|
14
16
|
import { aiConversations } from "./store";
|
|
17
|
+
import type { AiApprovalTarget, AiWebsiteReceipt } from "./types";
|
|
18
|
+
|
|
19
|
+
const log = logger("ai:code-runtime");
|
|
20
|
+
const ISSUANCE_TIMEOUT_MS = 5_000;
|
|
15
21
|
|
|
16
22
|
const Reply = z.object({
|
|
17
23
|
ok: z.literal(true),
|
|
@@ -19,7 +25,20 @@ const Reply = z.object({
|
|
|
19
25
|
status: z.enum(["running", "busy", "done", "lost"]),
|
|
20
26
|
phase: z.enum(["starting", "running", "busy", "waiting_for_user"]).optional(),
|
|
21
27
|
result: z.unknown().optional(),
|
|
22
|
-
approvals: z.array(
|
|
28
|
+
approvals: z.array(
|
|
29
|
+
z.object({
|
|
30
|
+
id: z.uuid(),
|
|
31
|
+
approvalId: z.uuid().optional(),
|
|
32
|
+
message: z.string(),
|
|
33
|
+
decision: z.boolean().nullable(),
|
|
34
|
+
/** What the person may remember: the website or Action, derived by the host from the stored request. */
|
|
35
|
+
remember: z.object({ toolName: z.string(), approvalScope: z.string(), always: z.boolean() }).optional(),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
/** The next requests a website approval for this chat let through without asking, after `receiptsAfter`. */
|
|
39
|
+
receipts: z.array(z.object({ method: z.string(), url: z.string() })).default([]),
|
|
40
|
+
/** More receipts follow; the answer then leaves out the result until the last one is read. */
|
|
41
|
+
moreReceipts: z.boolean().default(false),
|
|
23
42
|
}),
|
|
24
43
|
});
|
|
25
44
|
type Context = ToolContext & {
|
|
@@ -29,6 +48,9 @@ type Context = ToolContext & {
|
|
|
29
48
|
locale?: string;
|
|
30
49
|
timeZone?: string;
|
|
31
50
|
reportProgress?: (message: string) => Promise<void>;
|
|
51
|
+
/** Relays a decision; the managed host consumes the linked platform approval before execution. */
|
|
52
|
+
requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
|
|
53
|
+
reportWebsiteReceipts?: (receipts: AiWebsiteReceipt[]) => Promise<void>;
|
|
32
54
|
};
|
|
33
55
|
|
|
34
56
|
/** The tab only renders progress and approvals; server calls own all execution and resumption. */
|
|
@@ -49,54 +71,64 @@ export const runManagedCodeTool =
|
|
|
49
71
|
await context.reportProgress?.(context.locale?.startsWith("de") ? "Ausführungshost verbinden" : "Connecting execution host");
|
|
50
72
|
const app = await getApp("assistant");
|
|
51
73
|
if (!app) throw new Error("Assistant code host is unavailable");
|
|
74
|
+
const issue = async (audience: { targetAppId: string; callingAppId: string; operation: string }) => {
|
|
75
|
+
// A token is signed before its request goes out, so a timed-out issuance
|
|
76
|
+
// never reached the host and is safe to repeat. Retry long enough for a
|
|
77
|
+
// stalled shared refresh to finish or fall back to the cached key. Every
|
|
78
|
+
// timeout used up a full attempt or ended a shared load, so this cannot spin.
|
|
79
|
+
const giveUpAt = Date.now() + IDENTITY_REFRESH_TIMEOUT_MS + ISSUANCE_TIMEOUT_MS;
|
|
80
|
+
while (true) {
|
|
81
|
+
try {
|
|
82
|
+
return await withActiveIdentitySigner(
|
|
83
|
+
"invocation",
|
|
84
|
+
(signer) =>
|
|
85
|
+
signInvocationToken({
|
|
86
|
+
...audience,
|
|
87
|
+
schemaHash: null,
|
|
88
|
+
authority: {
|
|
89
|
+
sub: actor.user.id,
|
|
90
|
+
principal_type: "user",
|
|
91
|
+
access_subject_type: "user",
|
|
92
|
+
access_subject_id: actor.user.id,
|
|
93
|
+
credential_kind: "session",
|
|
94
|
+
scopes: [],
|
|
95
|
+
},
|
|
96
|
+
signer,
|
|
97
|
+
issuer: signer.issuer,
|
|
98
|
+
}),
|
|
99
|
+
{ signal: context.signal, timeoutMs: ISSUANCE_TIMEOUT_MS },
|
|
100
|
+
);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (context.signal.aborted || !(error instanceof DOMException && error.name === "TimeoutError")) throw error;
|
|
103
|
+
if (Date.now() < giveUpAt) continue;
|
|
104
|
+
log.warn("Code call gave up on identity issuance", { tool: name, error: error.message });
|
|
105
|
+
throw new Error(
|
|
106
|
+
context.locale?.startsWith("de")
|
|
107
|
+
? "Der Identitätsdienst hat nicht rechtzeitig geantwortet. Prüfe, ob dieser Aufruf schon gestartet ist, bevor du ihn erneut ausführst."
|
|
108
|
+
: "The identity service did not respond in time. Check whether this call already started before running it again.",
|
|
109
|
+
{ cause: error },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// Polls run every 250 ms for as long as the code runs. Reuse both tokens
|
|
115
|
+
// while they stay valid for at least 10 s, so a long run or an approval
|
|
116
|
+
// round trip does not depend on a fresh issuance for every poll.
|
|
117
|
+
const usable = (token: Awaited<ReturnType<typeof signInvocationToken>> | undefined) =>
|
|
118
|
+
token && token.claims.exp * 1000 - Date.now() >= 10_000 ? token : undefined;
|
|
119
|
+
let signed: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
|
|
52
120
|
let callback: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
|
|
53
|
-
const request = async (decision?: { id: string; approved: boolean }) => {
|
|
121
|
+
const request = async ({ decision, receiptsAfter }: { decision?: { id: string; approved: boolean }; receiptsAfter: number }) => {
|
|
54
122
|
context.signal.throwIfAborted();
|
|
55
123
|
await authorizeCodeExecution(context.conversationId!, context.turnId!, actor.user.id);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
authority: {
|
|
65
|
-
sub: actor.user.id,
|
|
66
|
-
principal_type: "user",
|
|
67
|
-
access_subject_type: "user",
|
|
68
|
-
access_subject_id: actor.user.id,
|
|
69
|
-
credential_kind: "session",
|
|
70
|
-
scopes: [],
|
|
71
|
-
},
|
|
72
|
-
signer,
|
|
73
|
-
issuer: signer.issuer,
|
|
74
|
-
}),
|
|
75
|
-
{ signal: context.signal, timeoutMs: 5000 },
|
|
76
|
-
);
|
|
77
|
-
if (!callback || callback.claims.exp * 1000 - Date.now() < 10_000) {
|
|
78
|
-
callback = await withActiveIdentitySigner(
|
|
79
|
-
"invocation",
|
|
80
|
-
(signer) =>
|
|
81
|
-
signInvocationToken({
|
|
82
|
-
targetAppId: "core",
|
|
83
|
-
callingAppId: "assistant",
|
|
84
|
-
operation: codeCapabilityOperation(context.conversationId!, context.turnId!),
|
|
85
|
-
schemaHash: null,
|
|
86
|
-
authority: {
|
|
87
|
-
sub: actor.user.id,
|
|
88
|
-
principal_type: "user",
|
|
89
|
-
access_subject_type: "user",
|
|
90
|
-
access_subject_id: actor.user.id,
|
|
91
|
-
credential_kind: "session",
|
|
92
|
-
scopes: [],
|
|
93
|
-
},
|
|
94
|
-
signer,
|
|
95
|
-
issuer: signer.issuer,
|
|
96
|
-
}),
|
|
97
|
-
{ signal: context.signal, timeoutMs: 5000 },
|
|
98
|
-
);
|
|
99
|
-
}
|
|
124
|
+
signed = usable(signed) ?? (await issue({ targetAppId: "assistant", callingAppId: "core", operation: `tool:${name}` }));
|
|
125
|
+
callback =
|
|
126
|
+
usable(callback) ??
|
|
127
|
+
(await issue({
|
|
128
|
+
targetAppId: "core",
|
|
129
|
+
callingAppId: "assistant",
|
|
130
|
+
operation: codeCapabilityOperation(context.conversationId!, context.turnId!),
|
|
131
|
+
}));
|
|
100
132
|
const headers = new Headers({ authorization: `Bearer ${signed.token}`, "content-type": "application/json" });
|
|
101
133
|
headers.set(CODE_CAPABILITY_TOKEN_HEADER, callback.token);
|
|
102
134
|
if (context.locale) headers.set(LOCALE_HEADER, context.locale);
|
|
@@ -112,7 +144,7 @@ export const runManagedCodeTool =
|
|
|
112
144
|
signal: context.signal,
|
|
113
145
|
body: JSON.stringify({
|
|
114
146
|
conversationId: context.conversationId,
|
|
115
|
-
input: { turnId: context.turnId, callId: context.callId, name, args, ...(decision ? { decision } : {}) },
|
|
147
|
+
input: { turnId: context.turnId, callId: context.callId, name, args, receiptsAfter, ...(decision ? { decision } : {}) },
|
|
116
148
|
}),
|
|
117
149
|
});
|
|
118
150
|
const body = await readBoundedJson(response, 256 * 1024);
|
|
@@ -120,29 +152,41 @@ export const runManagedCodeTool =
|
|
|
120
152
|
throw new Error("Code host request failed; inspect the existing call before starting another execution");
|
|
121
153
|
return Reply.parse(body.data).data;
|
|
122
154
|
};
|
|
155
|
+
const backgroundApproval = async (): Promise<boolean> => {
|
|
156
|
+
throw new Error("Background code cannot request interactive approval. Update task grants in the normal chat.");
|
|
157
|
+
};
|
|
123
158
|
return waitForManagedCodeCall(
|
|
124
159
|
request,
|
|
125
|
-
runConfig.background
|
|
126
|
-
? {
|
|
127
|
-
...context,
|
|
128
|
-
requestApproval: async () => {
|
|
129
|
-
throw new Error("Background code cannot request interactive approval. Update task grants in the normal chat.");
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
: context,
|
|
160
|
+
runConfig.background ? { ...context, requestApproval: backgroundApproval, requestRelayedApproval: backgroundApproval } : context,
|
|
133
161
|
);
|
|
134
162
|
};
|
|
135
163
|
|
|
136
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Ordered reviews are replayed through Nessi before consuming a durable result. Each request a website approval let
|
|
166
|
+
* through is reported as a receipt as soon as the host names it, so the chat shows it with its full URL whatever the
|
|
167
|
+
* run's outcome. Receipts arrive page by page after the ones already reported.
|
|
168
|
+
*/
|
|
137
169
|
export async function waitForManagedCodeCall(
|
|
138
|
-
request: (decision?: { id: string; approved: boolean }) => Promise<z.
|
|
139
|
-
context: Pick<ToolContext, "signal" | "requestApproval"> & {
|
|
170
|
+
request: (input: { decision?: { id: string; approved: boolean }; receiptsAfter: number }) => Promise<z.input<typeof Reply>["data"]>,
|
|
171
|
+
context: Pick<ToolContext, "signal" | "requestApproval"> & {
|
|
172
|
+
locale?: string;
|
|
173
|
+
reportProgress?: (message: string) => Promise<void>;
|
|
174
|
+
/** Relays a decision; the managed host consumes the linked platform approval before execution. */
|
|
175
|
+
requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
|
|
176
|
+
reportWebsiteReceipts?: (receipts: AiWebsiteReceipt[]) => Promise<void>;
|
|
177
|
+
},
|
|
140
178
|
): Promise<z.infer<ReturnType<typeof z.json>>> {
|
|
141
179
|
const seen = new Set<string>();
|
|
142
180
|
let lastPhase: string | undefined;
|
|
181
|
+
let receiptsAfter = 0;
|
|
143
182
|
while (true) {
|
|
144
183
|
context.signal.throwIfAborted();
|
|
145
|
-
const state = await request();
|
|
184
|
+
const state = Reply.shape.data.parse(await request({ receiptsAfter }));
|
|
185
|
+
if (state.receipts.length) {
|
|
186
|
+
await context.reportWebsiteReceipts?.(state.receipts.map((receipt, index) => ({ index: receiptsAfter + index, ...receipt })));
|
|
187
|
+
receiptsAfter += state.receipts.length;
|
|
188
|
+
}
|
|
189
|
+
if (state.moreReceipts) continue;
|
|
146
190
|
if (state.phase && state.phase !== lastPhase) {
|
|
147
191
|
lastPhase = state.phase;
|
|
148
192
|
const labels = context.locale?.startsWith("de")
|
|
@@ -164,9 +208,13 @@ export async function waitForManagedCodeCall(
|
|
|
164
208
|
// Skipping already resolved reviews would shift Nessi's child action IDs.
|
|
165
209
|
for (const approval of state.approvals) {
|
|
166
210
|
if (seen.has(approval.id)) continue;
|
|
167
|
-
|
|
211
|
+
if (!context.requestRelayedApproval) throw new Error("Managed code approval requires a relay context");
|
|
212
|
+
const approved = await context.requestRelayedApproval(approval.message, {
|
|
213
|
+
approvalId: approval.approvalId,
|
|
214
|
+
target: approval.remember,
|
|
215
|
+
});
|
|
168
216
|
seen.add(approval.id);
|
|
169
|
-
if (approval.decision === null) await request({ id: approval.id, approved });
|
|
217
|
+
if (approval.decision === null) await request({ decision: { id: approval.id, approved }, receiptsAfter });
|
|
170
218
|
}
|
|
171
219
|
if (state.status === "done") {
|
|
172
220
|
// Nessi records a thrown error as a failed tool result the model can act on.
|
|
@@ -12,15 +12,6 @@ const ArtifactPath = z
|
|
|
12
12
|
.max(180)
|
|
13
13
|
.regex(/^[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)*$/)
|
|
14
14
|
.refine((path) => path.split("/").every((part) => part !== "." && part !== ".."));
|
|
15
|
-
const ArtifactFile = z
|
|
16
|
-
.object({
|
|
17
|
-
path: ArtifactPath,
|
|
18
|
-
content: z
|
|
19
|
-
.string()
|
|
20
|
-
.max(1024 * 1024)
|
|
21
|
-
.describe("Complete source file content; read existing content before replacing it."),
|
|
22
|
-
})
|
|
23
|
-
.strict();
|
|
24
15
|
const Icon = z
|
|
25
16
|
.string()
|
|
26
17
|
.regex(/^ti ti-[a-z0-9-]+$/)
|
|
@@ -39,15 +30,6 @@ const DatabaseSql = z.object({
|
|
|
39
30
|
params: z.array(z.json()).max(1000).default([]),
|
|
40
31
|
});
|
|
41
32
|
|
|
42
|
-
const CodeWriteInput = Id.extend({
|
|
43
|
-
expectedRevision: z.number().int().positive(),
|
|
44
|
-
entry: ArtifactPath.optional().describe("index.html for an app with an interface, or the script that code_run executes."),
|
|
45
|
-
files: z
|
|
46
|
-
.array(z.union([ArtifactFile, z.object({ path: ArtifactPath, fromFile: AiFileReference }).strict()]))
|
|
47
|
-
.min(1)
|
|
48
|
-
.max(64),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
33
|
const DbName = z
|
|
52
34
|
.string()
|
|
53
35
|
.regex(/^[A-Za-z][A-Za-z0-9_]*$/)
|
|
@@ -305,11 +287,33 @@ export const CODE_SOURCE_TOOLS = {
|
|
|
305
287
|
"Create a private reusable App: an index.html interface with optional style.css and app.js, published actions and optional persistence. Starts with a minimal index.html. For one-off analysis, use code_run with code instead. Returns id and entry path; write source with code_write. Optional icon uses the complete class, e.g. ti ti-chart-bar; omit it when unsure. Does not run or share anything.",
|
|
306
288
|
input: ArtifactCreate.pick({ title: true, description: true, icon: true }),
|
|
307
289
|
},
|
|
290
|
+
// Source text stays top-level because some providers corrupt strings nested in arrays.
|
|
291
|
+
// Imports carry only references, so they keep an array and one review per batch.
|
|
308
292
|
code_write: {
|
|
309
293
|
description:
|
|
310
|
-
"
|
|
311
|
-
|
|
312
|
-
|
|
294
|
+
"Save one complete source file against expectedRevision from code_read, code_create or the previous write. Other files stay unchanged. Write a multi-file app with one call per file and wait for each result, passing its returned revision as the next expectedRevision; calls sent together in one response share a stale revision, so all but the first fail with CONFLICT. Returns revision, the lines and length of every file, and compiler diagnostics for the saved state; an import of a file not yet written is reported as missing until it is saved. Never runs. Copy exact chat, Project or App files with code_import.",
|
|
295
|
+
input: Id.extend({
|
|
296
|
+
expectedRevision: z.number().int().positive().describe("Revision from code_read, code_create or the previous write."),
|
|
297
|
+
path: ArtifactPath.describe("Relative path of the file to save."),
|
|
298
|
+
content: z
|
|
299
|
+
.string()
|
|
300
|
+
.max(1024 * 1024)
|
|
301
|
+
.describe("Complete file content as plain text; read existing content before replacing it."),
|
|
302
|
+
entry: ArtifactPath.optional().describe("index.html for an app with an interface, or the script that code_run executes."),
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
code_import: {
|
|
306
|
+
description:
|
|
307
|
+
"Atomically import exact chat, Project or App file references as UTF-8 source files against expectedRevision, without printing their bytes. One fresh review covers the whole batch because source may be shared or published. Use code_file_stat first; source file and bundle byte limits apply. Returns revision, the lines and length of every file, imported and diagnostics. Write source text with code_write.",
|
|
308
|
+
review: true,
|
|
309
|
+
input: Id.extend({
|
|
310
|
+
expectedRevision: z.number().int().positive(),
|
|
311
|
+
entry: ArtifactPath.optional().describe("index.html for an app with an interface, or the script that code_run executes."),
|
|
312
|
+
files: z
|
|
313
|
+
.array(z.object({ path: ArtifactPath, fromFile: AiFileReference }).strict())
|
|
314
|
+
.min(1)
|
|
315
|
+
.max(64),
|
|
316
|
+
}),
|
|
313
317
|
},
|
|
314
318
|
code_remove: {
|
|
315
319
|
description:
|
|
@@ -21,6 +21,7 @@ import { defineAiTool } from "./tools";
|
|
|
21
21
|
const WRITE_TOOLS = new Set([
|
|
22
22
|
"code_create",
|
|
23
23
|
"code_write",
|
|
24
|
+
"code_import",
|
|
24
25
|
"code_remove",
|
|
25
26
|
"code_update",
|
|
26
27
|
"code_fork",
|
|
@@ -131,7 +132,7 @@ export function createCodeSourceTool(name: CodeSourceToolName) {
|
|
|
131
132
|
if (!response.ok) throw new Error("Assistant tool request failed.");
|
|
132
133
|
return { data: reply.data.data, status: response.status };
|
|
133
134
|
};
|
|
134
|
-
if ("review" in definition &&
|
|
135
|
+
if ("review" in definition && definition.review) {
|
|
135
136
|
const preview = z.object({ data: z.object({ message: z.string().min(1) }) }).parse((await send(true)).data).data;
|
|
136
137
|
if (!authorizeDatabase && !(await context.requestApproval(preview.message)))
|
|
137
138
|
throw new Error("The user declined this change. No mutation was sent.");
|
|
@@ -191,5 +192,6 @@ export const createCodeSourceTools = () => [
|
|
|
191
192
|
createCodeSourceTool("code_update"),
|
|
192
193
|
createCodeSourceTool("code_create"),
|
|
193
194
|
createCodeSourceTool("code_write"),
|
|
195
|
+
createCodeSourceTool("code_import"),
|
|
194
196
|
createCodeSourceTool("code_remove"),
|
|
195
197
|
];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { chartDataIssue, chartDateTime, chartDrawingIssue } from "./chart-block";
|
|
2
3
|
|
|
3
4
|
const ToneSchema = z.enum(["neutral", "blue", "teal", "green", "amber", "red"]);
|
|
4
5
|
|
|
@@ -14,6 +15,202 @@ export const CloudAiCardInputSchema = z.object({
|
|
|
14
15
|
});
|
|
15
16
|
export const CloudAiCardOutputSchema = z.object({ displayed: z.boolean() });
|
|
16
17
|
|
|
18
|
+
/** The chart palette has eight colors; a ninth series would repeat one. */
|
|
19
|
+
export const CLOUD_AI_CHART_MAX_SERIES = 8;
|
|
20
|
+
/** Charts draw on a 480-unit wide plane: more values per list than units cannot be told apart. */
|
|
21
|
+
export const CLOUD_AI_CHART_MAX_VALUES = 480;
|
|
22
|
+
|
|
23
|
+
// Every chart schema is built by a function whose call is marked pure. Bundles that only use other exports of this
|
|
24
|
+
// module, such as every Studio app frame through `@k2b/cloud/ai/browser`, can then leave the chart contract out.
|
|
25
|
+
const chartParts = () => {
|
|
26
|
+
const ChartLabel = z.string().trim().min(1).max(80);
|
|
27
|
+
const ChartDate = z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?)?$/)
|
|
30
|
+
.refine((value) => chartDateTime(value) !== undefined, "Not a valid date.");
|
|
31
|
+
const ChartPoint = z.object({
|
|
32
|
+
x: z
|
|
33
|
+
.union([z.number(), ChartDate])
|
|
34
|
+
.describe(
|
|
35
|
+
"A number, a date YYYY-MM-DD, or a date and time YYYY-MM-DDTHH:mm or YYYY-MM-DDTHH:mm:ss in the user's time zone without an offset.",
|
|
36
|
+
),
|
|
37
|
+
y: z.number(),
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
ChartLabel,
|
|
41
|
+
ChartAxis: z.object({
|
|
42
|
+
label: ChartLabel.optional(),
|
|
43
|
+
domain: z.array(z.number()).length(2).optional().describe("Exact [min, max]; must contain every value."),
|
|
44
|
+
ticks: z.number().int().min(2).max(12).optional(),
|
|
45
|
+
scale: z.enum(["linear", "log"]).optional().describe("log needs values above zero."),
|
|
46
|
+
}),
|
|
47
|
+
ChartCategory: z.object({ label: ChartLabel, value: z.number() }),
|
|
48
|
+
ChartSlice: z.object({ label: ChartLabel, value: z.number().min(0) }),
|
|
49
|
+
ChartSeries: (points: z.ZodArray<typeof ChartPoint>) => z.object({ label: ChartLabel.optional(), data: points }),
|
|
50
|
+
ChartPoints: z.array(ChartPoint).max(CLOUD_AI_CHART_MAX_VALUES),
|
|
51
|
+
ChartBins: z
|
|
52
|
+
.union([
|
|
53
|
+
z.number().int().min(1).max(CLOUD_AI_CHART_MAX_VALUES),
|
|
54
|
+
z
|
|
55
|
+
.array(z.number())
|
|
56
|
+
.min(2)
|
|
57
|
+
.max(CLOUD_AI_CHART_MAX_VALUES + 1),
|
|
58
|
+
])
|
|
59
|
+
.describe("A bin count or ascending bin edges."),
|
|
60
|
+
ChartCommon: {
|
|
61
|
+
title: z.string().trim().min(1).max(160),
|
|
62
|
+
subtitle: z.string().trim().min(1).max(300).optional().describe("Unit, period or source, shown under the title."),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const chartKinds = () => {
|
|
68
|
+
const { ChartAxis, ChartBins, ChartCategory, ChartCommon, ChartLabel, ChartPoints, ChartSeries, ChartSlice } = chartParts();
|
|
69
|
+
return z.discriminatedUnion("kind", [
|
|
70
|
+
z.object({
|
|
71
|
+
kind: z.literal("bar"),
|
|
72
|
+
...ChartCommon,
|
|
73
|
+
data: z.array(ChartCategory).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
|
|
74
|
+
yAxis: ChartAxis.optional(),
|
|
75
|
+
colorByBar: z.boolean().optional(),
|
|
76
|
+
showValues: z.boolean().optional(),
|
|
77
|
+
legend: z.boolean().optional(),
|
|
78
|
+
}),
|
|
79
|
+
z.object({
|
|
80
|
+
kind: z.literal("line"),
|
|
81
|
+
...ChartCommon,
|
|
82
|
+
series: z
|
|
83
|
+
.array(
|
|
84
|
+
ChartSeries(ChartPoints.min(2, "A line needs at least two points per series; show single values as a bar or scatter chart.")),
|
|
85
|
+
)
|
|
86
|
+
.min(1)
|
|
87
|
+
.max(CLOUD_AI_CHART_MAX_SERIES),
|
|
88
|
+
xAxis: ChartAxis.optional(),
|
|
89
|
+
yAxis: ChartAxis.optional(),
|
|
90
|
+
legend: z.boolean().optional(),
|
|
91
|
+
area: z.boolean().optional(),
|
|
92
|
+
smooth: z.boolean().optional(),
|
|
93
|
+
}),
|
|
94
|
+
z.object({
|
|
95
|
+
kind: z.literal("scatter"),
|
|
96
|
+
...ChartCommon,
|
|
97
|
+
series: z
|
|
98
|
+
.array(ChartSeries(ChartPoints.min(1)))
|
|
99
|
+
.min(1)
|
|
100
|
+
.max(CLOUD_AI_CHART_MAX_SERIES),
|
|
101
|
+
xAxis: ChartAxis.optional(),
|
|
102
|
+
yAxis: ChartAxis.optional(),
|
|
103
|
+
legend: z.boolean().optional(),
|
|
104
|
+
}),
|
|
105
|
+
z.object({
|
|
106
|
+
kind: z.enum(["pie", "donut"]),
|
|
107
|
+
...ChartCommon,
|
|
108
|
+
data: z.array(ChartSlice).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
|
|
109
|
+
legend: z.boolean().optional(),
|
|
110
|
+
showLabels: z.boolean().optional(),
|
|
111
|
+
}),
|
|
112
|
+
z.object({
|
|
113
|
+
kind: z.literal("histogram"),
|
|
114
|
+
...ChartCommon,
|
|
115
|
+
data: z.array(z.number()).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
|
|
116
|
+
bins: ChartBins.optional(),
|
|
117
|
+
}),
|
|
118
|
+
z.object({
|
|
119
|
+
kind: z.literal("gauge"),
|
|
120
|
+
...ChartCommon,
|
|
121
|
+
value: z.number(),
|
|
122
|
+
min: z.number().optional(),
|
|
123
|
+
max: z.number().optional(),
|
|
124
|
+
label: ChartLabel.optional(),
|
|
125
|
+
unit: z.string().trim().min(1).max(12).optional(),
|
|
126
|
+
}),
|
|
127
|
+
z.object({
|
|
128
|
+
kind: z.literal("sparkline"),
|
|
129
|
+
...ChartCommon,
|
|
130
|
+
data: z.array(z.number()).min(2).max(CLOUD_AI_CHART_MAX_VALUES),
|
|
131
|
+
area: z.boolean().optional(),
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Checks run on complete input only, so out-of-range input fails on its bounds before anything is computed or drawn. */
|
|
137
|
+
const whenValid = { when: (payload: { issues: readonly unknown[] }) => payload.issues.length === 0 };
|
|
138
|
+
|
|
139
|
+
const chartSchema = () =>
|
|
140
|
+
chartKinds().superRefine((chart, context) => {
|
|
141
|
+
const issue = chartDataIssue(chart);
|
|
142
|
+
if (issue) context.addIssue({ code: "custom", message: issue });
|
|
143
|
+
}, whenValid);
|
|
144
|
+
const ChartSchema = /* @__PURE__ */ chartSchema();
|
|
145
|
+
|
|
146
|
+
/** One chart as the renderers read it: a `cloud.chart()` kind with its options. */
|
|
147
|
+
export type CloudAiChartInput = z.output<ReturnType<typeof chartKinds>>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Reads the arguments of a `chart` call without drawing them; undefined when they are not a chart. The tool schema
|
|
151
|
+
* has drawn them once already, so renderers draw only for display.
|
|
152
|
+
*/
|
|
153
|
+
export const parseCloudAiChartInput = (value: unknown): CloudAiChartInput | undefined => {
|
|
154
|
+
const parsed = ChartSchema.safeParse(value);
|
|
155
|
+
return parsed.success ? parsed.data : undefined;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const chartInputSchema = () => {
|
|
159
|
+
const { ChartAxis, ChartBins, ChartCategory, ChartCommon, ChartLabel, ChartPoints, ChartSeries } = chartParts();
|
|
160
|
+
return z
|
|
161
|
+
.object({
|
|
162
|
+
kind: z.enum(["bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline"]),
|
|
163
|
+
...ChartCommon,
|
|
164
|
+
data: z
|
|
165
|
+
.union([z.array(ChartCategory).min(1).max(CLOUD_AI_CHART_MAX_VALUES), z.array(z.number()).min(1).max(CLOUD_AI_CHART_MAX_VALUES)])
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("bar, pie, donut: [{ label, value }]. histogram: raw observations to count into bins. sparkline: values in order."),
|
|
168
|
+
series: z
|
|
169
|
+
.array(ChartSeries(ChartPoints.min(1)))
|
|
170
|
+
.min(1)
|
|
171
|
+
.max(CLOUD_AI_CHART_MAX_SERIES)
|
|
172
|
+
.optional()
|
|
173
|
+
.describe("line and scatter: one entry per series. A line needs at least two points per series."),
|
|
174
|
+
xAxis: ChartAxis.optional().describe("line and scatter."),
|
|
175
|
+
yAxis: ChartAxis.optional().describe("bar, line and scatter."),
|
|
176
|
+
legend: z
|
|
177
|
+
.boolean()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe(
|
|
180
|
+
"bar (with colorByBar), line, scatter, pie, donut. Defaults to true for several series, and for pie and donut unless showLabels is set.",
|
|
181
|
+
),
|
|
182
|
+
colorByBar: z.boolean().optional().describe("bar: one palette color per bar."),
|
|
183
|
+
showValues: z.boolean().optional().describe("bar: print each value on its bar."),
|
|
184
|
+
area: z.boolean().optional().describe("line and sparkline: fill the area under the line."),
|
|
185
|
+
smooth: z.boolean().optional().describe("line."),
|
|
186
|
+
showLabels: z.boolean().optional().describe("pie and donut: name each slice next to it instead of in a legend."),
|
|
187
|
+
bins: ChartBins.optional().describe("histogram: a bin count or ascending bin edges."),
|
|
188
|
+
value: z.number().optional().describe("gauge: the value to show."),
|
|
189
|
+
min: z.number().optional().describe("gauge: start of the scale, default 0."),
|
|
190
|
+
max: z.number().optional().describe("gauge: end of the scale, default 100."),
|
|
191
|
+
label: ChartLabel.optional().describe("gauge: what the value measures."),
|
|
192
|
+
unit: z.string().trim().min(1).max(12).optional().describe("gauge: unit after the value."),
|
|
193
|
+
})
|
|
194
|
+
.superRefine((value, context) => {
|
|
195
|
+
const parsed = ChartSchema.safeParse(value);
|
|
196
|
+
if (!parsed.success) {
|
|
197
|
+
for (const issue of parsed.error.issues) context.addIssue({ code: "custom", message: issue.message, path: issue.path });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const issue = chartDrawingIssue(parsed.data);
|
|
201
|
+
if (issue) context.addIssue({ code: "custom", message: issue });
|
|
202
|
+
}, whenValid);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The `cloud.chart()` options of Studio code as plain data, without `width`, `height` and `format` functions. Model
|
|
207
|
+
* providers accept only an object at the root of a tool schema, so every kind shares one object and each field names
|
|
208
|
+
* the kinds it belongs to; the kind's own rules are checked after that. Valid input also draws: values outside a
|
|
209
|
+
* domain, values at or below zero on a log axis, or a pie too small for its legend fail here instead of in the chat.
|
|
210
|
+
*/
|
|
211
|
+
export const CloudAiChartInputSchema = /* @__PURE__ */ chartInputSchema();
|
|
212
|
+
export const CloudAiChartOutputSchema = z.object({ displayed: z.boolean() });
|
|
213
|
+
|
|
17
214
|
const SurveyQuestionSchema = z.discriminatedUnion("type", [
|
|
18
215
|
z.object({
|
|
19
216
|
type: z.literal("single"),
|
|
@@ -100,6 +297,7 @@ export const CloudAiLocalBashOutputSchema = z.object({
|
|
|
100
297
|
|
|
101
298
|
export type CloudAiCardInput = z.infer<typeof CloudAiCardInputSchema>;
|
|
102
299
|
export type CloudAiCardOutput = z.infer<typeof CloudAiCardOutputSchema>;
|
|
300
|
+
export type CloudAiChartOutput = z.infer<typeof CloudAiChartOutputSchema>;
|
|
103
301
|
export type CloudAiSurveyInput = z.infer<typeof CloudAiSurveyInputSchema>;
|
|
104
302
|
export type CloudAiSurveyOutput = z.infer<typeof CloudAiSurveyOutputSchema>;
|
|
105
303
|
export type CloudAiTextEditorInput = z.infer<typeof CloudAiTextEditorInputSchema>;
|
package/src/ai/default-tools.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { createCodeSourceTools } from "./code-source-tools";
|
|
|
20
20
|
import {
|
|
21
21
|
CloudAiCardInputSchema,
|
|
22
22
|
CloudAiCardOutputSchema,
|
|
23
|
+
CloudAiChartInputSchema,
|
|
24
|
+
CloudAiChartOutputSchema,
|
|
23
25
|
CloudAiLocalBashInputSchema,
|
|
24
26
|
CloudAiLocalBashOutputSchema,
|
|
25
27
|
CloudAiSurveyInputSchema,
|
|
@@ -48,6 +50,10 @@ export {
|
|
|
48
50
|
CloudAiCardInputSchema,
|
|
49
51
|
type CloudAiCardOutput,
|
|
50
52
|
CloudAiCardOutputSchema,
|
|
53
|
+
type CloudAiChartInput,
|
|
54
|
+
CloudAiChartInputSchema,
|
|
55
|
+
type CloudAiChartOutput,
|
|
56
|
+
CloudAiChartOutputSchema,
|
|
51
57
|
type CloudAiLocalBashInput,
|
|
52
58
|
CloudAiLocalBashInputSchema,
|
|
53
59
|
type CloudAiLocalBashOutput,
|
|
@@ -73,6 +79,18 @@ export const createCloudAiCardTool = () =>
|
|
|
73
79
|
promptHint: "show one compact highlight card (metric, KPI, status) — not for tables, lists, or long text.",
|
|
74
80
|
}).clientView();
|
|
75
81
|
|
|
82
|
+
export const createCloudAiChartTool = () =>
|
|
83
|
+
defineAiTool({
|
|
84
|
+
name: "chart",
|
|
85
|
+
description:
|
|
86
|
+
"Show one chart in the chat from data you already have: bar, line (area: true fills it), scatter, pie, donut, histogram, gauge or sparkline. Cloud draws it with the renderer of cloud.chart() in Studio apps and gives the reader a data table and Copy data; no code, sandbox or app check is involved. Aggregate first. Compute derived numbers with calculate or code_run before charting them. Say what the chart shows in title and put the unit, period or source in subtitle. The chart has no state, filters or actions: for interaction show an app in the chat with code_present, and build a saved Studio App only when data must be kept or the app reused. After the chart, state the finding instead of repeating its values.",
|
|
87
|
+
inputSchema: CloudAiChartInputSchema,
|
|
88
|
+
outputSchema: CloudAiChartOutputSchema,
|
|
89
|
+
approval: "never",
|
|
90
|
+
promptHint:
|
|
91
|
+
"show data you already have as a chart in the chat, without code. For interaction use a chat app (code_present); for kept data or reuse, a saved Studio App.",
|
|
92
|
+
}).clientView();
|
|
93
|
+
|
|
76
94
|
export const createCloudAiSurveyTool = () =>
|
|
77
95
|
defineAiTool({
|
|
78
96
|
name: "survey",
|
|
@@ -182,7 +200,12 @@ export const createCloudAiCodeTools = () => [
|
|
|
182
200
|
}).server(runManagedCodeTool("code_export")),
|
|
183
201
|
];
|
|
184
202
|
|
|
185
|
-
export const createDefaultCloudAiTools = () => [
|
|
203
|
+
export const createDefaultCloudAiTools = () => [
|
|
204
|
+
createAiTodoTool(),
|
|
205
|
+
createCloudAiSurveyTool(),
|
|
206
|
+
createCloudAiTextEditorTool(),
|
|
207
|
+
createCloudAiChartTool(),
|
|
208
|
+
];
|
|
186
209
|
|
|
187
210
|
/** Built-ins advertised through discovery and loaded only when needed. */
|
|
188
211
|
export const CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES = new Set<string>([
|
|
@@ -190,6 +213,7 @@ export const CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES = new Set<string>([
|
|
|
190
213
|
...Object.keys(CODE_SOURCE_TOOLS),
|
|
191
214
|
"survey",
|
|
192
215
|
"text_editor",
|
|
216
|
+
"chart",
|
|
193
217
|
"list_files",
|
|
194
218
|
"write_file",
|
|
195
219
|
"markdown_to_pdf",
|