@k2b/cloud 0.45.0 → 0.47.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 (131) hide show
  1. package/package.json +11 -4
  2. package/scripts/build.ts +5 -1
  3. package/scripts/preload.ts +5 -1
  4. package/src/_internal/app-queries.ts +73 -0
  5. package/src/_internal/capabilities.ts +137 -9
  6. package/src/_internal/define-app.ts +40 -0
  7. package/src/_internal/heartbeat.ts +19 -0
  8. package/src/_internal/postgres-application-name.ts +11 -0
  9. package/src/_internal/process-sync.ts +9 -0
  10. package/src/_internal/readiness.ts +6 -1
  11. package/src/_internal/registry-validation.ts +17 -4
  12. package/src/_internal/registry.ts +32 -4
  13. package/src/_internal/resource-principals.ts +6 -0
  14. package/src/_internal/workload-credential.ts +153 -0
  15. package/src/_internal/workload-protocol.ts +36 -0
  16. package/src/ai/application-tasks.ts +5 -2
  17. package/src/ai/capabilities.ts +4 -2
  18. package/src/ai/chart-block.ts +36 -711
  19. package/src/ai/chat/map-messages.ts +270 -0
  20. package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
  21. package/src/ai/chat/map-notebook.tsx +301 -0
  22. package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
  23. package/src/ai/chat/map-widget.css +107 -0
  24. package/src/ai/chat/map-widget.tsx +1279 -0
  25. package/src/ai/chat/message-utils.ts +1 -0
  26. package/src/ai/chat/tool-names.ts +2 -0
  27. package/src/ai/chat/turn-layout.ts +2 -1
  28. package/src/ai/chat/turn-view.tsx +17 -1
  29. package/src/ai/default-tool-contracts.ts +21 -259
  30. package/src/ai/default-tools.ts +3 -0
  31. package/src/ai/executor.ts +13 -1
  32. package/src/ai/map-contracts.ts +209 -0
  33. package/src/ai/map-execution.ts +14 -0
  34. package/src/ai/map-notebook-save.ts +276 -0
  35. package/src/ai/map-notebook.ts +32 -0
  36. package/src/ai/map-opening-hours.ts +40 -0
  37. package/src/ai/map-routes.ts +84 -0
  38. package/src/ai/map-service.ts +288 -0
  39. package/src/ai/map-store.ts +137 -0
  40. package/src/ai/map-tool.ts +73 -0
  41. package/src/ai/migrate.ts +3 -0
  42. package/src/ai/protocol.ts +3 -1
  43. package/src/ai/routes.ts +2 -0
  44. package/src/ai/store.ts +19 -6
  45. package/src/ai/tools.ts +14 -0
  46. package/src/ai/types.ts +3 -0
  47. package/src/api/admin-app-keys.ts +66 -0
  48. package/src/api/admin-core-settings.ts +2 -0
  49. package/src/api/admin-identity.ts +27 -1
  50. package/src/api/admin-settings-env.ts +120 -0
  51. package/src/api/capabilities.ts +99 -15
  52. package/src/api/index.ts +3 -0
  53. package/src/api/mcp.ts +4 -2
  54. package/src/api/settings-ops.ts +35 -0
  55. package/src/app-key.ts +125 -0
  56. package/src/browser/CloudResourceSearch.tsx +48 -41
  57. package/src/browser/notifications.ts +25 -8
  58. package/src/capabilities/discussion-providers.ts +100 -0
  59. package/src/capabilities/executions.ts +2 -2
  60. package/src/capabilities/migrate.ts +10 -1
  61. package/src/capabilities/resource-readers.ts +69 -0
  62. package/src/capabilities/response.ts +4 -1
  63. package/src/capabilities/server-invoke.ts +64 -36
  64. package/src/capabilities/server.ts +17 -6
  65. package/src/capabilities/testing.ts +13 -0
  66. package/src/capabilities/types.ts +1 -1
  67. package/src/cli/admin/app-credentials.ts +32 -7
  68. package/src/cli/admin/app-keys.ts +226 -0
  69. package/src/cli/admin/index.ts +7 -1
  70. package/src/cli/admin/settings.ts +62 -0
  71. package/src/config/define-env.ts +63 -8
  72. package/src/config/env.ts +27 -21
  73. package/src/config/index.ts +12 -1
  74. package/src/config/workflow-ai.ts +28 -0
  75. package/src/contracts/capabilities.ts +137 -4
  76. package/src/contracts/discussions.ts +91 -0
  77. package/src/contracts/geo-document.ts +60 -0
  78. package/src/contracts/geo-snapshot.ts +86 -0
  79. package/src/contracts/geo.ts +14 -0
  80. package/src/contracts/index.ts +2 -0
  81. package/src/contracts/outgoing-mail.ts +1 -1
  82. package/src/contracts/resource-types.ts +21 -0
  83. package/src/contracts/settings-env.ts +21 -0
  84. package/src/contracts/settings-types.ts +6 -0
  85. package/src/geo/AddressAutocomplete.tsx +213 -0
  86. package/src/geo/messages.ts +29 -0
  87. package/src/geo/navigation.ts +25 -0
  88. package/src/geo/ui.ts +2 -0
  89. package/src/index.ts +9 -0
  90. package/src/runtime/clients.ts +72 -0
  91. package/src/runtime/connections.ts +37 -0
  92. package/src/runtime/resolve-connections.ts +91 -0
  93. package/src/server/client-ip.ts +1 -1
  94. package/src/services/accounts/app.ts +28 -6
  95. package/src/services/accounts/entities.ts +11 -4
  96. package/src/services/accounts/groups.ts +10 -4
  97. package/src/services/accounts/local-groups.ts +87 -38
  98. package/src/services/accounts/machine-group-management.ts +94 -0
  99. package/src/services/app-keys.ts +298 -0
  100. package/src/services/gateway.ts +6 -0
  101. package/src/services/geo/cache.ts +2 -1
  102. package/src/services/geo/geoapify.ts +3 -0
  103. package/src/services/identity/index.ts +3 -0
  104. package/src/services/identity/invocation-token.ts +110 -0
  105. package/src/services/identity/resource-readers.ts +80 -0
  106. package/src/services/identity/runtime-config.ts +14 -6
  107. package/src/services/identity/workload-runtime.ts +253 -0
  108. package/src/services/index.ts +4 -1
  109. package/src/services/outgoing-mail/resolve-host.ts +5 -4
  110. package/src/services/pdf/geo-snapshot.ts +107 -0
  111. package/src/services/pdf/index.ts +2 -0
  112. package/src/services/service-account-credentials.ts +63 -21
  113. package/src/services/settings/api.ts +3 -3
  114. package/src/services/settings/binding.ts +45 -0
  115. package/src/services/settings/core-settings.ts +99 -1
  116. package/src/services/settings/defaults.ts +16 -2
  117. package/src/services/settings/env-status.ts +69 -0
  118. package/src/services/settings/index.ts +69 -26
  119. package/src/services/settings/store.ts +73 -8
  120. package/src/services/settings/validation.ts +40 -0
  121. package/src/shared/index.ts +11 -10
  122. package/src/shared/markdown/extensions/tables.ts +10 -1
  123. package/src/ssr/AdminArea.tsx +2 -2
  124. package/src/ssr/AdminLayout.tsx +1 -1
  125. package/src/ssr/AdminSidebar.tsx +1 -1
  126. package/src/start.ts +36 -0
  127. package/src/styles/global.css +2 -0
  128. package/src/workflows/ai/index.ts +1 -0
  129. package/src/workflows/ai/runtime.ts +16 -8
  130. package/src/workflows/ai/settings.ts +5 -0
  131. package/src/shared/markdown/formula.ts +0 -1283
