@evandu/super-pi 0.1.131 → 0.1.132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +18 -0
  2. package/agents/super-pi-feishu/index.ts +88 -0
  3. package/agents/super-pi-knowledge/index.ts +1 -1
  4. package/agents/super-pi-task/core.ts +177 -0
  5. package/agents/super-pi-task/index.ts +59 -0
  6. package/dist/client/.super-pi-client-sources.json +11 -8
  7. package/dist/client/assets/CodeMirrorEditor-ktyL2PcX.js +2 -0
  8. package/dist/client/assets/{dist-C9AkgNbt.js → dist--igDAzRS.js} +1 -1
  9. package/dist/client/assets/{dist-DP2ygAFF.js → dist-60usx__o.js} +1 -1
  10. package/dist/client/assets/{dist-B448EWTM.js → dist-8pdzh6h1.js} +1 -1
  11. package/dist/client/assets/{dist-lQWWWxh9.js → dist-B8OZ-n_C.js} +1 -1
  12. package/dist/client/assets/{dist-hfpd-aWa.js → dist-BAJCrbnX.js} +1 -1
  13. package/dist/client/assets/{dist-B68Q8uXX.js → dist-BJi5HmH4.js} +1 -1
  14. package/dist/client/assets/{dist-COp25nhe.js → dist-BRjGckmQ.js} +1 -1
  15. package/dist/client/assets/{dist-Fhkmvjdx.js → dist-Bg9zLfex.js} +1 -1
  16. package/dist/client/assets/{dist-D7bj81KQ.js → dist-BjZUYxLd.js} +1 -1
  17. package/dist/client/assets/{dist-B1_1Ui9E.js → dist-Bv6P2Rcp.js} +1 -1
  18. package/dist/client/assets/{dist-ByIqsb5-.js → dist-C6iAHGvb.js} +1 -1
  19. package/dist/client/assets/{dist-Cybsoapc.js → dist-CMscy2Nn.js} +1 -1
  20. package/dist/client/assets/{dist-D6PoSxR5.js → dist-CPLCQDqy.js} +1 -1
  21. package/dist/client/assets/{dist-C6qSkkeN.js → dist-CgmISAMr.js} +1 -1
  22. package/dist/client/assets/{dist-DxoWS5MH.js → dist-CoszOk5k.js} +1 -1
  23. package/dist/client/assets/{dist-CpuexUlh.js → dist-Cu75NyQ0.js} +1 -1
  24. package/dist/client/assets/{dist-r5Af94tH.js → dist-DNZMXYNq.js} +1 -1
  25. package/dist/client/assets/{dist-BXVFgriA.js → dist-D_Zdnhm8.js} +1 -1
  26. package/dist/client/assets/{dist-CtdlYgGc.js → dist-DvHghuGd.js} +1 -1
  27. package/dist/client/assets/{dist-BcE6tJvc.js → dist-Hj_dWgPr.js} +1 -1
  28. package/dist/client/assets/{dist-DXNDtyHP.js → dist-Zhdkfvng.js} +1 -1
  29. package/dist/client/assets/{dist-BFlq-kiF.js → dist-vsMBLoxV.js} +1 -1
  30. package/dist/client/assets/{index-BApJ3Zbq.js → index-ChyemF2I.js} +23 -21
  31. package/dist/client/assets/{index-CXB6U5mz.css → index-D3MEG8uH.css} +1 -1
  32. package/dist/client/index.html +2 -2
  33. package/dist/server/application/feishu-service.d.ts +132 -0
  34. package/dist/server/application/feishu-service.js +264 -0
  35. package/dist/server/application/scheduled-task-service.d.ts +164 -0
  36. package/dist/server/application/scheduled-task-service.js +350 -0
  37. package/dist/server/command-handler.d.ts +25 -1
  38. package/dist/server/command-handler.js +60 -1
  39. package/dist/server/feishu-cli.d.ts +207 -0
  40. package/dist/server/feishu-cli.js +419 -0
  41. package/dist/server/files.js +3 -3
  42. package/dist/server/http-server.js +212 -1
  43. package/dist/server/index.js +29 -2
  44. package/dist/server/knowledge.js +10 -2
  45. package/dist/server/persistence/app-state-repository.d.ts +13 -1
  46. package/dist/server/persistence/sqlite-state-repository.js +3 -0
  47. package/dist/server/pi-session.js +3 -0
  48. package/dist/server/runtime.d.ts +21 -0
  49. package/dist/server/runtime.js +58 -6
  50. package/dist/server/scheduled-tasks.d.ts +101 -0
  51. package/dist/server/scheduled-tasks.js +138 -0
  52. package/dist/server/server-context.d.ts +16 -0
  53. package/dist/server/store.d.ts +30 -0
  54. package/dist/server/store.js +81 -2
  55. package/dist/server/task-tool.d.ts +18 -0
  56. package/dist/server/task-tool.js +18 -0
  57. package/dist/server/types.d.ts +56 -0
  58. package/package.json +17 -4
  59. package/dist/.super-pi-check-inputs.json +0 -4
  60. package/dist/client/assets/CodeMirrorEditor-BlQhH3vs.js +0 -2
