@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.
Files changed (136) hide show
  1. package/package.json +4 -2
  2. package/src/_internal/app-presentation.ts +22 -1
  3. package/src/_internal/app-roles.ts +14 -0
  4. package/src/_internal/capabilities.ts +113 -5
  5. package/src/_internal/capability-sentences.ts +413 -0
  6. package/src/_internal/define-app.ts +24 -5
  7. package/src/_internal/registry-validation.ts +5 -0
  8. package/src/_internal/registry.ts +50 -6
  9. package/src/_internal/widget-declarations.ts +53 -0
  10. package/src/access/PermissionEditor.tsx +43 -24
  11. package/src/ai/app-skill-store.ts +298 -0
  12. package/src/ai/app-skills.ts +105 -0
  13. package/src/ai/approval-routes.ts +40 -4
  14. package/src/ai/approvals.ts +68 -10
  15. package/src/ai/browser.ts +10 -0
  16. package/src/ai/capabilities.ts +68 -9
  17. package/src/ai/chart-block.ts +380 -0
  18. package/src/ai/chat/action-sentences.ts +49 -0
  19. package/src/ai/chat/blocks.tsx +97 -40
  20. package/src/ai/chat/chart-block.tsx +79 -0
  21. package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
  22. package/src/ai/chat/message-actions.tsx +3 -1
  23. package/src/ai/chat/message-utils.ts +3 -0
  24. package/src/ai/chat/messages.ts +26 -2
  25. package/src/ai/chat/tool-groups.ts +66 -12
  26. package/src/ai/chat/tool-names.ts +6 -2
  27. package/src/ai/chat/turn-layout.ts +10 -2
  28. package/src/ai/chat/turn-view.tsx +123 -36
  29. package/src/ai/chat/web-tools.tsx +95 -1
  30. package/src/ai/client/controller.ts +6 -3
  31. package/src/ai/code-runtime-tools.ts +109 -61
  32. package/src/ai/code-source-contracts.ts +25 -21
  33. package/src/ai/code-source-tools.ts +3 -1
  34. package/src/ai/default-tool-contracts.ts +198 -0
  35. package/src/ai/default-tools.ts +25 -1
  36. package/src/ai/executor.ts +189 -35
  37. package/src/ai/fetch-file-tool.ts +41 -4
  38. package/src/ai/firecrawl-tools.ts +87 -4
  39. package/src/ai/index.ts +17 -1
  40. package/src/ai/migrate.ts +172 -14
  41. package/src/ai/protocol.ts +22 -1
  42. package/src/ai/routes.ts +11 -0
  43. package/src/ai/runtime-tools.ts +1 -0
  44. package/src/ai/runtime.ts +46 -7
  45. package/src/ai/skill-content.ts +54 -0
  46. package/src/ai/skills-routes.ts +22 -13
  47. package/src/ai/skills.ts +177 -231
  48. package/src/ai/store.ts +66 -1
  49. package/src/ai/tool-audit.ts +11 -1
  50. package/src/ai/tool-sentences.ts +38 -0
  51. package/src/ai/tools.ts +91 -3
  52. package/src/ai/types.ts +47 -1
  53. package/src/ai/web-provenance.ts +103 -0
  54. package/src/api/admin-ai-skills.ts +35 -16
  55. package/src/api/capabilities.ts +3 -2
  56. package/src/api/fanout.ts +128 -0
  57. package/src/api/help.ts +8 -3
  58. package/src/api/mcp.ts +4 -1
  59. package/src/api/search.ts +31 -106
  60. package/src/api/widgets.ts +291 -101
  61. package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
  62. package/src/browser/CloudResourceSearch.tsx +1 -1
  63. package/src/browser/command-shortcuts.ts +21 -6
  64. package/src/browser/ndjson.ts +34 -0
  65. package/src/browser/reload.ts +46 -0
  66. package/src/browser/search-stream.ts +4 -26
  67. package/src/browser/spotlight-position.ts +4 -1
  68. package/src/browser/widgets.ts +30 -0
  69. package/src/capabilities/executions.ts +3 -0
  70. package/src/capabilities/server.ts +20 -2
  71. package/src/cli/access.ts +48 -14
  72. package/src/cli/admin/ai-skills.ts +59 -39
  73. package/src/cli/commands.ts +20 -0
  74. package/src/cli/index.ts +16 -0
  75. package/src/cli/plugin.ts +30 -0
  76. package/src/contracts/announcement-cookie.ts +62 -0
  77. package/src/contracts/announcements.ts +14 -52
  78. package/src/contracts/app.ts +34 -2
  79. package/src/contracts/capabilities.ts +24 -0
  80. package/src/contracts/pwa-paths.ts +7 -2
  81. package/src/contracts/pwa.ts +2 -7
  82. package/src/contracts/registry.ts +10 -1
  83. package/src/contracts/shared.ts +5 -0
  84. package/src/contracts/widgets.ts +67 -2
  85. package/src/desktop/solid.tsx +6 -7
  86. package/src/index.ts +1 -0
  87. package/src/server/help.ts +8 -1
  88. package/src/server/index.ts +1 -0
  89. package/src/server/widget-request.ts +27 -0
  90. package/src/services/accounts/users.ts +15 -0
  91. package/src/services/approvals/index.ts +225 -0
  92. package/src/services/approvals/migrate.ts +33 -0
  93. package/src/services/help/index.ts +18 -5
  94. package/src/services/help/types.ts +3 -2
  95. package/src/services/identity/constants.ts +2 -0
  96. package/src/services/identity/key-ring.ts +123 -42
  97. package/src/services/identity/runtime-config.ts +13 -2
  98. package/src/shared/app-presentation.ts +11 -0
  99. package/src/shared/email-html.ts +43 -8
  100. package/src/ssr/AdminLayout.tsx +4 -7
  101. package/src/ssr/AppLaunchpad.island.tsx +1 -1
  102. package/src/ssr/AppLaunchpadPanel.tsx +12 -3
  103. package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
  104. package/src/ssr/GlobalSearchDialog.tsx +1 -43
  105. package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
  106. package/src/ssr/Layout.tsx +38 -14
  107. package/src/ssr/LayoutFooter.tsx +1 -1
  108. package/src/ssr/LayoutHeader.tsx +17 -20
  109. package/src/ssr/LayoutHelp.tsx +1 -1
  110. package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
  111. package/src/ssr/LayoutRail.tsx +1 -2
  112. package/src/ssr/PageTrail.tsx +23 -0
  113. package/src/ssr/PageTrailRow.island.tsx +20 -0
  114. package/src/ssr/PageTrailRuntime.island.tsx +26 -0
  115. package/src/ssr/ProfilePreferences.island.tsx +7 -3
  116. package/src/ssr/PwaRuntime.island.tsx +1 -1
  117. package/src/ssr/app-navigation.ts +5 -7
  118. package/src/ssr/global-search-host.tsx +55 -0
  119. package/src/ssr/help.ts +3 -3
  120. package/src/ssr/islands/index.ts +1 -0
  121. package/src/ssr/layout-runtime.ts +60 -0
  122. package/src/ssr/mobile-menu-history.ts +1 -1
  123. package/src/ssr/page-trail-runtime.ts +313 -0
  124. package/src/ssr/page-trail.browser-harness.tsx +216 -0
  125. package/src/ssr/page-trail.ts +59 -0
  126. package/src/ssr/platform-messages.ts +4 -0
  127. package/src/ssr/rail-context.ts +4 -3
  128. package/src/ssr/runtime.ts +14 -1
  129. package/src/styles/effects.css +21 -4
  130. package/src/styles/global.css +35 -2
  131. package/src/styles/utilities-layout.css +24 -1
  132. package/src/styles/utilities-navigation.css +6 -18
  133. package/src/ai/code-mode-skill.ts +0 -114
  134. package/src/ai/data-analysis-skill.ts +0 -10
  135. package/src/ai/grids-skill.ts +0 -158
  136. 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(z.object({ id: z.uuid(), message: z.string(), decision: z.boolean().nullable() })),
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
- const signed = await withActiveIdentitySigner(
57
- "invocation",
58
- (signer) =>
59
- signInvocationToken({
60
- targetAppId: "assistant",
61
- callingAppId: "core",
62
- operation: `tool:${name}`,
63
- schemaHash: null,
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
- /** Ordered reviews are replayed through Nessi before consuming a durable result. */
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.infer<typeof Reply>["data"]>,
139
- context: Pick<ToolContext, "signal" | "requestApproval"> & { locale?: string; reportProgress?: (message: string) => Promise<void> },
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
- const approved = await context.requestApproval(approval.message);
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
- "Atomically save complete source files against expectedRevision from code_read/create. Other files stay unchanged. Returns revision and compiler diagnostics; never runs. fromFile imports one exact chat/Project/App file reference as UTF-8 source without printing bytes. File imports receive fresh review because source may be shared or published. Use code_file_stat first; source file and bundle byte limits apply.",
311
- review: (input: unknown) => CodeWriteInput.parse(input).files.some((file) => "fromFile" in file),
312
- input: CodeWriteInput,
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 && (typeof definition.review === "function" ? definition.review(input) : definition.review)) {
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>;
@@ -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 = () => [createAiTodoTool(), createCloudAiSurveyTool(), createCloudAiTextEditorTool()];
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",