@@ -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
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { chartDataIssue, chartDateTime, chartDrawingIssue, chartSteppingIssue, cloudAiChartMarks } from "./chart-block";
2
+ import { chartDataIssue, chartDrawingIssue, cloudAiChartMarks } from "./chart-block";
3
3
 
4
4
  const ToneSchema = z.enum(["neutral", "blue", "teal", "green", "amber", "red"]);
5
5
 
@@ -28,261 +28,30 @@ export const CLOUD_AI_VIEW_MAX_MARKS = CLOUD_AI_CHART_MAX_SERIES * CLOUD_AI_CHAR
28
28
  */
29
29
  export const CLOUD_AI_GROUP_MAX_CHARTS = 6;
30
30
 
31
- // Every chart schema is built by a function whose call is marked pure, so a bundle that imports neither chart contract
32
- // leaves them out. App frames include the app contract through `cloud.chart()`.
33
- export const cloudAiNumberFormatSchema = () =>
34
- z
35
- .strictObject({
36
- style: z.enum(["number", "percent", "currency"]).optional(),
37
- currency: z
38
- .string()
39
- .regex(/^[A-Z]{3}$/)
40
- .optional(),
41
- // Units fit after a value in the shared renderer, like the existing gauge unit.
42
- unit: z.string().trim().min(1).max(12).optional(),
43
- // Intl.NumberFormat supports this precision; table formats already use 0..10.
44
- decimals: z.number().int().min(0).max(10).optional(),
45
- compact: z.boolean().optional(),
46
- })
47
- .superRefine((value, ctx) => {
48
- if ((value.style === "currency") !== (value.currency !== undefined))
49
- ctx.addIssue({ code: "custom", path: ["currency"], message: "Currency is required exactly when style is currency." });
50
- });
51
- const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
52
- export const cloudAiFormatSchema = () => z.union([cloudAiNumberFormatSchema(), dateFormatSchema()]);
53
- export type CloudAiNumberFormat = z.output<ReturnType<typeof cloudAiNumberFormatSchema>>;
54
- export type CloudAiDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
55
- export type CloudAiFormat = CloudAiNumberFormat | CloudAiDateFormat;
31
+ import {
32
+ type ChartInput as CloudAiChartInput,
33
+ type ChartDateFormat as CloudAiDateFormat,
34
+ type ChartFormat as CloudAiFormat,
35
+ type ChartNumberFormat as CloudAiNumberFormat,
36
+ chartKinds,
37
+ chartParts,
38
+ chartSchema,
39
+ chartKindSchema as cloudAiChartKindSchema,
40
+ chartFormatSchema as cloudAiFormatSchema,
41
+ chartNumberFormatSchema as cloudAiNumberFormatSchema,
42
+ } from "@k2b/editor/charts";
56
43
 