@@ -8,8 +8,8 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-BApJ3Zbq.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-CXB6U5mz.css">
11
+ <script type="module" crossorigin src="/assets/index-ChyemF2I.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-D3MEG8uH.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -0,0 +1,132 @@
1
+ import { FeishuCli } from "../feishu-cli.js";
2
+ /**
3
+ * The largest request the Feishu endpoint accepts.
4
+ *
5
+ * The Markdown of a document rides in the request body, so the cap is generous for a document and
6
+ * small enough that a runaway paste is refused rather than accepted.
7
+ */
8
+ export declare const MAX_FEISHU_REQUEST_BYTES: number;
9
+ /**
10
+ * WHAT THE PANEL NEEDS TO SHOW, in words a person can act on.
11
+ */
12
+ export type FeishuStatus = {
13
+ /** Whether the official CLI is on this machine at all. */
14
+ installed: boolean;
15
+ version?: string;
16
+ /** The application the CLI is configured with, when it has one. */
17
+ appId?: string;
18
+ /** Whether a person has authorized the CLI to act as them. */
19
+ userReady: boolean;
20
+ /** Whether the CLI can act as the application itself. */
21
+ botReady: boolean;
22
+ /** The identity the CLI will use by default. */
23
+ identity?: "user" | "bot";
24
+ /** Whether the agent can read and write documents right now. */
25
+ connected: boolean;
26
+ /** What to do next, in one sentence. */
27
+ message: string;
28
+ };
29
+ /** The outcome of starting the connect flow. */
30
+ export type FeishuConnectResult = {
31
+ ok: boolean;
32
+ /** Which step the person is on: creating an application, or authorizing their account. */
33
+ step?: "app" | "login";
34
+ /** The URL to open in a browser. */
35
+ verificationUri?: string;
36
+ /** The device code, kept server-side until the person says they are done. */
37
+ deviceCode?: string;
38
+ message: string;
39
+ /** Feishu's own "what to do about it", when there was one. */
40
+ hint?: string;
41
+ };
42
+ /**
43
+ * THE FEISHU SERVICE: what an agent can do with the cloud documents of a Feishu enterprise.
44
+ *
45
+ * IT OWNS NO CREDENTIAL. The official CLI owns them, in the OS keychain, and this service only
46
+ * runs commands. That is the whole point of going through the CLI: Super Pi never sees an App
47
+ * Secret, never sees a user token, and never writes a credential file to disk.
48
+ */
49
+ export declare class FeishuService {
50
+ private readonly cli;
51
+ /** The device code of a login that is waiting for the person to authorize it. */
52
+ private pendingDeviceCode?;
53
+ constructor(cli?: FeishuCli);
54
+ /**
55
+ * WHAT THE CLI CAN DO RIGHT NOW.
56
+ *
57
+ * The three states a person can be in, and what each one means for the agent:
58
+ *
59
+ * - The CLI is not installed: nothing works, and the fix is one command.
60
+ * - The CLI has no application: `config init --new` creates one through a browser confirmation,
61
+ * so the developer console is never visited.
62
+ * - The CLI has an application but no person logged in: the agent can still act as the
63
+ * application, but it cannot reach that person's own documents until they authorize it.
64
+ */
65
+ status(): Promise<FeishuStatus>;
66
+ /**
67
+ * STARTS THE CONNECT FLOW.
68
+ *
69
+ * Two steps, and which one runs is decided by what the CLI already has:
70
+ *
71
+ * - No application: `config init --new` registers one through a browser confirmation. It BLOCKS
72
+ * until the person confirms, so it runs in the background and the URL is taken from its output.
73
+ * - An application but no person: `auth login --no-wait --json` asks for a device code and hands
74
+ * back the URL to open. The code is kept here until the person says they are done.
75
+ */
76
+ connect(): Promise<FeishuConnectResult>;
77
+ /**
78
+ * FINISHES THE LOGIN, once the person says they authorized it.
79
+ *
80
+ * The device code is handed back to the CLI, which completes the exchange and stores the token
81
+ * in the keychain. Super Pi never sees the token itself.
82
+ */
83
+ completeConnect(): Promise<FeishuConnectResult>;
84
+ /**
85
+ * READS A DOCUMENT.
86
+ *
87
+ * The CLI resolves a knowledge-base link itself, so a `/wiki/` URL is passed through untouched -
88
+ * and it answers with Markdown, which is the shape a model wants.
89
+ */
90
+ read(input: {
91
+ url: string;
92
+ }): Promise<{
93
+ documentId?: string;
94
+ revisionId?: number;
95
+ title?: string;
96
+ url: string;
97
+ format: "markdown";
98
+ content: string;
99
+ }>;
100
+ /**
101
+ * WRITES A DOCUMENT.
102
+ *
103
+ * THREE MODES, and the difference between them is who decides what survives:
104
+ *
105
+ * - `create` makes a new document. Nothing can be lost, because nothing was there.
106
+ * - `append` adds to the end. Nothing can be lost either.
107
+ * - `replace` replaces the whole document. This one CAN lose a person's work, so it needs the
108
+ * revision the caller read - the same rule the file panel's write lives by, and for the same
109
+ * reason: an agent told to "把这份报告改成正式版" would otherwise destroy a hand edit made an
110
+ * hour ago, with no refusal and no warning.
111
+ *
112
+ * THE REVISION IS THE ETAG. The CLI passes it as the base revision of the edit, and Feishu
113
+ * refuses the edit when the document has moved on - so the check is enforced by the API rather
114
+ * than by a comparison Super Pi makes and then has to keep honest.
115
+ */
116
+ write(input: {
117
+ url?: string;
118
+ content: string;
119
+ mode?: "create" | "append" | "replace";
120
+ title?: string;
121
+ baseRevisionId?: string;
122
+ }): Promise<{
123
+ documentId?: string;
124
+ revisionId?: number;
125
+ url?: string;
126
+ mode: "create" | "append" | "replace";
127
+ }>;
128
+ /**
129
+ * THE TOOL CALL, as the extension sends it.
130
+ */
131
+ tool(body: unknown): Promise<unknown>;
132
+ }
@@ -0,0 +1,264 @@
1
+ import { FeishuCli, FeishuCliError, errorHint, errorMessage } from "../feishu-cli.js";
2
+ /**
3
+ * The largest request the Feishu endpoint accepts.
4
+ *
5
+ * The Markdown of a document rides in the request body, so the cap is generous for a document and
6
+ * small enough that a runaway paste is refused rather than accepted.
7
+ */
8
+ export const MAX_FEISHU_REQUEST_BYTES = 4 * 1024 * 1024;
9
+ /**
10
+ * THE FEISHU SERVICE: what an agent can do with the cloud documents of a Feishu enterprise.
11
+ *
12
+ * IT OWNS NO CREDENTIAL. The official CLI owns them, in the OS keychain, and this service only
13
+ * runs commands. That is the whole point of going through the CLI: Super Pi never sees an App
14
+ * Secret, never sees a user token, and never writes a credential file to disk.
15
+ */
16
+ export class FeishuService {
17
+ cli;
18
+ /** The device code of a login that is waiting for the person to authorize it. */
19
+ pendingDeviceCode;
20
+ constructor(cli = new FeishuCli()) {
21
+ this.cli = cli;
22
+ }
23
+ /**
24
+ * WHAT THE CLI CAN DO RIGHT NOW.
25
+ *
26
+ * The three states a person can be in, and what each one means for the agent:
27
+ *
28
+ * - The CLI is not installed: nothing works, and the fix is one command.
29
+ * - The CLI has no application: `config init --new` creates one through a browser confirmation,
30
+ * so the developer console is never visited.
31
+ * - The CLI has an application but no person logged in: the agent can still act as the
32
+ * application, but it cannot reach that person's own documents until they authorize it.
33
+ */
34
+ async status() {
35
+ const status = await this.cli.status();
36
+ if (!status.installed) {
37
+ return {
38
+ installed: false,
39
+ userReady: false,
40
+ botReady: false,
41
+ connected: false,
42
+ message: "没有检测到官方飞书 CLI(lark-cli)。请先在终端运行 npm i -g @larksuite/cli 装上它,再回到这里点「连接飞书账号」。如果已经装了但 Super Pi 找不到(两边的 PATH 不一样),用环境变量 SUPER_PI_FEISHU_CLI 指向它的完整路径再重启 Super Pi。",
43
+ };
44
+ }
45
+ if (!status.appId) {
46
+ return {
47
+ installed: true,
48
+ ...(status.version ? { version: status.version } : {}),
49
+ userReady: false,
50
+ botReady: false,
51
+ connected: false,
52
+ message: "飞书 CLI 已就绪,但还没有应用。点「连接飞书账号」,它会给你一个浏览器链接,确认之后应用就建好了——不用去开发者后台注册。",
53
+ };
54
+ }
55
+ if (!status.userReady) {
56
+ return {
57
+ installed: true,
58
+ ...(status.version ? { version: status.version } : {}),
59
+ appId: status.appId,
60
+ userReady: false,
61
+ botReady: status.botReady,
62
+ connected: false,
63
+ ...(status.identity ? { identity: status.identity } : {}),
64
+ message: "应用已配置,但还没有登录你的账号。点「连接飞书账号」用浏览器授权,授权之后 Agent 就能以你的身份读写你自己的文档,不用逐篇加协作者。",
65
+ };
66
+ }
67
+ return {
68
+ installed: true,
69
+ ...(status.version ? { version: status.version } : {}),
70
+ appId: status.appId,
71
+ userReady: true,
72
+ botReady: status.botReady,
73
+ connected: true,
74
+ identity: status.identity ?? "user",
75
+ message: "已连接:Agent 会以你的身份读写飞书云文档。",
76
+ };
77
+ }
78
+ /**
79
+ * STARTS THE CONNECT FLOW.
80
+ *
81
+ * Two steps, and which one runs is decided by what the CLI already has:
82
+ *
83
+ * - No application: `config init --new` registers one through a browser confirmation. It BLOCKS
84
+ * until the person confirms, so it runs in the background and the URL is taken from its output.
85
+ * - An application but no person: `auth login --no-wait --json` asks for a device code and hands
86
+ * back the URL to open. The code is kept here until the person says they are done.
87
+ */
88
+ async connect() {
89
+ const status = await this.status();
90
+ if (!status.installed) {
91
+ return { ok: false, message: status.message };
92
+ }
93
+ if (!status.appId) {
94
+ const verificationUri = await this.cli.startAppCreation();
95
+ return {
96
+ ok: true,
97
+ step: "app",
98
+ ...(verificationUri ? { verificationUri } : {}),
99
+ message: verificationUri
100
+ ? "在浏览器里打开这个链接确认一下,应用就建好了(CLI 会把应用和凭据存进系统钥匙串)。确认完点「我已完成」。"
101
+ : "CLI 没有打印出链接。请在终端里运行 lark-cli config init --new,按提示扫码确认,然后回到这里点「连接飞书账号」。",
102
+ };
103
+ }
104
+ try {
105
+ const login = await this.cli.beginLogin();
106
+ this.pendingDeviceCode = login.deviceCode;
107
+ return {
108
+ ok: true,
109
+ step: "login",
110
+ ...(login.verificationUri ? { verificationUri: login.verificationUri } : {}),
111
+ deviceCode: login.deviceCode,
112
+ message: login.verificationUri
113
+ ? "在浏览器里打开这个链接,用你的飞书账号确认授权,然后点「我已完成」。"
114
+ : "CLI 已经发起设备码授权。请在浏览器里确认,然后点「我已完成」。",
115
+ };
116
+ }
117
+ catch (error) {
118
+ return { ok: false, message: messageOf(error), ...(hintOf(error) ? { hint: hintOf(error) } : {}) };
119
+ }
120
+ }
121
+ /**
122
+ * FINISHES THE LOGIN, once the person says they authorized it.
123
+ *
124
+ * The device code is handed back to the CLI, which completes the exchange and stores the token
125
+ * in the keychain. Super Pi never sees the token itself.
126
+ */
127
+ async completeConnect() {
128
+ if (!this.pendingDeviceCode) {
129
+ return { ok: false, message: "没有待完成的登录。点「连接飞书账号」重新开始。" };
130
+ }
131
+ const deviceCode = this.pendingDeviceCode;
132
+ this.pendingDeviceCode = undefined;
133
+ try {
134
+ const envelope = await this.cli.completeLogin(deviceCode);
135
+ if (envelope.ok === false) {
136
+ return { ok: false, message: errorMessage(envelope), ...(errorHint(envelope) ? { hint: errorHint(envelope) } : {}) };
137
+ }
138
+ return { ok: true, message: "已连接。现在 Agent 能以你的身份读写飞书云文档了。" };
139
+ }
140
+ catch (error) {
141
+ return { ok: false, message: messageOf(error), ...(hintOf(error) ? { hint: hintOf(error) } : {}) };
142
+ }
143
+ }
144
+ /**
145
+ * READS A DOCUMENT.
146
+ *
147
+ * The CLI resolves a knowledge-base link itself, so a `/wiki/` URL is passed through untouched -
148
+ * and it answers with Markdown, which is the shape a model wants.
149
+ */
150
+ async read(input) {
151
+ const document = await this.cli.fetchDocument(input.url);
152
+ return {
153
+ ...(document.documentId ? { documentId: document.documentId } : {}),
154
+ ...(document.revisionId !== undefined ? { revisionId: document.revisionId } : {}),
155
+ ...(document.title ? { title: document.title } : {}),
156
+ url: input.url,
157
+ format: "markdown",
158
+ content: document.content,
159
+ };
160
+ }
161
+ /**
162
+ * WRITES A DOCUMENT.
163
+ *
164
+ * THREE MODES, and the difference between them is who decides what survives:
165
+ *
166
+ * - `create` makes a new document. Nothing can be lost, because nothing was there.
167
+ * - `append` adds to the end. Nothing can be lost either.
168
+ * - `replace` replaces the whole document. This one CAN lose a person's work, so it needs the
169
+ * revision the caller read - the same rule the file panel's write lives by, and for the same
170
+ * reason: an agent told to "把这份报告改成正式版" would otherwise destroy a hand edit made an
171
+ * hour ago, with no refusal and no warning.
172
+ *
173
+ * THE REVISION IS THE ETAG. The CLI passes it as the base revision of the edit, and Feishu
174
+ * refuses the edit when the document has moved on - so the check is enforced by the API rather
175
+ * than by a comparison Super Pi makes and then has to keep honest.
176
+ */
177
+ async write(input) {
178
+ const mode = input.mode ?? (input.url ? "append" : "create");
179
+ const content = String(input.content ?? "");
180
+ if (!content.trim())
181
+ throw new Error("要写入的内容是空的。");
182
+ if (mode === "create") {
183
+ const title = input.title?.trim() || titleFromMarkdown(content);
184
+ const created = await this.cli.createDocument(title, content);
185
+ return {
186
+ ...(created.documentId ? { documentId: created.documentId } : {}),
187
+ ...(created.revisionId !== undefined ? { revisionId: created.revisionId } : {}),
188
+ ...(created.url ? { url: created.url } : {}),
189
+ mode,
190
+ };
191
+ }
192
+ if (!input.url)
193
+ throw new Error("追加或覆盖需要给出文档链接或 token。");
194
+ if (mode === "replace") {
195
+ if (!input.baseRevisionId) {
196
+ throw new Error("覆盖前需要确认文档版本:先用 feishu_doc_read 读一次这篇文档,把它返回的 revisionId 作为 baseRevisionId 传入。覆盖会清空整篇文档,必须先看过。");
197
+ }
198
+ const revision = Number(input.baseRevisionId);
199
+ if (!Number.isFinite(revision)) {
200
+ throw new Error(`baseRevisionId 不是有效的版本号:${input.baseRevisionId}`);
201
+ }
202
+ const updated = await this.cli.updateDocument(input.url, "overwrite", content, revision);
203
+ return {
204
+ ...(updated.revisionId !== undefined ? { revisionId: updated.revisionId } : {}),
205
+ url: input.url,
206
+ mode,
207
+ };
208
+ }
209
+ const updated = await this.cli.updateDocument(input.url, "append", content);
210
+ return {
211
+ ...(updated.revisionId !== undefined ? { revisionId: updated.revisionId } : {}),
212
+ url: input.url,
213
+ mode,
214
+ };
215
+ }
216
+ /**
217
+ * THE TOOL CALL, as the extension sends it.
218
+ */
219
+ async tool(body) {
220
+ const request = (body ?? {});
221
+ const action = String(request.action ?? "read");
222
+ if (action === "read") {
223
+ return this.read({ url: String(request.url ?? "") });
224
+ }
225
+ if (action === "write") {
226
+ const mode = request.mode === "create" || request.mode === "append" || request.mode === "replace" ? request.mode : undefined;
227
+ return this.write({
228
+ ...(request.url ? { url: String(request.url) } : {}),
229
+ content: String(request.content ?? ""),
230
+ ...(mode ? { mode } : {}),
231
+ ...(request.title ? { title: String(request.title) } : {}),
232
+ ...(request.baseRevisionId ? { baseRevisionId: String(request.baseRevisionId) } : {}),
233
+ });
234
+ }
235
+ throw new Error(`未知的飞书工具操作:${action}`);
236
+ }
237
+ }
238
+ /** The message of a failure, whatever shape the failure has. */
239
+ function messageOf(error) {
240
+ if (error instanceof FeishuCliError)
241
+ return error.message;
242
+ if (error instanceof Error)
243
+ return error.message;
244
+ return String(error);
245
+ }
246
+ /** The hint of a failure, when it carries one. */
247
+ function hintOf(error) {
248
+ return error instanceof FeishuCliError ? error.hint : undefined;
249
+ }
250
+ /**
251
+ * A TITLE FROM THE CONTENT, for a document created without one.
252
+ *
253
+ * The first heading is what a person would have called it, and a generated title is better than
254
+ * "未命名文档" only when there is something to take.
255
+ */
256
+ function titleFromMarkdown(content) {
257
+ for (const line of content.split("\n")) {
258
+ const heading = /^#{1,6}\s+(.+?)\s*$/.exec(line.trim());
259
+ if (heading?.[1])
260
+ return heading[1].slice(0, 800);
261
+ }
262
+ const firstLine = content.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
263
+ return (firstLine ? firstLine.slice(0, 800) : "未命名文档");
264
+ }
@@ -0,0 +1,164 @@
1
+ import type { EventBroadcaster } from "../server-context.js";
2
+ import type { Store } from "../store.js";
3
+ import { describeSchedule, type ScheduledTask, type ScheduleSpec } from "../scheduled-tasks.js";
4
+ import type { ModelSelection } from "../types.js";
5
+ import type { ConversationService } from "./conversation-service.js";
6
+ import type { RunService } from "./run-service.js";
7
+ /** The constructor's last parameter: the only seams this service admits. */
8
+ export type ScheduledTaskServiceOptions = {
9
+ /**
10
+ * The TEST SEAM for the clock: how often the sweep runs. Production never passes this and gets the
11
+ * fifteen-second sweep; a test passes a few milliseconds and waits for real time instead of
12
+ * faking timers - the same role `sessionFactory` plays in `PiRpcRuntime`.
13
+ */
14
+ sweepIntervalMs?: number;
15
+ };
16
+ /** What a caller wants a task to be. Everything but the id and the bookkeeping is optional. */
17
+ export type ScheduledTaskInput = {
18
+ name?: string;
19
+ prompt?: string;
20
+ schedule?: unknown;
21
+ workspaceId?: string;
22
+ model?: ModelSelection | null;
23
+ agentId?: string | null;
24
+ enabled?: boolean;
25
+ };
26
+ /**
27
+ * THE SCHEDULED TASKS SERVICE: the list, the clock, and the runs.
28
+ *
29
+ * WHAT LIVES HERE, AND WHY NOT IN THE STORE. The store owns the record and the disk; this owns the
30
+ * clock and the act of running. Keeping the two apart is what lets the store stay a thing that can be
31
+ * opened, repaired and closed without a timer running behind it - and it is why a scheduled task
32
+ * survives a restart as a RECORD while the thing that fires it is built fresh from that record.
33
+ */
34
+ export declare class ScheduledTaskService {
35
+ private readonly store;
36
+ private readonly runService;
37
+ private readonly conversationService;
38
+ private readonly broadcaster;
39
+ /** The runs currently in flight, by task id. A task in here is not fired again. */
40
+ private readonly inFlight;
41
+ private timer?;
42
+ private stopped;
43
+ constructor(store: Store, runService: RunService, conversationService: ConversationService, broadcaster: EventBroadcaster, options?: ScheduledTaskServiceOptions);
44
+ private readonly sweepIntervalMs;
45
+ /**
46
+ * Starts the clock.
47
+ *
48
+ * `unref()` so the sweep never holds the process open: the shutdown sequence closes the server and
49
+ * the store, and a pending interval would keep the process alive after that, which is the difference
50
+ * between a server that exits and one that has to be killed.
51
+ */
52
+ start(): void;
53
+ /**
54
+ * Stops the clock and waits for whatever is running.
55
+ *
56
+ * The in-flight runs are NOT aborted: a task that is halfway through its answer is a real answer in
57
+ * a real conversation, and killing it because the server is shutting down would throw that work
58
+ * away. The sweep stops, the runs finish, and the next start picks the schedule up from what is on
59
+ * disk.
60
+ */
61
+ stop(): Promise<void>;
62
+ /** Every task on record, newest first. */
63
+ list(): ScheduledTask[];
64
+ /** The tasks of ONE workspace, which is what the panel shows. */
65
+ listForWorkspace(workspaceId: string | undefined): ScheduledTask[];
66
+ /**
67
+ * The task, if it belongs to THIS workspace - and a plain "not found" if it does not.
68
+ *
69
+ * WHY THE OWNERSHIP CHECK IS HERE, IN THE SERVICE. A task is a thing that RUNS in a workspace: it
70
+ * opens its conversation there, uses that workspace's defaults and its tools. A caller in another
71
+ * workspace cannot even list these tasks - `listForWorkspace` is the scoped read - so it must not
72
+ * be able to change or delete them either. Before this, `update` and `delete` acted on an id alone:
73
+ * a task created from workspace B could be rewritten and then deleted by a conversation in
74
+ * workspace A, and it STILL RAN in B, so A's prompt executed inside B with B's model and B's tools
75
+ * - an invisible cross-workspace write (measured by scripts/scheduled-task-scope.test.mjs, ledger
76
+ * TEST-026).
77
+ *
78
+ * THE REFUSAL DOES NOT SAY WHERE IT IS. "Not found" is the whole answer: a caller must not be able
79
+ * to learn that a task exists in a workspace it cannot see.
80
+ */
81
+ requireForWorkspace(id: string, workspaceId: string): ScheduledTask;
82
+ /**
83
+ * Creates a task.
84
+ *
85
+ * `createdBy` is recorded because the two kinds are not the same thing to a reader: a task the user
86
+ * asked for and a task an agent invented look identical in the list, and the difference is exactly
87
+ * what a person wants to know before they delete it.
88
+ */
89
+ create(input: ScheduledTaskInput, createdBy: "user" | "agent"): Promise<ScheduledTask>;
90
+ /**
91
+ * Changes a task.
92
+ *
93
+ * EVERY CHANGE RECOMPUTES `nextRunAt`, because a task whose prompt was edited an hour before its
94
+ * slot must not fire at the old slot with the old schedule - and a task whose schedule was edited
95
+ * must not keep the old next time. The one exception is a manual trigger, which is not a change at
96
+ * all: it runs the task once and leaves the clock where it was.
97
+ */
98
+ update(id: string, input: ScheduledTaskInput): Promise<ScheduledTask>;
99
+ /** Turns a task on or off. */
100
+ setEnabled(id: string, enabled: boolean): Promise<ScheduledTask>;
101
+ /** Removes a task. Its conversation stays: it holds the answers the task produced. */
102
+ remove(id: string): Promise<void>;
103
+ /**
104
+ * Runs a task NOW, once.
105
+ *
106
+ * The clock is untouched: a manual run does not make the next scheduled run earlier or later, and it
107
+ * does not count as the task having fired. This is the difference between 「先试一次」 and 「改一下周期」,
108
+ * and conflating them would make 「先试一次」 silently rewrite the user's schedule.
109
+ *
110
+ * THE ACK RETURNS BEFORE THE RUN FINISHES. The run itself takes as long as a model turn takes -
111
+ * seconds, usually, sometimes minutes - and a command that waited for it would hold the socket's
112
+ * whole command chain hostage for that long. The outcome is not lost: it lands in `lastRunStatus`
113
+ * and arrives at the panel through the ordinary state broadcast, where a person reads it.
114
+ */
115
+ trigger(id: string): Promise<void>;
116
+ /**
117
+ * The sweep: every task whose moment has come runs once.
118
+ *
119
+ * The tasks that are due are collected BEFORE any of them runs, and each one advances its own
120
+ * `nextRunAt` before it runs - so a task that takes three minutes cannot be fired again by the next
121
+ * sweep, and two sweeps can never both pick up the same task.
122
+ */
123
+ private sweep;
124
+ /**
125
+ * THE ACTUAL RUN, for both the clock and the button.
126
+ *
127
+ * The task gets its OWN conversation, created on the first run, and the prompt is sent into it
128
+ * verbatim as a user message - the same path a person's own message takes. That sameness is the point:
129
+ * a scheduled task has no special powers, so it cannot do anything a message could not, and it gets
130
+ * the same model, the same tools, the same memory.
131
+ */
132
+ private fire;
133
+ /** The body of a run: get a conversation, put the prompt into it, remember how it went. */
134
+ private runTask;
135
+ /**
136
+ * The conversation the task runs in, created the first time it is needed.
137
+ *
138
+ * LAZY, so that creating a task does not create a conversation nobody has spoken in. The name says
139
+ * what the conversation is, because it appears in the conversation list like any other and the user
140
+ * has to be able to tell it apart from their own work.
141
+ */
142
+ private ensureConversation;
143
+ /**
144
+ * Puts the task's model and agent on its conversation.
145
+ *
146
+ * EVERY RUN, not only the first: the user can change the model in the panel, and the next run is the
147
+ * one that has to use it. A failure here is the task's failure - a model that Pi no longer offers
148
+ * cannot be silently ignored, because the run would then happen on some other model and the panel
149
+ * would say it succeeded.
150
+ */
151
+ private applyConfiguration;
152
+ /** Writes down how the run went, and tells the panel. */
153
+ private record;
154
+ /** The task, or a refusal in words the reader can act on. */
155
+ private require;
156
+ }
157
+ /**
158
+ * The schedule in words, for the text an agent reads back after creating a task.
159
+ *
160
+ * Exported from here rather than from `scheduled-tasks.ts` so that the HTTP layer and the WS layer
161
+ * say the same thing about one schedule.
162
+ */
163
+ export { describeSchedule };
164
+ export type { ScheduledTask, ScheduleSpec };