@pi-unipi/subagents 2.6.1 → 2.6.2
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 +89 -78
- package/agents/delegate.md +14 -0
- package/agents/oracle.md +78 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/worker.md +59 -0
- package/package.json +19 -17
- package/prompts/council.md +48 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +43 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/subagents/SKILL.md +49 -0
- package/skills/subagents/references/constraints-and-recipes.md +259 -0
- package/skills/subagents/references/execution-controls.md +454 -0
- package/skills/subagents/references/management-authoring-rpc.md +161 -0
- package/skills/subagents/references/multi-lane-orchestration.md +39 -0
- package/skills/subagents/references/prompting-and-roles.md +267 -0
- package/src/acceptance.ts +331 -0
- package/src/agent-manager.ts +405 -0
- package/src/agent-memory.ts +300 -0
- package/src/agent-overrides.ts +183 -0
- package/src/agent-runner.ts +265 -0
- package/src/async-runner.ts +417 -0
- package/src/authority-policy.ts +47 -0
- package/src/budgets.ts +285 -0
- package/src/child-safety.ts +140 -0
- package/src/config.ts +226 -0
- package/src/conversation-viewer.ts +281 -0
- package/src/core-compat.ts +94 -0
- package/src/custom-agents.ts +323 -0
- package/src/file-system-retry.ts +50 -0
- package/src/fleet-data.ts +61 -0
- package/src/fleet-view.ts +316 -0
- package/src/foreground-detach.ts +59 -0
- package/src/fork-context.ts +285 -0
- package/src/global.d.ts +7 -0
- package/src/guide.ts +129 -0
- package/src/index.ts +1149 -0
- package/src/mission-state.ts +133 -0
- package/src/mission-store.ts +434 -0
- package/src/model-resolver.ts +79 -0
- package/src/output-limits.ts +142 -0
- package/src/parity-types.ts +315 -0
- package/src/pi-args.ts +161 -0
- package/src/pi-spawn.ts +147 -0
- package/src/result-files.ts +232 -0
- package/src/result-watcher.ts +221 -0
- package/src/retained-children.ts +176 -0
- package/src/run-fanout-budget.ts +356 -0
- package/src/scheduled-runs.ts +351 -0
- package/src/schemas.ts +263 -0
- package/src/slash-commands.ts +77 -0
- package/src/supervisor-channel.ts +249 -0
- package/src/tool-handler.ts +1144 -0
- package/src/types.ts +238 -0
- package/src/widget.ts +447 -0
- package/src/workflow-script.ts +737 -0
- package/src/workflow-worker.ts +384 -0
- package/src/worktree.ts +614 -0
- package/dist/agent-manager.d.ts +0 -81
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -295
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -51
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -262
- package/dist/agent-runner.js.map +0 -1
- package/dist/config.d.ts +0 -24
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -132
- package/dist/config.js.map +0 -1
- package/dist/conversation-viewer.d.ts +0 -40
- package/dist/conversation-viewer.d.ts.map +0 -1
- package/dist/conversation-viewer.js +0 -276
- package/dist/conversation-viewer.js.map +0 -1
- package/dist/core-compat.d.ts +0 -21
- package/dist/core-compat.d.ts.map +0 -1
- package/dist/core-compat.js +0 -86
- package/dist/core-compat.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -14
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -113
- package/dist/custom-agents.js.map +0 -1
- package/dist/file-lock.d.ts +0 -42
- package/dist/file-lock.d.ts.map +0 -1
- package/dist/file-lock.js +0 -91
- package/dist/file-lock.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -758
- package/dist/index.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -19
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -61
- package/dist/model-resolver.js.map +0 -1
- package/dist/types.d.ts +0 -98
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -47
- package/dist/types.js.map +0 -1
- package/dist/widget.d.ts +0 -56
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -396
- package/dist/widget.js.map +0 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Durable scheduled runs
|
|
3
|
+
*
|
|
4
|
+
* Ported from pi-subagents src/runs/background/scheduled-runs.ts (core).
|
|
5
|
+
* Schedules persist per project under OUR layout:
|
|
6
|
+
* ~/.unipi/schedules/<project-hash>/<id>.json. One-shot (+delay or ISO with
|
|
7
|
+
* timezone) and fixed-interval (30m/6h/2d/2w) triggers; overlap policy skip;
|
|
8
|
+
* catchUp none|latest; pause/resume/run/delete; run history receipts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
|
|
16
|
+
export type ScheduleTrigger =
|
|
17
|
+
| { kind: "once"; at: string; atMs: number }
|
|
18
|
+
| { kind: "interval"; every: string; everyMs: number; anchorAt: string; nextRunAt: string };
|
|
19
|
+
|
|
20
|
+
export interface ScheduleRecord {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
trigger: ScheduleTrigger;
|
|
26
|
+
/** Launch parameters captured for the run (agent + task). */
|
|
27
|
+
agent: string;
|
|
28
|
+
task: string;
|
|
29
|
+
overlap: "skip";
|
|
30
|
+
catchUp: "none" | "latest";
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
paused: boolean;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
activeRunId?: string;
|
|
36
|
+
lastRunId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScheduleRunRecord {
|
|
40
|
+
schemaVersion: 1;
|
|
41
|
+
id: string;
|
|
42
|
+
scheduleId: string;
|
|
43
|
+
plannedAt: string;
|
|
44
|
+
dueReason: "timer" | "run-due" | "manual";
|
|
45
|
+
state: "pending" | "running" | "completed" | "failed" | "skipped";
|
|
46
|
+
startedAt?: string;
|
|
47
|
+
completedAt?: string;
|
|
48
|
+
asyncRunId?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Parsing (reference semantics)
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
export function parseScheduledRunTime(at: string, now = Date.now()): number {
|
|
57
|
+
const trimmed = at.trim();
|
|
58
|
+
const relative = trimmed.match(/^\+(\d+)(s|m|h|d)$/);
|
|
59
|
+
if (relative) {
|
|
60
|
+
const amount = Number(relative[1]);
|
|
61
|
+
if (!Number.isSafeInteger(amount) || amount < 1) {
|
|
62
|
+
throw new Error(`Invalid at value "${at}". Relative delays must be positive, such as "+10m".`);
|
|
63
|
+
}
|
|
64
|
+
const unitMs = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[relative[2] as "s" | "m" | "h" | "d"];
|
|
65
|
+
const result = now + amount * unitMs;
|
|
66
|
+
if (!Number.isSafeInteger(result)) throw new Error(`Invalid at value "${at}". Relative delay is too large.`);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
const iso = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/);
|
|
70
|
+
if (!iso) throw new Error(`Invalid at value "${at}". Use a one-shot delay such as "+10m" or an ISO timestamp with timezone.`);
|
|
71
|
+
const year = Number(iso[1]);
|
|
72
|
+
const month = Number(iso[2]);
|
|
73
|
+
const day = Number(iso[3]);
|
|
74
|
+
const hour = Number(iso[4]);
|
|
75
|
+
const minute = Number(iso[5]);
|
|
76
|
+
const second = iso[6] === undefined ? 0 : Number(iso[6]);
|
|
77
|
+
const zone = iso[7]!;
|
|
78
|
+
const offsetHour = zone === "Z" ? 0 : Number(zone.slice(1, 3));
|
|
79
|
+
const offsetMinute = zone === "Z" ? 0 : Number(zone.slice(4, 6));
|
|
80
|
+
const daysInMonth = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
|
|
81
|
+
const parsed = Date.parse(trimmed);
|
|
82
|
+
if (
|
|
83
|
+
month < 1 || month > 12 || day < 1 || day > daysInMonth || hour > 23 || minute > 59 ||
|
|
84
|
+
second > 59 || offsetHour > 23 || offsetMinute > 59 || !Number.isFinite(parsed)
|
|
85
|
+
) {
|
|
86
|
+
throw new Error(`Invalid at value "${at}". Use a valid ISO timestamp.`);
|
|
87
|
+
}
|
|
88
|
+
if (parsed <= now) throw new Error(`Scheduled time ${new Date(parsed).toISOString()} is in the past.`);
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseScheduleInterval(every: string): number {
|
|
93
|
+
const match = every.trim().match(/^(\d+)(m|h|d|w)$/);
|
|
94
|
+
if (!match) {
|
|
95
|
+
throw new Error(`Invalid every value "${every}". This first recurring slice supports fixed intervals such as "30m", "6h", "2d", or "2w".`);
|
|
96
|
+
}
|
|
97
|
+
const amount = Number(match[1]);
|
|
98
|
+
if (!Number.isSafeInteger(amount) || amount < 1) throw new Error(`Invalid every value "${every}". Interval must be positive.`);
|
|
99
|
+
const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[match[2] as "m" | "h" | "d" | "w"];
|
|
100
|
+
const result = amount * unitMs;
|
|
101
|
+
if (!Number.isSafeInteger(result)) throw new Error(`Invalid every value "${every}". Interval is too large.`);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Store — OUR layout: ~/.unipi/schedules/<project-hash>/
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
export function scheduleStorePath(projectRoot: string, storeRoot?: string): string {
|
|
110
|
+
const resolved = path.resolve(projectRoot);
|
|
111
|
+
if (!storeRoot) return path.join(homedir(), ".unipi", "schedules", projectHashKey(resolved));
|
|
112
|
+
const projectKey = createHash("sha256").update(resolved).digest("hex").slice(0, 20);
|
|
113
|
+
return path.join(storeRoot.startsWith("~/") ? path.join(homedir(), storeRoot.slice(2)) : storeRoot, projectKey);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function projectHashKey(projectRoot: string): string {
|
|
117
|
+
return createHash("sha256").update(projectRoot).digest("hex").slice(0, 16);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeAtomic(filePath: string, value: unknown): void {
|
|
121
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
122
|
+
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
123
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
124
|
+
fs.renameSync(tmp, filePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class ScheduledRunManager {
|
|
128
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
private projectRoot: string,
|
|
132
|
+
private opts: {
|
|
133
|
+
storeRoot?: string;
|
|
134
|
+
maxPending?: number;
|
|
135
|
+
/** Launch a due schedule. Returns the async run id. */
|
|
136
|
+
launch: (record: ScheduleRecord, dueReason: ScheduleRunRecord["dueReason"]) => Promise<string>;
|
|
137
|
+
pollIntervalMs?: number;
|
|
138
|
+
},
|
|
139
|
+
) {}
|
|
140
|
+
|
|
141
|
+
get storeDir(): string {
|
|
142
|
+
return scheduleStorePath(this.projectRoot, this.opts.storeRoot);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private recordPath(id: string): string {
|
|
146
|
+
return path.join(this.storeDir, `${id}.json`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private historyPath(id: string): string {
|
|
150
|
+
return path.join(this.storeDir, `${id}.runs.json`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
create(input: {
|
|
154
|
+
name: string;
|
|
155
|
+
agent: string;
|
|
156
|
+
task: string;
|
|
157
|
+
at?: string;
|
|
158
|
+
every?: string;
|
|
159
|
+
catchUp?: "none" | "latest";
|
|
160
|
+
timeoutMs?: number;
|
|
161
|
+
}, now = Date.now()): ScheduleRecord {
|
|
162
|
+
if (!input.name?.trim()) throw new Error("schedule name must be a non-empty string.");
|
|
163
|
+
if (!input.agent?.trim()) throw new Error("schedule requires an agent.");
|
|
164
|
+
if (!input.task?.trim()) throw new Error("schedule requires a task.");
|
|
165
|
+
let trigger: ScheduleTrigger;
|
|
166
|
+
if (input.at !== undefined && input.every !== undefined) {
|
|
167
|
+
throw new Error("Use either 'at' (one-shot) or 'every' (recurring), not both.");
|
|
168
|
+
}
|
|
169
|
+
if (input.at !== undefined) {
|
|
170
|
+
const atMs = parseScheduledRunTime(input.at, now);
|
|
171
|
+
trigger = { kind: "once", at: input.at.trim(), atMs };
|
|
172
|
+
} else if (input.every !== undefined) {
|
|
173
|
+
const everyMs = parseScheduleInterval(input.every);
|
|
174
|
+
trigger = {
|
|
175
|
+
kind: "interval",
|
|
176
|
+
every: input.every.trim(),
|
|
177
|
+
everyMs,
|
|
178
|
+
anchorAt: new Date(now).toISOString(),
|
|
179
|
+
nextRunAt: new Date(now + everyMs).toISOString(),
|
|
180
|
+
};
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error("schedule requires 'at' (one-shot) or 'every' (recurring).");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// maxPending cap
|
|
186
|
+
const existing = this.list();
|
|
187
|
+
const maxPending = this.opts.maxPending ?? 20;
|
|
188
|
+
if (existing.filter((r) => !r.paused && r.trigger.kind === "interval").length >= maxPending) {
|
|
189
|
+
throw new Error(`Pending schedule cap reached (${maxPending}). Delete or pause schedules first.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const record: ScheduleRecord = {
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
id: randomUUID(),
|
|
195
|
+
name: input.name.trim(),
|
|
196
|
+
cwd: path.resolve(this.projectRoot),
|
|
197
|
+
trigger,
|
|
198
|
+
agent: input.agent.trim(),
|
|
199
|
+
task: input.task.trim(),
|
|
200
|
+
overlap: "skip",
|
|
201
|
+
catchUp: input.catchUp ?? "latest",
|
|
202
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
203
|
+
paused: false,
|
|
204
|
+
createdAt: new Date(now).toISOString(),
|
|
205
|
+
updatedAt: new Date(now).toISOString(),
|
|
206
|
+
};
|
|
207
|
+
writeAtomic(this.recordPath(record.id), record);
|
|
208
|
+
return record;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
list(): ScheduleRecord[] {
|
|
212
|
+
let entries: fs.Dirent[];
|
|
213
|
+
try {
|
|
214
|
+
entries = fs.readdirSync(this.storeDir, { withFileTypes: true });
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const records: ScheduleRecord[] = [];
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name.endsWith(".runs.json")) continue;
|
|
221
|
+
try {
|
|
222
|
+
records.push(JSON.parse(fs.readFileSync(path.join(this.storeDir, entry.name), "utf8")) as ScheduleRecord);
|
|
223
|
+
} catch {
|
|
224
|
+
// Skip corrupt records.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
show(idOrPrefix: string): ScheduleRecord | undefined {
|
|
231
|
+
return this.list().find((r) => r.id.startsWith(idOrPrefix));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private save(record: ScheduleRecord): void {
|
|
235
|
+
writeAtomic(this.recordPath(record.id), record);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setPaused(idOrPrefix: string, paused: boolean): ScheduleRecord {
|
|
239
|
+
const record = this.show(idOrPrefix);
|
|
240
|
+
if (!record) throw new Error(`No schedule matches "${idOrPrefix}".`);
|
|
241
|
+
record.paused = paused;
|
|
242
|
+
record.updatedAt = new Date().toISOString();
|
|
243
|
+
this.save(record);
|
|
244
|
+
return record;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
delete(idOrPrefix: string): boolean {
|
|
248
|
+
const record = this.show(idOrPrefix);
|
|
249
|
+
if (!record) return false;
|
|
250
|
+
fs.rmSync(this.recordPath(record.id), { force: true });
|
|
251
|
+
fs.rmSync(this.historyPath(record.id), { force: true });
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Run a schedule immediately (manual trigger). */
|
|
256
|
+
async runNow(idOrPrefix: string): Promise<ScheduleRunRecord> {
|
|
257
|
+
const record = this.show(idOrPrefix);
|
|
258
|
+
if (!record) throw new Error(`No schedule matches "${idOrPrefix}".`);
|
|
259
|
+
return this.execute(record, "manual");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async execute(record: ScheduleRecord, dueReason: ScheduleRunRecord["dueReason"]): Promise<ScheduleRunRecord> {
|
|
263
|
+
// Overlap policy: skip when a run is already active.
|
|
264
|
+
if (record.activeRunId) {
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion: 1,
|
|
267
|
+
id: randomUUID(),
|
|
268
|
+
scheduleId: record.id,
|
|
269
|
+
plannedAt: new Date().toISOString(),
|
|
270
|
+
dueReason,
|
|
271
|
+
state: "skipped",
|
|
272
|
+
error: "overlap: previous run still active",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const runRecord: ScheduleRunRecord = {
|
|
277
|
+
schemaVersion: 1,
|
|
278
|
+
id: randomUUID(),
|
|
279
|
+
scheduleId: record.id,
|
|
280
|
+
plannedAt: new Date().toISOString(),
|
|
281
|
+
dueReason,
|
|
282
|
+
state: "running",
|
|
283
|
+
startedAt: new Date().toISOString(),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const asyncRunId = await this.opts.launch(record, dueReason);
|
|
288
|
+
runRecord.asyncRunId = asyncRunId;
|
|
289
|
+
runRecord.state = "completed";
|
|
290
|
+
} catch (error) {
|
|
291
|
+
runRecord.state = "failed";
|
|
292
|
+
runRecord.error = error instanceof Error ? error.message : String(error);
|
|
293
|
+
}
|
|
294
|
+
runRecord.completedAt = new Date().toISOString();
|
|
295
|
+
|
|
296
|
+
// Update the schedule + append history.
|
|
297
|
+
record.lastRunId = runRecord.id;
|
|
298
|
+
record.activeRunId = undefined;
|
|
299
|
+
record.updatedAt = new Date().toISOString();
|
|
300
|
+
if (record.trigger.kind === "interval") {
|
|
301
|
+
record.trigger.nextRunAt = new Date(Date.now() + record.trigger.everyMs).toISOString();
|
|
302
|
+
}
|
|
303
|
+
this.save(record);
|
|
304
|
+
|
|
305
|
+
const history = this.readHistory(record.id);
|
|
306
|
+
history.push(runRecord);
|
|
307
|
+
writeAtomic(this.historyPath(record.id), history);
|
|
308
|
+
return runRecord;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
readHistory(idOrPrefix: string): ScheduleRunRecord[] {
|
|
312
|
+
const record = this.show(idOrPrefix);
|
|
313
|
+
if (!record) return [];
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(fs.readFileSync(this.historyPath(record.id), "utf8")) as ScheduleRunRecord[];
|
|
316
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
317
|
+
} catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Tick: launch all due schedules. Called by the poll timer. */
|
|
323
|
+
async runDue(): Promise<ScheduleRunRecord[]> {
|
|
324
|
+
const results: ScheduleRunRecord[] = [];
|
|
325
|
+
const now = Date.now();
|
|
326
|
+
for (const record of this.list()) {
|
|
327
|
+
if (record.paused) continue;
|
|
328
|
+
let due = false;
|
|
329
|
+
if (record.trigger.kind === "once") {
|
|
330
|
+
due = record.trigger.atMs <= now;
|
|
331
|
+
} else {
|
|
332
|
+
due = Date.parse(record.trigger.nextRunAt) <= now;
|
|
333
|
+
}
|
|
334
|
+
if (due) results.push(await this.execute(record, "timer"));
|
|
335
|
+
}
|
|
336
|
+
return results;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
startPolling(): void {
|
|
340
|
+
if (this.timer) return;
|
|
341
|
+
this.timer = setInterval(() => {
|
|
342
|
+
void this.runDue().catch(() => {});
|
|
343
|
+
}, this.opts.pollIntervalMs ?? 60_000);
|
|
344
|
+
this.timer.unref?.();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
stopPolling(): void {
|
|
348
|
+
if (this.timer) clearInterval(this.timer);
|
|
349
|
+
this.timer = undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Tool parameter schemas (pi-subagents parity)
|
|
3
|
+
*
|
|
4
|
+
* Ported from nicobailon/pi-subagents src/extension/schemas.ts. Parameter
|
|
5
|
+
* names follow the reference (they are arguments, not tool names — no
|
|
6
|
+
* convention clash); our legacy spawn_helper params (type/prompt/description/
|
|
7
|
+
* run_in_background/max_turns) remain accepted as aliases.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
|
|
12
|
+
const TurnBudgetOverride = Type.Object(
|
|
13
|
+
{
|
|
14
|
+
maxTurns: Type.Integer({ minimum: 1 }),
|
|
15
|
+
graceTurns: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
description:
|
|
20
|
+
"Optional assistant-turn budget. At maxTurns the child is asked to wrap up; after graceTurns (default 1) additional assistant turns it is aborted and partial output is returned.",
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const ToolBudgetBlock = Type.Unsafe({
|
|
25
|
+
anyOf: [
|
|
26
|
+
{ type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
27
|
+
{ type: "string", enum: ["*"] },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const ToolBudgetOverride = Type.Object(
|
|
32
|
+
{
|
|
33
|
+
soft: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
34
|
+
hard: Type.Integer({ minimum: 1 }),
|
|
35
|
+
block: Type.Optional(ToolBudgetBlock),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
description:
|
|
40
|
+
"Optional child tool-call budget. soft nudges the child to finalize; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize. Final assistant text is never blocked.",
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const UsageBudgetLimitOverride = Type.Object(
|
|
45
|
+
{
|
|
46
|
+
soft: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
47
|
+
hard: Type.Number({ exclusiveMinimum: 0 }),
|
|
48
|
+
},
|
|
49
|
+
{ additionalProperties: false },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const UsageBudgetOverride = Type.Object(
|
|
53
|
+
{
|
|
54
|
+
tokens: Type.Optional(UsageBudgetLimitOverride),
|
|
55
|
+
costUsd: Type.Optional(UsageBudgetLimitOverride),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
description:
|
|
60
|
+
"Optional root-only reported-usage budget. Soft limits are status-only. Hard limits prevent later child launches after usage is reconciled; running children are not stopped.",
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const MaxOutputOverride = Type.Object(
|
|
65
|
+
{
|
|
66
|
+
bytes: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
67
|
+
lines: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
68
|
+
},
|
|
69
|
+
{ additionalProperties: false, description: "Final output truncation limits. Defaults: 200KB / 5000 lines." },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* spawn_helper parameter surface. Legacy fields (type, prompt, description,
|
|
74
|
+
* run_in_background, max_turns) are the unipi convention and stay primary;
|
|
75
|
+
* reference fields (agent, task, action, workflowScript, budgets, ...) join
|
|
76
|
+
* them. Execution paths map: agent→type, task→prompt, async→run_in_background.
|
|
77
|
+
*/
|
|
78
|
+
export const SpawnHelperParams = Type.Object({
|
|
79
|
+
// ---- unipi legacy (preserved) ----
|
|
80
|
+
type: Type.Optional(
|
|
81
|
+
Type.String({ description: "Agent type (alias of agent). Enabled builtin or custom type." }),
|
|
82
|
+
),
|
|
83
|
+
prompt: Type.Optional(
|
|
84
|
+
Type.String({ description: "The task for the agent to perform (alias of task)." }),
|
|
85
|
+
),
|
|
86
|
+
description: Type.Optional(
|
|
87
|
+
Type.String({ description: "A short (3-5 word) description of the task." }),
|
|
88
|
+
),
|
|
89
|
+
run_in_background: Type.Optional(
|
|
90
|
+
Type.Boolean({ description: "Run in background (alias of async). Returns helper ID immediately." }),
|
|
91
|
+
),
|
|
92
|
+
max_turns: Type.Optional(
|
|
93
|
+
Type.Number({ minimum: 1, description: "Max agentic turns before stopping." }),
|
|
94
|
+
),
|
|
95
|
+
|
|
96
|
+
// ---- reference surface ----
|
|
97
|
+
agent: Type.Optional(
|
|
98
|
+
Type.String({ description: "Agent for one-child execution, or target for management actions." }),
|
|
99
|
+
),
|
|
100
|
+
task: Type.Optional(
|
|
101
|
+
Type.String({
|
|
102
|
+
description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript.",
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
action: Type.Optional(
|
|
106
|
+
Type.String({
|
|
107
|
+
minLength: 1,
|
|
108
|
+
description:
|
|
109
|
+
"Optional management/control action (list, get, status, steer, stop, doctor, ...). Omit for execution.",
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
id: Type.Optional(Type.String({ description: "Run id/prefix for status, interrupt, steer." })),
|
|
113
|
+
runId: Type.Optional(Type.String({ description: "Target run ID. Prefer id." })),
|
|
114
|
+
dir: Type.Optional(Type.String({ description: "Async run directory for status/stop/resume/steer." })),
|
|
115
|
+
index: Type.Optional(
|
|
116
|
+
Type.Integer({ minimum: 0, description: "Zero-based child index for actions targeting a specific child." }),
|
|
117
|
+
),
|
|
118
|
+
view: Type.Optional(
|
|
119
|
+
Type.String({
|
|
120
|
+
enum: ["fleet", "transcript"],
|
|
121
|
+
description: "Optional status view: fleet surface or transcript tail.",
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
lines: Type.Optional(
|
|
125
|
+
Type.Integer({ minimum: 1, maximum: 500, description: "Max transcript lines for status view=transcript. Default 80." }),
|
|
126
|
+
),
|
|
127
|
+
topic: Type.Optional(Type.String({ description: "Guide topic for action=guide." })),
|
|
128
|
+
message: Type.Optional(
|
|
129
|
+
Type.String({ description: "Follow-up message for resume, live guidance for steer." }),
|
|
130
|
+
),
|
|
131
|
+
mode: Type.Optional(
|
|
132
|
+
Type.String({
|
|
133
|
+
enum: ["steer", "follow_up", "auto"],
|
|
134
|
+
description: "Delivery mode for action=steer.",
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
additional: Type.Optional(
|
|
138
|
+
Type.Integer({
|
|
139
|
+
minimum: 1,
|
|
140
|
+
description: "Positive launches to add with action=grant-spawn-budget.",
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
144
|
+
mission: Type.Optional(
|
|
145
|
+
Type.Unsafe({
|
|
146
|
+
anyOf: [{ type: "object", additionalProperties: true }, { type: "boolean", enum: [false] }],
|
|
147
|
+
description: "Mission object, or false for no mission.",
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
config: Type.Optional(
|
|
151
|
+
Type.Unsafe({
|
|
152
|
+
anyOf: [{ type: "object", additionalProperties: true }, { type: "string" }],
|
|
153
|
+
description: "Agent config for create/update. Object or JSON string.",
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
workflowScript: Type.Optional(
|
|
157
|
+
Type.String({
|
|
158
|
+
minLength: 1,
|
|
159
|
+
description:
|
|
160
|
+
"Trusted inline JavaScript statement body. Use await runs.run(key, {agent, task}) for one child, runs.all([...]) for parallel fanout, await runs.steer(key, message, opts) to steer. Plain helper functions or explicit Promise chains only; nested async helpers are rejected. Explicit return for output. async:true for background (default), async:false only when the parent must block.",
|
|
161
|
+
}),
|
|
162
|
+
),
|
|
163
|
+
chatProgress: Type.Optional(
|
|
164
|
+
Type.String({
|
|
165
|
+
enum: ["auto", "off", "live-card"],
|
|
166
|
+
description: "WorkflowScript chat progress projection.",
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
isolation: Type.Optional(
|
|
170
|
+
Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation." }),
|
|
171
|
+
),
|
|
172
|
+
worktree: Type.Optional(
|
|
173
|
+
Type.Boolean({ description: "Managed child isolation via separate git worktrees." }),
|
|
174
|
+
),
|
|
175
|
+
context: Type.Optional(
|
|
176
|
+
Type.String({
|
|
177
|
+
enum: ["fresh", "fork", "profile"],
|
|
178
|
+
description:
|
|
179
|
+
"'fresh' or 'fork' to branch from parent session, or 'profile' for the agent's declared defaultContext.",
|
|
180
|
+
}),
|
|
181
|
+
),
|
|
182
|
+
async: Type.Optional(
|
|
183
|
+
Type.Boolean({ description: "Run in background. Set false only when the parent must block." }),
|
|
184
|
+
),
|
|
185
|
+
timeoutMs: Type.Optional(
|
|
186
|
+
Type.Integer({ minimum: 1, description: "Run-level max runtime ms. Alias maxRuntimeMs." }),
|
|
187
|
+
),
|
|
188
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs." })),
|
|
189
|
+
toolTimeoutMs: Type.Optional(
|
|
190
|
+
Type.Integer({ minimum: 1, description: "Optional hard per-tool-call timeout in ms." }),
|
|
191
|
+
),
|
|
192
|
+
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
193
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
194
|
+
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
195
|
+
agentScope: Type.Optional(
|
|
196
|
+
Type.String({
|
|
197
|
+
description: "Agent discovery scope: 'user', 'project', or 'both' (default both; project wins collisions).",
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
cwd: Type.Optional(Type.String({ description: "Execution cwd override." })),
|
|
201
|
+
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts. Default true." })),
|
|
202
|
+
includeProgress: Type.Optional(
|
|
203
|
+
Type.Boolean({ description: "Include full progress in result. Default false." }),
|
|
204
|
+
),
|
|
205
|
+
sessionDir: Type.Optional(Type.String({ description: "Directory for child session logs." })),
|
|
206
|
+
output: Type.Optional(
|
|
207
|
+
Type.Unsafe({
|
|
208
|
+
anyOf: [{ type: "string" }, { type: "boolean" }],
|
|
209
|
+
description: "Default child output file (string), or false to disable.",
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
outputMode: Type.Optional(
|
|
213
|
+
Type.String({
|
|
214
|
+
enum: ["inline", "file-only"],
|
|
215
|
+
description: "Return saved output inline (default) or only a concise file reference.",
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
model: Type.Optional(
|
|
219
|
+
Type.String({
|
|
220
|
+
description: 'Model override. "provider/modelId" or fuzzy name. Omit to inherit parent model.',
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
thinking: Type.Optional(
|
|
224
|
+
Type.String({
|
|
225
|
+
description: "Thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit parent.",
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
maxOutput: Type.Optional(MaxOutputOverride),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/** get_helper_result parameter surface (parity with subagent_wait). */
|
|
232
|
+
export const GetHelperResultParams = Type.Object({
|
|
233
|
+
id: Type.Optional(
|
|
234
|
+
Type.String({
|
|
235
|
+
description:
|
|
236
|
+
"Helper/async run id (or prefix) to wait for. Omit to wait across every active run started in this session.",
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
nonBlocking: Type.Optional(
|
|
240
|
+
Type.Boolean({
|
|
241
|
+
description:
|
|
242
|
+
"When true, resolve id to one exact run, persist a wake subscription, and return immediately. The originating session is woken on completion/failure/attention. Requires id; cannot combine with all.",
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
all: Type.Optional(
|
|
246
|
+
Type.Boolean({
|
|
247
|
+
description:
|
|
248
|
+
"Wait for ALL active runs to finish. Default false: return when the first run finishes.",
|
|
249
|
+
}),
|
|
250
|
+
),
|
|
251
|
+
timeoutMs: Type.Optional(
|
|
252
|
+
Type.Integer({
|
|
253
|
+
minimum: 1,
|
|
254
|
+
description: "Give up waiting after this many milliseconds (runs keep going). Default 1800000.",
|
|
255
|
+
}),
|
|
256
|
+
),
|
|
257
|
+
stopOnAttention: Type.Optional(
|
|
258
|
+
Type.Boolean({
|
|
259
|
+
description:
|
|
260
|
+
"Blocking waits stop when a run needs attention by default. Set false to wait through idle or long-thinking attention.",
|
|
261
|
+
}),
|
|
262
|
+
),
|
|
263
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — /unipi:* slash commands (Phase 7)
|
|
3
|
+
*
|
|
4
|
+
* subagents-fleet: open the fleet inspector surface.
|
|
5
|
+
* subagents-doctor: run the doctor report.
|
|
6
|
+
* subagents-guide: print a guide topic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { AgentManager } from "./agent-manager.js";
|
|
11
|
+
import { listAsyncRunSummaries } from "./fleet-data.js";
|
|
12
|
+
import { buildGuideText } from "./guide.js";
|
|
13
|
+
|
|
14
|
+
export interface SlashDeps {
|
|
15
|
+
manager: AgentManager;
|
|
16
|
+
config: { maxConcurrent: number; enabled: boolean; types: Record<string, { enabled?: boolean }> };
|
|
17
|
+
asyncDirRoot: string;
|
|
18
|
+
retainedDir?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerSlashCommands(pi: ExtensionAPI, getCtx: () => ExtensionContext | undefined, deps: SlashDeps): void {
|
|
22
|
+
const send = (markdown: string): void => {
|
|
23
|
+
pi.sendMessage(
|
|
24
|
+
{ customType: "unipi-response", content: markdown, display: true },
|
|
25
|
+
{ deliverAs: "followUp" },
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
pi.registerCommand("unipi:subagents-fleet", {
|
|
30
|
+
description: "Show active subagent fleet (in-process + process runs)",
|
|
31
|
+
handler: async (_args, _ctx) => {
|
|
32
|
+
const inProcess = deps.manager.listAgents().filter((a) => a.status === "running" || a.status === "queued");
|
|
33
|
+
const asyncRuns = listAsyncRunSummaries(deps.asyncDirRoot).filter(
|
|
34
|
+
(r) => r.state === "running" || r.state === "queued" || r.state === "pending",
|
|
35
|
+
);
|
|
36
|
+
const lines: string[] = ["Active subagent fleet:", ""];
|
|
37
|
+
if (inProcess.length === 0 && asyncRuns.length === 0) {
|
|
38
|
+
lines.push(" (no active work — use spawn_helper to delegate)");
|
|
39
|
+
} else {
|
|
40
|
+
if (inProcess.length > 0) {
|
|
41
|
+
lines.push(`In-process (${inProcess.length}):`);
|
|
42
|
+
for (const record of inProcess) {
|
|
43
|
+
lines.push(` - ${record.id} [${record.type}] ${record.status}: ${record.description}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (asyncRuns.length > 0) {
|
|
47
|
+
lines.push(`Process runs (${asyncRuns.length}):`);
|
|
48
|
+
for (const run of asyncRuns) {
|
|
49
|
+
lines.push(` - ${run.runId} [${run.agent}] ${run.state}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push("", "FleetView panel: ↓ to inspect · enter opens transcripts.");
|
|
53
|
+
}
|
|
54
|
+
send(lines.join("\n"));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
pi.registerCommand("unipi:subagents-doctor", {
|
|
59
|
+
description: "Subagents configuration and capacity diagnosis",
|
|
60
|
+
handler: async () => {
|
|
61
|
+
send(
|
|
62
|
+
"Run the full report via spawn_helper({ action: \"doctor\" }). Quick check:\n" +
|
|
63
|
+
`- extension enabled: ${deps.config.enabled}\n` +
|
|
64
|
+
`- known agent types: ${deps.manager.getKnownTypes().length}\n` +
|
|
65
|
+
`- max concurrent: ${deps.config.maxConcurrent}`,
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pi.registerCommand("unipi:subagents-guide", {
|
|
71
|
+
description: "Subagents guide (topic argument optional)",
|
|
72
|
+
handler: async (args, _ctx) => {
|
|
73
|
+
const topic = args?.trim() || "overview";
|
|
74
|
+
send(buildGuideText(topic));
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|