57
- export const cloudAiChartKindSchema = () =>
58
- z.enum(["bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline", "boxplot", "heatmap", "barGauge", "stat"]);
44
+ export type { CloudAiChartInput, CloudAiDateFormat, CloudAiFormat, CloudAiNumberFormat };
45
+ // Portable formats and chart types retain their existing Cloud export names.
46
+ export { cloudAiChartKindSchema, cloudAiFormatSchema, cloudAiNumberFormatSchema };
59
47
 
60
- const chartParts = (limits = true) => {
61
- const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
62
- const ChartTooltip = z
63
- .strictObject({ label: ChartLabel.optional(), format: cloudAiNumberFormatSchema().optional() })
64
- .describe("All kinds: what tooltips and the data table call the value and how they format it; axes keep their own format.");
65
- // Missing observations stay numeric so the renderer can omit them with its finite-value checks.
66
- const ChartNumber = limits
67
- ? z.number()
68
- : z
69
- .custom<number>((value) => typeof value === "number", "Expected a number, null or undefined.")
70
- .nullable()
71
- .optional()
72
- .transform((value) => value ?? Number.NaN);
73
- const list = <T extends z.ZodType>(item: T, min: number, max?: number, message?: string) => {
74
- const array = z.array(item);
75
- const bounded = limits && min > 0 ? array.min(min) : array;
76
- return limits && max !== undefined ? bounded.max(max, message) : bounded;
77
- };
78
- const ChartDate = z
79
- .string()
80
- .regex(/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?)?$/)
81
- .refine((value) => chartDateTime(value) !== undefined, "Not a valid date.");
82
- const ChartPoint = z.object({
83
- x: z.union([ChartNumber, ChartDate]).describe("line, scatter: number or YYYY-MM-DD[THH:mm[:ss]], wall-clock time without an offset."),
84
- y: ChartNumber.describe("line, scatter: value."),
85
- });
86
- // As many lines as series a reader can tell apart.
87
- const ChartReferences = list(
88
- z.object({ value: z.union([z.number(), ChartDate]), axis: z.enum(["x", "y"]).optional(), label: ChartLabel.optional() }),
89
- 0,
90
- CLOUD_AI_CHART_MAX_SERIES,
91
- `Use at most ${CLOUD_AI_CHART_MAX_SERIES} references.`,
92
- );
93
- // One palette color per band.
94
- const ChartThresholds = list(
95
- z.object({ value: z.number(), label: ChartLabel.optional() }),
96
- 1,
97
- CLOUD_AI_CHART_MAX_SERIES,
98
- `Use at most ${CLOUD_AI_CHART_MAX_SERIES} thresholds.`,
99
- );
100
- return {
101
- ChartLabel,
102
- ChartTooltip,
103
- ChartNumber,
104
- list,
105
- ChartReferences,
106
- ChartThresholds,
107
- ChartAxis: z.object({
108
- label: ChartLabel.optional(),
109
- domain: z.array(z.number()).length(2).optional().describe("Exact [min, max]; must contain every value and reference."),
110
- ticks: (limits ? z.number().int().min(2).max(12) : z.number().int().min(2)).optional(),
111
- scale: z.enum(["linear", "log"]).optional().describe("log needs values above zero."),
112
- format: cloudAiFormatSchema().optional(),
113
- }),
114
- ChartCategory: z.object({ label: ChartLabel, value: ChartNumber }),
115
- ChartSlice: z.object({ label: ChartLabel, value: limits ? z.number().min(0) : ChartNumber }),
116
- ChartSeries: (points: z.ZodArray<typeof ChartPoint>) =>
117
- z.object({
118
- label: ChartLabel.optional(),
119
- data: points,
120
- role: z.literal("reference").optional(),
121
- }),
122
- ChartPoints: list(ChartPoint, 0, CLOUD_AI_CHART_MAX_VALUES),
123
- ChartBins: z
124
- .union([
125
- limits ? z.number().int().min(1).max(CLOUD_AI_CHART_MAX_VALUES) : z.number().int().min(1),
126
- limits
127
- ? z
128
- .array(z.number())
129
- .min(2)
130
- .max(CLOUD_AI_CHART_MAX_VALUES + 1)
131
- : z.array(z.number()).min(2),
132
- ])
133
- .describe("histogram: a bin count or ascending bin edges."),
134
- ChartCommon: {
135
- title: (limits ? z.string().trim().min(1).max(160) : z.string()).describe("All kinds and groups: chart title."),
136
- subtitle: (limits ? z.string().trim().min(1).max(300) : z.string())
137
- .optional()
138
- .describe("All kinds and groups: unit, period or source."),
139
- },
140
- ChartUnit: limits ? z.string().trim().min(1).max(12) : z.string(),
141
- ChartBox: z.object({ label: ChartLabel, values: list(ChartNumber, 1) }),
142
- ChartCell: z.object({ x: ChartLabel, y: ChartLabel, value: ChartNumber }),
143
- ChartBarGauge: z.object({ label: ChartLabel, value: ChartNumber, min: z.number().optional(), max: z.number().optional() }),
144
- };
145
- };
146
-
147
- const chartKinds = (limits = true) => {
148
- const {
149
- ChartNumber,
150
- list,
151
- ChartAxis,
152
- ChartBins,
153
- ChartCategory,
154
- ChartCommon,
155
- ChartTooltip,
156
- ChartLabel,
157
- ChartPoints,
158
- ChartSeries,
159
- ChartSlice,
160
- ChartReferences,
161
- ChartThresholds,
162
- ChartUnit,
163
- ChartBox,
164
- ChartCell,
165
- ChartBarGauge,
166
- } = chartParts(limits);
167
- const ChartKindCommon = { ...ChartCommon, tooltip: ChartTooltip.optional() };
168
- const numberFormat = cloudAiNumberFormatSchema();
169
- return z.discriminatedUnion("kind", [
170
- z.object({
171
- kind: z.literal("bar"),
172
- ...ChartKindCommon,
173
- data: list(ChartCategory, 1, CLOUD_AI_CHART_MAX_VALUES),
174
- xAxis: ChartAxis.pick({ label: true }).strict().optional(),
175
- yAxis: ChartAxis.optional(),
176
- colorByBar: z.boolean().optional(),
177
- showValues: z.boolean().optional(),
178
- legend: z.boolean().optional(),
179
- references: ChartReferences.optional(),
180
- }),
181
- z.object({
182
- kind: z.literal("line"),
183
- ...ChartKindCommon,
184
- series: list(
185
- ChartSeries(
186
- limits
187
- ? ChartPoints.min(2, "A line needs at least two points per series; show single values as a bar or scatter chart.")
188
- : ChartPoints,
189
- ).extend({ style: z.enum(["solid", "dashed", "dotted"]).optional() }),
190
- 1,
191
- CLOUD_AI_CHART_MAX_SERIES,
192
- ),
193
- xAxis: ChartAxis.optional(),
194
- yAxis: ChartAxis.optional(),
195
- legend: z.boolean().optional(),
196
- area: z.boolean().optional(),
197
- smooth: z.boolean().optional(),
198
- references: ChartReferences.optional(),
199
- }),
200
- z.object({
201
- kind: z.literal("scatter"),
202
- ...ChartKindCommon,
203
- series: list(ChartSeries(limits ? ChartPoints.min(1) : ChartPoints), 1, CLOUD_AI_CHART_MAX_SERIES),
204
- xAxis: ChartAxis.optional(),
205
- yAxis: ChartAxis.optional(),
206
- legend: z.boolean().optional(),
207
- references: ChartReferences.optional(),
208
- }),
209
- z.object({
210
- kind: z.enum(["pie", "donut"]),
211
- ...ChartKindCommon,
212
- data: list(ChartSlice, 1, CLOUD_AI_CHART_MAX_VALUES),
213
- legend: z.boolean().optional(),
214
- showLabels: z.boolean().optional(),
215
- format: numberFormat.optional(),
216
- }),
217
- z.object({
218
- kind: z.literal("histogram"),
219
- ...ChartKindCommon,
220
- data: list(ChartNumber, 1, CLOUD_AI_CHART_MAX_VALUES),
221
- bins: ChartBins.optional(),
222
- references: ChartReferences.optional(),
223
- }),
224
- z.object({
225
- kind: z.literal("gauge"),
226
- ...ChartKindCommon,
227
- value: ChartNumber,
228
- min: z.number().optional(),
229
- max: z.number().optional(),
230
- label: ChartLabel.optional(),
231
- unit: ChartUnit.optional(),
232
- format: numberFormat.optional(),
233
- thresholds: ChartThresholds.optional(),
234
- }),
235
- z.object({
236
- kind: z.literal("sparkline"),
237
- ...ChartKindCommon,
238
- data: list(ChartNumber, 2, CLOUD_AI_CHART_MAX_VALUES),
239
- area: z.boolean().optional(),
240
- }),
241
- z.object({
242
- kind: z.literal("boxplot"),
243
- ...ChartKindCommon,
244
- data: list(ChartBox, 1, CLOUD_AI_CHART_MAX_VALUES),
245
- yAxis: ChartAxis.optional(),
246
- references: ChartReferences.optional(),
247
- }),
248
- z.object({
249
- kind: z.literal("heatmap"),
250
- ...ChartKindCommon,
251
- data: list(ChartCell, 1, CLOUD_AI_VIEW_MAX_MARKS),
252
- format: numberFormat.optional(),
253
- showValues: z.boolean().optional(),
254
- min: z.number().optional(),
255
- max: z.number().optional(),
256
- }),
257
- z.object({
258
- kind: z.literal("barGauge"),
259
- ...ChartKindCommon,
260
- // The shared renderer draws 280 units high and a bar gauge row is 28 units, so 8 rows fit without a taller drawing.
261
- data: list(ChartBarGauge, 1, CLOUD_AI_CHART_MAX_SERIES, `Use at most ${CLOUD_AI_CHART_MAX_SERIES} bar gauge items.`),
262
- min: z.number().optional(),
263
- max: z.number().optional(),
264
- unit: ChartUnit.optional(),
265
- format: numberFormat.optional(),
266
- thresholds: ChartThresholds.optional(),
267
- }),
268
- z.object({
269
- kind: z.literal("stat"),
270
- ...ChartKindCommon,
271
- value: ChartNumber,
272
- format: numberFormat.optional(),
273
- unit: ChartUnit.optional(),
274
- delta: limits ? z.number().optional() : ChartNumber,
275
- better: z.enum(["up", "down"]).optional(),
276
- sparkline: list(ChartNumber, 2, CLOUD_AI_CHART_MAX_VALUES).optional(),
277
- }),
278
- ]);
279
- };
48
+ const chartLimits = { maxSeries: CLOUD_AI_CHART_MAX_SERIES, maxValues: CLOUD_AI_CHART_MAX_VALUES, maxMarks: CLOUD_AI_VIEW_MAX_MARKS };
280
49
 
