@k2b/cloud 0.44.0 → 0.46.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 (137) hide show
  1. package/package.json +9 -2
  2. package/src/_internal/app-queries.ts +73 -0
  3. package/src/_internal/capabilities.ts +137 -9
  4. package/src/_internal/define-app.ts +2 -0
  5. package/src/_internal/registry-validation.ts +17 -4
  6. package/src/_internal/registry.ts +32 -4
  7. package/src/_internal/resource-principals.ts +36 -0
  8. package/src/access/GroupCoverage.tsx +20 -5
  9. package/src/access/PermissionEditor.tsx +11 -2
  10. package/src/access/PrincipalAvatar.tsx +30 -21
  11. package/src/access/messages.ts +4 -4
  12. package/src/account/EntitySearch.tsx +12 -0
  13. package/src/ai/capabilities.ts +4 -2
  14. package/src/ai/chat/map-messages.ts +270 -0
  15. package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
  16. package/src/ai/chat/map-notebook.tsx +301 -0
  17. package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
  18. package/src/ai/chat/map-widget.css +107 -0
  19. package/src/ai/chat/map-widget.tsx +1279 -0
  20. package/src/ai/chat/message-utils.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +2 -0
  22. package/src/ai/chat/turn-layout.ts +2 -1
  23. package/src/ai/chat/turn-view.tsx +17 -1
  24. package/src/ai/default-tools.ts +3 -0
  25. package/src/ai/dictation-runtime.ts +6 -2
  26. package/src/ai/executor.ts +22 -4
  27. package/src/ai/index.ts +1 -0
  28. package/src/ai/map-contracts.ts +209 -0
  29. package/src/ai/map-execution.ts +14 -0
  30. package/src/ai/map-notebook-save.ts +276 -0
  31. package/src/ai/map-notebook.ts +32 -0
  32. package/src/ai/map-opening-hours.ts +40 -0
  33. package/src/ai/map-routes.ts +84 -0
  34. package/src/ai/map-service.ts +288 -0
  35. package/src/ai/map-store.ts +137 -0
  36. package/src/ai/map-tool.ts +73 -0
  37. package/src/ai/migrate.ts +3 -0
  38. package/src/ai/project-tool.ts +4 -4
  39. package/src/ai/projects-authorization.ts +53 -0
  40. package/src/ai/projects-routes.ts +56 -46
  41. package/src/ai/projects.ts +100 -79
  42. package/src/ai/protocol.ts +3 -1
  43. package/src/ai/recent-resources.ts +3 -3
  44. package/src/ai/routes.ts +32 -30
  45. package/src/ai/skill-tool.ts +4 -4
  46. package/src/ai/skills-routes.ts +27 -26
  47. package/src/ai/skills.ts +120 -81
  48. package/src/ai/store.ts +19 -6
  49. package/src/ai/tools.ts +14 -0
  50. package/src/ai/types.ts +3 -0
  51. package/src/api/accounts-entities.ts +6 -3
  52. package/src/api/admin-ai-skills.ts +4 -1
  53. package/src/api/auth.ts +9 -2
  54. package/src/api/capabilities.ts +90 -12
  55. package/src/api/mcp.ts +4 -2
  56. package/src/browser/notifications.ts +25 -8
  57. package/src/browser/session.ts +39 -0
  58. package/src/browser/shared-text-history-validation.ts +151 -0
  59. package/src/browser/shared-text-history.ts +464 -0
  60. package/src/browser/shared-text-offline-authorized.ts +29 -0
  61. package/src/browser/shared-text-offline.ts +351 -0
  62. package/src/browser/shared-text-provider.ts +714 -0
  63. package/src/browser/shared-text-table-contract.ts +150 -0
  64. package/src/browser/shared-text.ts +19 -0
  65. package/src/capabilities/discussion-providers.ts +99 -0
  66. package/src/capabilities/executions.ts +2 -2
  67. package/src/capabilities/migrate.ts +10 -1
  68. package/src/capabilities/resource-readers.ts +58 -0
  69. package/src/capabilities/server-invoke.ts +23 -13
  70. package/src/capabilities/server.ts +17 -6
  71. package/src/capabilities/testing.ts +13 -0
  72. package/src/contracts/capabilities.ts +137 -4
  73. package/src/contracts/discussions.ts +91 -0
  74. package/src/contracts/geo-document.ts +60 -0
  75. package/src/contracts/geo-snapshot.ts +86 -0
  76. package/src/contracts/geo.ts +14 -0
  77. package/src/contracts/index.ts +2 -0
  78. package/src/contracts/outgoing-mail.ts +1 -1
  79. package/src/contracts/resource-types.ts +21 -0
  80. package/src/contracts/shared.ts +2 -1
  81. package/src/geo/AddressAutocomplete.tsx +213 -0
  82. package/src/geo/messages.ts +29 -0
  83. package/src/geo/navigation.ts +25 -0
  84. package/src/geo/ui.ts +2 -0
  85. package/src/server/api/respond.ts +10 -2
  86. package/src/server/index.ts +1 -0
  87. package/src/server/services/access.ts +23 -10
  88. package/src/server/services/index.ts +1 -0
  89. package/src/services/accounts/app.ts +56 -17
  90. package/src/services/accounts/entities.ts +36 -16
  91. package/src/services/accounts/group-sql.ts +23 -0
  92. package/src/services/accounts/groups.ts +26 -17
  93. package/src/services/accounts/identities.ts +8 -1
  94. package/src/services/accounts/identity-reconciliation.ts +3 -1
  95. package/src/services/accounts/local-groups.ts +141 -27
  96. package/src/services/accounts/machine-group-management.ts +94 -0
  97. package/src/services/accounts/users.ts +38 -8
  98. package/src/services/geo/cache.ts +2 -1
  99. package/src/services/geo/geoapify.ts +3 -0
  100. package/src/services/identity/invocation-token.ts +110 -0
  101. package/src/services/mandates/index.ts +2 -2
  102. package/src/services/mandates/policy.ts +1 -0
  103. package/src/services/pdf/geo-snapshot.ts +107 -0
  104. package/src/services/pdf/index.ts +2 -0
  105. package/src/services/session/index.ts +12 -3
  106. package/src/shared/email-html.ts +1 -0
  107. package/src/shared/markdown/extensions/katex.ts +8 -1
  108. package/src/shared/markdown/extensions/tables.ts +14 -1
  109. package/src/shared/markdown/index.ts +22 -11
  110. package/src/shared-text/anchors.ts +29 -0
  111. package/src/shared-text/awareness.ts +87 -0
  112. package/src/shared-text/changes.ts +27 -0
  113. package/src/shared-text/documents.ts +428 -77
  114. package/src/shared-text/events.ts +12 -2
  115. package/src/shared-text/index.ts +16 -1
  116. package/src/shared-text/log.ts +24 -5
  117. package/src/shared-text/snapshots.ts +69 -23
  118. package/src/shared-text/structural-history.ts +377 -0
  119. package/src/shared-text/table-actions.ts +432 -0
  120. package/src/shared-text/types.ts +24 -5
  121. package/src/ssr/AdminArea.tsx +2 -2
  122. package/src/ssr/AdminLayout.tsx +1 -1
  123. package/src/ssr/AdminSidebar.tsx +1 -1
  124. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  125. package/src/ssr/CloudShell.island.tsx +19 -3
  126. package/src/ssr/Layout.tsx +1 -1
  127. package/src/ssr/MobileProfileActions.tsx +2 -2
  128. package/src/ssr/ProfilePreferences.tsx +2 -1
  129. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  130. package/src/ssr/admin-navigation.ts +1 -11
  131. package/src/ssr/app-launchpad.tsx +6 -1
  132. package/src/ssr/platform-messages.ts +2 -0
  133. package/src/ssr/profile-actions.ts +3 -6
  134. package/src/ssr/shell-data.tsx +1 -1
  135. package/src/styles/global.css +2 -0
  136. package/src/workflows/language/expressions.ts +3 -2
  137. package/src/workflows/runtime/executor.ts +2 -1
