@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.
- package/README.md +18 -0
- package/agents/super-pi-feishu/index.ts +88 -0
- package/agents/super-pi-knowledge/index.ts +1 -1
- package/agents/super-pi-task/core.ts +177 -0
- package/agents/super-pi-task/index.ts +59 -0
- package/dist/client/.super-pi-client-sources.json +11 -8
- package/dist/client/assets/CodeMirrorEditor-ktyL2PcX.js +2 -0
- package/dist/client/assets/{dist-C9AkgNbt.js → dist--igDAzRS.js} +1 -1
- package/dist/client/assets/{dist-DP2ygAFF.js → dist-60usx__o.js} +1 -1
- package/dist/client/assets/{dist-B448EWTM.js → dist-8pdzh6h1.js} +1 -1
- package/dist/client/assets/{dist-lQWWWxh9.js → dist-B8OZ-n_C.js} +1 -1
- package/dist/client/assets/{dist-hfpd-aWa.js → dist-BAJCrbnX.js} +1 -1
- package/dist/client/assets/{dist-B68Q8uXX.js → dist-BJi5HmH4.js} +1 -1
- package/dist/client/assets/{dist-COp25nhe.js → dist-BRjGckmQ.js} +1 -1
- package/dist/client/assets/{dist-Fhkmvjdx.js → dist-Bg9zLfex.js} +1 -1
- package/dist/client/assets/{dist-D7bj81KQ.js → dist-BjZUYxLd.js} +1 -1
- package/dist/client/assets/{dist-B1_1Ui9E.js → dist-Bv6P2Rcp.js} +1 -1
- package/dist/client/assets/{dist-ByIqsb5-.js → dist-C6iAHGvb.js} +1 -1
- package/dist/client/assets/{dist-Cybsoapc.js → dist-CMscy2Nn.js} +1 -1
- package/dist/client/assets/{dist-D6PoSxR5.js → dist-CPLCQDqy.js} +1 -1
- package/dist/client/assets/{dist-C6qSkkeN.js → dist-CgmISAMr.js} +1 -1
- package/dist/client/assets/{dist-DxoWS5MH.js → dist-CoszOk5k.js} +1 -1
- package/dist/client/assets/{dist-CpuexUlh.js → dist-Cu75NyQ0.js} +1 -1
- package/dist/client/assets/{dist-r5Af94tH.js → dist-DNZMXYNq.js} +1 -1
- package/dist/client/assets/{dist-BXVFgriA.js → dist-D_Zdnhm8.js} +1 -1
- package/dist/client/assets/{dist-CtdlYgGc.js → dist-DvHghuGd.js} +1 -1
- package/dist/client/assets/{dist-BcE6tJvc.js → dist-Hj_dWgPr.js} +1 -1
- package/dist/client/assets/{dist-DXNDtyHP.js → dist-Zhdkfvng.js} +1 -1
- package/dist/client/assets/{dist-BFlq-kiF.js → dist-vsMBLoxV.js} +1 -1
- package/dist/client/assets/{index-BApJ3Zbq.js → index-ChyemF2I.js} +23 -21
- package/dist/client/assets/{index-CXB6U5mz.css → index-D3MEG8uH.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/application/feishu-service.d.ts +132 -0
- package/dist/server/application/feishu-service.js +264 -0
- package/dist/server/application/scheduled-task-service.d.ts +164 -0
- package/dist/server/application/scheduled-task-service.js +350 -0
- package/dist/server/command-handler.d.ts +25 -1
- package/dist/server/command-handler.js +60 -1
- package/dist/server/feishu-cli.d.ts +207 -0
- package/dist/server/feishu-cli.js +419 -0
- package/dist/server/files.js +3 -3
- package/dist/server/http-server.js +212 -1
- package/dist/server/index.js +29 -2
- package/dist/server/knowledge.js +10 -2
- package/dist/server/persistence/app-state-repository.d.ts +13 -1
- package/dist/server/persistence/sqlite-state-repository.js +3 -0
- package/dist/server/pi-session.js +3 -0
- package/dist/server/runtime.d.ts +21 -0
- package/dist/server/runtime.js +58 -6
- package/dist/server/scheduled-tasks.d.ts +101 -0
- package/dist/server/scheduled-tasks.js +138 -0
- package/dist/server/server-context.d.ts +16 -0
- package/dist/server/store.d.ts +30 -0
- package/dist/server/store.js +81 -2
- package/dist/server/task-tool.d.ts +18 -0
- package/dist/server/task-tool.js +18 -0
- package/dist/server/types.d.ts +56 -0
- package/package.json +17 -4
- package/dist/.super-pi-check-inputs.json +0 -4
- package/dist/client/assets/CodeMirrorEditor-BlQhH3vs.js +0 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ModelSelection } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* WHEN A SCHEDULED TASK RUNS.
|
|
4
|
+
*
|
|
5
|
+
* FOUR SHAPES, AND NOT A CRON EXPRESSION. A cron string would bring a parser, a timezone story and a
|
|
6
|
+
* daylight-saving story to a panel that runs on the same machine as the server - and the two shapes
|
|
7
|
+
* people actually ask for are "every N minutes" and "every day at HH:MM". The set is closed on purpose:
|
|
8
|
+
* a schedule that cannot be expressed here is not a schedule the panel can edit, and the panel is the
|
|
9
|
+
* only place a user can see what they are agreeing to.
|
|
10
|
+
*
|
|
11
|
+
* - `interval`: every N minutes. The interval is anchored on the last run, not on the wall clock, so a
|
|
12
|
+
* task that takes four minutes to run does not slowly drift later every cycle.
|
|
13
|
+
* - `daily`: every day at a local `HH:MM`.
|
|
14
|
+
* - `weekly`: on the listed weekdays (0 = Sunday) at a local `HH:MM`.
|
|
15
|
+
* - `once`: at one instant. The task is finished after it fires.
|
|
16
|
+
*
|
|
17
|
+
* THE TIME IS LOCAL, not UTC and not a named zone, because the server and the panel are the same
|
|
18
|
+
* machine for every installation that exists. A "09:00" that means something different in the panel
|
|
19
|
+
* than in the scheduler would be the worst possible bug in this feature: it would fire at the wrong
|
|
20
|
+
* hour and look like it had fired at the right one.
|
|
21
|
+
*/
|
|
22
|
+
export type ScheduleSpec = {
|
|
23
|
+
kind: "interval";
|
|
24
|
+
everyMinutes: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "daily";
|
|
27
|
+
time: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "weekly";
|
|
30
|
+
weekdays: number[];
|
|
31
|
+
time: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "once";
|
|
34
|
+
at: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* ONE SCHEDULED TASK: a prompt, a model to run it with, and when to run it.
|
|
38
|
+
*
|
|
39
|
+
* `conversationId` is the task's OWN conversation, created lazily on the first run. The task never runs
|
|
40
|
+
* in somebody else's conversation: a scheduled run that landed in the user's working session would
|
|
41
|
+
* interleave with what they were doing, and would change that session's model out from under it.
|
|
42
|
+
*/
|
|
43
|
+
export type ScheduledTask = {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
/** The instruction, sent VERBATIM as the user message of the task's conversation. */
|
|
47
|
+
prompt: string;
|
|
48
|
+
/** The workspace the task belongs to: it decides the working directory and the defaults. */
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
/** The task's own conversation, created on the first run. Absent until then. */
|
|
51
|
+
conversationId?: string;
|
|
52
|
+
/** Overrides the workspace's default agent. */
|
|
53
|
+
agentId?: string;
|
|
54
|
+
/** Overrides the workspace's default model. A `provider`/`modelId` PAIR, never half of one. */
|
|
55
|
+
model?: ModelSelection;
|
|
56
|
+
schedule: ScheduleSpec;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
/** When the task fires next, in epoch milliseconds. Absent for a finished one-shot task. */
|
|
59
|
+
nextRunAt?: number;
|
|
60
|
+
lastRunAt?: number;
|
|
61
|
+
lastRunStatus?: "completed" | "failed" | "skipped";
|
|
62
|
+
lastRunMessage?: string;
|
|
63
|
+
/** Whether a person or the agent created it. Both are editable by both. */
|
|
64
|
+
createdBy: "user" | "agent";
|
|
65
|
+
createdAt: number;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
};
|
|
68
|
+
/** The shortest interval a task may ask for. A one-minute floor is what makes a task a task. */
|
|
69
|
+
export declare const MIN_INTERVAL_MINUTES = 1;
|
|
70
|
+
/** The longest interval: a week. Anything longer is a calendar arrangement, not an interval. */
|
|
71
|
+
export declare const MAX_INTERVAL_MINUTES: number;
|
|
72
|
+
/**
|
|
73
|
+
* Reads a schedule out of whatever a caller sent, or explains why it cannot.
|
|
74
|
+
*
|
|
75
|
+
* THE SERVER OWNS THIS, and the extension on the other side of the wire deliberately does NOT have a
|
|
76
|
+
* copy of it. The extension hands over what the model wrote; this function decides what it means. A
|
|
77
|
+
* second implementation on the extension side would be a second opinion about the same wire, and the two
|
|
78
|
+
* would disagree the first time a model wrote a schedule in a shape one of them accepted and the other
|
|
79
|
+
* did not - with the model's mistake showing up as a server error it cannot explain.
|
|
80
|
+
*/
|
|
81
|
+
export declare function normalizeSchedule(raw: unknown): ScheduleSpec;
|
|
82
|
+
/**
|
|
83
|
+
* WHEN THE TASK FIRES NEXT, given a moment to look forward from.
|
|
84
|
+
*
|
|
85
|
+
* `undefined` means "never again": a one-shot task whose moment has passed.
|
|
86
|
+
*
|
|
87
|
+
* The daily and weekly cases walk forward day by day in LOCAL time rather than adding 24 hours, so a
|
|
88
|
+
* schedule at 09:00 stays at 09:00 across a daylight-saving change instead of drifting to 08:00 or
|
|
89
|
+
* 10:00 for half the year.
|
|
90
|
+
*/
|
|
91
|
+
export declare function nextRunAt(spec: ScheduleSpec, from: number): number | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* THE SCHEDULE IN WORDS, for the panel and for the text the agent reads back.
|
|
94
|
+
*
|
|
95
|
+
* One implementation for both readers, because two descriptions of one schedule would eventually say
|
|
96
|
+
* two different things - and the agent's description is what the user is told when the agent says
|
|
97
|
+
* "I set up a daily task". If the two disagreed, the user would find out from the clock.
|
|
98
|
+
*/
|
|
99
|
+
export declare function describeSchedule(spec: ScheduleSpec): string;
|
|
100
|
+
/** A moment as `YYYY-MM-DD HH:MM`, in local time. */
|
|
101
|
+
export declare function formatMoment(ms: number): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** The shortest interval a task may ask for. A one-minute floor is what makes a task a task. */
|
|
2
|
+
export const MIN_INTERVAL_MINUTES = 1;
|
|
3
|
+
/** The longest interval: a week. Anything longer is a calendar arrangement, not an interval. */
|
|
4
|
+
export const MAX_INTERVAL_MINUTES = 60 * 24 * 7;
|
|
5
|
+
/**
|
|
6
|
+
* Reads a schedule out of whatever a caller sent, or explains why it cannot.
|
|
7
|
+
*
|
|
8
|
+
* THE SERVER OWNS THIS, and the extension on the other side of the wire deliberately does NOT have a
|
|
9
|
+
* copy of it. The extension hands over what the model wrote; this function decides what it means. A
|
|
10
|
+
* second implementation on the extension side would be a second opinion about the same wire, and the two
|
|
11
|
+
* would disagree the first time a model wrote a schedule in a shape one of them accepted and the other
|
|
12
|
+
* did not - with the model's mistake showing up as a server error it cannot explain.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeSchedule(raw) {
|
|
15
|
+
if (!raw || typeof raw !== "object")
|
|
16
|
+
throw new Error("周期必须是一个对象:{\"kind\":\"interval\",\"everyMinutes\":30} 这样写。");
|
|
17
|
+
const value = raw;
|
|
18
|
+
const kind = typeof value.kind === "string" ? value.kind.trim() : "";
|
|
19
|
+
if (kind === "interval") {
|
|
20
|
+
const everyMinutes = Number(value.everyMinutes);
|
|
21
|
+
if (!Number.isFinite(everyMinutes) || !Number.isInteger(everyMinutes))
|
|
22
|
+
throw new Error("每隔 N 分钟需要一个整数分钟数。");
|
|
23
|
+
if (everyMinutes < MIN_INTERVAL_MINUTES || everyMinutes > MAX_INTERVAL_MINUTES) {
|
|
24
|
+
throw new Error(`间隔分钟数要在 ${MIN_INTERVAL_MINUTES} 到 ${MAX_INTERVAL_MINUTES} 之间。`);
|
|
25
|
+
}
|
|
26
|
+
return { kind: "interval", everyMinutes };
|
|
27
|
+
}
|
|
28
|
+
if (kind === "daily")
|
|
29
|
+
return { kind: "daily", time: normalizeTime(value.time) };
|
|
30
|
+
if (kind === "weekly") {
|
|
31
|
+
const weekdays = normalizeWeekdays(value.weekdays);
|
|
32
|
+
return { kind: "weekly", weekdays, time: normalizeTime(value.time) };
|
|
33
|
+
}
|
|
34
|
+
if (kind === "once") {
|
|
35
|
+
const at = Number(value.at);
|
|
36
|
+
if (!Number.isFinite(at) || at <= 0)
|
|
37
|
+
throw new Error("一次性任务需要一个触发时刻(毫秒时间戳)。");
|
|
38
|
+
return { kind: "once", at };
|
|
39
|
+
}
|
|
40
|
+
throw new Error("周期只能是 interval(每隔 N 分钟)、daily(每天)、weekly(每周)或 once(仅一次)。");
|
|
41
|
+
}
|
|
42
|
+
/** `HH:MM`, twenty-four hour, leading zero or not. The only time shape this feature accepts. */
|
|
43
|
+
function normalizeTime(raw) {
|
|
44
|
+
if (typeof raw !== "string")
|
|
45
|
+
throw new Error("触发时刻要写成 \"HH:MM\",比如 \"09:00\"。");
|
|
46
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(raw.trim());
|
|
47
|
+
if (!match)
|
|
48
|
+
throw new Error("触发时刻要写成 \"HH:MM\",比如 \"09:00\"。");
|
|
49
|
+
const hours = Number(match[1]);
|
|
50
|
+
const minutes = Number(match[2]);
|
|
51
|
+
if (hours > 23 || minutes > 59)
|
|
52
|
+
throw new Error("触发时刻不是一个真实的时间。");
|
|
53
|
+
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
|
|
54
|
+
}
|
|
55
|
+
/** The weekdays of a weekly schedule, sorted and deduplicated. An empty list means every day. */
|
|
56
|
+
function normalizeWeekdays(raw) {
|
|
57
|
+
if (!Array.isArray(raw))
|
|
58
|
+
throw new Error("每周任务需要给出星期几,比如 [1,3] 表示周一和周三。");
|
|
59
|
+
const days = Array.from(new Set(raw.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0 && item <= 6)));
|
|
60
|
+
if (!days.length)
|
|
61
|
+
throw new Error("每周任务至少要有一天。");
|
|
62
|
+
return days.sort((a, b) => a - b);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* WHEN THE TASK FIRES NEXT, given a moment to look forward from.
|
|
66
|
+
*
|
|
67
|
+
* `undefined` means "never again": a one-shot task whose moment has passed.
|
|
68
|
+
*
|
|
69
|
+
* The daily and weekly cases walk forward day by day in LOCAL time rather than adding 24 hours, so a
|
|
70
|
+
* schedule at 09:00 stays at 09:00 across a daylight-saving change instead of drifting to 08:00 or
|
|
71
|
+
* 10:00 for half the year.
|
|
72
|
+
*/
|
|
73
|
+
export function nextRunAt(spec, from) {
|
|
74
|
+
if (spec.kind === "interval")
|
|
75
|
+
return from + spec.everyMinutes * 60_000;
|
|
76
|
+
if (spec.kind === "once")
|
|
77
|
+
return spec.at > from ? spec.at : undefined;
|
|
78
|
+
if (spec.kind === "daily")
|
|
79
|
+
return nextDailyAt(spec.time, from);
|
|
80
|
+
return nextWeeklyAt(spec.weekdays, spec.time, from);
|
|
81
|
+
}
|
|
82
|
+
/** The next moment at `HH:MM` strictly after `from`, in local time. */
|
|
83
|
+
function nextDailyAt(time, from) {
|
|
84
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
85
|
+
const candidate = new Date(from);
|
|
86
|
+
candidate.setHours(hours, minutes, 0, 0);
|
|
87
|
+
if (candidate.getTime() <= from)
|
|
88
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
89
|
+
return candidate.getTime();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The next moment at `HH:MM` on one of `weekdays`, strictly after `from`, in local time.
|
|
93
|
+
*
|
|
94
|
+
* At most eight days are examined, which covers every weekday list including an empty one: an empty
|
|
95
|
+
* list means "every day", and the daily case is a special case of it.
|
|
96
|
+
*/
|
|
97
|
+
function nextWeeklyAt(weekdays, time, from) {
|
|
98
|
+
const days = weekdays.length ? weekdays : [0, 1, 2, 3, 4, 5, 6];
|
|
99
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
100
|
+
for (let offset = 0; offset <= 7; offset += 1) {
|
|
101
|
+
const candidate = new Date(from);
|
|
102
|
+
candidate.setDate(candidate.getDate() + offset);
|
|
103
|
+
candidate.setHours(hours, minutes, 0, 0);
|
|
104
|
+
if (candidate.getTime() <= from)
|
|
105
|
+
continue;
|
|
106
|
+
if (days.includes(candidate.getDay()))
|
|
107
|
+
return candidate.getTime();
|
|
108
|
+
}
|
|
109
|
+
// Unreachable: within eight days every weekday in a full week comes round. The fallback keeps the
|
|
110
|
+
// function total rather than returning `undefined` for a shape that cannot happen.
|
|
111
|
+
return nextDailyAt(time, from);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* THE SCHEDULE IN WORDS, for the panel and for the text the agent reads back.
|
|
115
|
+
*
|
|
116
|
+
* One implementation for both readers, because two descriptions of one schedule would eventually say
|
|
117
|
+
* two different things - and the agent's description is what the user is told when the agent says
|
|
118
|
+
* "I set up a daily task". If the two disagreed, the user would find out from the clock.
|
|
119
|
+
*/
|
|
120
|
+
export function describeSchedule(spec) {
|
|
121
|
+
if (spec.kind === "interval")
|
|
122
|
+
return `每隔 ${spec.everyMinutes} 分钟`;
|
|
123
|
+
if (spec.kind === "daily")
|
|
124
|
+
return `每天 ${spec.time}`;
|
|
125
|
+
if (spec.kind === "weekly")
|
|
126
|
+
return `每周${spec.weekdays.map(weekdayName).join("、")} ${spec.time}`;
|
|
127
|
+
return `仅一次:${formatMoment(spec.at)}`;
|
|
128
|
+
}
|
|
129
|
+
/** 0 = Sunday, which is what `Date.getDay()` says. */
|
|
130
|
+
function weekdayName(day) {
|
|
131
|
+
return ["日", "一", "二", "三", "四", "五", "六"][day] ?? "?";
|
|
132
|
+
}
|
|
133
|
+
/** A moment as `YYYY-MM-DD HH:MM`, in local time. */
|
|
134
|
+
export function formatMoment(ms) {
|
|
135
|
+
const date = new Date(ms);
|
|
136
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
137
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
138
|
+
}
|
|
@@ -42,6 +42,14 @@ export interface ServerContext {
|
|
|
42
42
|
web?: import("./web.js").WebService;
|
|
43
43
|
/** The agent-facing document tools. Optional so the server can start without them. */
|
|
44
44
|
documentService?: import("./application/document-service.js").DocumentService;
|
|
45
|
+
/**
|
|
46
|
+
* THE FEISHU TOOLS.
|
|
47
|
+
*
|
|
48
|
+
* Optional because the credential may be missing, not because the feature may be missing: the
|
|
49
|
+
* service is always constructed, and a call made before the credential is configured is refused
|
|
50
|
+
* with the message that says what to do - which a missing service could not say.
|
|
51
|
+
*/
|
|
52
|
+
feishu?: import("./application/feishu-service.js").FeishuService;
|
|
45
53
|
paths: ServerPaths;
|
|
46
54
|
store: Store;
|
|
47
55
|
runtime: AgentRuntime;
|
|
@@ -55,6 +63,14 @@ export interface ServerContext {
|
|
|
55
63
|
catalogService: CatalogService;
|
|
56
64
|
/** The providers a person added through the settings window. */
|
|
57
65
|
providerService: ProviderService;
|
|
66
|
+
/**
|
|
67
|
+
* The scheduled tasks and the clock behind them.
|
|
68
|
+
*
|
|
69
|
+
* In the context because TWO layers need it: the command handler routes the panel's five commands
|
|
70
|
+
* into it, and the HTTP layer exposes the agent-facing endpoint that lets a running turn create a
|
|
71
|
+
* task of its own.
|
|
72
|
+
*/
|
|
73
|
+
scheduledTaskService?: import("./application/scheduled-task-service.js").ScheduledTaskService;
|
|
58
74
|
execService: ExecService;
|
|
59
75
|
toolRegistry: ToolRegistry;
|
|
60
76
|
toolRunner: ToolRunner;
|
package/dist/server/store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppStateRepository } from "./persistence/app-state-repository.js";
|
|
2
2
|
import { defaultJsonStatePath } from "./persistence/json-state-repository.js";
|
|
3
|
+
import type { ScheduledTask } from "./scheduled-tasks.js";
|
|
3
4
|
import type { WorkflowRun } from "./workflow.js";
|
|
4
5
|
import type { AppState, Conversation, Message, MessageDelivery, MessageImage, ModelSelection, SubagentRun, SubagentRunUpdate, TodoItem, ToolCallQuestion, ToolCallArg, ToolCallBlock, ToolResultSummary, Workspace } from "./types.js";
|
|
5
6
|
/** Mints the id a forked conversation will have, BEFORE its Pi transcript exists.
|
|
@@ -45,6 +46,14 @@ export declare const SUBAGENT_RUN_RETENTION = 100;
|
|
|
45
46
|
* request that needed several specialists rather than a unit of work.
|
|
46
47
|
*/
|
|
47
48
|
export declare const WORKFLOW_RETENTION = 50;
|
|
49
|
+
/**
|
|
50
|
+
* How many scheduled tasks are kept.
|
|
51
|
+
*
|
|
52
|
+
* A task is a standing arrangement, not a record of something that happened, so the bound here is much
|
|
53
|
+
* looser than the run retention: a person who keeps a daily task for a year has one task, and the
|
|
54
|
+
* hundredth is already a sign that the list is being used as a log rather than as a list of intentions.
|
|
55
|
+
*/
|
|
56
|
+
export declare const SCHEDULED_TASK_RETENTION = 100;
|
|
48
57
|
/**
|
|
49
58
|
* Everything a revert decided, before anything is changed.
|
|
50
59
|
*
|
|
@@ -688,6 +697,27 @@ export declare class Store {
|
|
|
688
697
|
setBackgroundTask(summary: AppState["backgroundTasks"][number]): Promise<void>;
|
|
689
698
|
removeBackgroundTask(id: string): Promise<void>;
|
|
690
699
|
clearBackgroundTasks(): Promise<void>;
|
|
700
|
+
/**
|
|
701
|
+
* Writes a scheduled task - new or changed - and persists it.
|
|
702
|
+
*
|
|
703
|
+
* ONE METHOD FOR BOTH, because the two are the same write. A task that has not run and a task that
|
|
704
|
+
* has run are the same kind of thing with different field values, and splitting them into
|
|
705
|
+
* "create" and "update" would leave the caller to decide which of two paths a given change takes -
|
|
706
|
+
* which is exactly the kind of decision that ends up made twice, differently, in two places.
|
|
707
|
+
*
|
|
708
|
+
* The value is cloned on the way in and on the way out, like every other store method that hands a
|
|
709
|
+
* record back: the caller mutates its own copy freely, and the copy on disk is the copy that was
|
|
710
|
+
* written.
|
|
711
|
+
*/
|
|
712
|
+
setScheduledTask(task: ScheduledTask): Promise<ScheduledTask>;
|
|
713
|
+
/**
|
|
714
|
+
* Removes a scheduled task, and persists the removal.
|
|
715
|
+
*
|
|
716
|
+
* AN EMPTY LIST IS STORED AS ABSENCE, for the same reason `setTodos` stores an empty todo list as
|
|
717
|
+
* the absence of the key: a state file that says "scheduledTasks: []" claims the user considered a
|
|
718
|
+
* schedule and found none, and a fresh install has never considered anything.
|
|
719
|
+
*/
|
|
720
|
+
removeScheduledTask(id: string): Promise<void>;
|
|
691
721
|
/**
|
|
692
722
|
* Applies the fields a run's life can change, and nothing else.
|
|
693
723
|
*
|
package/dist/server/store.js
CHANGED
|
@@ -245,6 +245,27 @@ function trimWorkflows(workflows) {
|
|
|
245
245
|
}
|
|
246
246
|
workflows.splice(0, workflows.length, ...workflows.filter((plan) => !doomed.has(plan.id)));
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* How many scheduled tasks are kept.
|
|
250
|
+
*
|
|
251
|
+
* A task is a standing arrangement, not a record of something that happened, so the bound here is much
|
|
252
|
+
* looser than the run retention: a person who keeps a daily task for a year has one task, and the
|
|
253
|
+
* hundredth is already a sign that the list is being used as a log rather than as a list of intentions.
|
|
254
|
+
*/
|
|
255
|
+
export const SCHEDULED_TASK_RETENTION = 100;
|
|
256
|
+
/** Evicts the OLDEST scheduled tasks once there are more than `SCHEDULED_TASK_RETENTION`. */
|
|
257
|
+
function trimScheduledTasks(tasks) {
|
|
258
|
+
const excess = tasks.length - SCHEDULED_TASK_RETENTION;
|
|
259
|
+
if (excess <= 0)
|
|
260
|
+
return;
|
|
261
|
+
const doomed = new Set();
|
|
262
|
+
for (const task of [...tasks].sort((a, b) => a.createdAt - b.createdAt)) {
|
|
263
|
+
if (doomed.size >= excess)
|
|
264
|
+
break;
|
|
265
|
+
doomed.add(task.id);
|
|
266
|
+
}
|
|
267
|
+
tasks.splice(0, tasks.length, ...tasks.filter((task) => !doomed.has(task.id)));
|
|
268
|
+
}
|
|
248
269
|
/**
|
|
249
270
|
* Why a plan the previous process left `running` is stopped rather than resumed, in the same spirit and
|
|
250
271
|
* for the same audience as `ORPHANED_RUN_ERROR`.
|
|
@@ -576,9 +597,15 @@ export class Store {
|
|
|
576
597
|
// it is repaired again on every single boot - and until it is, anything reading the file sees the
|
|
577
598
|
// un-normalised shape rather than the one the store is actually running on.
|
|
578
599
|
const defaulted = ["workspaces", "conversations", "subagentRuns"].filter((key) => !Array.isArray(state[key])).length > 0
|
|
579
|
-
|| !Array.isArray(state.backgroundTasks)
|
|
600
|
+
|| !Array.isArray(state.backgroundTasks)
|
|
601
|
+
|| (state.scheduledTasks !== undefined && !Array.isArray(state.scheduledTasks));
|
|
580
602
|
if (!Array.isArray(state.backgroundTasks))
|
|
581
603
|
state.backgroundTasks = [];
|
|
604
|
+
// A scheduled-task list that is not a list is not a list of tasks. The key is dropped rather than
|
|
605
|
+
// coerced, so a corrupted file reads as "this install has no scheduled tasks" - the same thing a
|
|
606
|
+
// fresh install reads - instead of crashing the panel on every poll.
|
|
607
|
+
if (state.scheduledTasks !== undefined && !Array.isArray(state.scheduledTasks))
|
|
608
|
+
delete state.scheduledTasks;
|
|
582
609
|
// Every sweep runs before the store is constructed, so a single persist below publishes them together.
|
|
583
610
|
// Reconciling them in separate writes would put a file on disk in which one kind of orphan was fixed
|
|
584
611
|
// and the other was not - a half-repaired state that still needs a second restart to finish.
|
|
@@ -1970,6 +1997,50 @@ export class Store {
|
|
|
1970
1997
|
this.state.backgroundTasks = [];
|
|
1971
1998
|
await this.persistCollections("backgroundTasks");
|
|
1972
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Writes a scheduled task - new or changed - and persists it.
|
|
2002
|
+
*
|
|
2003
|
+
* ONE METHOD FOR BOTH, because the two are the same write. A task that has not run and a task that
|
|
2004
|
+
* has run are the same kind of thing with different field values, and splitting them into
|
|
2005
|
+
* "create" and "update" would leave the caller to decide which of two paths a given change takes -
|
|
2006
|
+
* which is exactly the kind of decision that ends up made twice, differently, in two places.
|
|
2007
|
+
*
|
|
2008
|
+
* The value is cloned on the way in and on the way out, like every other store method that hands a
|
|
2009
|
+
* record back: the caller mutates its own copy freely, and the copy on disk is the copy that was
|
|
2010
|
+
* written.
|
|
2011
|
+
*/
|
|
2012
|
+
async setScheduledTask(task) {
|
|
2013
|
+
if (!this.state.scheduledTasks)
|
|
2014
|
+
this.state.scheduledTasks = [];
|
|
2015
|
+
const stored = structuredClone(task);
|
|
2016
|
+
const index = this.state.scheduledTasks.findIndex((item) => item?.id === stored.id);
|
|
2017
|
+
if (index < 0)
|
|
2018
|
+
this.state.scheduledTasks.push(stored);
|
|
2019
|
+
else
|
|
2020
|
+
this.state.scheduledTasks[index] = stored;
|
|
2021
|
+
trimScheduledTasks(this.state.scheduledTasks);
|
|
2022
|
+
await this.persistCollections("scheduledTasks");
|
|
2023
|
+
return structuredClone(stored);
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Removes a scheduled task, and persists the removal.
|
|
2027
|
+
*
|
|
2028
|
+
* AN EMPTY LIST IS STORED AS ABSENCE, for the same reason `setTodos` stores an empty todo list as
|
|
2029
|
+
* the absence of the key: a state file that says "scheduledTasks: []" claims the user considered a
|
|
2030
|
+
* schedule and found none, and a fresh install has never considered anything.
|
|
2031
|
+
*/
|
|
2032
|
+
async removeScheduledTask(id) {
|
|
2033
|
+
if (!this.state.scheduledTasks)
|
|
2034
|
+
return;
|
|
2035
|
+
const remaining = this.state.scheduledTasks.filter((item) => item?.id !== id);
|
|
2036
|
+
if (remaining.length === this.state.scheduledTasks.length)
|
|
2037
|
+
return;
|
|
2038
|
+
if (remaining.length)
|
|
2039
|
+
this.state.scheduledTasks = remaining;
|
|
2040
|
+
else
|
|
2041
|
+
delete this.state.scheduledTasks;
|
|
2042
|
+
await this.persistCollections("scheduledTasks");
|
|
2043
|
+
}
|
|
1973
2044
|
/**
|
|
1974
2045
|
* Applies the fields a run's life can change, and nothing else.
|
|
1975
2046
|
*
|
|
@@ -2063,7 +2134,15 @@ export class Store {
|
|
|
2063
2134
|
await this.persist();
|
|
2064
2135
|
}
|
|
2065
2136
|
async persistCollections(key) {
|
|
2066
|
-
|
|
2137
|
+
const value = this.state[key];
|
|
2138
|
+
// A collection that is EMPTY is absent from the state, and an absent collection has no row to
|
|
2139
|
+
// write. The row is removed instead, because a repository that can only write rows would have to
|
|
2140
|
+
// store `[]` - which is a different claim from "the user never made one".
|
|
2141
|
+
if (value === undefined) {
|
|
2142
|
+
await this.persistTransaction({ kind: "collections.remove", name: key });
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
await this.persistTransaction({ kind: "collections.replace", values: { [key]: structuredClone(value) } });
|
|
2067
2146
|
}
|
|
2068
2147
|
async persistMetadata(conversationId) {
|
|
2069
2148
|
const { messages: _messages, ...metadata } = this.requireConversation(conversationId);
|
|
@@ -173,6 +173,24 @@ export declare const TODO_TOOL_NAME = "todowrite";
|
|
|
173
173
|
* tool then refuses in words instead of posting somewhere that does not exist.
|
|
174
174
|
*/
|
|
175
175
|
export declare const TODO_ENDPOINT_ENV = "SUPER_PI_TODO_ENDPOINT";
|
|
176
|
+
/**
|
|
177
|
+
* The name the model calls to manage the SCHEDULED TASKS.
|
|
178
|
+
*
|
|
179
|
+
* `scheduled_task`, with an underscore, because this is not one of OpenCode's tools and there is no
|
|
180
|
+
* prior name to match. The name says what it is: a task that runs on a schedule.
|
|
181
|
+
*/
|
|
182
|
+
export declare const SCHEDULED_TASK_TOOL_NAME = "scheduled_task";
|
|
183
|
+
/**
|
|
184
|
+
* The endpoint the `scheduled_task` tool dials. Another name in the same wire, written out on both
|
|
185
|
+
* sides for the same reason `TODO_ENDPOINT_ENV` states: `agents/super-pi-task/core.ts` is loaded by
|
|
186
|
+
* Pi's module loader and this file by tsc, so the two copies have to agree by hand -
|
|
187
|
+
* `scripts/scheduled-task-tool.test.mjs` asserts that they do.
|
|
188
|
+
*
|
|
189
|
+
* A SEPARATE variable rather than a path derived from `TODO_ENDPOINT_ENV`, for the reason that one
|
|
190
|
+
* states: a Pi can legitimately have one and not the other, and a derived path could not tell those
|
|
191
|
+
* apart from a misconfiguration.
|
|
192
|
+
*/
|
|
193
|
+
export declare const SCHEDULED_TASK_ENDPOINT_ENV = "SUPER_PI_SCHEDULED_TASK_ENDPOINT";
|
|
176
194
|
/**
|
|
177
195
|
* How many todos may be stored, and how long one may be.
|
|
178
196
|
*
|
package/dist/server/task-tool.js
CHANGED
|
@@ -230,6 +230,24 @@ export const TODO_TOOL_NAME = "todowrite";
|
|
|
230
230
|
* tool then refuses in words instead of posting somewhere that does not exist.
|
|
231
231
|
*/
|
|
232
232
|
export const TODO_ENDPOINT_ENV = "SUPER_PI_TODO_ENDPOINT";
|
|
233
|
+
/**
|
|
234
|
+
* The name the model calls to manage the SCHEDULED TASKS.
|
|
235
|
+
*
|
|
236
|
+
* `scheduled_task`, with an underscore, because this is not one of OpenCode's tools and there is no
|
|
237
|
+
* prior name to match. The name says what it is: a task that runs on a schedule.
|
|
238
|
+
*/
|
|
239
|
+
export const SCHEDULED_TASK_TOOL_NAME = "scheduled_task";
|
|
240
|
+
/**
|
|
241
|
+
* The endpoint the `scheduled_task` tool dials. Another name in the same wire, written out on both
|
|
242
|
+
* sides for the same reason `TODO_ENDPOINT_ENV` states: `agents/super-pi-task/core.ts` is loaded by
|
|
243
|
+
* Pi's module loader and this file by tsc, so the two copies have to agree by hand -
|
|
244
|
+
* `scripts/scheduled-task-tool.test.mjs` asserts that they do.
|
|
245
|
+
*
|
|
246
|
+
* A SEPARATE variable rather than a path derived from `TODO_ENDPOINT_ENV`, for the reason that one
|
|
247
|
+
* states: a Pi can legitimately have one and not the other, and a derived path could not tell those
|
|
248
|
+
* apart from a misconfiguration.
|
|
249
|
+
*/
|
|
250
|
+
export const SCHEDULED_TASK_ENDPOINT_ENV = "SUPER_PI_SCHEDULED_TASK_ENDPOINT";
|
|
233
251
|
/**
|
|
234
252
|
* How many todos may be stored, and how long one may be.
|
|
235
253
|
*
|
package/dist/server/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type ModelFilterRule = {
|
|
|
5
5
|
label?: string;
|
|
6
6
|
};
|
|
7
7
|
import type { WorkflowRun } from "./workflow.js";
|
|
8
|
+
import type { ScheduledTask } from "./scheduled-tasks.js";
|
|
8
9
|
/**
|
|
9
10
|
* The lifecycle states a `Conversation.status` moves through while a turn is in flight. `waiting` is the
|
|
10
11
|
* gap between "prompt handed to the LLM" and "the first response token arrived" - holding it apart from
|
|
@@ -835,6 +836,7 @@ export type AppState = {
|
|
|
835
836
|
backgroundTasks: BackgroundTaskSummary[];
|
|
836
837
|
todosByConversation?: Record<string, TodoItem[]>;
|
|
837
838
|
workflows?: WorkflowRun[];
|
|
839
|
+
scheduledTasks?: ScheduledTask[];
|
|
838
840
|
customAgentDirs?: string[];
|
|
839
841
|
};
|
|
840
842
|
export type TodoItem = {
|
|
@@ -1372,6 +1374,60 @@ export type ClientCommandBody =
|
|
|
1372
1374
|
/** Restarts the Super Pi server by spawning the same restart script used by the auto-updater. */
|
|
1373
1375
|
| {
|
|
1374
1376
|
type: "super_pi.restart";
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* A scheduled task is created.
|
|
1380
|
+
*
|
|
1381
|
+
* The schedule arrives as an opaque value and is validated on the SERVER, in the one place that owns
|
|
1382
|
+
* what a schedule means. The browser cannot send a half-valid schedule, because it does not know
|
|
1383
|
+
* what a half-valid schedule is; it sends the fields the panel collected and the server decides
|
|
1384
|
+
* whether they describe a schedule.
|
|
1385
|
+
*/
|
|
1386
|
+
| {
|
|
1387
|
+
type: "scheduledTask.create";
|
|
1388
|
+
name?: string;
|
|
1389
|
+
prompt: string;
|
|
1390
|
+
schedule: unknown;
|
|
1391
|
+
workspaceId: string;
|
|
1392
|
+
model?: ModelSelection | null;
|
|
1393
|
+
agentId?: string | null;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* A scheduled task is changed. Every field is optional, and the ones that are absent are the ones
|
|
1397
|
+
* that do not change - a panel that sends the whole task back would fight the scheduler's own
|
|
1398
|
+
* bookkeeping over `nextRunAt`, which is not the panel's to write.
|
|
1399
|
+
*/
|
|
1400
|
+
| {
|
|
1401
|
+
type: "scheduledTask.update";
|
|
1402
|
+
id: string;
|
|
1403
|
+
name?: string;
|
|
1404
|
+
prompt?: string;
|
|
1405
|
+
schedule?: unknown;
|
|
1406
|
+
model?: ModelSelection | null;
|
|
1407
|
+
agentId?: string | null;
|
|
1408
|
+
}
|
|
1409
|
+
/** A scheduled task is removed. Its conversation and the answers it produced stay. */
|
|
1410
|
+
| {
|
|
1411
|
+
type: "scheduledTask.delete";
|
|
1412
|
+
id: string;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* A scheduled task runs NOW, once, without touching its schedule.
|
|
1416
|
+
*
|
|
1417
|
+
* It is a separate command rather than a flag on `update` because it is a different act: `update`
|
|
1418
|
+
* changes what the task IS, and this changes what the task DID. The one thing it shares with the
|
|
1419
|
+
* clock is the run itself - it goes through the same path, so a manual run and a scheduled run
|
|
1420
|
+
* behave identically.
|
|
1421
|
+
*/
|
|
1422
|
+
| {
|
|
1423
|
+
type: "scheduledTask.trigger";
|
|
1424
|
+
id: string;
|
|
1425
|
+
}
|
|
1426
|
+
/** A scheduled task is turned on or off. */
|
|
1427
|
+
| {
|
|
1428
|
+
type: "scheduledTask.toggle";
|
|
1429
|
+
id: string;
|
|
1430
|
+
enabled: boolean;
|
|
1375
1431
|
} | ({
|
|
1376
1432
|
type: "extension.ui.answer";
|
|
1377
1433
|
conversationId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.132",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,9 +39,11 @@
|
|
|
39
39
|
"start": "node dist/server/index.js",
|
|
40
40
|
"restart:server": "node scripts/restart-server.mjs",
|
|
41
41
|
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
42
|
-
"check": "node scripts/run-check.mjs"
|
|
42
|
+
"check": "node scripts/run-check.mjs",
|
|
43
|
+
"test": "node scripts/test.mjs"
|
|
43
44
|
},
|
|
44
45
|
"check:logic": [
|
|
46
|
+
"scripts/test-runner.test.mjs",
|
|
45
47
|
"scripts/stream-checkpoint.test.mjs",
|
|
46
48
|
"scripts/stream-transport.test.mjs",
|
|
47
49
|
"scripts/inline-thinking.test.mjs",
|
|
@@ -104,6 +106,7 @@
|
|
|
104
106
|
"scripts/file-content-search.test.mjs",
|
|
105
107
|
"scripts/file-diff-client.test.mjs",
|
|
106
108
|
"scripts/editor-languages.test.mjs",
|
|
109
|
+
"scripts/editor-blame.test.mjs",
|
|
107
110
|
"scripts/task-tool.test.mjs",
|
|
108
111
|
"scripts/tool-register-env.test.mjs",
|
|
109
112
|
"scripts/bg-task-zombie.test.mjs",
|
|
@@ -122,6 +125,8 @@
|
|
|
122
125
|
"scripts/provider-pi-reads.test.mjs",
|
|
123
126
|
"scripts/active-memory.test.mjs",
|
|
124
127
|
"scripts/knowledge-base.test.mjs",
|
|
128
|
+
"scripts/knowledge-import-path.test.mjs",
|
|
129
|
+
"scripts/knowledge-import-identity.test.mjs",
|
|
125
130
|
"scripts/tool-runner.test.mjs",
|
|
126
131
|
"scripts/tool-runtime-api.test.mjs",
|
|
127
132
|
"scripts/tool-sharing.test.mjs",
|
|
@@ -142,7 +147,11 @@
|
|
|
142
147
|
"scripts/web-tools.test.mjs",
|
|
143
148
|
"scripts/document-service.test.mjs",
|
|
144
149
|
"scripts/document-block-parse.test.mjs",
|
|
145
|
-
"scripts/document-write-overwrite.test.mjs"
|
|
150
|
+
"scripts/document-write-overwrite.test.mjs",
|
|
151
|
+
"scripts/feishu.test.mjs",
|
|
152
|
+
"scripts/scheduled-tasks.test.mjs",
|
|
153
|
+
"scripts/scheduled-task-scope.test.mjs",
|
|
154
|
+
"scripts/scheduled-task-tool.test.mjs"
|
|
146
155
|
],
|
|
147
156
|
"check:browser": [
|
|
148
157
|
"scripts/interrupt-browser.test.mjs",
|
|
@@ -159,6 +168,7 @@
|
|
|
159
168
|
"scripts/session-state-animation-browser.test.mjs",
|
|
160
169
|
"scripts/agent-vis-card-browser.test.mjs",
|
|
161
170
|
"scripts/queue-dock.test.mjs",
|
|
171
|
+
"scripts/scheduled-tasks-browser.test.mjs",
|
|
162
172
|
"scripts/md-render-browser.test.mjs",
|
|
163
173
|
"scripts/right-sidebar-browser.test.mjs",
|
|
164
174
|
"scripts/sidebar-tools-browser.test.mjs",
|
|
@@ -166,6 +176,8 @@
|
|
|
166
176
|
"scripts/session-context-menu-browser.test.mjs",
|
|
167
177
|
"scripts/tool-call-blocks-browser.test.mjs",
|
|
168
178
|
"scripts/files-tab-browser.test.mjs",
|
|
179
|
+
"scripts/editor-blame-browser.test.mjs",
|
|
180
|
+
"scripts/file-panel-external-poll-browser.test.mjs",
|
|
169
181
|
"scripts/file-quickopen-browser.test.mjs",
|
|
170
182
|
"scripts/file-content-search-browser.test.mjs",
|
|
171
183
|
"scripts/file-changes-browser.test.mjs",
|
|
@@ -193,7 +205,8 @@
|
|
|
193
205
|
"scripts/terminal-browser.test.mjs",
|
|
194
206
|
"scripts/ux-polish-browser.test.mjs",
|
|
195
207
|
"scripts/prompt-templates-browser.test.mjs",
|
|
196
|
-
"scripts/documents-browser.test.mjs"
|
|
208
|
+
"scripts/documents-browser.test.mjs",
|
|
209
|
+
"scripts/feishu-browser.test.mjs"
|
|
197
210
|
],
|
|
198
211
|
"dependencies": {
|
|
199
212
|
"@codemirror/commands": "^6.11.1",
|