281
50
  /** Checks run on complete input only, so out-of-range input fails on its bounds before anything is computed or drawn. */
282
51
  const whenValid = { when: (payload: { issues: readonly unknown[] }) => payload.issues.length === 0 };
283
52
 
284
53
  export const cloudAiChartSchema = () =>
285
- chartKinds().superRefine((chart, context) => {
54
+ chartKinds(chartLimits).superRefine((chart, context) => {
286
55
  const total = cloudAiChartMarks(chart);
287
56
  const issue =
288
57
  total > CLOUD_AI_VIEW_MAX_MARKS
@@ -291,16 +60,9 @@ export const cloudAiChartSchema = () =>
291
60
  if (issue) context.addIssue({ code: "custom", message: issue });
292
61
  }, whenValid);
293
62
  /** Apps retain the renderer's permissive data contract, except values that would stall tick generation. */
294
- export const cloudAiAppChartSchema = () =>
295
- chartKinds(false).superRefine((chart, context) => {
296
- const issue = chartSteppingIssue(chart);
297
- if (issue) context.addIssue({ code: "custom", message: issue });
298
- }, whenValid);
63
+ export const cloudAiAppChartSchema = chartSchema;
299
64
  const ChartSchema = /* @__PURE__ */ cloudAiChartSchema();
300
65
 
301
- /** One chart as the renderers read it: a `cloud.chart()` kind with its options. */
302
- export type CloudAiChartInput = z.output<ReturnType<typeof chartKinds>>;
303
-
304
66
  /**
305
67
  * Reads one chart without drawing it; undefined when it is not a chart. Writers have drawn it once already, so
306
68
  * renderers draw only for display.
@@ -311,7 +73,7 @@ export const parseCloudAiChartInput = (value: unknown): CloudAiChartInput | unde
311
73
  };
312
74
 
313
75
  export const cloudAiGroupSchema = () => {
314
- const { ChartCommon } = chartParts();
76
+ const { ChartCommon } = chartParts(chartLimits);
315
77
  return z
316
78
  .strictObject({
317
79
  ...ChartCommon,
@@ -354,7 +116,7 @@ const flatChartSchema = () => {
354
116
  ChartThresholds,
355
117
  ChartUnit,
356
118
  ChartTooltip,
357
- } = chartParts();
119
+ } = chartParts(chartLimits);
358
120
  return z.object({
359
121
  kind: cloudAiChartKindSchema().optional().describe("Single charts: kind. Omit for groups."),
360
122
  ...ChartCommon,
@@ -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(),
@@ -547,7 +547,7 @@ const createEventMapper = (
547
547
  args: "args" in patch ? patch.args : existing?.args,
548
548
  status: patch.status ?? existing?.status ?? "running",
549
549
  at: patch.at ?? existing?.at,
550
- result: "result" in patch ? patch.result : existing?.result,
550
+ result: patch.isError && existing?.result !== undefined ? existing.result : "result" in patch ? patch.result : existing?.result,
551
551
  isError: "isError" in patch ? patch.isError : existing?.isError,
552
552
  approval: approval && !allowRememberedApprovals ? { ...approval, allowAlways: false, allowChat: false } : approval,
553
553
  review: patch.review ?? existing?.review,
@@ -1080,6 +1080,11 @@ export class AiTurnExecutor {
1080
1080
  const dynamicToolRuntimeContext = {
1081
1081
  turnId,
1082
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
+ }),
1083
1088
  reportWebsiteReceipts: (callId: string, receipts: AiWebsiteReceipt[]) => pipeline.reportWebsiteReceipts(callId, receipts),
1084
1089
  localToolsEnabled: activeTools.some((tool) => tool.def.name === "local_upload"),
1085
1090
  attachedFilePaths: new Set(config.files?.attached.map((file) => file.path) ?? []),
@@ -2501,6 +2506,13 @@ class StreamPipeline {
2501
2506
  await this.maybeSnapshot();
2502
2507
  }
2503
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
+
2504
2516
  async reportToolProgress(callId: string, progress: string): Promise<void> {
2505
2517
  const block = this.blocks.find((block) => block.kind === "tool" && block.callId === callId);
2506
2518
  if (!block || block.kind !== "tool" || block.progress === progress) return;
@@ -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();