@@ -327,6 +327,7 @@ export const displayToolName = (name: string, locale: string) => {
327
327
 
328
328
  const BUILT_IN_TOOL_ICONS = new Map<string, string>([
329
329
  ["card", "ti ti-layout-cards"],
330
+ ["map", "ti ti-map-search"],
330
331
  ["cloud_card", "ti ti-layout-cards"],
331
332
  ["chart", "ti ti-chart-bar"],
332
333
  ["survey", "ti ti-forms"],
@@ -10,6 +10,7 @@ const toolNames = i18n.define({
10
10
  en: {
11
11
  calculate: "Calculate",
12
12
  card: "Card",
13
+ map: "Places and maps",
13
14
  chart: "Chart",
14
15
  survey: "Survey",
15
16
  text_editor: "Text editor",
@@ -86,6 +87,7 @@ const toolNames = i18n.define({
86
87
  de: {
87
88
  calculate: "Rechnen",
88
89
  card: "Karte",
90
+ map: "Orte und Karten",
89
91
  chart: "Diagramm",
90
92
  survey: "Umfrage",
91
93
  text_editor: "Texteditor",
@@ -70,11 +70,12 @@ const resultTarget = (block: ToolBlock): string | null => {
70
70
  };
71
71
 
72
72
  const isResult = (block: ToolBlock, phase: AiTurnPhase, codePresentations: boolean): boolean => {
73
+ if (block.name === "map" && block.result && typeof block.result === "object" && "map" in block.result) return true;
73
74
  if (isFailedTool(block) || block.status === "rejected" || block.status === "awaiting_approval") return false;
74
75
  // A running delivery reserves its place once its arguments arrived: a new version of an earlier result must find the
75
76
  // earlier place before it takes one of its own.
76
77
  const delivered = block.status === "completed" || (live(phase) && block.status === "running" && block.args !== undefined);
77
- if (block.name === "present" || isCardToolName(block.name) || isChartToolName(block.name)) return delivered;
78
+ if (block.name === "map" || block.name === "present" || isCardToolName(block.name) || isChartToolName(block.name)) return delivered;
78
79
  if (block.name === "code_present") return codePresentations && delivered;
79
80
  return hasCapabilityTable(block);
80
81
  };
@@ -23,6 +23,7 @@ import { CompactionBlockView, CompactToolRow, SurveyToolView, TextEditorToolView
23
23
  import { CapabilityTablePreview } from "./capability-table";
24
24
  import { CloudChartBlock } from "./chart-block";
25
25
  import { PresentToolBlock } from "./file-tools";
26
+ import { AiMapToolBlock } from "./map-widget";
26
27
  import { useAiChatActions } from "./message-actions";
27
28
  import { aiToolIcon, displayToolName, isCardToolName, isChartToolName, isRecord, isSurveyToolName } from "./message-utils";
28
29
  import { aiChatMessages } from "./messages";
@@ -438,6 +439,9 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
438
439
  <Show when={block()}>
439
440
  {(current) => (
440
441
  <Switch>
442
+ <Match when={current().name === "map"}>
443
+ <AiMapToolBlock block={current()} />
444
+ </Match>
441
445
  <Match when={current().name === "present"}>
442
446
  <PresentToolBlock block={current()} />
443
447
  </Match>
@@ -653,7 +657,18 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
653
657
  const chatActions = useAiChatActions();
654
658
  const layout = () => props.segment().layout;
655
659
  const results = createMemo(() => new Map(layout().results.map((result) => [result.id, result])));
656
- const resultIds = createMemo(() => [...results().keys()]);
660
+ const isMapFollowup = (id: string) => {
661
+ const block = results().get(id)?.block;
662
+ return (
663
+ block?.name === "map" &&
664
+ typeof block.result === "object" &&
665
+ block.result !== null &&
666
+ "anchor" in block.result &&
667
+ block.result.anchor === false
668
+ );
669
+ };
670
+ const resultIds = createMemo(() => [...results().keys()].filter((id) => !isMapFollowup(id)));
671
+ const mapFollowupIds = createMemo(() => [...results().keys()].filter(isMapFollowup));
657
672
  const actions = createMemo(() => new Map(layout().actions.map((action) => [action.id, action])));
658
673
  const actionIds = createMemo(() => [...actions().keys()]);
659
674
  const live = () => isLive(props.segment().phase);
@@ -709,6 +724,7 @@ export function AiTurnView(props: { segment: Accessor<AiTurnSegment>; disclosure
709
724
  earlier={() => props.segment().earlier}
710
725
  />
711
726
  </Show>
727
+ <For each={mapFollowupIds()}>{(id) => <AiTurnResultView result={() => results().get(id)} receipt={() => false} />}</For>
712
728
  <Show when={props.segment().scheduledTask?.stop ? undefined : props.segment().scheduledTask?.reason}>
713
729
  {(reason) => (
714
730
  <Chat.Activity
@@ -56,6 +56,7 @@ import {
56
56
  CloudAiLocalWriteInputSchema,
57
57
  CloudAiLocalWriteOutputSchema,
58
58
  } from "./local-tool-contracts";
59
+ import { createCloudAiMapTool } from "./map-tool";
59
60
  import { createCloudAiMarkdownToPdfTool } from "./markdown-pdf-tool";
60
61
  import { createAiTodoTool } from "./todo-tool";
61
62
  import { defineAiTool } from "./tools";
@@ -303,6 +304,7 @@ export const CLOUD_AI_DEFERRED_BUILTIN_TOOL_NAMES = new Set<string>([
303
304
  "markdown_to_pdf",
304
305
  "html_to_pdf",
305
306
  "present",
307
+ "map",
306
308
  "read_cloud_resource",
307
309
  ]);
308
310
 
@@ -314,6 +316,7 @@ export const createConfiguredDefaultCloudAiTools = async (config?: {
314
316
  }) => {
315
317
  const tools: AiRuntimeTool[] = [
316
318
  ...createDefaultCloudAiTools(),
319
+ createCloudAiMapTool(),
317
320
  ...createCodeSourceTools(),
318
321
  createCloudAiListFilesTool(),
319
322
  createCloudAiReadFileTool(),
@@ -1,5 +1,6 @@
1
1
  import { sql } from "bun";
2
2
  import { lazySync } from "../_internal/process-sync";
3
+ import { get as getUser } from "../services/accounts/users";
3
4
  import { logger, trace } from "../services/logging";
4
5
  import { superviseRuntimeTask } from "../services/runtime-lifecycle";
5
6
  import { aiModelAccess } from "./model-access";
@@ -85,8 +86,11 @@ export const processAiDictation = async (
85
86
  if (!conversation) throw new Error("Conversation unavailable.");
86
87
  const subject = { type: "user" as const, userId: task.user_id };
87
88
  await aiModelAccess.assertAllowed(task.model_profile_id, subject);
88
- if (conversation.project_id && !(await aiProjects.snapshot(conversation.project_id, subject)))
89
- throw new Error("Project access unavailable.");
89
+ if (conversation.project_id) {
90
+ const user = await getUser({ id: task.user_id });
91
+ if (!user || !(await aiProjects.snapshot(conversation.project_id, { actor: { kind: "user", user }, accessSubject: subject })))
92
+ throw new Error("Project access unavailable.");
93
+ }
90
94
  const result = await run({
91
95
  task: "dictation",
92
96
  traceParent: span,
@@ -57,6 +57,7 @@ import { answerOpenToolCalls } from "./open-tool-calls";
57
57
  import { type AiUserPrefs, aiActorUser, aiUserPrefs } from "./prefs";
58
58
  import { createCloudAiReadProjectKnowledgeTool, createCloudAiSearchProjectTool } from "./project-tool";
59
59
  import { aiProjects } from "./projects";
60
+ import type { AiResourceContext } from "./projects-authorization";
60
61
  import {
61
62
  AI_TURN_LEASE_MS,
62
63
  type AiTurnBlock,
@@ -306,6 +307,11 @@ const accessSubjectForActor = (actor: RequestActor | undefined): AccessSubject |
306
307
  return { type: "service_account", serviceAccountId: actor.serviceAccount.id };
307
308
  };
308
309
 
310
+ const resourceContextForActor = (actor: RequestActor | undefined): AiResourceContext | null => {
311
+ const accessSubject = accessSubjectForActor(actor);
312
+ return actor && accessSubject ? { actor, accessSubject } : null;
313
+ };
314
+
309
315
  export type ExecutorConfig = {
310
316
  leaseOwner: string;
311
317
  heartbeatMs: number;
@@ -541,7 +547,7 @@ const createEventMapper = (
541
547
  args: "args" in patch ? patch.args : existing?.args,
542
548
  status: patch.status ?? existing?.status ?? "running",
543
549
  at: patch.at ?? existing?.at,
544
- result: "result" in patch ? patch.result : existing?.result,
550
+ result: patch.isError && existing?.result !== undefined ? existing.result : "result" in patch ? patch.result : existing?.result,
545
551
  isError: "isError" in patch ? patch.isError : existing?.isError,
546
552
  approval: approval && !allowRememberedApprovals ? { ...approval, allowAlways: false, allowChat: false } : approval,
547
553
  review: patch.review ?? existing?.review,
@@ -883,7 +889,7 @@ export class AiTurnExecutor {
883
889
  if (allowed) material.tools = material.tools.filter((tool) => allowed.has(tool.def.name));
884
890
  chatId ||= conversation?.shortId ?? "";
885
891
  if (config.project) {
886
- const subject = accessSubjectForActor(material.actor);
892
+ const subject = resourceContextForActor(material.actor);
887
893
  const project = subject ? await aiProjects.getByShortId(config.project.id, subject, "read") : null;
888
894
  if (!project) {
889
895
  throw new AiTurnFailure("not_allowed", "Project access is no longer available.");
@@ -966,11 +972,11 @@ export class AiTurnExecutor {
966
972
  const timeZone = String((await coreSettings.get<string>("app.timezone")) || "").trim() || "UTC";
967
973
  const promptLocale = normalizeLocale(config.locale ?? (await coreSettings.get<string>("app.locale")));
968
974
  const project = config.project;
969
- const projectSubject = project ? accessSubjectForActor(material.actor) : null;
975
+ const projectSubject = project ? resourceContextForActor(material.actor) : null;
970
976
  // Skills are an Assistant/default-tool capability. Custom and structured
971
977
  // executions must not gain an implicit database dependency or extra tools.
972
978
  const skillSubject =
973
- defaultToolSource && resolved.profile.capabilities.includes("tools") ? accessSubjectForActor(material.actor) : null;
979
+ defaultToolSource && resolved.profile.capabilities.includes("tools") ? resourceContextForActor(material.actor) : null;
974
980
  const availableSkills = skillSubject
975
981
  ? (await aiSkills.list(skillSubject, { authority: toolAuthority })).filter((skill) => skill.enabled)
976
982
  : [];
@@ -1074,6 +1080,11 @@ export class AiTurnExecutor {
1074
1080
  const dynamicToolRuntimeContext = {
1075
1081
  turnId,
1076
1082
  reportToolProgress: (callId: string, message: string) => pipeline.reportToolProgress(callId, message),
1083
+ reportToolResult: (callId: string, result: unknown) => pipeline.reportToolResult(callId, result),
1084
+ maxToolResultChars: resolveAiToolResultMaxChars({
1085
+ contextWindow: resolved.provider.contextWindow,
1086
+ configuredMaxChars: settings.maxToolResultChars,
1087
+ }),
1077
1088
  reportWebsiteReceipts: (callId: string, receipts: AiWebsiteReceipt[]) => pipeline.reportWebsiteReceipts(callId, receipts),
1078
1089
  localToolsEnabled: activeTools.some((tool) => tool.def.name === "local_upload"),
1079
1090
  attachedFilePaths: new Set(config.files?.attached.map((file) => file.path) ?? []),
@@ -2495,6 +2506,13 @@ class StreamPipeline {
2495
2506
  await this.maybeSnapshot();
2496
2507
  }
2497
2508
 
2509
+ async reportToolResult(callId: string, result: unknown): Promise<void> {
2510
+ const block = this.blocks.find((block) => block.kind === "tool" && block.callId === callId);
2511
+ if (!block || block.kind !== "tool" || block.status !== "running") return;
2512
+ await this.emitOp(this.mapper.setTool(callId, { result }));
2513
+ await this.maybeSnapshot();
2514
+ }
2515
+
2498
2516
  async reportToolProgress(callId: string, progress: string): Promise<void> {
2499
2517
  const block = this.blocks.find((block) => block.kind === "tool" && block.callId === callId);
2500
2518
  if (!block || block.kind !== "tool" || block.progress === progress) return;
package/src/ai/index.ts CHANGED
@@ -290,6 +290,7 @@ export {
290
290
  type AiProjectReference,
291
291
  aiProjects,
292
292
  } from "./projects";
293
+ export { type AiResourceContext, AiResourceCredentialError } from "./projects-authorization";
293
294
  export {
294
295
  type AiProjectsRoutes,
295
296
  aiProjectsRoutes,
@@ -0,0 +1,209 @@
1
+ import { z } from "zod";
2
+ import {
3
+ GeoAddressSchema,
4
+ GeoAvailabilityDataSchema,
5
+ GeoCategorySchema,
6
+ GeoCostsSchema,
7
+ GeoPlaceSchema,
8
+ GeoPlaceSearchInputSchema,
9
+ GeoPointSchema,
10
+ } from "../contracts/geo";
11
+ import {
12
+ GeoRouteChangesSchema,
13
+ GeoRouteOptionsSchema,
14
+ GeoRoutePlanSchema,
15
+ GeoRouteSchema,
16
+ GeoRouteUpdateDataSchema,
17
+ } from "../contracts/geo-route";
18
+
19
+ /** Provider website text is optional and is not guaranteed to be a valid URL. */
20
+ export const aiMapWebsite = (value: string | undefined): string | undefined => {
21
+ try {
22
+ const url = new URL(value ?? "");
23
+ return url.protocol === "https:" || url.protocol === "http:" ? value : undefined;
24
+ } catch {
25
+ return undefined;
26
+ }
27
+ };
28
+
29
+ const label = z.string().trim().min(1).max(160);
30
+ const web = z.object({ placeId: z.string().max(2048), description: z.string().max(600), url: z.url().max(2000) }).strict();
31
+ // A map may accumulate two full Places result pages. Subsequent searches replace the secondary group.
32
+ export const AiMapSchema = z
33
+ .object({
34
+ id: z.uuid(),
35
+ conversationId: z.string(),
36
+ revision: z.number().int().positive(),
37
+ title: label,
38
+ selectionId: z.string().optional(),
39
+ timeZone: z.string().optional(),
40
+ kind: z.enum(["places", "route"]),
41
+ pending: z.boolean().optional(),
42
+ pendingTurnId: z.uuid().optional(),
43
+ retryUsesMarks: z.boolean().optional(),
44
+ originCallId: z.string().optional(),
45
+ anchorSeq: z.number().int().positive().optional(),
46
+ viewedAt: z.iso.datetime().optional(),
47
+ lastSearch: z
48
+ .object({
49
+ categories: z.array(GeoCategorySchema),
50
+ name: GeoPlaceSearchInputSchema.shape.name,
51
+ near: z.string().optional(),
52
+ placeId: z.string().optional(),
53
+ radiusMeters: z.number().optional(),
54
+ title: label,
55
+ along: z.object({ fromKm: z.number(), toKm: z.number(), bufferMeters: z.number().optional() }).optional(),
56
+ })
57
+ .optional(),
58
+ searchAddress: z.object({ text: z.string(), address: GeoAddressSchema }).strict().optional(),
59
+ availability: GeoAvailabilityDataSchema,
60
+ groups: z.array(z.object({ title: label, places: z.array(GeoPlaceSchema).max(20) }).strict()).max(2),
61
+ web: z.array(web).max(40),
62
+ route: GeoRouteSchema.optional(),
63
+ plan: GeoRoutePlanSchema.optional(),
64
+ preview: z.object({ result: GeoRouteUpdateDataSchema, changes: GeoRouteChangesSchema, title: label }).strict().optional(),
65
+ undo: z.object({ route: GeoRouteSchema, title: label }).strict().optional(),
66
+ receipt: z.object({ title: label, at: z.iso.datetime() }).strict().optional(),
67
+ candidates: z.array(GeoAddressSchema).max(5).optional(),
68
+ ambiguousStopId: z.string().optional(),
69
+ picker: z.array(GeoPlaceSchema).max(5).optional(),
70
+ retryInsertStopId: z.uuid().optional(),
71
+ issue: z
72
+ .object({
73
+ code: z.string(),
74
+ scope: z.enum(["person", "cloud"]).optional(),
75
+ resetsAt: z.string().optional(),
76
+ retryAfterSeconds: z.number().optional(),
77
+ costs: GeoCostsSchema.optional(),
78
+ requiredCredits: z.number().nonnegative().optional(),
79
+ availableCredits: z.number().optional(),
80
+ turnId: z.uuid().optional(),
81
+ requiresCostConfirmation: z.literal(true).optional(),
82
+ shortfallCredits: z.number().nonnegative().optional(),
83
+ })
84
+ .strict()
85
+ .optional(),
86
+ })
87
+ .strict();
88
+ export type AiMap = z.infer<typeof AiMapSchema>;
89
+
90
+ const search = z
91
+ .object({
92
+ categories: z.array(GeoCategorySchema).min(1).max(5),
93
+ name: GeoPlaceSearchInputSchema.shape.name,
94
+ near: z.string().min(1).max(300).optional(),
95
+ placeId: z.string().max(2048).optional(),
96
+ radiusMeters: z.number().min(1).max(50000).optional(),
97
+ along: z
98
+ .object({ fromKm: z.number().nonnegative(), toKm: z.number().positive(), bufferMeters: z.number().min(100).max(5000).optional() })
99
+ .strict()
100
+ .optional(),
101
+ title: label,
102
+ })
103
+ .strict();
104
+ // Model-facing inputs cannot inject points, polylines, or a provider result. These are resolved on the server.
105
+ const stop = z
106
+ .object({
107
+ id: z.string().min(1).max(40),
108
+ address: z.string().min(1).max(300).optional(),
109
+ placeId: z.string().max(2048).optional(),
110
+ label: label.optional(),
111
+ })
112
+ .strict()
113
+ .refine((value) => Boolean(value.address) !== Boolean(value.placeId), "Supply exactly one address or verified place ID");
114
+ export const AiMapCommandSchema = z.discriminatedUnion("type", [
115
+ z.object({ type: z.literal("places"), search, web: z.array(web).max(20).optional() }).strict(),
116
+ z
117
+ .object({
118
+ type: z.literal("route"),
119
+ stops: z
120
+ .array(stop)
121
+ .min(2)
122
+ .max(26)
123
+ .refine((stops) => new Set(stops.map((s) => s.id)).size === stops.length, "Stop IDs must be unique"),
124
+ options: GeoRouteOptionsSchema.optional(),
125
+ title: label,
126
+ })
127
+ .strict(),
128
+ z.object({ type: z.literal("search"), search, insert: z.boolean().optional(), web: z.array(web).max(20).optional() }).strict(),
129
+ z.object({ type: z.literal("describe"), web: z.array(web).max(40) }).strict(),
130
+ z
131
+ .object({
132
+ type: z.literal("preview"),
133
+ change: z.discriminatedUnion("type", [
134
+ z.object({ type: z.literal("insert"), placeId: z.string().max(2048), afterStopId: z.string().optional() }).strict(),
135
+ z.object({ type: z.literal("move"), stopId: z.string(), toIndex: z.number().int().min(0).max(25) }).strict(),
136
+ z.object({ type: z.literal("optimize") }).strict(),
137
+ z
138
+ .object({
139
+ type: z.literal("options"),
140
+ options: GeoRouteOptionsSchema,
141
+ leg: z.object({ fromStopId: z.string(), toStopId: z.string() }).strict().optional(),
142
+ })
143
+ .strict(),
144
+ ]),
145
+ })
146
+ .strict(),
147
+ z.object({ type: z.literal("choose"), index: z.number().int().min(0).max(4) }).strict(),
148
+ z.object({ type: z.literal("accept") }).strict(),
149
+ z.object({ type: z.literal("discard") }).strict(),
150
+ z.object({ type: z.literal("undo") }).strict(),
151
+ z.object({ type: z.literal("retry") }).strict(),
152
+ ]);
153
+ export const AiMapBrowserCommandSchema = z.union([
154
+ AiMapCommandSchema,
155
+ z.object({ type: z.literal("location"), destinationId: z.string(), point: GeoPointSchema }).strict(),
156
+ z
157
+ .object({ type: z.literal("repair"), stopId: z.string(), address: z.string().trim().min(1).max(300), point: GeoPointSchema.optional() })
158
+ .strict(),
159
+ ]);
160
+ export type AiMapCommand = z.infer<typeof AiMapBrowserCommandSchema>;
161
+ export const AiMapToolInputSchema = z
162
+ .object({ mapId: z.uuid().optional(), revision: z.number().int().positive().optional(), command: AiMapCommandSchema })
163
+ .strict();
164
+ export const AiMapReferenceSchema = AiMapSchema.pick({
165
+ id: true,
166
+ conversationId: true,
167
+ revision: true,
168
+ title: true,
169
+ kind: true,
170
+ pending: true,
171
+ anchorSeq: true,
172
+ }).extend({ reference: z.literal(true) });
173
+ export const AiMapToolResultSchema = z
174
+ .object({ map: z.union([AiMapSchema, AiMapReferenceSchema]), anchor: z.boolean(), summary: z.unknown().optional() })
175
+ .strict();
176
+ /** Geometry belongs to the durable map, never to the model context or streaming result budget. */
177
+ export const aiMapToolResult = (map: AiMap, anchor: boolean): z.infer<typeof AiMapToolResultSchema> => ({
178
+ anchor,
179
+ map: {
180
+ reference: true,
181
+ id: map.id,
182
+ conversationId: map.conversationId,
183
+ revision: map.revision,
184
+ title: map.title,
185
+ kind: map.kind,
186
+ pending: map.pending,
187
+ anchorSeq: map.anchorSeq,
188
+ },
189
+ summary: {
190
+ mapId: map.id,
191
+ revision: map.revision,
192
+ places: map.groups.map((g) => ({
193
+ title: g.title.slice(0, 64),
194
+ places: g.places.map((p, i) => ({
195
+ id: map.groups.indexOf(g) === 0 ? String(i + 1) : String.fromCharCode(65 + i),
196
+ name: p.name.slice(0, 48),
197
+ })),
198
+ })),
199
+ candidates: map.candidates?.map((p, index) => ({ index, address: p.formatted.slice(0, 96) })),
200
+ picker: map.picker?.map((p, i) => ({ id: `pick-${i + 1}`, name: p.name.slice(0, 48), atMeters: p.atMeters })),
201
+ stops: map.route?.stops.map((s) => ({ id: s.id, label: s.label?.slice(0, 48) })),
202
+ distanceMeters: map.route?.distanceMeters,
203
+ durationSeconds: map.route?.durationSeconds,
204
+ preview: map.preview ? { title: map.preview.title, delta: map.preview.result.delta } : undefined,
205
+ issue: map.issue,
206
+ },
207
+ });
208
+ export type AiMapToolResult = z.infer<typeof AiMapToolResultSchema>;
209
+ export const AiMapActionSchema = z.object({ revision: z.number().int().positive(), command: AiMapBrowserCommandSchema }).strict();
@@ -0,0 +1,14 @@
1
+ import type { AiMapCommand } from "./map-contracts";
2
+ import { type AiMapContext, applyAiMap } from "./map-service";
3
+ import { type AiMapStored, aiMaps } from "./map-store";
4
+
5
+ /** Lock and check the revision before provider work; a concurrent or stale click cannot spend against this map. */
6
+ export async function executeStoredAiMap(
7
+ conversationId: string,
8
+ ownerUserId: string,
9
+ before: AiMapStored,
10
+ command: AiMapCommand,
11
+ context: AiMapContext,
12
+ ) {
13
+ return aiMaps.mutate(conversationId, ownerUserId, before.map.revision, before.map.id, (current) => applyAiMap(current, command, context));
14
+ }