@k2b/cloud 0.23.0 → 0.24.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 (45) hide show
  1. package/package.json +2 -2
  2. package/src/_internal/capabilities.ts +230 -75
  3. package/src/_internal/define-app.ts +12 -3
  4. package/src/_internal/fixtures/filesv2-manifest-cloud-v0.29.0.json +887 -0
  5. package/src/_internal/page-responses.ts +1 -1
  6. package/src/_internal/registry.ts +78 -3
  7. package/src/ai/chat/blocks.tsx +2 -1
  8. package/src/ai/chat/messages.ts +6 -0
  9. package/src/ai/client/controller.ts +65 -20
  10. package/src/ai/client/transport.ts +5 -5
  11. package/src/ai/code-mode-skill.ts +2 -2
  12. package/src/ai/index.ts +0 -7
  13. package/src/ai/live-events.ts +1 -262
  14. package/src/ai/live.ts +31 -2
  15. package/src/ai/migrate.ts +35 -16
  16. package/src/ai/runtime.ts +1 -6
  17. package/src/ai/solid.ts +0 -5
  18. package/src/api/help.ts +2 -2
  19. package/src/api/pwa-phone.ts +10 -3
  20. package/src/api/search/schemas.ts +34 -0
  21. package/src/api/search.ts +118 -50
  22. package/src/browser/CloudResourceSearch.browser-harness.tsx +11 -0
  23. package/src/browser/CloudResourceSearch.tsx +208 -80
  24. package/src/browser/resource-search-messages.ts +15 -2
  25. package/src/browser/search-stream.ts +151 -0
  26. package/src/contracts/capabilities.ts +89 -24
  27. package/src/contracts/capability-compatibility.ts +28 -14
  28. package/src/contracts/file-provider.ts +158 -0
  29. package/src/contracts/index.ts +1 -0
  30. package/src/events/live-engine.ts +11 -7
  31. package/src/events/live.ts +32 -32
  32. package/src/server/help.ts +3 -3
  33. package/src/services/index.ts +1 -1
  34. package/src/services/outbox.ts +29 -115
  35. package/src/services/pdf/markdown.ts +22 -2
  36. package/src/services/pwa-devices.ts +74 -8
  37. package/src/shared/markdown/formula.ts +122 -26
  38. package/src/shared/markdown/index.ts +27 -14
  39. package/src/ssr/PwaLayout.tsx +2 -1
  40. package/src/styles/resource-search.css +19 -0
  41. package/src/ai/client/live-connection.ts +0 -141
  42. package/src/ai/live-messages.ts +0 -45
  43. package/src/ai/live-outbox.ts +0 -102
  44. package/src/ai/live-routes.ts +0 -412
  45. package/src/shared/markdown/extensions/info-blocks.ts +0 -79
