@k2b/cloud 0.37.0 → 0.39.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 +8 -3
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/define-app.ts +28 -2
- package/src/_internal/help-catalog.ts +123 -4
- package/src/_internal/help.ts +27 -6
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +30 -5
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/Avatar.tsx +20 -2
- package/src/account/ui.ts +1 -1
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/browser.ts +11 -0
- package/src/ai/chart-block.ts +44 -13
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/chart-block.tsx +2 -11
- package/src/ai/chat/messages.ts +10 -0
- package/src/ai/chat/turn-view.tsx +35 -3
- package/src/ai/chat/view-block.tsx +7 -2
- package/src/ai/chat/web-tools.tsx +23 -6
- package/src/ai/chat-task-contracts.ts +28 -3
- package/src/ai/chat-task-reasons-migrate.ts +68 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +460 -66
- package/src/ai/default-tool-contracts.ts +30 -13
- package/src/ai/fetch-file-tool.ts +37 -3
- package/src/ai/index.ts +12 -0
- package/src/ai/migrate.ts +46 -3
- package/src/ai/readable-html.ts +1057 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +27 -1
- package/src/ai/runtime.ts +12 -27
- package/src/ai/store.ts +5 -3
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-wording.ts +45 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +12 -1
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-outgoing-mail.ts +4 -2
- package/src/api/app-approval.ts +9 -2
- package/src/api/approvals.ts +1 -1
- package/src/api/help.ts +23 -4
- package/src/api/index.ts +2 -1
- package/src/api/mcp.ts +8 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/FileChooser.tsx +11 -3
- package/src/browser/app-approval.ts +7 -2
- package/src/browser/choose-files.browser-harness.tsx +4 -1
- package/src/browser/composer-catalog.ts +103 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +150 -0
- package/src/browser/file-chooser-messages.ts +4 -0
- package/src/browser/file-providers.ts +14 -1
- package/src/browser/panel-messages.ts +24 -0
- package/src/browser/panel-slot.ts +354 -0
- package/src/browser/panel.ts +28 -0
- package/src/browser/provider-folder.tsx +58 -5
- package/src/browser/resource-picker.tsx +17 -2
- package/src/cli/admin/workflows.ts +1 -0
- package/src/config/env.ts +14 -0
- package/src/contracts/app-approval.ts +49 -9
- package/src/contracts/app.ts +14 -0
- package/src/contracts/file-provider.ts +48 -1
- package/src/contracts/outgoing-mail.ts +8 -1
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/shared.ts +2 -0
- package/src/server/help.ts +136 -10
- package/src/server/services/access.ts +5 -1
- package/src/server/services/geo.ts +21 -4
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/allowed-network-fetch.ts +189 -0
- package/src/services/app-approval.ts +158 -38
- package/src/services/approvals/device.ts +2 -0
- package/src/services/help/index.ts +46 -5
- package/src/services/help/store.ts +26 -5
- package/src/services/help/types.ts +10 -2
- package/src/services/index.ts +22 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/outgoing-mail/dispatcher.ts +7 -2
- package/src/services/outgoing-mail/resolve-host.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +18 -3
- package/src/services/outgoing-mail/transport.ts +24 -13
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +67 -43
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/help.ts +41 -0
- package/src/shared/index.ts +9 -1
- package/src/shared/markdown/extensions/guided-help.ts +208 -64
- package/src/shared/markdown/formula.ts +8 -8
- package/src/shared/markdown/index.ts +72 -11
- package/src/shared/redirect.ts +12 -0
- package/src/ssr/CloudShell.island.tsx +86 -9
- package/src/ssr/Layout.tsx +62 -11
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
- package/src/ssr/LayoutHelp.tsx +569 -410
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/PanelLayout.tsx +27 -0
- package/src/ssr/PanelRuntime.island.tsx +87 -0
- package/src/ssr/PanelSlot.tsx +108 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +72 -25
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +100 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +15 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +431 -0
- package/src/ssr/help-messages.ts +60 -38
- package/src/ssr/help.ts +3 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-help-article.ts +41 -0
- package/src/ssr/layout-help-navigation.ts +0 -12
- package/src/ssr/layout-help-registry.tsx +4 -11
- package/src/ssr/layout-help-search.ts +9 -12
- package/src/ssr/layout-help-start.ts +68 -0
- package/src/ssr/panel-slot.browser-harness.tsx +110 -0
- package/src/ssr/platform-messages.ts +34 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +11 -0
- package/src/ssr/shell-shortcuts.ts +19 -0
- package/src/styles/effects.css +689 -185
- package/src/styles/global.css +269 -55
- package/src/styles/panel.css +373 -0
- package/src/styles/resource-search.css +25 -0
- package/src/styles/tokens.css +23 -6
- package/src/styles/utilities-navigation.css +5 -21
- package/src/workflows/store/events.ts +24 -0
- package/src/workflows/store/index.ts +8 -0
- package/src/workflows/store/observability.ts +6 -1
- package/src/workflows/store/retention.ts +157 -0
|
@@ -3,7 +3,7 @@ import { userFromActor } from "../server/actor";
|
|
|
3
3
|
import { type ApiErrorResponse, respond } from "../server/api";
|
|
4
4
|
import type { AuthContext, RequestActor } from "../server/middleware/auth";
|
|
5
5
|
import { err, fail } from "../server/services";
|
|
6
|
-
import type { AiToolApprovalContext } from "./approvals";
|
|
6
|
+
import type { AiToolApprovalChangeContext, AiToolApprovalContext } from "./approvals";
|
|
7
7
|
import { personalAiModelPolicy } from "./personal-agent";
|
|
8
8
|
import type { AiModelPolicy } from "./types";
|
|
9
9
|
|
|
@@ -13,6 +13,7 @@ export type AiChatRequestContext = {
|
|
|
13
13
|
ownerUserId: string;
|
|
14
14
|
modelPolicy: AiModelPolicy;
|
|
15
15
|
toolApprovalContext: AiToolApprovalContext;
|
|
16
|
+
toolApprovalChangeContext: AiToolApprovalChangeContext;
|
|
16
17
|
/** A person in a signed-in browser session, not `cld`, an API key, or another delegated credential. */
|
|
17
18
|
signedInSession: boolean;
|
|
18
19
|
};
|
|
@@ -28,6 +29,11 @@ export const resolveAiChatRequestContext = async <T extends AuthContext>(
|
|
|
28
29
|
ownerUserId: user.id,
|
|
29
30
|
modelPolicy: personalAiModelPolicy,
|
|
30
31
|
toolApprovalContext: { actorUserId: user.id },
|
|
32
|
+
toolApprovalChangeContext: {
|
|
33
|
+
actorUserId: user.id,
|
|
34
|
+
actor,
|
|
35
|
+
requestId: c.req.header("x-request-id") ?? null,
|
|
36
|
+
},
|
|
31
37
|
signedInSession: actor.kind === "user" && !actor.delegation && c.get("credentialKind") === "session",
|
|
32
38
|
};
|
|
33
39
|
};
|
package/src/ai/routes.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
respond,
|
|
18
18
|
v,
|
|
19
19
|
} from "../server";
|
|
20
|
+
import { isAppSessionActor } from "../server/actor";
|
|
20
21
|
import { isRequestCredentialCurrent } from "../server/middleware/auth";
|
|
21
22
|
import { approvalDecisionChannel } from "../services/approvals";
|
|
22
23
|
import { ImageMetadataError } from "../services/image-metadata";
|
|
@@ -28,6 +29,7 @@ import { AI_AUDIO_MAX_BYTES } from "./audio-format";
|
|
|
28
29
|
import { buildAiCapabilityCatalog } from "./capabilities";
|
|
29
30
|
import { aiChatMessages } from "./chat/messages";
|
|
30
31
|
import { getAiChatQuotas } from "./chat-quotas";
|
|
32
|
+
import { aiChatTasks } from "./chat-tasks";
|
|
31
33
|
import { AI_CLIENT_TOOL_ACTION_MAX_BYTES, aiClientToolOutputSchemas } from "./client-tool-results";
|
|
32
34
|
import { createCloudAiLocalBashTool, createCloudAiLocalTools, createConfiguredDefaultCloudAiTools } from "./default-tools";
|
|
33
35
|
import { enqueueAiDictation } from "./dictation-runtime";
|
|
@@ -784,6 +786,13 @@ export const aiRoutes = (() => {
|
|
|
784
786
|
ownerUserId: ctx.ownerUserId,
|
|
785
787
|
});
|
|
786
788
|
if (!archived) return notFound(c);
|
|
789
|
+
// The chat is archived either way; admission pauses a routine this step missed before its next run.
|
|
790
|
+
await aiChatTasks.pauseForArchivedChat({ conversationId: conversation.id, userId: ctx.ownerUserId }).catch((error) =>
|
|
791
|
+
logger("ai:chat-tasks").warn("Could not pause the routines of an archived chat", {
|
|
792
|
+
conversationId: conversation.id,
|
|
793
|
+
error: error instanceof Error ? error.message : String(error),
|
|
794
|
+
}),
|
|
795
|
+
);
|
|
787
796
|
return respond(c, ok({ ok: true }));
|
|
788
797
|
})
|
|
789
798
|
.post("/conversations/:conversationId/restore", async (c) => {
|
|
@@ -791,10 +800,27 @@ export const aiRoutes = (() => {
|
|
|
791
800
|
if (ctx instanceof Response) return ctx;
|
|
792
801
|
const conversation = await loadConversation(c, ctx, true);
|
|
793
802
|
if (!conversation) return notFound(c);
|
|
803
|
+
await aiChatTasks.markRestoreStarted({ conversationId: conversation.id, userId: ctx.ownerUserId });
|
|
794
804
|
const restored = await aiConversations.restoreConversation({
|
|
795
805
|
conversationId: conversation.id,
|
|
796
806
|
ownerUserId: ctx.ownerUserId,
|
|
797
807
|
});
|
|
808
|
+
// A restored chat must not leave archive reasons on routines that stay paused.
|
|
809
|
+
// The phone app may restore the chat and relabel pauses, but may not resume background authority.
|
|
810
|
+
// If this step fails, the routine recovery relabels them after a grace period; it never resumes.
|
|
811
|
+
if (restored)
|
|
812
|
+
await aiChatTasks
|
|
813
|
+
.resumeForRestoredChat({
|
|
814
|
+
conversationId: conversation.id,
|
|
815
|
+
userId: ctx.ownerUserId,
|
|
816
|
+
resume: !isAppSessionActor(c.get("actor")),
|
|
817
|
+
})
|
|
818
|
+
.catch((error) =>
|
|
819
|
+
logger("ai:chat-tasks").warn("Could not resume the routines of a restored chat", {
|
|
820
|
+
conversationId: conversation.id,
|
|
821
|
+
error: error instanceof Error ? error.message : String(error),
|
|
822
|
+
}),
|
|
823
|
+
);
|
|
798
824
|
return restored ? respond(c, ok(await publicConversationFor(restored, c.get("accessSubject")))) : notFound(c);
|
|
799
825
|
})
|
|
800
826
|
.post("/conversations/:conversationId/viewed", async (c) => {
|
|
@@ -1168,7 +1194,7 @@ export const aiRoutes = (() => {
|
|
|
1168
1194
|
client: c.req.header(APPROVAL_CLIENT_HEADER),
|
|
1169
1195
|
}),
|
|
1170
1196
|
},
|
|
1171
|
-
toolApprovalContext: ctx.
|
|
1197
|
+
toolApprovalContext: ctx.toolApprovalChangeContext,
|
|
1172
1198
|
signedInSession: ctx.signedInSession,
|
|
1173
1199
|
});
|
|
1174
1200
|
if (!result.ok) return toAiActionFailureResponse(c, result);
|
package/src/ai/runtime.ts
CHANGED
|
@@ -8,7 +8,13 @@ import { type ApprovalDecisionChannel, approvals } from "../services/approvals";
|
|
|
8
8
|
import { logger } from "../services/logging";
|
|
9
9
|
import { superviseRuntimeTask } from "../services/runtime-lifecycle";
|
|
10
10
|
import { coreSettings } from "../services/settings/api";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
AI_WEBSITE_APPROVAL_TOOL,
|
|
13
|
+
type AiToolApprovalChangeContext,
|
|
14
|
+
type AiToolApprovalContext,
|
|
15
|
+
aiTurnAllowsRememberedApprovals,
|
|
16
|
+
rememberAiToolApproval,
|
|
17
|
+
} from "./approvals";
|
|
12
18
|
import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-models";
|
|
13
19
|
import { startAiDictationRuntime } from "./dictation-runtime";
|
|
14
20
|
import { AiTurnExecutor, type ExecutorConfig } from "./executor";
|
|
@@ -20,8 +26,9 @@ import { aiQuotas } from "./quotas";
|
|
|
20
26
|
import { parseAiResourceMarker } from "./resource-markers";
|
|
21
27
|
import { isAiVisionModelConfigured } from "./settings";
|
|
22
28
|
import { aiConversations } from "./store";
|
|
23
|
-
import {
|
|
29
|
+
import { publishAiWireEvent } from "./stream";
|
|
24
30
|
import { aiToolAudit } from "./tool-audit";
|
|
31
|
+
import { finishAiTurnAbort } from "./turn-abort";
|
|
25
32
|
import type {
|
|
26
33
|
AiChatTurnRunConfig,
|
|
27
34
|
AiClientToolId,
|
|
@@ -273,32 +280,10 @@ export const submitAiCompaction = async (input: SubmitAiCompactionInput): Promis
|
|
|
273
280
|
// ---------------------------------------------------------------------------
|
|
274
281
|
|
|
275
282
|
export const abortAiTurn = async (input: { conversationId: string; turnId: string }): Promise<{ ok: true }> => {
|
|
276
|
-
// Capture
|
|
283
|
+
// Capture wire coordinates before the cancellation flag can wake its owner.
|
|
277
284
|
const active = await aiConversations.getActiveTurn({ conversationId: input.conversationId }).catch(() => null);
|
|
278
285
|
const request = await aiConversations.requestTurnAbort({ ...input, reason: "user" });
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// Tell any live owner to stop (its heartbeat also detects the cancel flag).
|
|
282
|
-
await publishAiTurnAbort(input).catch(() => undefined);
|
|
283
|
-
|
|
284
|
-
if (request.ownerless) {
|
|
285
|
-
const finalized = await aiConversations.completeTurn({ ...input, status: "aborted", error: null });
|
|
286
|
-
const config = await aiConversations.getTurnRunConfig(input);
|
|
287
|
-
if (finalized === "completed" && !(config?.kind === "chat" && config.background)) {
|
|
288
|
-
const attempt = active?.turn.id === input.turnId ? active.turn.attempt : 1;
|
|
289
|
-
const seq = (active?.turn.id === input.turnId ? active.liveSeq : 0) + 1;
|
|
290
|
-
await publishAiWireEvent({
|
|
291
|
-
v: 1,
|
|
292
|
-
conversationId: input.conversationId,
|
|
293
|
-
turnId: input.turnId,
|
|
294
|
-
attempt,
|
|
295
|
-
seq,
|
|
296
|
-
type: "turn_finished",
|
|
297
|
-
status: "aborted",
|
|
298
|
-
error: null,
|
|
299
|
-
}).catch(() => undefined);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
286
|
+
await finishAiTurnAbort(input, request, active);
|
|
302
287
|
return { ok: true };
|
|
303
288
|
};
|
|
304
289
|
|
|
@@ -331,7 +316,7 @@ export const submitAiTurnAction = async (input: {
|
|
|
331
316
|
callId: string;
|
|
332
317
|
action: AiTurnActionInput;
|
|
333
318
|
decidedBy: { userId: string; via: ApprovalDecisionChannel; deviceId?: string };
|
|
334
|
-
toolApprovalContext?:
|
|
319
|
+
toolApprovalContext?: AiToolApprovalChangeContext;
|
|
335
320
|
/** Whether a person decides in a signed-in browser session; only such a decision may remember a website. */
|
|
336
321
|
signedInSession?: boolean;
|
|
337
322
|
}): Promise<{ ok: true } | { ok: false; status: 400 | 403 | 404 | 409; message: string }> => {
|
package/src/ai/store.ts
CHANGED
|
@@ -2992,12 +2992,14 @@ export const aiConversations: AiConversationService = {
|
|
|
2992
2992
|
return Boolean(rows[0]);
|
|
2993
2993
|
},
|
|
2994
2994
|
|
|
2995
|
-
requestTurnAbort: async (input) => {
|
|
2995
|
+
requestTurnAbort: async (input, options) => {
|
|
2996
2996
|
const reason = input.reason ?? "user";
|
|
2997
|
-
const rows = await sql<TurnRow[]>`
|
|
2997
|
+
const rows = await (options?.db ?? sql)<TurnRow[]>`
|
|
2998
2998
|
UPDATE ai.turns
|
|
2999
2999
|
SET cancel_requested_at = COALESCE(cancel_requested_at, now()),
|
|
3000
|
-
cancellation_reason =
|
|
3000
|
+
cancellation_reason = CASE WHEN ${reason} = 'routine_deleted' THEN ${reason}
|
|
3001
|
+
WHEN ${reason} = 'routine_paused' AND cancellation_reason IS DISTINCT FROM 'routine_deleted' THEN ${reason}
|
|
3002
|
+
ELSE COALESCE(cancellation_reason, ${reason}) END
|
|
3001
3003
|
WHERE id = ${input.turnId}
|
|
3002
3004
|
AND conversation_id = ${input.conversationId}
|
|
3003
3005
|
AND status IN ('queued', 'running', 'waiting_for_action')
|
package/src/ai/timeline.ts
CHANGED
|
@@ -36,6 +36,9 @@ const isAssistantPart = (entry: AiStoredMessage): boolean =>
|
|
|
36
36
|
entry.kind === "message" && (entry.message.role === "assistant" || entry.message.role === "tool_result");
|
|
37
37
|
|
|
38
38
|
const sameLoop = (entry: AiStoredMessage, loopId: string | null): boolean => (loopId ? entry.loopId === loopId : entry.loopId === null);
|
|
39
|
+
/** Each delivered routine run is its own answer, with its own run link and system line, also when runs follow each other. */
|
|
40
|
+
const sameRoutineRun = (entry: AiStoredMessage, first: AiStoredMessage): boolean =>
|
|
41
|
+
(entry.meta?.scheduledTask?.occurrenceId ?? null) === (first.meta?.scheduledTask?.occurrenceId ?? null);
|
|
39
42
|
|
|
40
43
|
const assistantActionEntry = (entries: AiStoredMessage[]): AiStoredMessage | null => {
|
|
41
44
|
const assistantEntries = entries.filter((entry) => entry.kind === "message" && entry.message.role === "assistant");
|
|
@@ -107,7 +110,12 @@ export const buildAiMessageTimeline = (messages: AiStoredMessage[]): AiMessageTi
|
|
|
107
110
|
pendingSteers = [];
|
|
108
111
|
const entries = [entry];
|
|
109
112
|
index += 1;
|
|
110
|
-
while (
|
|
113
|
+
while (
|
|
114
|
+
index < messages.length &&
|
|
115
|
+
isAssistantPart(messages[index]!) &&
|
|
116
|
+
sameLoop(messages[index]!, loopId) &&
|
|
117
|
+
(loopId !== null || sameRoutineRun(messages[index]!, entry))
|
|
118
|
+
) {
|
|
111
119
|
entries.push(messages[index]!);
|
|
112
120
|
index += 1;
|
|
113
121
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CapabilityActionWording,
|
|
3
|
+
capabilityActionOutcome,
|
|
4
|
+
capabilityActionSubject,
|
|
5
|
+
resolveCapabilityActionSentences,
|
|
6
|
+
} from "../_internal/capability-sentences";
|
|
7
|
+
import { displayToolName, isRecord } from "./chat/message-utils";
|
|
8
|
+
import type { AiTurnBlock } from "./protocol";
|
|
9
|
+
import { CLOUD_AI_TOOL_PRESENTATION } from "./tool-sentences";
|
|
10
|
+
|
|
11
|
+
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How the chat words one call that acts on something: an app Action through its saved presentation, a
|
|
15
|
+
* Cloud tool through Cloud's own catalog of the same shape. `null` for calls that only read.
|
|
16
|
+
*/
|
|
17
|
+
export const toolWording = (block: ToolBlock, locale: string): CapabilityActionWording | null => {
|
|
18
|
+
const presentation = block.presentation;
|
|
19
|
+
if (presentation?.kind === "capability") {
|
|
20
|
+
if (presentation.capabilityKind !== "action") return null;
|
|
21
|
+
return {
|
|
22
|
+
title: presentation.title,
|
|
23
|
+
...(presentation.sentences ? { sentences: presentation.sentences } : {}),
|
|
24
|
+
...(presentation.fields ? { fields: presentation.fields } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const sentences = resolveCapabilityActionSentences(block.name, CLOUD_AI_TOOL_PRESENTATION, locale);
|
|
28
|
+
return sentences ? { title: displayToolName(block.name, locale), sentences } : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** What the call does or would do, as one sentence; `null` for calls that only read. */
|
|
32
|
+
export const toolSubject = (block: ToolBlock, locale: string, timeZone: string): string | null => {
|
|
33
|
+
const wording = toolWording(block, locale);
|
|
34
|
+
return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone }) : null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The sentence for one outcome, or `null` when the tool has none that renders for this call. */
|
|
38
|
+
export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun", locale: string, timeZone: string): string | null => {
|
|
39
|
+
const wording = toolWording(block, locale);
|
|
40
|
+
if (!wording) return null;
|
|
41
|
+
const result = isRecord(block.result) ? block.result : undefined;
|
|
42
|
+
// App Actions return their data inside the result envelope; Cloud tools return the data itself.
|
|
43
|
+
const data = block.presentation?.kind === "capability" ? result?.data : block.result;
|
|
44
|
+
return capabilityActionOutcome(wording, key, { input: block.args, data }, { locale, timeZone });
|
|
45
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { aiConversations } from "./store";
|
|
2
|
+
import { publishAiTurnAbort, publishAiWireEvent } from "./stream";
|
|
3
|
+
import type { AiTurnAbortRequest } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Finish a committed cancellation request; shared by interactive and routine cancellation. */
|
|
6
|
+
export const finishAiTurnAbort = async (
|
|
7
|
+
input: { conversationId: string; turnId: string },
|
|
8
|
+
request: AiTurnAbortRequest,
|
|
9
|
+
active: Awaited<ReturnType<typeof aiConversations.getActiveTurn>> = null,
|
|
10
|
+
): Promise<void> => {
|
|
11
|
+
if (!request.found) return;
|
|
12
|
+
// Tell any live owner to stop (its heartbeat also detects the cancel flag).
|
|
13
|
+
await publishAiTurnAbort(input).catch(() => undefined);
|
|
14
|
+
|
|
15
|
+
if (request.ownerless) {
|
|
16
|
+
const finalized = await aiConversations.completeTurn({ ...input, status: "aborted", error: null });
|
|
17
|
+
const config = await aiConversations.getTurnRunConfig(input);
|
|
18
|
+
if (finalized === "completed" && !(config?.kind === "chat" && config.background)) {
|
|
19
|
+
const attempt = active?.turn.id === input.turnId ? active.turn.attempt : 1;
|
|
20
|
+
const seq = (active?.turn.id === input.turnId ? active.liveSeq : 0) + 1;
|
|
21
|
+
await publishAiWireEvent({
|
|
22
|
+
v: 1,
|
|
23
|
+
conversationId: input.conversationId,
|
|
24
|
+
turnId: input.turnId,
|
|
25
|
+
attempt,
|
|
26
|
+
seq,
|
|
27
|
+
type: "turn_finished",
|
|
28
|
+
status: "aborted",
|
|
29
|
+
error: null,
|
|
30
|
+
}).catch(() => undefined);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
package/src/ai/types.ts
CHANGED
|
@@ -298,6 +298,10 @@ export type AiStoredMessage = {
|
|
|
298
298
|
sourceHref?: string;
|
|
299
299
|
};
|
|
300
300
|
scheduledTask?: {
|
|
301
|
+
reason?: import("./chat-task-reasons").ChatTaskReason;
|
|
302
|
+
/** The message text is only the reason sentence; the chat words the reason itself instead. */
|
|
303
|
+
reasonOnly?: true;
|
|
304
|
+
stop?: { cause: "paused" | "deleted"; done: string[] };
|
|
301
305
|
taskId: string;
|
|
302
306
|
occurrenceId: string;
|
|
303
307
|
scheduledFor: string;
|
|
@@ -613,6 +617,10 @@ export type AiConversationFileSnapshot = {
|
|
|
613
617
|
};
|
|
614
618
|
|
|
615
619
|
export type AiBackgroundRunContext = {
|
|
620
|
+
scheduledFor?: string;
|
|
621
|
+
trigger?: "scheduled" | "manual";
|
|
622
|
+
timezone?: string;
|
|
623
|
+
stopDeliveredAt?: string;
|
|
616
624
|
taskId: string;
|
|
617
625
|
occurrenceId: string;
|
|
618
626
|
context: Array<{ seq: number; kind: "message" | "summary"; message: Message }>;
|
|
@@ -977,7 +985,10 @@ export type AiConversationService = {
|
|
|
977
985
|
seq: number;
|
|
978
986
|
}): Promise<boolean>;
|
|
979
987
|
/** Record an abort wish. The caller finalizes ownerless turns itself. */
|
|
980
|
-
requestTurnAbort(
|
|
988
|
+
requestTurnAbort(
|
|
989
|
+
input: { conversationId: string; turnId: string; reason?: string },
|
|
990
|
+
options?: { db: import("bun").SQL },
|
|
991
|
+
): Promise<AiTurnAbortRequest>;
|
|
981
992
|
completeTurn(input: {
|
|
982
993
|
conversationId: string;
|
|
983
994
|
turnId: string;
|