@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.
- package/package.json +9 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- 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
|
|
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
|
package/src/ai/default-tools.ts
CHANGED
|
@@ -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
|
|
89
|
-
|
|
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,
|
package/src/ai/executor.ts
CHANGED
|
@@ -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 =
|
|
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 ?
|
|
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") ?
|
|
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
|
@@ -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
|
+
}
|