@@ -23,7 +23,7 @@ export const createPageResponses = (html: HtmlFn<PageOptions>) => {
23
23
  // Auth middleware may reject before ssr() initializes page metadata.
24
24
  c.set("page", { lang: getLocale(c) });
25
25
  c.header("Cache-Control", "private, no-store");
26
- await preloadLayoutAnnouncements(c);
26
+ if (options.layout !== "pwa") await preloadLayoutAnnouncements(c);
27
27
  // Load JSX only after the application's SSR plugin has been installed.
28
28
  const { renderPageError } = await import("../ssr/PageError");
29
29
  const response = await html(renderPageError(c, status, options), c.get("page"));
@@ -1,6 +1,7 @@
1
1
  import type { EphemeralConfig } from "@k2b/sync";
2
2
  import type { AppAppearanceColor } from "../contracts/app";
3
3
  import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
4
+ import { fileProviderIssues } from "../contracts/file-provider";
4
5
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
5
6
  import type { DashboardWidgetPresentation } from "../contracts/widgets";
6
7
  import { resolveAppPresentations } from "../shared/app-presentation";
@@ -174,24 +175,98 @@ const capabilityEndpoint = (baseUrl: string): string | null => {
174
175
  const appAccent = (value: string | undefined): AppAppearanceColor | undefined =>
175
176
  /^#[0-9a-f]{6}$/i.test(value ?? "") ? (value as AppAppearanceColor) : undefined;
176
177
 
178
+ /** Last manifest per app whose left-out entries were checked, so each registered manifest logs once. */
179
+ const checkedLeftOutEntries = new Map<string, string>();
180
+
181
+ /**
182
+ * Logs the entries of a registered manifest that this release left out, typically because the app runs a
183
+ * newer Cloud release. Core answers those operations as not found, so the operator needs their names.
184
+ * A left-out file provider is named too, because the app then quietly stops offering files. `manifest` is
185
+ * the manifest as read, before the file-provider contract check, which logs its own rejections.
186
+ */
187
+ const reportLeftOutEntries = (sent: unknown, manifest: CapabilityManifest): void => {
188
+ if (checkedLeftOutEntries.get(manifest.appId) === manifest.manifestHash) return;
189
+ checkedLeftOutEntries.set(manifest.appId, manifest.manifestHash);
190
+ const kept = new Set([...manifest.types, ...manifest.queries, ...manifest.actions, ...manifest.commands].map((entry) => entry.localId));
191
+ const groups = sent as Record<string, unknown>;
192
+ const leftOut = (["types", "queries", "actions", "commands"] as const).flatMap((group) => {
193
+ const entries = groups[group];
194
+ if (!Array.isArray(entries)) return [];
195
+ return entries.flatMap((entry: unknown) => {
196
+ const localId = entry && typeof entry === "object" ? (entry as { localId?: unknown }).localId : undefined;
197
+ return typeof localId === "string" && !kept.has(localId) ? [`${group}/${localId}`] : [];
198
+ });
199
+ });
200
+ if (groups.fileProvider !== undefined && !manifest.fileProvider) leftOut.push("fileProvider");
201
+ if (leftOut.length === 0) return;
202
+ console.warn(
203
+ JSON.stringify({
204
+ level: "warn",
205
+ source: "capability-registry",
206
+ message: "Left out capability entries this Cloud release cannot read",
207
+ appId: manifest.appId,
208
+ manifestHash: manifest.manifestHash,
209
+ leftOutCount: leftOut.length,
210
+ leftOut: leftOut.slice(0, 20),
211
+ }),
212
+ );
213
+ };
214
+
215
+ /** Last checked manifest per app: its hash and whether its file provider passed the contract check. */
216
+ const checkedFileProviders = new Map<string, { manifestHash: string; valid: boolean }>();
217
+
218
+ /**
219
+ * Ignores a file provider that does not match the contract, logging once per manifest; the app's other
220
+ * capabilities stay available. The delivered `manifestHash` stays, because it identifies what the app registered.
221
+ */
222
+ const withValidFileProvider = (manifest: CapabilityManifest): CapabilityManifest => {
223
+ if (!manifest.fileProvider) return manifest;
224
+ const checked = checkedFileProviders.get(manifest.appId);
225
+ let valid = checked?.manifestHash === manifest.manifestHash ? checked.valid : undefined;
226
+ if (valid === undefined) {
227
+ const issues = fileProviderIssues(manifest);
228
+ valid = issues.length === 0;
229
+ checkedFileProviders.set(manifest.appId, { manifestHash: manifest.manifestHash, valid });
230
+ if (!valid) {
231
+ console.error(
232
+ JSON.stringify({
233
+ level: "error",
234
+ source: "capability-registry",
235
+ message: "Ignored a file provider that does not match the file-provider contract",
236
+ appId: manifest.appId,
237
+ manifestHash: manifest.manifestHash,
238
+ issues: issues
239
+ .slice(0, 10)
240
+ .map(({ function: name, localId, code, path, message }) => ({ function: name, localId, code, path, message })),
241
+ }),
242
+ );
243
+ }
244
+ }
245
+ if (valid) return manifest;
246
+ const { fileProvider: _ignored, ...rest } = manifest;
247
+ return rest;
248
+ };
249
+
250
+ /** Ignores record fields from a newer release; the endpoint always comes from the live app registry. */
177
251
  export const resolveLiveCapabilityRegistryEntry = (
178
252
  key: string,
179
253
  value: unknown,
180
254
  app: AppRegistryEntry | undefined,
181
255
  ): CapabilityRegistryEntry | null => {
182
256
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
183
- if (Object.keys(value).some((field) => field !== "appId" && field !== "manifest" && field !== "presentation")) return null;
184
257
  const record = value as Partial<CapabilityRegistryRecord>;
185
258
  if (!app || typeof record.appId !== "string" || key !== `capabilities/${record.appId}` || record.appId !== app.id) return null;
186
259
  if (!app.capabilities) return null;
187
260
  const endpoint = capabilityEndpoint(app.baseUrl);
188
261
  if (!endpoint) return null;
189
262
  try {
190
- const manifest = parseCapabilityManifest(record.manifest, app.id);
191
- const presentation = compileCapabilityPresentation(manifest, record.presentation);
263
+ const read = parseCapabilityManifest(record.manifest, app.id);
264
+ const manifest = withValidFileProvider(read);
265
+ const presentation = compileCapabilityPresentation(manifest, record.presentation, "reader");
192
266
  if (app.capabilities.protocolVersion !== manifest.protocolVersion || app.capabilities.manifestHash !== manifest.manifestHash) {
193
267
  return null;
194
268
  }
269
+ reportLeftOutEntries(record.manifest, read);
195
270
  return {
196
271
  appId: app.id,
197
272
  appName: app.name,
@@ -705,11 +705,12 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
705
705
 
706
706
  /** Render one unified turn block. Shared by persisted assistant groups and the live turn. */
707
707
  export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; streaming?: boolean; active?: boolean }) {
708
+ const locale = useLocale();
708
709
  // The id/kind remain stable while the immutable block value changes during a turn.
709
710
  return (
710
711
  <Switch>
711
712
  <Match when={props.block.kind === "text" && props.block}>
712
- {(block) => <AssistantMarkdownBlock html={markdown.renderSync(block().text)} />}
713
+ {(block) => <AssistantMarkdownBlock html={markdown.renderSync(block().text, { locale: locale() })} />}
713
714
  </Match>
714
715
  <Match when={props.block.kind === "thinking" && props.block}>
715
716
  {(block) => <ThinkingBlockView text={block().text} streaming={props.streaming} />}
@@ -22,6 +22,9 @@ const messages = i18n.define({
22
22
  thinking: "Thinking",
23
23
  showReasoning: "Show reasoning",
24
24
  byteRange: ({ start, end }: { start: string; end: string }) => `Bytes ${start}–${end}`,
25
+ streamLoginRequired: "Your session has ended. Sign in again to continue this chat.",
26
+ streamAccessDenied: "You no longer have access to this chat.",
27
+ streamNotFound: "This chat is no longer available.",
25
28
  },
26
29
  de: {
27
30
  backgroundRun: "Hintergrundlauf",
@@ -43,6 +46,9 @@ const messages = i18n.define({
43
46
  thinking: "Denkt nach",
44
47
  showReasoning: "Denkprozess anzeigen",
45
48
  byteRange: ({ start, end }) => `Bytes ${start}–${end}`,
49
+ streamLoginRequired: "Deine Sitzung ist abgelaufen. Melde dich erneut an, um diesen Chat fortzusetzen.",
50
+ streamAccessDenied: "Du hast keinen Zugriff mehr auf diesen Chat.",
51
+ streamNotFound: "Dieser Chat ist nicht mehr verfügbar.",
46
52
  },
47
53
  },
48
54
  });
@@ -1,6 +1,7 @@
1
1
  import { type Accessor, createMemo, createSignal, onCleanup } from "solid-js";
2
2
  import { createStore, reconcile } from "solid-js/store";
3
3
  import { type AiAttachmentRef, aiAttachmentMarker } from "../attachments";
4
+ import { aiChatMessages } from "../chat/messages";
4
5
  import { type AiStreamEvent, type AiTurnBlock, type AiTurnSnapshot, steerMessageBlockId } from "../protocol";
5
6
  import { type AiResourceMarker, aiResourceMarker } from "../resource-markers";
6
7
  import type {
@@ -24,7 +25,14 @@ import {
24
25
  reduceProjection,
25
26
  visibleMessages,
26
27
  } from "./projection";
27
- import { type AiConversationStreamTransport, type AiStreamHandle, aiSseConversationStreamTransport } from "./transport";
28
+ import {
29
+ type AiConversationStreamTransport,
30
+ AiStreamError,
31
+ type AiStreamErrorCode,
32
+ type AiStreamHandle,
33
+ aiSseConversationStreamTransport,
34
+ terminalAiStreamErrorCode,
35
+ } from "./transport";
28
36
 
29
37
  type ComposerDraftInput = {
30
38
  draftContent?: AiDraftContentPart[];
@@ -108,7 +116,7 @@ export type CreateAiChatControllerOptions = {
108
116
  frontendTools?: Record<string, AiFrontendToolHandler>;
109
117
  /** Explicitly advertise only client tools with a connected handler. */
110
118
  clientToolIds?: AiClientToolId[];
111
- /** Overrides the default SSE transport, for example with a shared WebSocket channel. */
119
+ /** Overrides the default SSE transport of the conversation stream. */
112
120
  streamTransport?: AiConversationStreamTransport;
113
121
  };
114
122
 
@@ -142,6 +150,21 @@ const completeFrontendToolBlock = (blocks: AiTurnBlock[], callId: string, result
142
150
  const isActiveConversationLoading = (activeConversationId: string | null, loadingConversationId: string | null): boolean =>
143
151
  activeConversationId !== null && loadingConversationId === activeConversationId;
144
152
 
153
+ /**
154
+ * Why the stream ended, in the request's locale: the server renders it into
155
+ * `<html lang>`, which is also what `useLocale()` of `@k2b/ui` resolves in an
156
+ * island. The controller stays headless and does not load the component library.
157
+ */
158
+ const streamErrorText = (code: AiStreamErrorCode): string => {
159
+ const t = aiChatMessages(typeof document === "undefined" ? "en" : document.documentElement.lang || "en");
160
+ if (code === "login_required") return t.streamLoginRequired;
161
+ return code === "access_denied" ? t.streamAccessDenied : t.streamNotFound;
162
+ };
163
+
164
+ /** A chat that cannot continue says why in the page's language; any other error shows its own message. */
165
+ const errorText = (error: unknown, fallback: string): string =>
166
+ error instanceof AiStreamError ? streamErrorText(error.code) : error instanceof Error ? error.message : fallback;
167
+
145
168
  export const createAiChatController = (options: CreateAiChatControllerOptions) => {
146
169
  const [activeConversationId, setActiveConversationIdSignal] = createSignal<string | null>(options.initialConversationId ?? null);
147
170
  const [globalError, setGlobalError] = createSignal<string | null>(options.initialError ?? null);
@@ -330,13 +353,18 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
330
353
  // or action in this chat can subscribe again.
331
354
  onError: (error) => {
332
355
  if (!isCurrentStreamSession(streamSession, session)) return;
333
- closeStream();
334
- streamError = error.message;
335
- setConversationError(conversationId, error.message);
356
+ endStream(conversationId, error);
336
357
  },
337
358
  });
338
359
  };
339
360
 
361
+ /** Stops the chat's stream for good and shows why. */
362
+ const endStream = (conversationId: string, error: Error) => {
363
+ closeStream();
364
+ streamError = errorText(error, error.message);
365
+ setConversationError(conversationId, streamError);
366
+ };
367
+
340
368
  // ---- frontend tools ---------------------------------------------------
341
369
 
342
370
  const runFrontendTools = () => {
@@ -393,13 +421,20 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
393
421
  } catch {}
394
422
  };
395
423
 
424
+ /** Loads a chat. A status after which no retry can load it throws an `AiStreamError`, like the stream's. */
425
+ const fetchDetail = async (conversationId: string, fallback: string): Promise<AiConversationDetail> => {
426
+ const response = await fetch(url(`/conversations/${conversationId}`));
427
+ if (response.ok) return (await response.json()) as AiConversationDetail;
428
+ const message = await readError(response, fallback);
429
+ const ended = terminalAiStreamErrorCode(response.status);
430
+ throw ended ? new AiStreamError(ended, message) : new Error(message);
431
+ };
432
+
396
433
  const loadDetail = async (conversationId: string, shouldReportError: () => boolean): Promise<AiConversationDetail | null> => {
397
434
  try {
398
- return await request<AiConversationDetail>(`/conversations/${conversationId}`, { method: "GET" }, "Failed to open conversation");
435
+ return await fetchDetail(conversationId, "Failed to open conversation");
399
436
  } catch (loadError) {
400
- if (shouldReportError()) {
401
- setConversationError(conversationId, loadError instanceof Error ? loadError.message : "Failed to open conversation");
402
- }
437
+ if (shouldReportError()) setConversationError(conversationId, errorText(loadError, "Failed to open conversation"));
403
438
  return null;
404
439
  }
405
440
  };
@@ -474,22 +509,31 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
474
509
  };
475
510
 
476
511
  let conversationRefreshGeneration = 0;
477
- const refreshActiveConversation = async (): Promise<void> => {
512
+ /**
513
+ * Loads the active chat again. Resolves `false` when it no longer exists or
514
+ * is no longer readable: the chat then ends like its stream, with the reason
515
+ * as its error, because no retry can load it. A response that a newer
516
+ * refresh or a newer opening of a chat overtook changes nothing.
517
+ */
518
+ const refreshActiveConversation = async (): Promise<boolean> => {
478
519
  const conversationId = activeConversationId();
479
- if (!conversationId) return;
520
+ if (!conversationId) return true;
480
521
  const refreshGeneration = ++conversationRefreshGeneration;
481
522
  const openGeneration = conversationOpenGeneration;
482
- const detail = await request<AiConversationDetail>(
483
- `/conversations/${conversationId}`,
484
- { method: "GET" },
485
- "Failed to refresh conversation",
486
- );
487
- if (
523
+ const overtaken = () =>
488
524
  !isActiveConversation(conversationId) ||
489
525
  refreshGeneration !== conversationRefreshGeneration ||
490
- openGeneration !== conversationOpenGeneration
491
- )
492
- return;
526
+ openGeneration !== conversationOpenGeneration;
527
+ let detail: AiConversationDetail;
528
+ try {
529
+ detail = await fetchDetail(conversationId, "Failed to refresh conversation");
530
+ } catch (refreshError) {
531
+ if (!(refreshError instanceof AiStreamError)) throw refreshError;
532
+ if (overtaken()) return true;
533
+ endStream(conversationId, refreshError);
534
+ return false;
535
+ }
536
+ if (overtaken()) return true;
493
537
  const currentDraft = state.conversation?.draft;
494
538
  if (currentDraft && currentDraft.revision > detail.conversation.draft.revision) detail.conversation.draft = currentDraft;
495
539
  const windowOldest = detail.messages[0]?.seq;
@@ -499,6 +543,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
499
543
  if (detail.timeline) setTimeline(conversationId, detail.timeline);
500
544
  setRunError(conversationRunError(detail.conversation));
501
545
  openStream(conversationId);
546
+ return true;
502
547
  };
503
548
 
504
549
  const requestMessagesPage = async (conversationId: string, before: number, limit: number): Promise<AiMessagesPage> => {
@@ -13,10 +13,7 @@ export type AiConversationStreamTransport = {
13
13
  }) => AiStreamHandle;
14
14
  };
15
15
 
16
- /**
17
- * Why a conversation stream ended for good. The codes match the live
18
- * WebSocket's turn errors and revocations.
19
- */
16
+ /** Why a conversation stream ended for good. A client shows its own text for the code. */
20
17
  export type AiStreamErrorCode = "login_required" | "access_denied" | "not_found";
21
18
 
22
19
  /** A stream that will not recover by reconnecting; `message` comes from the server when it sent one. */
@@ -36,6 +33,9 @@ const TERMINAL_STATUS_CODES: Readonly<Record<number, AiStreamErrorCode>> = {
36
33
  404: "not_found",
37
34
  };
38
35
 
36
+ /** The code of an HTTP status after which a conversation cannot continue, by retrying or reconnecting. */
37
+ export const terminalAiStreamErrorCode = (status: number): AiStreamErrorCode | undefined => TERMINAL_STATUS_CODES[status];
38
+
39
39
  const terminalStreamError = async (response: Response, code: AiStreamErrorCode): Promise<AiStreamError> => {
40
40
  const body: unknown = await response.json().catch(() => null);
41
41
  const message =
@@ -116,7 +116,7 @@ export const subscribeAiStream = (input: {
116
116
  input.onStatus?.(reconnectDelay === RECONNECT_BASE_MS ? "connecting" : "reconnecting");
117
117
  const response = await fetchStream(input.url, { signal: attempt.signal, headers: { Accept: "text/event-stream" } });
118
118
  if (stopped) return;
119
- const terminalCode = TERMINAL_STATUS_CODES[response.status];
119
+ const terminalCode = terminalAiStreamErrorCode(response.status);
120
120
  if (terminalCode) {
121
121
  // The connect timeout still bounds reading the error body.
122
122
  const error = await terminalStreamError(response, terminalCode);
@@ -4,7 +4,7 @@ import type { AiSkillTemplate } from "./skills";
4
4
 
5
5
  export const ASSISTANT_CODE_MODE_SKILL = {
6
6
  "key": "assistant:code-mode",
7
- "version": 56,
7
+ "version": 57,
8
8
  "name": "assistant-code-mode",
9
9
  "description": "Inspect and transform unfamiliar data, analyze files, compare results across Cloud apps, or build and improve interactive and agent-only Apps in Assistant Studio. Use for quick code experiments, data analysis, file generation, resource SQL queries and combining discovered Cloud capabilities. For plain arithmetic or date offsets, answer directly or use calculate.",
10
10
  "instructions": "# Assistant code mode\n\nChoose the smallest useful result: one-off answer, exported file, or reusable\nStudio App. Apps may expose agent actions, a display-only dashboard, or both.\nPersistence is optional. One-off scripts stay in their chat and cannot be shared. Reuse an\nexisting Cloud feature when it fits. For a\nquick reading of an uploaded PDF or Office document, `read_file` can return\nMarkdown; use code for exact cells, calculations, original PDF text or positions.\n\n## Start from the contract\n\nLoad the needed `code_*` tools individually through `load_tools` and read their\ninput schemas. They are Assistant tools, not capabilities or functions inside\ncode. Discover other Cloud operations before using `capabilities.run`.\n\nRuntime namespaces are globals: no imports or package installation are needed.\nOnly relative imports of the resource's own source files are supported. There is\nno DOM or native network access. Before using a namespace, read its reference\nbelow for signatures, options and return values. Do not invent methods or infer\nan API from a familiar library. For discovered Cloud capabilities and external\nAPIs, obtain their actual contracts separately.\n\nInspect supplied data before joining, filtering or calculating: column names,\ntypes, units, date ranges and missing values. Ask only for decisions or inputs\nthat cannot be established from available evidence. For several real steps,\nkeep a short `todo_write` plan and update it as work changes; skip ceremony for a\nsmall experiment. A failed experiment should change the next hypothesis.\n\n## First file script\n\nPass exact current-chat manifest paths as `code_run.inputPaths`, and this entry\nas `code_run.code` for a small CSV:\n\n```js\nexport default async () => {\n const [input] = await files.list();\n if (!input) throw new Error(\"Select a CSV input.\");\n const rows = await sheet.fromCsv(await files.read(input.name));\n return { rows: rows.length, columns: Object.keys(rows[0] ?? {}), sample: rows.slice(0, 3) };\n};\n```\n\n`input.name` is the full path, such as `/sales.csv`; pass it unchanged to\n`files.read`, which returns a `File`. CSV rows are objects keyed by headers:\n`rows[0]` is already data. Do not drop it. For older Excel CSVs, use\n`sheet.fromCsv(file, {encoding:\"windows-1252\"})`. Inspect actual headings first.\nFor a tiny experiment without files, `export default () => ({answer:42})` suffices.\nEach run has fresh variables. No saved resource or UI is required.\n\n## Reference routing\n\nRead only the rows relevant to the task. Each link describes its own complete\nsupported surface; links within references add related workflows when needed.\n\n| Task / API | Read |\n| --- | --- |\n| Source entry, input/output files, pickers, CSV, IDs | [Runtime and files](/skills/assistant-code-mode/references/runtime.md) |\n| Inspect PDF pages, read PDF text/positions or XLSX/ODS cells, write ODS | [Documents](/skills/assistant-code-mode/references/documents.md) |\n| Generate a PDF, save one in Files, embed attachments, combine invoice HTML and XML | [PDF generation](/skills/assistant-code-mode/references/pdf.md) |\n| Exact amounts, taxes, allocation, localized money | [Money](/skills/assistant-code-mode/references/money.md) |\n| Export DATEV bookings or SEPA transfers | [DATEV and SEPA](/skills/assistant-code-mode/references/finance.md) |\n| Parse a CAMT bank report | [Bank reports](/skills/assistant-code-mode/references/camt.md) |\n| Calculate, create or read electronic invoices/XML/PDF attachments | [Electronic invoices](/skills/assistant-code-mode/references/einvoice.md) |\n| Controls, layouts and dialogs | [UI and dialogs](/skills/assistant-code-mode/references/ui.md), [Analytics UI](/skills/assistant-code-mode/references/analytics.md) |\n| Chart types, series and axes | [Charts](/skills/assistant-code-mode/references/charts.md) |\n| Long processing, progress, cancellation | [Background work](/skills/assistant-code-mode/references/work.md) |\n| Persist JSON or files locally/shared | [Storage](/skills/assistant-code-mode/references/storage.md) |\n| Copy files between stores; list and download Filesv2 beside Grids documents | [File transfers](/skills/assistant-code-mode/references/files.md) |\n| Resource SQL, schema, row CRUD, imports | [Database](/skills/assistant-code-mode/references/database.md) |\n| Generate text, classify data or extract structured fields | [AI calculations](/skills/assistant-code-mode/references/ai.md) |\n| Discovered Cloud queries/actions | [Capability calls](/skills/assistant-code-mode/references/capabilities.md) |\n| External HTTPS and personal secrets | [HTTP and secrets](/skills/assistant-code-mode/references/http.md) |\n| Call a published App action; declare handlers | [App actions](/skills/assistant-code-mode/references/app-actions.md) |\n| Reuse work across chats, create or edit an App | [Source workflow](/skills/assistant-code-mode/references/source-workflow.md) |\n| Publish, restore, copy | [Publishing](/skills/assistant-code-mode/references/publishing.md) |\n| Find recipients or change App/Skill sharing | [Access](/skills/assistant-code-mode/references/access.md) |\n| Inspect, export, clear server data, or delete an App | [Management](/skills/assistant-code-mode/references/management.md) |\n| Execute, inspect, interact, export, stop, diagnose errors | [Run and debug](/skills/assistant-code-mode/references/debugging.md) |\n| Unfamiliar inputs or cross-app investigation | [Investigation](/skills/assistant-code-mode/references/investigation.md) |\n| Complete app starters | [Examples](/skills/assistant-code-mode/references/examples.md) |\n\nFor a new app, read Source workflow and the closest complete example before\nwriting source, plus only the API references it uses. For analytical reports or\ndashboards, also load `assistant-data-analysis` for metrics and source validation.\n\n## Choose the delivery\n\nFor a one-off chart, calculator, or interactive analysis in this conversation,\nuse `code_run({code,inputPaths})`, test the controls, then\n`code_present({runId,title})`. Read [Chat visualizations](/skills/assistant-code-mode/references/chat.md).\nA successful run is visible to the agent only; present it before saying the\nuser can see it. No saved App or chat file is necessary.\n\nUse a Studio App when the user needs an independently accessible, reusable\napplication. Use `files.save`, `code_export`, and `present` when the requested\nresult is a file. These are separate delivery choices.\n\n## Verify and deliver\n\nRun the actual source (the saved revision for Apps) and test relevant controls with IDs returned by\n`code_run`/`code_interact`, including invalid inputs and picker fixtures. Creating,\ncompiling or saving source does not verify behavior. If `work.status` is\n`running`, wait with `code_inspect({runId,waitMs:30000})`; do not restart the job.\nInspect only when the returned snapshot needs more detail. Errors and\n`outputTruncated` are not successful complete results.\n\nFor a CSV, call `await files.save(sheet.toCsv(rows), \"result.csv\")` inside code;\nfor a spreadsheet, `await files.save(await sheet.toOds(sheets), \"result.ods\")`.\nThen call the **tool** `code_export` with the returned `runId` and captured file\nname, and `present` its returned chat path. `files.save` returns no path.\nReuse exported data via its path/version rather than retyping truncated output.\nReconcile row counts, exclusions and totals before reporting findings.\n\nOpen GUI apps with `code_open`. Saving or testing does\nnot replace a user's already-running app. Stop runs no longer needed that retain\nUI, jobs or output files. Never claim an unexecuted result is verified.\n\nAgent execution runs independently of the user's tab. Agent local storage is\ntemporary; shared storage, database writes and external actions are real, even\nin tests. Cancellation and source restore do not undo them. Apps select local\nfiles explicitly; they never gain implicit access to chat attachments. Use\n`code_secret` for credentials, never chat or app controls. Honor normal access\nand approval decisions; availability is not authorization for unrelated actions.",
@@ -64,7 +64,7 @@ export const ASSISTANT_CODE_MODE_SKILL = {
64
64
  },
65
65
  {
66
66
  "path": "references/files.md",
67
- "content": "# Explicit file references and transfers\n\nFiles belong to a chat, Project, or App shared store. A location is\n`{scope:\"chat\"|\"project\"|\"app\",id,path}`. A reference adds an opaque string\n`version`. The current chat ID is supplied as `Chat:` in the platform context;\nuse it for `scope:\"chat\"` rather than guessing an ID. Reuse returned locations and references exactly; access to a location\nnever grants access to its whole store or to another resource.\n\nLoad only `code_files`, `code_file_stat`, and `code_file_copy` as needed.\n\n1. `code_files({scope,id,after?:string,limit?:number})` returns `container`,\n `items:[{location,path,size,mediaType}]`, and `nextAfter`. Limit defaults to\n 100, maximum 1,000. Continue with `nextAfter` until null.\n2. `code_file_stat({file:location})` returns `{exists:false}` or\n `{exists:true,reference,size,mediaType}`. It does not print file bytes.\n3. `code_file_copy({source:reference,destination:location,expectedVersion})`\n copies bytes on the server. For a new destination use `expectedVersion:null`;\n replacing a file requires its exact current version from `code_file_stat`.\n The result contains the destination `reference,size,mediaType`.\n\nEvery copy receives fresh user review with the exact source, destination, and\noverwrite scope. App and Project files may be readable by other authorized\nusers; copying a private chat attachment there is an explicit disclosure.\nRejection does not copy anything. Source versions, destination versions, current\npermissions, and the destination's byte limits are checked again during execution.\nAfter a conflict, inspect current state and prepare a new review before retrying.\n\nApp file reads and writes require Use, not Manage. Project files require Read\nfor sources and Write for destinations. Chat files require ownership. Transfers\nwork without a running UI and do not mount other chat attachments implicitly.\n\nFor example, inspect an invoice attachment, copy its reference into the target\nApp's shared file store, then call the App's published `importFiles` action with\nthe destination key expected by its discovered input schema. Do not pass a chat\npath to an App and assume it can read it. Action handlers see only their explicit\ninput and authorized App data. Use [App actions](app-actions.md) for discovery.\n\nFor small UTF-8 files that should become App source, use\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nThis is a reviewed import with the same source references, not a chat-only\nspecial case. Source file/bundle limits still apply; keep larger or private\nruntime data in shared files or the database instead of embedding it in source.\n\nKnown rejections return `CONFLICT` for an occupied or changed destination and\n`STORAGE_FULL` for a destination byte limit. No destination bytes were written.\nChoose another path or reduce the file size, then prepare a new review.\n\n\n## List Filesv2 files beside Grids documents\n\nDiscover the installed contracts first. Call `filesv2.bases.list`, then\n`filesv2.entry.list` for a folder or `filesv2.entry.search-in-base` for names\nbelow a known path. Keep the filters unchanged and follow `data.next` as\n`after` until null, even after an empty page. Each `data.items` entry includes\nits own `{type:\"filesv2.entry\",id}` ref and file metadata. Keep that ref in the\nStudio list, alongside the `grids.document` refs from `grids.document.list`.\nUse both `type` and `id` as the identity; dispatch each type to its own\noperations. Grids uses its own `page` cursor, not Filesv2's `data.next`.\n\nFilesv2 refs identify a base and path, including long paths; they do not grant\naccess or pin a content version. Moving or renaming changes the ref, and\nreplacing bytes at the same path keeps it. Refresh metadata when needed.\nNever construct storage URLs or turn files into public shares for this flow.\n\nOnly on a user's download request, call `filesv2.content.download` with the\nselected Filesv2 ref's exact `id`. Its `data` is `{url,method:\"GET\",expires}`.\nOffer that returned URL unchanged to the requesting user; it is a private\nbearer credential, not a stable resource link. It expires after 60 seconds;\nuse the returned `expires` timestamp and request a fresh lease when needed.\nDo not prefetch leases for list rows, persist them in App/shared data, or\ninclude them in logs. Do not send Cloud cookies or authorization headers to\nthe storage host. Grids documents keep their authenticated download path from\ntheir canonical reader; never send a `grids.document` ID to Filesv2.\n\nEach lease request checks the current user's storage and Unix permissions.\nA 403 means access is denied; a 404 means the ref or file is missing or the\nbase is no longer visible. Refresh the list and do not bypass the denial.\n`not_file` (400) means a folder was selected. `identity_changed` (409) requires\nrefreshing access/identity state before trying again. Storage unavailability is an\nerror, not an empty list. If a lease expires or a transfer fails, discard the\nURL and request a fresh lease through the same capability; if that is denied,\nstop. Revoking Cloud access prevents new leases; an already issued bearer\nlease can remain usable until expiry, subject to storage checks.\n\nFor analysis inside code, use `filesv2.content.read` and\n`capabilities.streams.read` instead of fetching a bearer URL. See\n[Capability calls](capabilities.md) for binary budgets and consent rules.\n"
67
+ "content": "# Explicit file references and transfers\n\nFiles belong to a chat, Project, or App shared store. A location is\n`{scope:\"chat\"|\"project\"|\"app\",id,path}`. A reference adds an opaque string\n`version`. The current chat ID is supplied as `Chat:` in the platform context;\nuse it for `scope:\"chat\"` rather than guessing an ID. Reuse returned locations and references exactly; access to a location\nnever grants access to its whole store or to another resource.\n\nLoad only `code_files`, `code_file_stat`, and `code_file_copy` as needed.\n\n1. `code_files({scope,id,after?:string,limit?:number})` returns `container`,\n `items:[{location,path,size,mediaType}]`, and `nextAfter`. Limit defaults to\n 100, maximum 1,000. Continue with `nextAfter` until null.\n2. `code_file_stat({file:location})` returns `{exists:false}` or\n `{exists:true,reference,size,mediaType}`. It does not print file bytes.\n3. `code_file_copy({source:reference,destination:location,expectedVersion})`\n copies bytes on the server. For a new destination use `expectedVersion:null`;\n replacing a file requires its exact current version from `code_file_stat`.\n The result contains the destination `reference,size,mediaType`.\n\nEvery copy receives fresh user review with the exact source, destination, and\noverwrite scope. App and Project files may be readable by other authorized\nusers; copying a private chat attachment there is an explicit disclosure.\nRejection does not copy anything. Source versions, destination versions, current\npermissions, and the destination's byte limits are checked again during execution.\nAfter a conflict, inspect current state and prepare a new review before retrying.\n\nApp file reads and writes require Use, not Manage. Project files require Read\nfor sources and Write for destinations. Chat files require ownership. Transfers\nwork without a running UI and do not mount other chat attachments implicitly.\n\nFor example, inspect an invoice attachment, copy its reference into the target\nApp's shared file store, then call the App's published `importFiles` action with\nthe destination key expected by its discovered input schema. Do not pass a chat\npath to an App and assume it can read it. Action handlers see only their explicit\ninput and authorized App data. Use [App actions](app-actions.md) for discovery.\n\nFor small UTF-8 files that should become App source, use\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nThis is a reviewed import with the same source references, not a chat-only\nspecial case. Source file/bundle limits still apply; keep larger or private\nruntime data in shared files or the database instead of embedding it in source.\n\nKnown rejections return `CONFLICT` for an occupied or changed destination and\n`STORAGE_FULL` for a destination byte limit. No destination bytes were written.\nChoose another path or reduce the file size, then prepare a new review.\n\n\n## List Filesv2 files beside Grids documents\n\nDiscover the installed contracts first. Call `filesv2.bases.list`, then\n`filesv2.entry.list` for a folder or `filesv2.entry.search-in-base` for names\nbelow a known path. Keep the filters unchanged and follow `data.next` as\n`after` until null, even after an empty page. Each `data.items` entry includes\nits own `{type:\"filesv2.entry\",id}` ref and file metadata. Keep that ref in the\nStudio list, alongside the `grids.document` refs from `grids.document.list`.\nUse both `type` and `id` as the identity; dispatch each type to its own\noperations. Grids uses its own `page` cursor, not Filesv2's `data.next`.\n\nFilesv2 refs are opaque; they do not grant access or pin a content version.\nOn storage with stable file IDs (`n:…` refs), a ref keeps naming the same file\nacross rename and move; elsewhere it names a base and path, so moving or\nrenaming changes it. Store refs exactly as returned and never build one from a\npath. Different refs can name the same file, because older path refs stay\nvalid. A 404 means not found or no longer visible; 503 is an outage, not a\ndeletion. Refresh metadata when needed. Never construct storage URLs or turn\nfiles into public shares for this flow.\n\nOnly on a user's download request, call `filesv2.content.download` with the\nselected Filesv2 ref's exact `id`. Its `data` is `{url,method:\"GET\",expires}`.\nOffer that returned URL unchanged to the requesting user; it is a private\nbearer credential, not a stable resource link. It expires after 60 seconds;\nuse the returned `expires` timestamp and request a fresh lease when needed.\nDo not prefetch leases for list rows, persist them in App/shared data, or\ninclude them in logs. Do not send Cloud cookies or authorization headers to\nthe storage host. Grids documents keep their authenticated download path from\ntheir canonical reader; never send a `grids.document` ID to Filesv2.\n\nEach lease request checks the current user's storage and Unix permissions.\nA 403 means access is denied; a 404 means the ref or file is missing or the\nbase is no longer visible. Refresh the list and do not bypass the denial.\n`not_file` (400) means a folder was selected. `identity_changed` (409) requires\nrefreshing access/identity state before trying again. Storage unavailability is an\nerror, not an empty list. If a lease expires or a transfer fails, discard the\nURL and request a fresh lease through the same capability; if that is denied,\nstop. Revoking Cloud access prevents new leases; an already issued bearer\nlease can remain usable until expiry, subject to storage checks.\n\nFor analysis inside code, use `filesv2.content.read` and\n`capabilities.streams.read` instead of fetching a bearer URL. See\n[Capability calls](capabilities.md) for binary budgets and consent rules.\n"
68
68
  },
69
69
  {
70
70
  "path": "references/finance.md",
package/src/ai/index.ts CHANGED
@@ -128,17 +128,10 @@ export {
128
128
  export { AI_IMAGE_INPUT_MAX_BYTES, AI_TURN_ATTACHMENT_MAX_ITEMS, AI_TURN_IMAGE_MAX_TOTAL_BYTES } from "./limits";
129
129
  export {
130
130
  AI_INVALIDATION_DOMAINS,
131
- AI_LIVE_WS_TYPE,
132
131
  type AiInvalidation,
133
132
  type AiInvalidationDomain,
134
133
  AiInvalidationDomainSchema,
135
134
  AiInvalidationSchema,
136
- type AiLiveClientMessage,
137
- AiLiveClientMessageSchema,
138
- AiLiveCursorSchema,
139
- type AiLiveServerMessage,
140
- AiLiveServerMessageSchema,
141
- parseAiLiveServerMessage,
142
135
  } from "./live-events";
143
136
  export { AI_ENRICH_CRON_SETTING_KEY, AI_MEMORY_LEARNING_CRON_SETTING_KEY, aiMaintenanceJobs } from "./maintenance";
144
137
  export {