@k2b/cloud 0.39.0 → 0.41.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/README.md +8 -0
- package/package.json +11 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +464 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +166 -262
- package/src/approvals/approval-card.ts +68 -0
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +123 -16
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +13 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +213 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
|
@@ -39,8 +39,11 @@ import { ReferenceText } from "./reference-text";
|
|
|
39
39
|
const VISIBLE_ROWS = 100;
|
|
40
40
|
/** Rows the viewport shows before it scrolls; it reserves their height from the first frame. */
|
|
41
41
|
const VIEWPORT_ROWS = 8;
|
|
42
|
-
/** A compact row
|
|
43
|
-
const ROW_REM =
|
|
42
|
+
/** A compact DataTable row and its totals row, and the quieter header row above them. */
|
|
43
|
+
const ROW_REM = 2.5;
|
|
44
|
+
const HEADER_REM = 2.125;
|
|
45
|
+
/** DataTable draws a hairline between rows only above this many columns. */
|
|
46
|
+
const HAIRLINE_ABOVE_COLUMNS = 8;
|
|
44
47
|
|
|
45
48
|
/** Loads a view file with the viewer's access and validates it again: a file is untrusted whoever wrote it. */
|
|
46
49
|
async function loadView(url: string, path: string, signal: AbortSignal): Promise<CloudAiView | null> {
|
|
@@ -75,9 +78,16 @@ type ChartSummary = Extract<CloudAiViewSummary, { view: "chart" }>;
|
|
|
75
78
|
type GroupSummary = Extract<CloudAiViewSummary, { view: "group" }>;
|
|
76
79
|
type ExplorerSummary = Extract<CloudAiViewSummary, { view: "explorer" }>;
|
|
77
80
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The header row, the rows up to the viewport's, and the totals row when there is one, inside the paper's 1px frame.
|
|
83
|
+
* Wide tables add a 1px hairline after each visible body row with a following body row; the footer adds none.
|
|
84
|
+
*/
|
|
85
|
+
const tableHeight = (summary: TableSummary) => {
|
|
86
|
+
const bodyRows = Math.max(1, Math.min(summary.rows, VIEWPORT_ROWS));
|
|
87
|
+
const rows = bodyRows + Number(summary.footer);
|
|
88
|
+
const lines = summary.columns > HAIRLINE_ABOVE_COLUMNS ? bodyRows - 1 + Number(summary.rows > VIEWPORT_ROWS) : 0;
|
|
89
|
+
return `calc(${HEADER_REM + rows * ROW_REM}rem + ${2 + lines}px)`;
|
|
90
|
+
};
|
|
81
91
|
|
|
82
92
|
/**
|
|
83
93
|
* A table view: title, Copy data and a sortable table in a viewport whose height follows the row count of its
|
|
@@ -55,7 +55,10 @@ export type AiChatTaskOccurrenceView = {
|
|
|
55
55
|
export const toAiChatTaskView = (task: AiChatTask): AiChatTaskView => ({
|
|
56
56
|
title: task.title,
|
|
57
57
|
name: task.name,
|
|
58
|
-
reason:
|
|
58
|
+
reason:
|
|
59
|
+
task.reasonCode && task.reasonAt
|
|
60
|
+
? { code: task.reasonCode, at: task.reasonAt, ...(task.reasonAction ? { action: task.reasonAction } : {}) }
|
|
61
|
+
: null,
|
|
59
62
|
id: task.shortId,
|
|
60
63
|
chatId: task.chatId,
|
|
61
64
|
chatTitle: task.chatTitle,
|
|
@@ -4,6 +4,7 @@ import { canonicalLocale } from "../shared/locale";
|
|
|
4
4
|
/** Called in a transaction; new columns and reason repairs commit together. */
|
|
5
5
|
export const migrateChatTaskReasons = async (sql: SQL): Promise<void> => {
|
|
6
6
|
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS reason_code TEXT`.simple();
|
|
7
|
+
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS reason_action TEXT`.simple();
|
|
7
8
|
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS reason_at TIMESTAMPTZ`.simple();
|
|
8
9
|
await sql`ALTER TABLE ai.chat_tasks ADD COLUMN IF NOT EXISTS title TEXT`.simple();
|
|
9
10
|
const [column] = await sql`SELECT 1 FROM information_schema.columns
|
|
@@ -39,6 +40,11 @@ export const migrateChatTaskReasons = async (sql: SQL): Promise<void> => {
|
|
|
39
40
|
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_reason_code_check
|
|
40
41
|
CHECK (reason_code IN ('paused', 'chat_archived', 'owner_inactive', 'failed', 'rights_changed', 'needs_approval'));
|
|
41
42
|
END IF;
|
|
43
|
+
-- Bounded only: an older Core clears reason_code without knowing reason_action, so readers pair the two.
|
|
44
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_chat_tasks_reason_action_length_check') THEN
|
|
45
|
+
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_reason_action_length_check
|
|
46
|
+
CHECK (reason_action IS NULL OR char_length(reason_action) BETWEEN 1 AND 200);
|
|
47
|
+
END IF;
|
|
42
48
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'ai_chat_tasks_title_check') THEN
|
|
43
49
|
ALTER TABLE ai.chat_tasks ADD CONSTRAINT ai_chat_tasks_title_check CHECK (char_length(btrim(title)) BETWEEN 1 AND 80);
|
|
44
50
|
END IF;
|
|
@@ -49,6 +55,7 @@ export const migrateChatTaskReasons = async (sql: SQL): Promise<void> => {
|
|
|
49
55
|
WHEN state = 'paused' THEN 'paused'
|
|
50
56
|
WHEN last_error IN ('Scheduled task mandate revision changed', 'Scheduled task mandate or sponsor is unavailable') THEN 'rights_changed'
|
|
51
57
|
ELSE 'failed' END,
|
|
58
|
+
reason_action = NULL,
|
|
52
59
|
reason_at = CASE WHEN state IN ('paused', 'needs_attention') THEN updated_at ELSE NULL END
|
|
53
60
|
WHERE NOT (
|
|
54
61
|
(state IN ('paused', 'needs_attention')) = (reason_code IS NOT NULL)
|
|
@@ -28,7 +28,7 @@ const messages = i18n.define({
|
|
|
28
28
|
failed: "The last run failed.",
|
|
29
29
|
rights_changed: "What it may do changed. Please confirm again.",
|
|
30
30
|
needs_approval: ({ action }: { action?: string }) =>
|
|
31
|
-
`${action ? `Tried to ${action}. ` : ""}That needs your approval every time, and routines never ask.`,
|
|
31
|
+
`${action ? `Tried to ${action.replace(/^\p{Lu}(?=\p{Ll})/u, (first) => first.toLowerCase())}. ` : ""}That needs your approval every time, and routines never ask.`,
|
|
32
32
|
stopped: ({ list }: { list: string }) =>
|
|
33
33
|
list
|
|
34
34
|
? `Stopped while the routine was running. Already done: ${list}. Nothing was undone.`
|
package/src/ai/chat-tasks.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Message } from "@k2b/nessi";
|
|
2
2
|
import { type SQL, sql } from "bun";
|
|
3
3
|
import { getCapability } from "../_internal/registry";
|
|
4
|
+
import { type CapabilityActionPolicy, capabilityActionPolicy } from "../contracts/capabilities";
|
|
4
5
|
import { logger } from "../services/logging";
|
|
5
6
|
import {
|
|
6
7
|
createMandate,
|
|
@@ -48,6 +49,7 @@ export type AiChatTask = {
|
|
|
48
49
|
locale: string | null;
|
|
49
50
|
reasonCode: ChatTaskReasonCode | null;
|
|
50
51
|
reasonAt: string | null;
|
|
52
|
+
reasonAction: string | null;
|
|
51
53
|
id: string;
|
|
52
54
|
shortId: string;
|
|
53
55
|
chatId: string;
|
|
@@ -88,6 +90,7 @@ type TaskRow = {
|
|
|
88
90
|
locale: string | null;
|
|
89
91
|
reason_code: ChatTaskReasonCode | null;
|
|
90
92
|
reason_at: Date | string | null;
|
|
93
|
+
reason_action: string | null;
|
|
91
94
|
id: string;
|
|
92
95
|
short_id: string;
|
|
93
96
|
conversation_id: string;
|
|
@@ -172,7 +175,14 @@ const validateTaskGrants = async (input: unknown): Promise<TaskGrant[]> => {
|
|
|
172
175
|
(item) => item.localId === grant.capabilityId,
|
|
173
176
|
);
|
|
174
177
|
if (!operation) throw new Error(`Capability unavailable: ${grant.appId}.${grant.capabilityId}`);
|
|
175
|
-
|
|
178
|
+
const policy = entry ? capabilityActionPolicy(entry.manifest, grant.capabilityId) : undefined;
|
|
179
|
+
if (grant.kind === "action" && policy && (!("silent" in policy) || policy.routines === false))
|
|
180
|
+
throw new Error(`Capability requires interactive approval: ${grant.appId}.${grant.capabilityId}`);
|
|
181
|
+
if (
|
|
182
|
+
grant.kind === "action" &&
|
|
183
|
+
!policy &&
|
|
184
|
+
(!("approval" in operation) || (operation.approval !== "none" && operation.approval !== "rememberable"))
|
|
185
|
+
)
|
|
176
186
|
throw new Error(`Capability always requires interactive approval: ${grant.appId}.${grant.capabilityId}`);
|
|
177
187
|
const properties = operation.inputSchema.properties ?? {};
|
|
178
188
|
for (const key of Object.keys(grant.fixedInput))
|
|
@@ -255,7 +265,7 @@ const stopTaskAdmission = async (
|
|
|
255
265
|
const state = chatTaskReasonState(code);
|
|
256
266
|
const [stopped] = await db<TaskRow[]>`
|
|
257
267
|
UPDATE ai.chat_tasks
|
|
258
|
-
SET state = ${state}, last_error = ${reason}, reason_code = ${code}, reason_at = now(),
|
|
268
|
+
SET state = ${state}, last_error = ${reason}, reason_code = ${code}, reason_action = NULL, reason_at = now(),
|
|
259
269
|
mandate_revision = COALESCE(${pausedMandateRevision ?? null}::bigint, mandate_revision),
|
|
260
270
|
revision = revision + 1, updated_at = now()
|
|
261
271
|
WHERE id = ${task.id}::uuid AND state = ${task.state} RETURNING *
|
|
@@ -387,6 +397,8 @@ const toTask = (row: TaskRow): AiChatTask => ({
|
|
|
387
397
|
locale: row.locale,
|
|
388
398
|
reasonCode: row.reason_code,
|
|
389
399
|
reasonAt: nullableIso(row.reason_at),
|
|
400
|
+
// An older Core may leave a stale action beside another code; the action belongs only to needs_approval.
|
|
401
|
+
reasonAction: row.reason_code === "needs_approval" ? row.reason_action : null,
|
|
390
402
|
id: row.id,
|
|
391
403
|
shortId: row.short_id,
|
|
392
404
|
chatId: row.conversation_short_id,
|
|
@@ -672,6 +684,7 @@ export const aiChatTasks = {
|
|
|
672
684
|
kind: "query" | "action";
|
|
673
685
|
input: unknown;
|
|
674
686
|
approval?: "none" | "rememberable" | "always";
|
|
687
|
+
policy?: CapabilityActionPolicy;
|
|
675
688
|
}): Promise<void> => {
|
|
676
689
|
const result = await validateMandateIssueAuthority({
|
|
677
690
|
mandateId: input.mandate.id,
|
|
@@ -681,6 +694,7 @@ export const aiChatTasks = {
|
|
|
681
694
|
operation: `capability.${input.kind === "query" ? "query" : "action.run"}:${input.capabilityId}`,
|
|
682
695
|
input: input.input,
|
|
683
696
|
capabilityApproval: input.approval,
|
|
697
|
+
capabilityPolicy: input.policy,
|
|
684
698
|
});
|
|
685
699
|
if (!result.ok) {
|
|
686
700
|
const policyDenied = result.error.code === "FORBIDDEN" && result.error.message === MANDATE_POLICY_DENIED_MESSAGE;
|
|
@@ -908,6 +922,7 @@ export const aiChatTasks = {
|
|
|
908
922
|
ELSE NULL
|
|
909
923
|
END,
|
|
910
924
|
reason_code = CASE WHEN ${authorityStopped} OR task.state = 'paused' OR (task.state = 'completed' AND ${scheduleChanged} = false) THEN task.reason_code ELSE NULL END,
|
|
925
|
+
reason_action = CASE WHEN ${authorityStopped} OR task.state = 'paused' OR (task.state = 'completed' AND ${scheduleChanged} = false) THEN task.reason_action ELSE NULL END,
|
|
911
926
|
reason_at = CASE WHEN ${authorityStopped} OR task.state = 'paused' OR (task.state = 'completed' AND ${scheduleChanged} = false) THEN task.reason_at ELSE NULL END,
|
|
912
927
|
revision = task.revision + 1, updated_at = now()
|
|
913
928
|
WHERE id = ${current.id}::uuid
|
|
@@ -970,7 +985,7 @@ export const aiChatTasks = {
|
|
|
970
985
|
const [restored] = await tx`SELECT 1 FROM ai.conversations WHERE id = ${input.conversationId}::uuid
|
|
971
986
|
AND archived_at IS NULL FOR SHARE`;
|
|
972
987
|
if (current?.reason_code === "chat_archived" && restored)
|
|
973
|
-
await tx`UPDATE ai.chat_tasks SET reason_code = 'paused', updated_at = now() WHERE id = ${current.id}::uuid`;
|
|
988
|
+
await tx`UPDATE ai.chat_tasks SET reason_code = 'paused', reason_action = NULL, updated_at = now() WHERE id = ${current.id}::uuid`;
|
|
974
989
|
});
|
|
975
990
|
} catch (error) {
|
|
976
991
|
if (!(error instanceof AiChatTaskAuthorityError)) throw error;
|
|
@@ -1008,7 +1023,8 @@ export const aiChatTasks = {
|
|
|
1008
1023
|
AND task.updated_at < now() - make_interval(secs => ${graceSeconds}::double precision)
|
|
1009
1024
|
ORDER BY task.id LIMIT ${limit}
|
|
1010
1025
|
FOR UPDATE OF task SKIP LOCKED FOR SHARE OF conversation SKIP LOCKED`;
|
|
1011
|
-
for (const row of rows)
|
|
1026
|
+
for (const row of rows)
|
|
1027
|
+
await tx`UPDATE ai.chat_tasks SET reason_code = 'paused', reason_action = NULL, updated_at = now() WHERE id = ${row.id}::uuid`;
|
|
1012
1028
|
return rows.length;
|
|
1013
1029
|
});
|
|
1014
1030
|
},
|
|
@@ -1063,6 +1079,7 @@ export const aiChatTasks = {
|
|
|
1063
1079
|
UPDATE ai.chat_tasks task
|
|
1064
1080
|
SET state = ${input.state === "paused" ? chatTaskReasonState("paused") : "active"}, mandate_revision = ${revision},
|
|
1065
1081
|
reason_code = ${input.state === "paused" ? "paused" : null},
|
|
1082
|
+
reason_action = NULL,
|
|
1066
1083
|
reason_at = CASE WHEN ${input.state} = 'active' THEN NULL WHEN task.reason_code = 'paused' THEN task.reason_at ELSE now() END,
|
|
1067
1084
|
last_error = CASE WHEN task.state = ${input.state} THEN task.last_error ELSE NULL END,
|
|
1068
1085
|
revision = CASE WHEN task.state = ${input.state} THEN task.revision ELSE task.revision + 1 END,
|
|
@@ -1278,7 +1295,7 @@ export const aiChatTasks = {
|
|
|
1278
1295
|
task.id AS task_row_id, task.short_id AS task_short_id, task.conversation_id, conversation.short_id AS conversation_short_id,
|
|
1279
1296
|
conversation.title AS conversation_title, task.sponsor_user_id, task.mandate_id, task.mandate_revision,
|
|
1280
1297
|
(SELECT policy FROM auth.mandates WHERE id = task.mandate_id) AS mandate_policy,
|
|
1281
|
-
task.title, task.locale, task.reason_code, task.reason_at, task.prompt, task.schedule_kind, task.run_at, task.cron,
|
|
1298
|
+
task.title, task.locale, task.reason_code, task.reason_at, task.reason_action, task.prompt, task.schedule_kind, task.run_at, task.cron,
|
|
1282
1299
|
task.timezone, task.state AS task_state, task.revision, task.last_error, task.created_at AS task_created_at, task.updated_at
|
|
1283
1300
|
FROM (
|
|
1284
1301
|
SELECT candidate.*,
|
|
@@ -1353,7 +1370,7 @@ export const aiChatTasks = {
|
|
|
1353
1370
|
const revision = await pauseTerminalTaskMandate(task, tx);
|
|
1354
1371
|
await tx`
|
|
1355
1372
|
UPDATE ai.chat_tasks
|
|
1356
|
-
SET state = ${chatTaskReasonState(input.code)}, reason_code = ${input.code}, reason_at = CASE WHEN reason_code = ${input.code} THEN COALESCE(reason_at, now()) ELSE now() END, last_error = ${input.error}, mandate_revision = ${revision},
|
|
1373
|
+
SET state = ${chatTaskReasonState(input.code)}, reason_code = ${input.code}, reason_action = NULL, reason_at = CASE WHEN reason_code = ${input.code} THEN COALESCE(reason_at, now()) ELSE now() END, last_error = ${input.error}, mandate_revision = ${revision},
|
|
1357
1374
|
revision = revision + 1, updated_at = now()
|
|
1358
1375
|
WHERE id = ${rows[0].task_id}::uuid AND revision = ${rows[0].task_revision}::bigint
|
|
1359
1376
|
`;
|
|
@@ -1481,6 +1498,13 @@ export const aiChatTasks = {
|
|
|
1481
1498
|
return { delivered: true as const, conversationId: occurrence.conversation_id, turnId };
|
|
1482
1499
|
}),
|
|
1483
1500
|
|
|
1501
|
+
markNeedsApproval: async (input: { turnId: string; action: string }): Promise<void> => {
|
|
1502
|
+
await sql`UPDATE ai.turns SET run_config = jsonb_set(run_config, '{background,needsApproval}',
|
|
1503
|
+
jsonb_build_object('action', ${input.action.slice(0, 200)}::text))
|
|
1504
|
+
WHERE id = ${input.turnId}::uuid AND run_config ? 'background'
|
|
1505
|
+
AND run_config #> '{background,needsApproval}' IS NULL`;
|
|
1506
|
+
},
|
|
1507
|
+
|
|
1484
1508
|
finalizeTurn: async (input: {
|
|
1485
1509
|
turnId: string;
|
|
1486
1510
|
status: AiTurnStatus;
|
|
@@ -1506,6 +1530,8 @@ export const aiChatTasks = {
|
|
|
1506
1530
|
return occurrence ? { occurrenceId: occurrence.id, failed: false } : null;
|
|
1507
1531
|
}
|
|
1508
1532
|
if (!task) return null;
|
|
1533
|
+
const needsApproval = turn.run_config.background?.needsApproval;
|
|
1534
|
+
const approvalError = needsApproval ? `NEEDS_APPROVAL: ${needsApproval.action} requires approval for every call` : null;
|
|
1509
1535
|
const rows = await tx<
|
|
1510
1536
|
{
|
|
1511
1537
|
occurrence_id: string;
|
|
@@ -1517,12 +1543,15 @@ export const aiChatTasks = {
|
|
|
1517
1543
|
task_revision: number | bigint;
|
|
1518
1544
|
current_revision: number | bigint;
|
|
1519
1545
|
turn_error: string | null;
|
|
1546
|
+
needs_approval: boolean;
|
|
1520
1547
|
completed_at: Date | string;
|
|
1521
1548
|
}[]
|
|
1522
1549
|
>`
|
|
1523
1550
|
UPDATE ai.chat_task_occurrences occurrence
|
|
1524
|
-
SET state = ${
|
|
1551
|
+
SET state = CASE WHEN ${approvalError}::text IS NOT NULL AND occurrence.task_revision = task.revision THEN 'failed'
|
|
1552
|
+
ELSE ${input.status === "completed" ? "completed" : "failed"} END,
|
|
1525
1553
|
error = CASE
|
|
1554
|
+
WHEN ${approvalError}::text IS NOT NULL AND occurrence.task_revision = task.revision THEN ${approvalError}
|
|
1526
1555
|
WHEN ${input.status} = 'completed' THEN NULL
|
|
1527
1556
|
ELSE COALESCE(${input.error ?? null}, turn.error, ${`Scheduled turn ${input.status}`})
|
|
1528
1557
|
END,
|
|
@@ -1531,10 +1560,12 @@ export const aiChatTasks = {
|
|
|
1531
1560
|
WHERE occurrence.task_id = task.id AND occurrence.turn_id = ${input.turnId}::uuid AND turn.id = occurrence.turn_id
|
|
1532
1561
|
AND occurrence.state = 'running'
|
|
1533
1562
|
RETURNING occurrence.id AS occurrence_id, occurrence.task_id, task.schedule_kind, occurrence.trigger,
|
|
1534
|
-
occurrence.scheduled_for, task.run_at, occurrence.task_revision, task.revision AS current_revision, turn.error AS turn_error, occurrence.completed_at
|
|
1563
|
+
occurrence.scheduled_for, task.run_at, occurrence.task_revision, task.revision AS current_revision, turn.error AS turn_error, (${approvalError}::text IS NOT NULL AND occurrence.task_revision = task.revision) AS needs_approval, occurrence.completed_at
|
|
1535
1564
|
`;
|
|
1536
1565
|
const row = rows[0];
|
|
1537
1566
|
if (!row) return null;
|
|
1567
|
+
const failed = row.needs_approval || input.status !== "completed";
|
|
1568
|
+
const failureCode = row.needs_approval ? "needs_approval" : "failed";
|
|
1538
1569
|
const responses = await tx<{ message: Message }[]>`
|
|
1539
1570
|
SELECT message FROM ai.task_messages WHERE loop_id = ${input.turnId} AND role = 'assistant' ORDER BY seq DESC
|
|
1540
1571
|
`;
|
|
@@ -1549,36 +1580,35 @@ export const aiChatTasks = {
|
|
|
1549
1580
|
.join("\n")
|
|
1550
1581
|
.trim();
|
|
1551
1582
|
})
|
|
1552
|
-
.find(Boolean) || (
|
|
1583
|
+
.find(Boolean) || (!failed ? "Background task completed." : "");
|
|
1553
1584
|
const unchangedSinceStart = Number(row.task_revision) === Number(row.current_revision);
|
|
1554
1585
|
const currentOnceSlot = row.run_at !== null && iso(row.run_at) === iso(row.scheduled_for);
|
|
1555
|
-
if (
|
|
1556
|
-
unchangedSinceStart &&
|
|
1557
|
-
input.status === "completed" &&
|
|
1558
|
-
row.schedule_kind === "once" &&
|
|
1559
|
-
row.trigger === "scheduled" &&
|
|
1560
|
-
currentOnceSlot
|
|
1561
|
-
) {
|
|
1586
|
+
if (unchangedSinceStart && !failed && row.schedule_kind === "once" && row.trigger === "scheduled" && currentOnceSlot) {
|
|
1562
1587
|
const revision = await pauseTerminalTaskMandate(task, tx);
|
|
1563
1588
|
await tx`
|
|
1564
1589
|
UPDATE ai.chat_tasks
|
|
1565
|
-
SET state = 'completed', last_error = NULL, reason_code = NULL, reason_at = NULL, mandate_revision = ${revision}, revision = revision + 1, updated_at = now()
|
|
1590
|
+
SET state = 'completed', last_error = NULL, reason_code = NULL, reason_action = NULL, reason_at = NULL, mandate_revision = ${revision}, revision = revision + 1, updated_at = now()
|
|
1566
1591
|
WHERE id = ${row.task_id}::uuid AND revision = ${row.task_revision}::bigint
|
|
1567
1592
|
`;
|
|
1568
|
-
} else if (unchangedSinceStart &&
|
|
1569
|
-
const error = input.error || row.turn_error || `Scheduled turn ${input.status}`;
|
|
1593
|
+
} else if (unchangedSinceStart && failed) {
|
|
1594
|
+
const error = (row.needs_approval ? approvalError : null) || input.error || row.turn_error || `Scheduled turn ${input.status}`;
|
|
1570
1595
|
const revision = await pauseTerminalTaskMandate(task, tx);
|
|
1571
1596
|
await tx`
|
|
1572
1597
|
UPDATE ai.chat_tasks
|
|
1573
|
-
SET state = ${chatTaskReasonState(
|
|
1598
|
+
SET state = ${chatTaskReasonState(failureCode)}, reason_code = ${failureCode},
|
|
1599
|
+
reason_action = ${row.needs_approval ? needsApproval!.action : null},
|
|
1600
|
+
reason_at = CASE WHEN reason_code = ${failureCode} THEN COALESCE(reason_at, now()) ELSE now() END, last_error = ${error}, mandate_revision = ${revision}, revision = revision + 1, updated_at = now()
|
|
1574
1601
|
WHERE id = ${row.task_id}::uuid AND revision = ${row.task_revision}::bigint
|
|
1575
1602
|
`;
|
|
1576
1603
|
}
|
|
1577
1604
|
const [current] = await tx<TaskRow[]>`SELECT * FROM ai.chat_tasks WHERE id = ${task.id}::uuid`;
|
|
1578
|
-
const reason: ChatTaskReason | null =
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1605
|
+
const reason: ChatTaskReason | null = failed
|
|
1606
|
+
? {
|
|
1607
|
+
code: failureCode,
|
|
1608
|
+
at: current?.reason_code === failureCode && current.reason_at ? iso(current.reason_at) : iso(row.completed_at),
|
|
1609
|
+
...(row.needs_approval ? { action: needsApproval!.action } : {}),
|
|
1610
|
+
}
|
|
1611
|
+
: null;
|
|
1582
1612
|
await deliverOccurrenceResult(
|
|
1583
1613
|
tx,
|
|
1584
1614
|
task,
|
|
@@ -1586,7 +1616,7 @@ export const aiChatTasks = {
|
|
|
1586
1616
|
resultText || (reason ? reasonSentence(task, reason, appLocale) : ""),
|
|
1587
1617
|
reason ? { reason, ...(resultText ? {} : { reasonOnly: true as const }) } : {},
|
|
1588
1618
|
);
|
|
1589
|
-
return { occurrenceId: row.occurrence_id, failed
|
|
1619
|
+
return { occurrenceId: row.occurrence_id, failed };
|
|
1590
1620
|
}),
|
|
1591
1621
|
|
|
1592
1622
|
listTerminalRunningTurns: async (limit = 100): Promise<Array<{ turnId: string; status: "completed" | "failed" | "aborted" }>> => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type Accessor, createMemo, createSignal, onCleanup } from "solid-js";
|
|
2
2
|
import { createStore, reconcile } from "solid-js/store";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CapabilityActionReceiptSchema } from "../../contracts/capabilities";
|
|
5
|
+
import { type AiActionCheck, preferredActionCheck } from "../action-review";
|
|
3
6
|
import { type AiAttachmentRef, aiAttachmentMarker } from "../attachments";
|
|
4
7
|
import { aiChatMessages } from "../chat/messages";
|
|
5
8
|
import { AI_TURN_LEASE_MS, type AiStreamEvent, type AiTurnBlock, type AiTurnSnapshot, steerMessageBlockId } from "../protocol";
|
|
@@ -1349,6 +1352,61 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1349
1352
|
return submitted;
|
|
1350
1353
|
};
|
|
1351
1354
|
|
|
1355
|
+
const checkAction = async (requestInput: { turnId: string; callId: string }): Promise<AiActionCheck> => {
|
|
1356
|
+
const conversationId = activeConversationId();
|
|
1357
|
+
if (!conversationId) throw new Error("No active conversation.");
|
|
1358
|
+
const check = z
|
|
1359
|
+
.object({
|
|
1360
|
+
state: z.enum(["checking", "done", "not_done", "pending", "failed"]),
|
|
1361
|
+
receipt: CapabilityActionReceiptSchema.optional(),
|
|
1362
|
+
at: z.iso.datetime().optional(),
|
|
1363
|
+
})
|
|
1364
|
+
.strict()
|
|
1365
|
+
.parse(
|
|
1366
|
+
await request<unknown>(
|
|
1367
|
+
`/conversations/${encodeURIComponent(conversationId)}/turns/${encodeURIComponent(requestInput.turnId)}/calls/${encodeURIComponent(requestInput.callId)}/check`,
|
|
1368
|
+
{ method: "POST" },
|
|
1369
|
+
"Failed to check the Action outcome",
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
const projection = isActiveConversation(conversationId) ? state : cache.get(conversationId);
|
|
1373
|
+
let effective = check;
|
|
1374
|
+
if (projection) {
|
|
1375
|
+
const storedMessage = projection.messages.find(
|
|
1376
|
+
(message) =>
|
|
1377
|
+
message.loopId === requestInput.turnId &&
|
|
1378
|
+
message.message.role === "assistant" &&
|
|
1379
|
+
message.message.content.some((block) => block.type === "tool_call" && block.id === requestInput.callId),
|
|
1380
|
+
);
|
|
1381
|
+
const liveBlock =
|
|
1382
|
+
projection.activeTurn?.turnId === requestInput.turnId
|
|
1383
|
+
? projection.activeTurn.blocks.find((block) => block.kind === "tool" && block.callId === requestInput.callId)
|
|
1384
|
+
: undefined;
|
|
1385
|
+
effective = preferredActionCheck(storedMessage?.meta?.toolChecks?.[requestInput.callId], effective);
|
|
1386
|
+
effective = preferredActionCheck(liveBlock?.kind === "tool" ? liveBlock.check : undefined, effective);
|
|
1387
|
+
const messages = projection.messages.map((message) =>
|
|
1388
|
+
message.loopId === requestInput.turnId &&
|
|
1389
|
+
message.message.role === "assistant" &&
|
|
1390
|
+
message.message.content.some((block) => block.type === "tool_call" && block.id === requestInput.callId)
|
|
1391
|
+
? { ...message, meta: { ...message.meta, toolChecks: { ...message.meta?.toolChecks, [requestInput.callId]: effective } } }
|
|
1392
|
+
: message,
|
|
1393
|
+
);
|
|
1394
|
+
const activeTurn =
|
|
1395
|
+
projection.activeTurn?.turnId === requestInput.turnId
|
|
1396
|
+
? {
|
|
1397
|
+
...projection.activeTurn,
|
|
1398
|
+
blocks: projection.activeTurn.blocks.map((block) =>
|
|
1399
|
+
block.kind === "tool" && block.callId === requestInput.callId ? { ...block, check: effective } : block,
|
|
1400
|
+
),
|
|
1401
|
+
}
|
|
1402
|
+
: projection.activeTurn;
|
|
1403
|
+
const next = { ...projection, messages, activeTurn };
|
|
1404
|
+
cache.set(conversationId, next);
|
|
1405
|
+
if (isActiveConversation(conversationId)) setState(reconcile(next));
|
|
1406
|
+
}
|
|
1407
|
+
return effective;
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1352
1410
|
const submitFrontendToolResult = async (request: { turnId: string; callId: string }, result: unknown) => {
|
|
1353
1411
|
const conversationId = activeConversationId();
|
|
1354
1412
|
if (!conversationId) return false;
|
|
@@ -1412,6 +1470,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1412
1470
|
clearMessageFeedback,
|
|
1413
1471
|
submitTurnAction,
|
|
1414
1472
|
respondToApproval,
|
|
1473
|
+
checkAction,
|
|
1415
1474
|
submitFrontendToolResult,
|
|
1416
1475
|
fileContentUrl,
|
|
1417
1476
|
};
|
package/src/ai/default-tools.ts
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
import { createCloudAiMarkdownToPdfTool } from "./markdown-pdf-tool";
|
|
60
60
|
import { createAiTodoTool } from "./todo-tool";
|
|
61
61
|
import { defineAiTool } from "./tools";
|
|
62
|
-
import type { AiDataBoundary, AiRuntimeTool } from "./types";
|
|
62
|
+
import type { AiChatTurnRunConfig, AiDataBoundary, AiRuntimeTool } from "./types";
|
|
63
63
|
import { createCloudAiViewImageTool } from "./vision-tool";
|
|
64
64
|
|
|
65
65
|
export {
|
|
@@ -276,6 +276,14 @@ export const createCloudAiCodeTools = () => [
|
|
|
276
276
|
}).server(runManagedCodeTool("code_export")),
|
|
277
277
|
];
|
|
278
278
|
|
|
279
|
+
/** Server-run Code Mode and declared client tools, excluding app opening and secret entry under a mandate. */
|
|
280
|
+
export const createAiTurnCodeAndClientTools = (config: Pick<AiChatTurnRunConfig, "clientToolIds" | "mandate">) =>
|
|
281
|
+
[createCloudAiLocalBashTool(), ...createCloudAiLocalTools(), ...createCloudAiCodeTools()].filter(
|
|
282
|
+
(tool) =>
|
|
283
|
+
(tool.location === "server" || config.clientToolIds?.some((name) => name === tool.def.name)) &&
|
|
284
|
+
!(config.mandate && ["code_open", "code_secret"].includes(tool.def.name)),
|
|
285
|
+
);
|
|
286
|
+
|
|
279
287
|
export const createDefaultCloudAiTools = () => [
|
|
280
288
|
createAiTodoTool(),
|
|
281
289
|
createCloudAiSurveyTool(),
|