@capybara-hq/kernel 0.1.0-rc.20 → 0.1.0-rc.22
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/bootstrap.json +10 -1
- package/dist/agents/admin-agents.d.ts +16 -3
- package/dist/agents/admin-agents.js +65 -7
- package/dist/agents/agents.schemas.d.ts +24 -6
- package/dist/agents/agents.service.d.ts +4 -3
- package/dist/agents/agents.service.js +39 -26
- package/dist/agents/definition.d.ts +44 -6
- package/dist/agents/definition.js +86 -18
- package/dist/agents/from-template.d.ts +6 -3
- package/dist/agents/from-template.js +57 -44
- package/dist/agents/org-templates.js +97 -41
- package/dist/agents/templates.d.ts +6 -0
- package/dist/agents/templates.js +15 -1
- package/dist/catalog/admin-catalog.controller.d.ts +2 -4
- package/dist/catalog/admin-catalog.controller.js +2 -6
- package/dist/catalog/catalog.module.js +3 -0
- package/dist/catalog/claude/report.d.ts +9 -2
- package/dist/catalog/claude/report.js +1 -0
- package/dist/catalog/claude/translate.js +1 -1
- package/dist/catalog/component-promotion.js +1 -2
- package/dist/catalog/component-state.d.ts +13 -1
- package/dist/catalog/component-state.js +14 -5
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/marketplaces.service.js +2 -1
- package/dist/catalog/org-catalog.controller.d.ts +13 -3
- package/dist/catalog/org-catalog.controller.js +26 -4
- package/dist/catalog/org-catalog.schemas.d.ts +13 -0
- package/dist/catalog/org-catalog.schemas.js +34 -0
- package/dist/catalog/org-plugin-uploads.controller.js +0 -1
- package/dist/catalog/org-plugin-uploads.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.js +0 -3
- package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
- package/dist/catalog/platform-components.d.ts +0 -1
- package/dist/catalog/platform-components.js +3 -8
- package/dist/catalog/plugin-import.d.ts +0 -8
- package/dist/catalog/plugin-import.js +4 -35
- package/dist/catalog/plugin-install.d.ts +2 -2
- package/dist/catalog/plugin-install.js +2 -2
- package/dist/catalog/plugin-package.d.ts +7 -0
- package/dist/catalog/plugin-package.js +27 -1
- package/dist/catalog/plugin-upgrade.d.ts +5 -6
- package/dist/catalog/plugin-upgrade.js +5 -23
- package/dist/catalog/requests.service.d.ts +66 -10
- package/dist/catalog/requests.service.js +196 -15
- package/dist/channels/actions.d.ts +2 -0
- package/dist/channels/actions.js +19 -4
- package/dist/channels/cards.d.ts +16 -2
- package/dist/channels/cards.js +40 -7
- package/dist/channels/channels.module.js +3 -0
- package/dist/channels/outbound.d.ts +22 -1
- package/dist/channels/outbound.js +116 -7
- package/dist/channels/phrases.d.ts +25 -0
- package/dist/channels/phrases.js +19 -0
- package/dist/channels/threads.d.ts +12 -0
- package/dist/channels/threads.js +34 -0
- package/dist/config/schema.d.ts +23 -1
- package/dist/config/schema.js +105 -5
- package/dist/conversations/conversations.service.d.ts +42 -9
- package/dist/conversations/conversations.service.js +57 -25
- package/dist/conversations/events.d.ts +106 -11
- package/dist/conversations/events.js +121 -18
- package/dist/conversations/message-context.d.ts +6 -3
- package/dist/conversations/message-context.js +14 -10
- package/dist/conversations/queue.d.ts +15 -2
- package/dist/conversations/queue.js +22 -3
- package/dist/conversations/queue.service.d.ts +12 -6
- package/dist/conversations/queue.service.js +36 -13
- package/dist/db/kernel-schema.d.ts +105 -3
- package/dist/devices/conversation-devices.d.ts +8 -1
- package/dist/devices/conversation-devices.js +8 -0
- package/dist/devices/devices.controller.d.ts +21 -4
- package/dist/devices/devices.controller.js +46 -8
- package/dist/devices/devices.schemas.d.ts +9 -1
- package/dist/devices/devices.schemas.js +11 -3
- package/dist/devices/devices.service.d.ts +176 -4
- package/dist/devices/devices.service.js +341 -6
- package/dist/devices/dto.d.ts +56 -3
- package/dist/devices/dto.js +51 -1
- package/dist/devices/public-suffix.d.ts +6 -0
- package/dist/devices/public-suffix.js +79 -0
- package/dist/errors.d.ts +46 -5
- package/dist/errors.js +46 -5
- package/dist/evals/reconstruct.d.ts +18 -13
- package/dist/evals/reconstruct.js +50 -36
- package/dist/evals/recorded-tools.d.ts +7 -7
- package/dist/evals/recorded-tools.js +4 -3
- package/dist/evals/trajectory.d.ts +6 -1
- package/dist/evals/trajectory.js +7 -0
- package/dist/extensions/bootstrap.d.ts +49 -10
- package/dist/extensions/bootstrap.js +104 -27
- package/dist/extensions/package.js +2 -2
- package/dist/http/api.module.js +5 -0
- package/dist/inbox/inbox.module.js +2 -1
- package/dist/inbox/inbox.service.d.ts +19 -4
- package/dist/inbox/inbox.service.js +14 -3
- package/dist/infra/config/kernel-config.service.js +11 -0
- package/dist/infra/events/kernel-events.d.ts +2 -1
- package/dist/lifecycle/kernel-lifecycle.d.ts +5 -1
- package/dist/lifecycle/kernel-lifecycle.js +16 -4
- package/dist/local-devices/device-policy.service.d.ts +65 -0
- package/dist/local-devices/device-policy.service.js +217 -0
- package/dist/local-devices/local-device-gateway.js +4 -1
- package/dist/local-devices/local-device-provider.d.ts +40 -3
- package/dist/local-devices/local-device-provider.js +109 -7
- package/dist/local-devices/local-devices.module.d.ts +2 -1
- package/dist/local-devices/local-devices.module.js +19 -3
- package/dist/local-devices/machines.d.ts +14 -3
- package/dist/local-devices/machines.js +41 -3
- package/dist/local-devices/network-grants.d.ts +104 -0
- package/dist/local-devices/network-grants.js +288 -0
- package/dist/local-devices/policy-summary.d.ts +20 -0
- package/dist/local-devices/policy-summary.js +63 -0
- package/dist/orgs/orgs.module.js +4 -10
- package/dist/orgs/roles.d.ts +1 -1
- package/dist/orgs/roles.js +4 -0
- package/dist/orgs/settings.controller.js +2 -1
- package/dist/orgs/settings.service.d.ts +164 -12
- package/dist/orgs/settings.service.js +212 -24
- package/dist/platform/exclusions.js +11 -1
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/admin-items.ops.js +1 -1
- package/dist/platform/ops/agent-skills.ops.js +2 -2
- package/dist/platform/ops/agent-templates.ops.js +2 -2
- package/dist/platform/ops/agents.ops.js +11 -15
- package/dist/platform/ops/devices.ops.js +3 -1
- package/dist/platform/ops/inbox.ops.js +19 -0
- package/dist/platform/ops/org-settings.ops.js +14 -5
- package/dist/platform/ops/platform.ops.d.ts +9 -1
- package/dist/platform/ops/platform.ops.js +34 -12
- package/dist/platform/ops/requests.ops.js +3 -2
- package/dist/platform/ops/schedules.ops.d.ts +21 -0
- package/dist/platform/ops/schedules.ops.js +470 -0
- package/dist/platform/ops/skills.ops.js +3 -3
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +3 -0
- package/dist/runtime/context/build.js +13 -4
- package/dist/runtime/context/compaction.js +6 -3
- package/dist/runtime/context/device-paths.d.ts +43 -21
- package/dist/runtime/context/device-paths.js +104 -21
- package/dist/runtime/context/run-context.d.ts +9 -0
- package/dist/runtime/context/run-context.js +8 -0
- package/dist/runtime/context/skills.d.ts +14 -1
- package/dist/runtime/context/skills.js +36 -5
- package/dist/runtime/maintenance.d.ts +4 -2
- package/dist/runtime/maintenance.js +16 -5
- package/dist/runtime/run-finish.d.ts +6 -1
- package/dist/runtime/run-finish.js +16 -2
- package/dist/runtime/run-queue.js +21 -2
- package/dist/runtime/run-worker.js +14 -33
- package/dist/runtime/runtime.module.js +3 -0
- package/dist/runtime/subagents/definition.d.ts +32 -14
- package/dist/runtime/subagents/definition.js +106 -109
- package/dist/runtime/subagents/dispatch.js +4 -3
- package/dist/runtime/subagents/preamble.d.ts +2 -1
- package/dist/runtime/subagents/preamble.js +2 -1
- package/dist/runtime/waits.js +14 -4
- package/dist/sandbox/sandbox.service.d.ts +120 -10
- package/dist/sandbox/sandbox.service.js +335 -28
- package/dist/sandbox/tiers.d.ts +86 -0
- package/dist/sandbox/tiers.js +96 -0
- package/dist/schedules/effective-settings.d.ts +16 -0
- package/dist/schedules/effective-settings.js +17 -0
- package/dist/schedules/errors.d.ts +8 -0
- package/dist/schedules/errors.js +8 -0
- package/dist/schedules/fire.d.ts +118 -0
- package/dist/schedules/fire.js +625 -0
- package/dist/schedules/grant-rules.d.ts +94 -0
- package/dist/schedules/grant-rules.js +126 -0
- package/dist/schedules/job-ids.d.ts +32 -0
- package/dist/schedules/job-ids.js +27 -0
- package/dist/schedules/pause.d.ts +9 -0
- package/dist/schedules/pause.js +33 -0
- package/dist/schedules/run-tracker.d.ts +81 -0
- package/dist/schedules/run-tracker.js +333 -0
- package/dist/schedules/scan.job.d.ts +64 -0
- package/dist/schedules/scan.job.js +303 -0
- package/dist/schedules/schedule-math.d.ts +31 -0
- package/dist/schedules/schedule-math.js +223 -0
- package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
- package/dist/schedules/scheduled-tasks.service.js +1193 -0
- package/dist/schedules/schedules.controller.d.ts +68 -0
- package/dist/schedules/schedules.controller.js +242 -0
- package/dist/schedules/schedules.module.d.ts +16 -0
- package/dist/schedules/schedules.module.js +65 -0
- package/dist/schedules/schedules.schemas.d.ts +316 -0
- package/dist/schedules/schedules.schemas.js +186 -0
- package/dist/schedules/store.d.ts +81 -0
- package/dist/schedules/store.js +390 -0
- package/dist/schedules/store.module.d.ts +6 -0
- package/dist/schedules/store.module.js +21 -0
- package/dist/schedules/types.d.ts +108 -0
- package/dist/schedules/types.js +1 -0
- package/dist/schedules/unattended.d.ts +117 -0
- package/dist/schedules/unattended.js +228 -0
- package/dist/setup/oauth-token-probe.js +8 -4
- package/dist/skills/agent-skills.service.d.ts +4 -8
- package/dist/skills/agent-skills.service.js +18 -53
- package/dist/skills/catalog.d.ts +65 -62
- package/dist/skills/catalog.js +109 -206
- package/dist/skills/conversation-catalog.d.ts +3 -3
- package/dist/skills/conversation-catalog.js +5 -12
- package/dist/skills/invoke.d.ts +2 -2
- package/dist/skills/skills.controller.d.ts +3 -1
- package/dist/skills/skills.service.d.ts +38 -37
- package/dist/skills/skills.service.js +168 -107
- package/dist/skills/usable.d.ts +57 -8
- package/dist/skills/usable.js +172 -38
- package/dist/tools/approvals.controller.d.ts +4 -2
- package/dist/tools/approvals.controller.js +17 -2
- package/dist/tools/approvals.schemas.d.ts +1 -0
- package/dist/tools/approvals.schemas.js +3 -1
- package/dist/tools/approvals.service.d.ts +182 -7
- package/dist/tools/approvals.service.js +597 -13
- package/dist/tools/builtin-names.d.ts +25 -4
- package/dist/tools/builtin-names.js +45 -4
- package/dist/tools/builtins/builtin-tools.d.ts +8 -0
- package/dist/tools/builtins/builtin-tools.js +23 -2
- package/dist/tools/builtins/sandbox/bash.js +16 -7
- package/dist/tools/builtins/sandbox/common.d.ts +1 -1
- package/dist/tools/builtins/sandbox/common.js +33 -10
- package/dist/tools/builtins/sandbox/edit.js +7 -3
- package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
- package/dist/tools/builtins/sandbox/file-state.js +33 -3
- package/dist/tools/builtins/sandbox/write.js +8 -4
- package/dist/tools/builtins/schedules.d.ts +107 -0
- package/dist/tools/builtins/schedules.js +789 -0
- package/dist/tools/elicitation.d.ts +2 -0
- package/dist/tools/elicitation.js +1 -1
- package/dist/tools/fixed-ask.d.ts +7 -0
- package/dist/tools/fixed-ask.js +9 -0
- package/dist/tools/network-asks.d.ts +109 -0
- package/dist/tools/network-asks.js +221 -0
- package/dist/tools/pipeline.d.ts +53 -6
- package/dist/tools/pipeline.js +202 -13
- package/dist/tools/policy.d.ts +21 -8
- package/dist/tools/policy.js +63 -9
- package/dist/tools/registry.d.ts +10 -1
- package/dist/tools/tools.module.js +12 -0
- package/package.json +2 -2
- package/src/migrations/0113_scheduled_tasks.sql +108 -0
- package/src/migrations/0114_network_asks.sql +23 -0
- package/src/migrations/0115_network_requests.sql +28 -0
- package/src/migrations/0116_skills_default_on.sql +37 -0
- package/src/migrations/0117_sandbox_tiers.sql +11 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { ToolError } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { ApiError } from "../../errors.js";
|
|
14
|
+
import { KernelDb } from "../../infra/database/kernel-db.js";
|
|
15
|
+
import { TransactionHost } from "../../infra/database/transaction-host.js";
|
|
16
|
+
import { Transactional } from "../../infra/database/transactional.decorator.js";
|
|
17
|
+
import { readScheduleSettings } from "../../schedules/effective-settings.js";
|
|
18
|
+
import { nextTick } from "../../schedules/schedule-math.js";
|
|
19
|
+
import { ScheduledTaskStore } from "../../schedules/store.js";
|
|
20
|
+
import { BUILTIN } from "../builtin-names.js";
|
|
21
|
+
import { localTime } from "./current-time.js";
|
|
22
|
+
import { clip, display } from "./sandbox/common.js";
|
|
23
|
+
/**
|
|
24
|
+
* Where the schedule tools find the task service: a registration port. `SchedulesModule` imports
|
|
25
|
+
* `ToolsModule` (its grants resolve the agent's tools through `ToolRegistry`, which lists these
|
|
26
|
+
* builtins), so the service cannot be injected into the builtins; `SchedulesModule` binds it here
|
|
27
|
+
* from `onModuleInit` instead, as the domain operations register into `PlatformOperations`. Null
|
|
28
|
+
* until bound (an application without `SchedulesModule`).
|
|
29
|
+
*/
|
|
30
|
+
let ScheduleToolsPort = class ScheduleToolsPort {
|
|
31
|
+
service = null;
|
|
32
|
+
bind(service) {
|
|
33
|
+
this.service = service;
|
|
34
|
+
}
|
|
35
|
+
get() {
|
|
36
|
+
return this.service;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
ScheduleToolsPort = __decorate([
|
|
40
|
+
Injectable()
|
|
41
|
+
], ScheduleToolsPort);
|
|
42
|
+
export { ScheduleToolsPort };
|
|
43
|
+
const NAME_MAX = 200;
|
|
44
|
+
const PROMPT_MAX = 20_000;
|
|
45
|
+
const TOOLS_MAX = 50;
|
|
46
|
+
/** How much of a new prompt an update's card shows. */
|
|
47
|
+
const PROMPT_SHOWN = 300;
|
|
48
|
+
const TOOL_KEYS_SCHEMA = {
|
|
49
|
+
type: "array",
|
|
50
|
+
maxItems: TOOLS_MAX,
|
|
51
|
+
items: { type: "string", minLength: 1, maxLength: 200 },
|
|
52
|
+
description: "Tool keys the task may use without asking when it runs unattended, e.g. " +
|
|
53
|
+
'["WebSearch", "mcp__docs__search"]. Only tools this agent has now; tools that always ask a ' +
|
|
54
|
+
"person (ScheduleCreate, ScheduleUpdate, ...) cannot be listed. A run that needs a tool not listed " +
|
|
55
|
+
"fails that call. The person sees and approves this list.",
|
|
56
|
+
};
|
|
57
|
+
const CRON_SCHEMA = {
|
|
58
|
+
type: "string",
|
|
59
|
+
minLength: 1,
|
|
60
|
+
maxLength: 100,
|
|
61
|
+
description: "A repeating schedule: a 5-field cron expression (minute hour day-of-month month " +
|
|
62
|
+
'day-of-week) in the user\'s time zone, e.g. "0 9 * * 1-5" for 09:00 on weekdays or ' +
|
|
63
|
+
'"30 8 * * *" for 08:30 every day. The organization sets a minimum interval (usually one ' +
|
|
64
|
+
"hour). Pass cron or runAt, not both.",
|
|
65
|
+
};
|
|
66
|
+
const RUN_AT_SCHEMA = {
|
|
67
|
+
type: "string",
|
|
68
|
+
minLength: 1,
|
|
69
|
+
maxLength: 40,
|
|
70
|
+
description: "A single run: a local date-time YYYY-MM-DDTHH:mm in the user's time zone, without an " +
|
|
71
|
+
'offset, e.g. "2026-12-01T15:00". Call CurrentTime first to know the date and time now. ' +
|
|
72
|
+
"Pass cron or runAt, not both.",
|
|
73
|
+
};
|
|
74
|
+
const MODE_SCHEMA = {
|
|
75
|
+
type: "string",
|
|
76
|
+
enum: ["new", "here"],
|
|
77
|
+
description: '"new": every run starts a new conversation with this agent. "here": every run posts the ' +
|
|
78
|
+
"prompt into this conversation (in an IM direct chat: the chat's current conversation), " +
|
|
79
|
+
'e.g. for "check this again at 3 pm".',
|
|
80
|
+
};
|
|
81
|
+
const CREATE_SPEC = {
|
|
82
|
+
name: BUILTIN.ScheduleCreate,
|
|
83
|
+
description: "Create a scheduled task for the user: this agent runs the prompt on a schedule (cron) or " +
|
|
84
|
+
"once at a time (runAt), in the user's time zone, acting as the user. Use it when the user " +
|
|
85
|
+
'asks for something recurring or later ("every weekday at 9 send me a brief", "remind me at ' +
|
|
86
|
+
'3 pm"). Every call asks the user to approve the whole task first. Runs happen without the ' +
|
|
87
|
+
"user present: write the prompt so it stands on its own, and list in approvedTools the tools " +
|
|
88
|
+
"a run needs without asking. Do not use it for work to do now.",
|
|
89
|
+
keyArgs: ["name"],
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
name: {
|
|
94
|
+
type: "string",
|
|
95
|
+
minLength: 1,
|
|
96
|
+
maxLength: NAME_MAX,
|
|
97
|
+
description: 'A short name for the task, e.g. "Morning brief".',
|
|
98
|
+
},
|
|
99
|
+
prompt: {
|
|
100
|
+
type: "string",
|
|
101
|
+
minLength: 1,
|
|
102
|
+
maxLength: PROMPT_MAX,
|
|
103
|
+
description: "The message each run starts with, written as the user's instruction to this agent.",
|
|
104
|
+
},
|
|
105
|
+
cron: CRON_SCHEMA,
|
|
106
|
+
runAt: RUN_AT_SCHEMA,
|
|
107
|
+
mode: MODE_SCHEMA,
|
|
108
|
+
approvedTools: TOOL_KEYS_SCHEMA,
|
|
109
|
+
},
|
|
110
|
+
required: ["name", "prompt", "mode"],
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const UPDATE_SPEC = {
|
|
115
|
+
name: BUILTIN.ScheduleUpdate,
|
|
116
|
+
description: "Change one of the user's scheduled tasks: its name, prompt, schedule, where it runs or its " +
|
|
117
|
+
"approved tools. Pass the task id (from ScheduleList or ScheduleCreate) and only the fields " +
|
|
118
|
+
"to change; approvedTools replaces the whole list. Every call asks the user to approve the " +
|
|
119
|
+
"change first. To pause or stop a task, use ScheduleDelete or tell the user to pause it on " +
|
|
120
|
+
"the task page.",
|
|
121
|
+
keyArgs: ["id"],
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
id: { type: "string", minLength: 1, maxLength: 100, description: "The task id." },
|
|
126
|
+
name: { type: "string", minLength: 1, maxLength: NAME_MAX, description: "A new name." },
|
|
127
|
+
prompt: {
|
|
128
|
+
type: "string",
|
|
129
|
+
minLength: 1,
|
|
130
|
+
maxLength: PROMPT_MAX,
|
|
131
|
+
description: "A new prompt for every later run.",
|
|
132
|
+
},
|
|
133
|
+
cron: CRON_SCHEMA,
|
|
134
|
+
runAt: RUN_AT_SCHEMA,
|
|
135
|
+
mode: MODE_SCHEMA,
|
|
136
|
+
approvedTools: {
|
|
137
|
+
...TOOL_KEYS_SCHEMA,
|
|
138
|
+
description: `${TOOL_KEYS_SCHEMA.description} Replaces the task's whole list; pass [] to remove every approved tool, omit it to keep the list.`,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["id"],
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const LIST_SPEC = {
|
|
146
|
+
name: BUILTIN.ScheduleList,
|
|
147
|
+
description: "List the user's scheduled tasks in this organization: id, name, schedule with its time " +
|
|
148
|
+
"zone, status, next run and where runs go. Use it to find a task's id before ScheduleUpdate " +
|
|
149
|
+
"or ScheduleDelete, or when the user asks what is scheduled. Read-only.",
|
|
150
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
151
|
+
};
|
|
152
|
+
const DELETE_SPEC = {
|
|
153
|
+
name: BUILTIN.ScheduleDelete,
|
|
154
|
+
description: "Delete one of the user's scheduled tasks for good: it never runs again; past conversations " +
|
|
155
|
+
"stay. Use it when the user asks to stop or remove a task; find the id with ScheduleList. " +
|
|
156
|
+
"Only the user's own tasks can be deleted.",
|
|
157
|
+
keyArgs: ["id"],
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
id: { type: "string", minLength: 1, maxLength: 100, description: "The task id." },
|
|
162
|
+
},
|
|
163
|
+
required: ["id"],
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const ToolKeys = z.array(z.string().min(1).max(200)).max(TOOLS_MAX);
|
|
168
|
+
const Mode = z.enum(["new", "here"]);
|
|
169
|
+
const CreateInput = z
|
|
170
|
+
.object({
|
|
171
|
+
name: z.string().min(1).max(NAME_MAX),
|
|
172
|
+
prompt: z.string().min(1).max(PROMPT_MAX),
|
|
173
|
+
cron: z.string().min(1).max(100).optional(),
|
|
174
|
+
runAt: z.string().min(1).max(40).optional(),
|
|
175
|
+
mode: Mode,
|
|
176
|
+
approvedTools: ToolKeys.optional(),
|
|
177
|
+
})
|
|
178
|
+
.strict();
|
|
179
|
+
const UpdateInput = z
|
|
180
|
+
.object({
|
|
181
|
+
id: z.string().min(1).max(100),
|
|
182
|
+
name: z.string().min(1).max(NAME_MAX).optional(),
|
|
183
|
+
prompt: z.string().min(1).max(PROMPT_MAX).optional(),
|
|
184
|
+
cron: z.string().min(1).max(100).optional(),
|
|
185
|
+
runAt: z.string().min(1).max(40).optional(),
|
|
186
|
+
mode: Mode.optional(),
|
|
187
|
+
approvedTools: ToolKeys.optional(),
|
|
188
|
+
})
|
|
189
|
+
.strict();
|
|
190
|
+
const IdInput = z.object({ id: z.string().min(1).max(100) }).strict();
|
|
191
|
+
// ---- model-facing texts (English, actionable) ----
|
|
192
|
+
const INVALID_CREATE_TEXT = "Invalid input; pass name, prompt, mode (new or here), exactly one of cron or runAt, and " +
|
|
193
|
+
"optionally approvedTools as an array of tool keys.";
|
|
194
|
+
const INVALID_UPDATE_TEXT = "Invalid input; pass the task id and the fields to change (name, prompt, cron or runAt, mode, " +
|
|
195
|
+
"approvedTools).";
|
|
196
|
+
const INVALID_ID_TEXT = "Invalid input; pass the task id from ScheduleList.";
|
|
197
|
+
const ONE_TRIGGER_TEXT = "Pass exactly one of cron (a repeating schedule) or runAt (a single run), not both and not neither.";
|
|
198
|
+
const BOTH_TRIGGERS_TEXT = "Pass at most one of cron or runAt: cron makes the task repeat, runAt makes it run once.";
|
|
199
|
+
const NOTHING_TO_UPDATE_TEXT = "Nothing to change; pass at least one of name, prompt, cron, runAt, mode or approvedTools with the id.";
|
|
200
|
+
export const SCHEDULES_UNAVAILABLE_TEXT = "Scheduled tasks are not available on this server; ask the user to manage the task on the scheduled tasks page instead.";
|
|
201
|
+
export const SCHEDULES_OFF_TEXT = "Scheduled tasks are turned off in this organization; ask an org admin to turn them on.";
|
|
202
|
+
/** The card recomputed at run time differs from the one the person approved. */
|
|
203
|
+
export const cardChangedText = (tool) => `The task's time zone, target, tools or current settings changed after the user approved this ${tool} call, so nothing was changed; call ${tool} again so the user can approve the current version.`;
|
|
204
|
+
export const NOT_A_MEMBER_TEXT = "The user is no longer a member of this organization, so their scheduled tasks cannot be managed here; ask them to check their membership.";
|
|
205
|
+
/** No task with that id is the person's own (missing, deleted, or another member's). */
|
|
206
|
+
export const notYoursText = (id) => `No scheduled task with id ${clip(id, 100)} is the user's own; call ScheduleList to see the ids of the user's tasks.`;
|
|
207
|
+
/** The service's refusal as an actionable English tool error. */
|
|
208
|
+
export function scheduleErrorText(e, ctx) {
|
|
209
|
+
const m = e.message.replace(/[.\s]+$/, "");
|
|
210
|
+
switch (e.code) {
|
|
211
|
+
case "schedule.not_found":
|
|
212
|
+
case "schedule.forbidden":
|
|
213
|
+
return notYoursText(ctx.id ?? "");
|
|
214
|
+
case "schedule.disabled":
|
|
215
|
+
return SCHEDULES_OFF_TEXT;
|
|
216
|
+
case "schedule.limit_reached":
|
|
217
|
+
return `${m}. Call ScheduleList and ask the user which task to delete with ScheduleDelete.`;
|
|
218
|
+
case "schedule.interval_too_short":
|
|
219
|
+
return `${m}. Use a less frequent cron expression, or ask an org admin to lower the minimum interval.`;
|
|
220
|
+
case "schedule.invalid_cron":
|
|
221
|
+
return `${m}. Pass 5 fields (minute hour day-of-month month day-of-week), e.g. "0 9 * * 1-5" for 09:00 on weekdays.`;
|
|
222
|
+
case "schedule.invalid_once_time":
|
|
223
|
+
return "runAt is not a local date-time; pass YYYY-MM-DDTHH:mm in the user's time zone without an offset, e.g. 2026-12-01T09:00.";
|
|
224
|
+
case "schedule.once_in_past":
|
|
225
|
+
return `runAt is not in the future in the user's time zone (${ctx.zone ?? "UTC"}); call CurrentTime and pass a later time.`;
|
|
226
|
+
case "schedule.invalid_time_zone":
|
|
227
|
+
return `${m}. Ask the user to set their time zone in their profile, then retry.`;
|
|
228
|
+
case "schedule.target_invalid":
|
|
229
|
+
return `${m}. Use mode "new" to start a new conversation for every run instead.`;
|
|
230
|
+
case "schedule.grant_not_allowed":
|
|
231
|
+
return `${m}. Remove it from approvedTools and call again.`;
|
|
232
|
+
case "forbidden":
|
|
233
|
+
return "The user's role in this organization cannot manage scheduled tasks (guests cannot); ask an org admin.";
|
|
234
|
+
case "conflict":
|
|
235
|
+
return `${m}. Check the task with ScheduleList before trying again.`;
|
|
236
|
+
default:
|
|
237
|
+
return `${m}. Check the arguments and retry.`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
241
|
+
const DAYS_EN = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
242
|
+
const DAYS_ZH = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
243
|
+
const INT = /^\d{1,2}$/;
|
|
244
|
+
/** A trigger in words, its zone included; an unusual cron reads as the expression itself. */
|
|
245
|
+
export function scheduleWords(t) {
|
|
246
|
+
const zone = t.timeZone;
|
|
247
|
+
if (t.kind === "once") {
|
|
248
|
+
const at = t.at.replace("T", " ").slice(0, 16);
|
|
249
|
+
return { zh: `仅一次,${at}(${zone})`, en: `once at ${at} (${zone})` };
|
|
250
|
+
}
|
|
251
|
+
const f = t.expr.trim().split(/\s+/);
|
|
252
|
+
const [mi = "", h = "", dom = "", mon = "", dow = ""] = f;
|
|
253
|
+
const plain = f.length === 5 && INT.test(mi) && Number(mi) < 60;
|
|
254
|
+
const atTime = INT.test(h) && Number(h) < 24 ? `${pad2(Number(h))}:${pad2(Number(mi))}` : null;
|
|
255
|
+
let w = null;
|
|
256
|
+
if (plain && h === "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
257
|
+
w = { zh: `每小时第 ${Number(mi)} 分`, en: `every hour at minute ${Number(mi)}` };
|
|
258
|
+
}
|
|
259
|
+
else if (plain && atTime !== null && mon === "*") {
|
|
260
|
+
if (dom === "*" && dow === "*")
|
|
261
|
+
w = { zh: `每天 ${atTime}`, en: `every day at ${atTime}` };
|
|
262
|
+
else if (dom === "*" && dow === "1-5") {
|
|
263
|
+
w = { zh: `每个工作日 ${atTime}`, en: `every weekday at ${atTime}` };
|
|
264
|
+
}
|
|
265
|
+
else if (dom === "*" && /^[0-7]$/.test(dow)) {
|
|
266
|
+
const d = Number(dow) % 7;
|
|
267
|
+
w = { zh: `每${DAYS_ZH[d]} ${atTime}`, en: `every ${DAYS_EN[d]} at ${atTime}` };
|
|
268
|
+
}
|
|
269
|
+
else if (dow === "*" && INT.test(dom) && Number(dom) >= 1 && Number(dom) <= 31) {
|
|
270
|
+
w = {
|
|
271
|
+
zh: `每月 ${Number(dom)} 日 ${atTime}`,
|
|
272
|
+
en: `on day ${Number(dom)} of every month at ${atTime}`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const expr = f.join(" ");
|
|
277
|
+
const out = w ?? { zh: `按 cron「${expr}」`, en: `on the cron schedule "${expr}"` };
|
|
278
|
+
return { zh: `${out.zh}(${zone})`, en: `${out.en} (${zone})` };
|
|
279
|
+
}
|
|
280
|
+
/** The next fire time in the trigger's zone, `YYYY-MM-DD HH:mm`, or null when there is none. */
|
|
281
|
+
function nextRunWords(t, now) {
|
|
282
|
+
const next = nextTick(t, new Date(now));
|
|
283
|
+
return next === null ? null : localTime(next, t.timeZone).local.slice(0, 16).replace("T", " ");
|
|
284
|
+
}
|
|
285
|
+
function targetWords(t, currentConversationId) {
|
|
286
|
+
if (t.mode === "new")
|
|
287
|
+
return { zh: "每次运行新建对话", en: "a new conversation for every run" };
|
|
288
|
+
if (t.mode === "chat") {
|
|
289
|
+
const n = clip(t.connectionName, 60);
|
|
290
|
+
return { zh: `${n} 的私聊`, en: `the direct chat on ${n}` };
|
|
291
|
+
}
|
|
292
|
+
return t.conversationId === currentConversationId
|
|
293
|
+
? { zh: "当前对话", en: "this conversation" }
|
|
294
|
+
: { zh: `对话 ${t.conversationId}`, en: `conversation ${t.conversationId}` };
|
|
295
|
+
}
|
|
296
|
+
function toolsWords(keys) {
|
|
297
|
+
return keys.length === 0
|
|
298
|
+
? { zh: "无", en: "none" }
|
|
299
|
+
: { zh: keys.join("、"), en: keys.join(", ") };
|
|
300
|
+
}
|
|
301
|
+
/** A stored task's target; a chat's connection name is read for the words. */
|
|
302
|
+
async function storedTarget(db, task) {
|
|
303
|
+
if (task.mode === "chat" && task.chat !== null) {
|
|
304
|
+
const c = await db
|
|
305
|
+
.selectFrom("channel_connections")
|
|
306
|
+
.select("name")
|
|
307
|
+
.where("id", "=", task.chat.connectionId)
|
|
308
|
+
.executeTakeFirst();
|
|
309
|
+
return { mode: "chat", chat: task.chat, connectionName: c?.name ?? task.chat.connectionId };
|
|
310
|
+
}
|
|
311
|
+
if (task.mode === "conversation" && task.conversationId !== null) {
|
|
312
|
+
return { mode: "conversation", conversationId: task.conversationId };
|
|
313
|
+
}
|
|
314
|
+
return { mode: "new" };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* `mode: "here"` (spec §8): the conversation's IM direct chat when the conversation is delivered
|
|
318
|
+
* to a chat and is the person's live DM thread there, else this conversation.
|
|
319
|
+
*/
|
|
320
|
+
export async function hereTarget(db, orgId, accountId, conversationId) {
|
|
321
|
+
const dm = await db
|
|
322
|
+
.selectFrom("channel_threads as t")
|
|
323
|
+
.innerJoin("channel_deliveries as d", (j) => j
|
|
324
|
+
.onRef("d.conversation_id", "=", "t.conversation_id")
|
|
325
|
+
.onRef("d.connection_id", "=", "t.connection_id"))
|
|
326
|
+
.innerJoin("channel_connections as c", "c.id", "t.connection_id")
|
|
327
|
+
.select(["t.connection_id", "t.chat_id", "c.name"])
|
|
328
|
+
.where("t.conversation_id", "=", conversationId)
|
|
329
|
+
.where("t.account_id", "=", accountId)
|
|
330
|
+
.where("t.archived_at", "is", null)
|
|
331
|
+
.where("t.thread_key", "like", "dm:%")
|
|
332
|
+
.where("c.org_id", "=", orgId)
|
|
333
|
+
.where("c.deleted_at", "is", null)
|
|
334
|
+
.executeTakeFirst();
|
|
335
|
+
if (dm) {
|
|
336
|
+
return {
|
|
337
|
+
mode: "chat",
|
|
338
|
+
chat: { connectionId: dm.connection_id, chatId: dm.chat_id, chatType: "dm" },
|
|
339
|
+
connectionName: dm.name,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return { mode: "conversation", conversationId };
|
|
343
|
+
}
|
|
344
|
+
const targetFields = (t) => t.mode === "chat"
|
|
345
|
+
? { mode: "chat", chat: t.chat }
|
|
346
|
+
: t.mode === "conversation"
|
|
347
|
+
? { mode: "conversation", conversationId: t.conversationId }
|
|
348
|
+
: { mode: "new" };
|
|
349
|
+
const sameTarget = (a, b) => a.mode === b.mode &&
|
|
350
|
+
(a.mode !== "conversation" ||
|
|
351
|
+
a.conversationId === b.conversationId) &&
|
|
352
|
+
(a.mode !== "chat" ||
|
|
353
|
+
(a.chat.connectionId === b.chat.connectionId &&
|
|
354
|
+
a.chat.chatId === b.chat.chatId));
|
|
355
|
+
const uniqueSorted = (keys) => [...new Set(keys)].sort();
|
|
356
|
+
/** cron / runAt as the service's trigger; undefined when neither is given. */
|
|
357
|
+
function triggerOf(i) {
|
|
358
|
+
if (i.cron !== undefined)
|
|
359
|
+
return { kind: "cron", expr: i.cron.trim() };
|
|
360
|
+
if (i.runAt !== undefined)
|
|
361
|
+
return { kind: "once", at: i.runAt.trim() };
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const withZone = (t, timeZone) => t.kind === "cron"
|
|
365
|
+
? { kind: "cron", expr: t.expr, timeZone }
|
|
366
|
+
: { kind: "once", at: t.at, timeZone };
|
|
367
|
+
/** A card as stored on the approval (the same clipping at ask time and at run time). */
|
|
368
|
+
const cardOf = (w) => ({ zh: clip(w.zh, 1000), en: clip(w.en, 1000) });
|
|
369
|
+
/** The fixed ask of a create or update: every call asks, only "once", with the kernel's card. */
|
|
370
|
+
const ask = (key, summary) => ({
|
|
371
|
+
key,
|
|
372
|
+
policy: "ask",
|
|
373
|
+
remember: false,
|
|
374
|
+
summary: cardOf(summary),
|
|
375
|
+
});
|
|
376
|
+
const deny = (key, denyReason) => ({
|
|
377
|
+
key,
|
|
378
|
+
policy: "deny",
|
|
379
|
+
remember: false,
|
|
380
|
+
denyReason,
|
|
381
|
+
});
|
|
382
|
+
/** The live role of the person in the org, or null when they are not a member. */
|
|
383
|
+
async function roleOf(db, orgId, accountId) {
|
|
384
|
+
const m = await db
|
|
385
|
+
.selectFrom("memberships")
|
|
386
|
+
.select("role")
|
|
387
|
+
.where("org_id", "=", orgId)
|
|
388
|
+
.where("account_id", "=", accountId)
|
|
389
|
+
.executeTakeFirst();
|
|
390
|
+
return (m?.role ?? null);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* `ScheduleUpdate`'s write in one transaction: the field change, then (with `approvedTools`) the
|
|
394
|
+
* task's grants replaced by exactly that list. Each service call is `@Transactional` and joins
|
|
395
|
+
* this one, so a refused grant rolls the field change back too.
|
|
396
|
+
*/
|
|
397
|
+
let ScheduleUpdates = class ScheduleUpdates {
|
|
398
|
+
txHost;
|
|
399
|
+
db;
|
|
400
|
+
store;
|
|
401
|
+
constructor(txHost, db, store) {
|
|
402
|
+
this.txHost = txHost;
|
|
403
|
+
this.db = db;
|
|
404
|
+
this.store = store;
|
|
405
|
+
}
|
|
406
|
+
async apply(svc, orgId, accountId, task, patch, approvedTools) {
|
|
407
|
+
const updated = Object.keys(patch).length > 0 ? await svc.update(orgId, accountId, task.id, patch) : task;
|
|
408
|
+
if (approvedTools !== undefined) {
|
|
409
|
+
const next = uniqueSorted(approvedTools);
|
|
410
|
+
const held = (await this.store.listGrants(this.db.db, task.id)).map((g) => g.toolKey);
|
|
411
|
+
for (const k of held.filter((k) => !next.includes(k))) {
|
|
412
|
+
await svc.removeGrant(orgId, accountId, task.id, k);
|
|
413
|
+
}
|
|
414
|
+
const added = next.filter((k) => !held.includes(k));
|
|
415
|
+
if (added.length > 0) {
|
|
416
|
+
await svc.addGrants(orgId, accountId, task.id, added.map((toolKey) => ({ toolKey })));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return updated;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
__decorate([
|
|
423
|
+
Transactional()
|
|
424
|
+
], ScheduleUpdates.prototype, "apply", null);
|
|
425
|
+
ScheduleUpdates = __decorate([
|
|
426
|
+
Injectable(),
|
|
427
|
+
__param(0, Inject(TransactionHost)),
|
|
428
|
+
__param(1, Inject(KernelDb)),
|
|
429
|
+
__param(2, Inject(ScheduledTaskStore))
|
|
430
|
+
], ScheduleUpdates);
|
|
431
|
+
export { ScheduleUpdates };
|
|
432
|
+
/** The four schedule builtins. */
|
|
433
|
+
export function scheduleTools(deps) {
|
|
434
|
+
/**
|
|
435
|
+
* The service, or a refusal (the tools are hidden then too: {@link unavailable}). Defensive:
|
|
436
|
+
* `SchedulesModule` is reachable on every role (through ChannelsModule / KernelModule), so the
|
|
437
|
+
* port is expected to be bound; the path stays for an application built without it.
|
|
438
|
+
*/
|
|
439
|
+
const service = async () => {
|
|
440
|
+
const s = deps.service();
|
|
441
|
+
if (s === null)
|
|
442
|
+
throw new ToolError(SCHEDULES_UNAVAILABLE_TEXT);
|
|
443
|
+
return s;
|
|
444
|
+
};
|
|
445
|
+
/** Not loaded on this replica, or turned off in the org: one read per listing. */
|
|
446
|
+
const unavailable = async (scope) => {
|
|
447
|
+
if (deps.service() === null)
|
|
448
|
+
return SCHEDULES_UNAVAILABLE_TEXT;
|
|
449
|
+
const memoKey = "schedules:enabled";
|
|
450
|
+
let enabled = scope.memo?.get(memoKey);
|
|
451
|
+
if (enabled === undefined) {
|
|
452
|
+
enabled = readScheduleSettings(deps.db(), deps.config, scope.orgId).then((s) => s.enabled);
|
|
453
|
+
scope.memo?.set(memoKey, enabled);
|
|
454
|
+
}
|
|
455
|
+
return (await enabled) ? null : SCHEDULES_OFF_TEXT;
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* The card the person approved for this call (`approvals.summary` of the call's latest
|
|
459
|
+
* approved attempt); undefined when the call has no approval (a direct call outside the
|
|
460
|
+
* pipeline: the pipeline never runs a fixed ask without one).
|
|
461
|
+
*/
|
|
462
|
+
async function approvedCard(ctx) {
|
|
463
|
+
if (ctx.runId === undefined)
|
|
464
|
+
return undefined;
|
|
465
|
+
const row = await deps
|
|
466
|
+
.db()
|
|
467
|
+
.selectFrom("approvals")
|
|
468
|
+
.select("summary")
|
|
469
|
+
.where("run_id", "=", ctx.runId)
|
|
470
|
+
.where("call_id", "=", ctx.callId)
|
|
471
|
+
.where("status", "=", "approved")
|
|
472
|
+
.orderBy("attempt", "desc")
|
|
473
|
+
.limit(1)
|
|
474
|
+
.executeTakeFirst();
|
|
475
|
+
return row === undefined ? undefined : row.summary;
|
|
476
|
+
}
|
|
477
|
+
/** Refuses the run when the card it would show now differs from the approved one. */
|
|
478
|
+
async function sameAsApproved(ctx, tool, now) {
|
|
479
|
+
const shown = await approvedCard(ctx);
|
|
480
|
+
if (shown === undefined)
|
|
481
|
+
return;
|
|
482
|
+
const c = cardOf(now);
|
|
483
|
+
if (shown === null || shown.zh !== c.zh || shown.en !== c.en) {
|
|
484
|
+
throw new ToolError(cardChangedText(tool));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/** The person's live role, refused when they are no longer a member. */
|
|
488
|
+
async function liveRole(ctx) {
|
|
489
|
+
const role = await roleOf(deps.db(), ctx.orgId, ctx.accountId);
|
|
490
|
+
if (role === null)
|
|
491
|
+
throw new ToolError(NOT_A_MEMBER_TEXT);
|
|
492
|
+
return role;
|
|
493
|
+
}
|
|
494
|
+
/** Runs a service call, turning its refusals into actionable tool errors. */
|
|
495
|
+
async function call(fn, ctx) {
|
|
496
|
+
try {
|
|
497
|
+
return await fn();
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
if (e instanceof ApiError)
|
|
501
|
+
throw new ToolError(scheduleErrorText(e, ctx));
|
|
502
|
+
throw e;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function createPlan(input, ctx) {
|
|
506
|
+
const trigger = triggerOf(input);
|
|
507
|
+
const zone = await deps.timeZoneOf(ctx.orgId, ctx.conversationId);
|
|
508
|
+
const target = input.mode === "here"
|
|
509
|
+
? await hereTarget(deps.db(), ctx.orgId, ctx.accountId, ctx.conversationId)
|
|
510
|
+
: { mode: "new" };
|
|
511
|
+
return { trigger, zone, target, tools: uniqueSorted(input.approvedTools ?? []) };
|
|
512
|
+
}
|
|
513
|
+
function createCard(input, p, conversationId) {
|
|
514
|
+
const when = scheduleWords(withZone(p.trigger, p.zone));
|
|
515
|
+
const where = targetWords(p.target, conversationId);
|
|
516
|
+
const tools = toolsWords(p.tools);
|
|
517
|
+
const name = clip(input.name, NAME_MAX);
|
|
518
|
+
return {
|
|
519
|
+
zh: `创建定时任务「${name}」:${when.zh};运行位置:${where.zh};预先批准的工具:${tools.zh}`,
|
|
520
|
+
en: `Create the scheduled task "${name}": ${when.en}; runs in ${where.en}; approved tools: ${tools.en}`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const create = {
|
|
524
|
+
spec: CREATE_SPEC,
|
|
525
|
+
defaultPolicy: "ask",
|
|
526
|
+
unavailable,
|
|
527
|
+
async callPolicy(raw, ctx) {
|
|
528
|
+
const key = BUILTIN.ScheduleCreate;
|
|
529
|
+
const parsed = CreateInput.safeParse(raw);
|
|
530
|
+
if (!parsed.success)
|
|
531
|
+
return deny(key, INVALID_CREATE_TEXT);
|
|
532
|
+
if ((parsed.data.cron === undefined) === (parsed.data.runAt === undefined)) {
|
|
533
|
+
return deny(key, ONE_TRIGGER_TEXT);
|
|
534
|
+
}
|
|
535
|
+
const p = await createPlan(parsed.data, ctx);
|
|
536
|
+
return ask(key, createCard(parsed.data, p, ctx.conversationId));
|
|
537
|
+
},
|
|
538
|
+
async run(raw, ctx) {
|
|
539
|
+
const parsed = CreateInput.safeParse(raw);
|
|
540
|
+
if (!parsed.success)
|
|
541
|
+
throw new ToolError(INVALID_CREATE_TEXT);
|
|
542
|
+
const input = parsed.data;
|
|
543
|
+
const svc = await service();
|
|
544
|
+
const p = await createPlan(input, ctx);
|
|
545
|
+
if (p.trigger === undefined || (input.cron !== undefined && input.runAt !== undefined)) {
|
|
546
|
+
throw new ToolError(ONE_TRIGGER_TEXT);
|
|
547
|
+
}
|
|
548
|
+
// the zone and target are recomputed now: they must be the ones the person approved
|
|
549
|
+
await sameAsApproved(ctx, BUILTIN.ScheduleCreate, createCard(input, p, ctx.conversationId));
|
|
550
|
+
const trigger = p.trigger;
|
|
551
|
+
const task = await call(() => svc.create(ctx.orgId, ctx.accountId, {
|
|
552
|
+
agentId: ctx.agentId,
|
|
553
|
+
name: input.name,
|
|
554
|
+
prompt: input.prompt,
|
|
555
|
+
trigger,
|
|
556
|
+
timeZone: p.zone,
|
|
557
|
+
...targetFields(p.target),
|
|
558
|
+
grants: p.tools.map((toolKey) => ({ toolKey })),
|
|
559
|
+
}), { zone: p.zone });
|
|
560
|
+
const when = scheduleWords(task.trigger);
|
|
561
|
+
const where = targetWords(p.target, ctx.conversationId);
|
|
562
|
+
const next = nextRunWords(task.trigger, deps.now());
|
|
563
|
+
return {
|
|
564
|
+
content: [
|
|
565
|
+
{
|
|
566
|
+
type: "text",
|
|
567
|
+
text: `Created scheduled task "${task.name}" (id ${task.id}): runs ${when.en} in ` +
|
|
568
|
+
`${where.en}${next === null ? "" : `; next run ${next}`}. Approved tools: ` +
|
|
569
|
+
`${toolsWords(p.tools).en}.`,
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
display: display(`已创建定时任务「${task.name}」 · ${when.zh}`, `Created scheduled task "${task.name}" · ${when.en}`),
|
|
573
|
+
};
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
// ---- ScheduleUpdate ----
|
|
577
|
+
/** The task's approved tool keys, sorted. */
|
|
578
|
+
const grantKeys = async (taskId) => uniqueSorted((await deps.store.listGrants(deps.db(), taskId)).map((g) => g.toolKey));
|
|
579
|
+
/** The person's own live task, or null. */
|
|
580
|
+
async function ownTask(orgId, accountId, id) {
|
|
581
|
+
const task = await deps.store.getTask(deps.db(), orgId, id);
|
|
582
|
+
return task !== null && task.accountId === accountId ? task : null;
|
|
583
|
+
}
|
|
584
|
+
async function updateCard(input, task, grants, ctx) {
|
|
585
|
+
const zh = [];
|
|
586
|
+
const en = [];
|
|
587
|
+
if (input.name !== undefined && input.name.trim() !== task.name) {
|
|
588
|
+
zh.push(`名称「${clip(task.name, 80)}」→「${clip(input.name, 80)}」`);
|
|
589
|
+
en.push(`name "${clip(task.name, 80)}" → "${clip(input.name, 80)}"`);
|
|
590
|
+
}
|
|
591
|
+
if (input.prompt !== undefined && input.prompt.trim() !== task.prompt) {
|
|
592
|
+
const shown = clip(input.prompt, PROMPT_SHOWN);
|
|
593
|
+
zh.push(`新提示词「${shown}」`);
|
|
594
|
+
en.push(`new prompt "${shown}"`);
|
|
595
|
+
}
|
|
596
|
+
const trigger = triggerOf(input);
|
|
597
|
+
if (trigger !== undefined) {
|
|
598
|
+
const before = scheduleWords(task.trigger);
|
|
599
|
+
const after = scheduleWords(withZone(trigger, task.trigger.timeZone));
|
|
600
|
+
zh.push(`时间 ${before.zh} → ${after.zh}`);
|
|
601
|
+
en.push(`schedule ${before.en} → ${after.en}`);
|
|
602
|
+
}
|
|
603
|
+
if (input.mode !== undefined) {
|
|
604
|
+
const db = deps.db();
|
|
605
|
+
const before = await storedTarget(db, task);
|
|
606
|
+
const after = input.mode === "here"
|
|
607
|
+
? await hereTarget(db, ctx.orgId, ctx.accountId, ctx.conversationId)
|
|
608
|
+
: { mode: "new" };
|
|
609
|
+
if (!sameTarget(before, after)) {
|
|
610
|
+
const b = targetWords(before, ctx.conversationId);
|
|
611
|
+
const a = targetWords(after, ctx.conversationId);
|
|
612
|
+
zh.push(`运行位置 ${b.zh} → ${a.zh}`);
|
|
613
|
+
en.push(`runs in ${b.en} → ${a.en}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (input.approvedTools !== undefined) {
|
|
617
|
+
const next = uniqueSorted(input.approvedTools);
|
|
618
|
+
const added = next.filter((k) => !grants.includes(k));
|
|
619
|
+
const removed = grants.filter((k) => !next.includes(k));
|
|
620
|
+
const all = toolsWords(next);
|
|
621
|
+
const delta = [
|
|
622
|
+
...(added.length > 0
|
|
623
|
+
? [{ zh: `新增 ${added.join("、")}`, en: `adds ${added.join(", ")}` }]
|
|
624
|
+
: []),
|
|
625
|
+
...(removed.length > 0
|
|
626
|
+
? [{ zh: `移除 ${removed.join("、")}`, en: `removes ${removed.join(", ")}` }]
|
|
627
|
+
: []),
|
|
628
|
+
];
|
|
629
|
+
const dZh = delta.length > 0 ? `(${delta.map((d) => d.zh).join(";")})` : "(不变)";
|
|
630
|
+
const dEn = delta.length > 0 ? ` (${delta.map((d) => d.en).join("; ")})` : " (unchanged)";
|
|
631
|
+
zh.push(`预先批准的工具:${all.zh}${dZh}`);
|
|
632
|
+
en.push(`approved tools: ${all.en}${dEn}`);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// the tools the task keeps are part of what the person approves
|
|
636
|
+
const kept = toolsWords(grants);
|
|
637
|
+
zh.push(`预先批准的工具:${kept.zh}(不变)`);
|
|
638
|
+
en.push(`approved tools: ${kept.en} (unchanged)`);
|
|
639
|
+
}
|
|
640
|
+
const name = clip(task.name, 80);
|
|
641
|
+
const none = { zh: "没有改动", en: "no changes" };
|
|
642
|
+
return {
|
|
643
|
+
zh: `修改定时任务「${name}」:${zh.length > 0 ? zh.join(";") : none.zh}`,
|
|
644
|
+
en: `Change the scheduled task "${name}": ${en.length > 0 ? en.join("; ") : none.en}`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const nothingToUpdate = (i) => i.name === undefined &&
|
|
648
|
+
i.prompt === undefined &&
|
|
649
|
+
i.cron === undefined &&
|
|
650
|
+
i.runAt === undefined &&
|
|
651
|
+
i.mode === undefined &&
|
|
652
|
+
i.approvedTools === undefined;
|
|
653
|
+
const update = {
|
|
654
|
+
spec: UPDATE_SPEC,
|
|
655
|
+
defaultPolicy: "ask",
|
|
656
|
+
unavailable,
|
|
657
|
+
async callPolicy(raw, ctx) {
|
|
658
|
+
const key = BUILTIN.ScheduleUpdate;
|
|
659
|
+
const parsed = UpdateInput.safeParse(raw);
|
|
660
|
+
if (!parsed.success)
|
|
661
|
+
return deny(key, INVALID_UPDATE_TEXT);
|
|
662
|
+
const input = parsed.data;
|
|
663
|
+
if (input.cron !== undefined && input.runAt !== undefined) {
|
|
664
|
+
return deny(key, BOTH_TRIGGERS_TEXT);
|
|
665
|
+
}
|
|
666
|
+
if (nothingToUpdate(input))
|
|
667
|
+
return deny(key, NOTHING_TO_UPDATE_TEXT);
|
|
668
|
+
const task = await ownTask(ctx.orgId, ctx.accountId, input.id);
|
|
669
|
+
if (task === null)
|
|
670
|
+
return deny(key, notYoursText(input.id));
|
|
671
|
+
const grants = await grantKeys(task.id);
|
|
672
|
+
return ask(key, await updateCard(input, task, grants, ctx));
|
|
673
|
+
},
|
|
674
|
+
async run(raw, ctx) {
|
|
675
|
+
const parsed = UpdateInput.safeParse(raw);
|
|
676
|
+
if (!parsed.success)
|
|
677
|
+
throw new ToolError(INVALID_UPDATE_TEXT);
|
|
678
|
+
const input = parsed.data;
|
|
679
|
+
if (input.cron !== undefined && input.runAt !== undefined) {
|
|
680
|
+
throw new ToolError(BOTH_TRIGGERS_TEXT);
|
|
681
|
+
}
|
|
682
|
+
if (nothingToUpdate(input))
|
|
683
|
+
throw new ToolError(NOTHING_TO_UPDATE_TEXT);
|
|
684
|
+
const svc = await service();
|
|
685
|
+
const task = await ownTask(ctx.orgId, ctx.accountId, input.id);
|
|
686
|
+
if (task === null)
|
|
687
|
+
throw new ToolError(notYoursText(input.id));
|
|
688
|
+
await sameAsApproved(ctx, BUILTIN.ScheduleUpdate, await updateCard(input, task, await grantKeys(task.id), ctx));
|
|
689
|
+
const patch = {};
|
|
690
|
+
if (input.name !== undefined)
|
|
691
|
+
patch.name = input.name;
|
|
692
|
+
if (input.prompt !== undefined)
|
|
693
|
+
patch.prompt = input.prompt;
|
|
694
|
+
const trigger = triggerOf(input);
|
|
695
|
+
if (trigger !== undefined)
|
|
696
|
+
patch.trigger = trigger;
|
|
697
|
+
if (input.mode !== undefined) {
|
|
698
|
+
const target = input.mode === "here"
|
|
699
|
+
? await hereTarget(deps.db(), ctx.orgId, ctx.accountId, ctx.conversationId)
|
|
700
|
+
: { mode: "new" };
|
|
701
|
+
const f = targetFields(target);
|
|
702
|
+
patch.mode = f.mode;
|
|
703
|
+
patch.conversationId = f.conversationId ?? null;
|
|
704
|
+
patch.chat = f.chat ?? null;
|
|
705
|
+
}
|
|
706
|
+
const zone = task.trigger.timeZone;
|
|
707
|
+
const updated = await call(() => deps.updates.apply(svc, ctx.orgId, ctx.accountId, task, patch, input.approvedTools), { id: input.id, zone });
|
|
708
|
+
const grants = await grantKeys(updated.id);
|
|
709
|
+
const when = scheduleWords(updated.trigger);
|
|
710
|
+
const where = targetWords(await storedTarget(deps.db(), updated), ctx.conversationId);
|
|
711
|
+
const next = updated.status === "active" ? nextRunWords(updated.trigger, deps.now()) : null;
|
|
712
|
+
return {
|
|
713
|
+
content: [
|
|
714
|
+
{
|
|
715
|
+
type: "text",
|
|
716
|
+
text: `Updated scheduled task "${updated.name}" (id ${updated.id}): runs ${when.en} in ` +
|
|
717
|
+
`${where.en}; status ${updated.status}${next === null ? "" : `; next run ${next}`}. ` +
|
|
718
|
+
`Approved tools: ${toolsWords(grants).en}.`,
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
display: display(`已修改定时任务「${updated.name}」`, `Updated scheduled task "${updated.name}"`),
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
// ---- ScheduleList ----
|
|
726
|
+
const list = {
|
|
727
|
+
spec: LIST_SPEC,
|
|
728
|
+
defaultPolicy: "allow",
|
|
729
|
+
unavailable,
|
|
730
|
+
async run(_raw, ctx) {
|
|
731
|
+
const svc = await service();
|
|
732
|
+
await liveRole(ctx);
|
|
733
|
+
const tasks = await call(() => svc.list(ctx.orgId, { accountId: ctx.accountId }), {});
|
|
734
|
+
if (tasks.length === 0) {
|
|
735
|
+
return {
|
|
736
|
+
content: [
|
|
737
|
+
{ type: "text", text: "The user has no scheduled tasks in this organization." },
|
|
738
|
+
],
|
|
739
|
+
display: display("没有定时任务", "No scheduled tasks"),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
const db = deps.db();
|
|
743
|
+
const lines = [];
|
|
744
|
+
for (const t of tasks) {
|
|
745
|
+
const where = targetWords(await storedTarget(db, t), ctx.conversationId).en;
|
|
746
|
+
const status = t.status === "paused" && t.pauseReason !== null ? `paused (${t.pauseReason})` : t.status;
|
|
747
|
+
const next = t.nextRunAt === null
|
|
748
|
+
? "none"
|
|
749
|
+
: localTime(t.nextRunAt, t.trigger.timeZone).local.slice(0, 16).replace("T", " ");
|
|
750
|
+
lines.push(`- ${t.id}: "${t.name}", ${scheduleWords(t.trigger).en}, ${status}, next run ${next}, ` +
|
|
751
|
+
`runs in ${where}`);
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
content: [
|
|
755
|
+
{
|
|
756
|
+
type: "text",
|
|
757
|
+
text: `The user's scheduled tasks (${tasks.length}):\n${lines.join("\n")}`,
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
display: display(`列出定时任务(${tasks.length} 个)`, `Listed ${tasks.length} scheduled tasks`),
|
|
761
|
+
};
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
// ---- ScheduleDelete ----
|
|
765
|
+
const remove = {
|
|
766
|
+
spec: DELETE_SPEC,
|
|
767
|
+
// deleting one's own task only narrows what runs (spec §11)
|
|
768
|
+
defaultPolicy: "allow",
|
|
769
|
+
unavailable,
|
|
770
|
+
async run(raw, ctx) {
|
|
771
|
+
const parsed = IdInput.safeParse(raw);
|
|
772
|
+
if (!parsed.success)
|
|
773
|
+
throw new ToolError(INVALID_ID_TEXT);
|
|
774
|
+
const { id } = parsed.data;
|
|
775
|
+
const svc = await service();
|
|
776
|
+
// only the person's own task, even for an org admin (who may delete others' on the page)
|
|
777
|
+
const task = await ownTask(ctx.orgId, ctx.accountId, id);
|
|
778
|
+
if (task === null)
|
|
779
|
+
throw new ToolError(notYoursText(id));
|
|
780
|
+
await liveRole(ctx);
|
|
781
|
+
await call(() => svc.remove(ctx.orgId, { accountId: ctx.accountId }, task.id), { id });
|
|
782
|
+
return {
|
|
783
|
+
content: [{ type: "text", text: `Deleted scheduled task "${task.name}" (id ${task.id}).` }],
|
|
784
|
+
display: display(`已删除定时任务「${task.name}」`, `Deleted scheduled task "${task.name}"`),
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
return [create, update, list, remove];
|
|
789
|
+
}
|