pi-long-task 0.6.0 → 0.7.1
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/CHANGELOG.md +30 -0
- package/README.md +77 -5
- package/package.json +1 -1
- package/src/coordinator.ts +461 -55
- package/src/goal_orchestrator.ts +11 -0
- package/src/goal_todo_execution.ts +4 -0
- package/src/goal_todo_generation.ts +12 -10
- package/src/index.ts +13 -1
- package/src/network_recovery.ts +2 -2
- package/src/planner_config.ts +214 -0
- package/src/planner_progress.ts +156 -0
- package/src/render.ts +36 -0
- package/src/session_guard.ts +121 -10
- package/src/todo_generator.ts +83 -5
- package/src/types.ts +36 -0
- package/src/worker_capabilities.ts +103 -0
- package/src/worker_config.ts +74 -7
- package/src/worker_session.ts +47 -9
package/src/session_guard.ts
CHANGED
|
@@ -15,13 +15,30 @@ export interface GuardedSessionPromptOptions {
|
|
|
15
15
|
gracefulShutdownPrompt?: string;
|
|
16
16
|
diagnostics?: string[];
|
|
17
17
|
onEvent?: (event: unknown) => void;
|
|
18
|
+
/** Bounded elapsed-time checkpoints while the primary deadline is active. */
|
|
19
|
+
progressCheckpointsMs?: readonly number[];
|
|
20
|
+
onProgressCheckpoint?: (elapsedMs: number) => void;
|
|
21
|
+
/** Called exactly when a positive grace period begins. */
|
|
22
|
+
onGracePeriodStart?: (gracePeriodMs: number) => void;
|
|
18
23
|
dispose?: boolean;
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
const MAX_CAPTURED_SESSION_EVENTS = 512;
|
|
27
|
+
|
|
21
28
|
export interface GuardedSessionPromptResult {
|
|
22
29
|
assistantText: string;
|
|
30
|
+
/** True when the primary prompt deadline elapsed, even if the prompt safely completed during grace. */
|
|
23
31
|
timedOut: boolean;
|
|
32
|
+
/** True only when a timed-out prompt settled during the configured grace period. */
|
|
33
|
+
completedDuringGrace: boolean;
|
|
34
|
+
/** True when non-whitespace assistant output was observed before prompt termination. */
|
|
35
|
+
outputObserved: boolean;
|
|
36
|
+
/** True when the session had to be stopped because its grace period expired. */
|
|
37
|
+
graceExpired: boolean;
|
|
38
|
+
/** True when the session was stopped for any reason, including hard timeout. */
|
|
24
39
|
aborted: boolean;
|
|
40
|
+
/** True only when the caller's AbortSignal cancelled the prompt. */
|
|
41
|
+
cancelled: boolean;
|
|
25
42
|
error?: string;
|
|
26
43
|
/** Untouched prompt failure for coordinator-level provider/transport classification. */
|
|
27
44
|
failure?: unknown;
|
|
@@ -39,14 +56,19 @@ export async function runGuardedSessionPrompt(
|
|
|
39
56
|
const events: unknown[] = [];
|
|
40
57
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
41
58
|
let assistantText = "";
|
|
59
|
+
let currentAssistantTextChunks: string[] = [];
|
|
60
|
+
let outputObserved = false;
|
|
42
61
|
let timedOut = false;
|
|
62
|
+
let graceExpired = false;
|
|
43
63
|
let aborted = false;
|
|
64
|
+
let cancelled = false;
|
|
44
65
|
let error: string | undefined;
|
|
45
66
|
let failure: unknown;
|
|
46
67
|
let promptSettled = false;
|
|
47
68
|
let finished = false;
|
|
48
69
|
let unsubscribe: (() => void) | undefined;
|
|
49
70
|
let complete: (() => void) | undefined;
|
|
71
|
+
const assistantTextAtStart = latestAssistantText(session, [], "");
|
|
50
72
|
|
|
51
73
|
const completed = new Promise<void>((resolve) => {
|
|
52
74
|
complete = resolve;
|
|
@@ -90,6 +112,14 @@ export async function runGuardedSessionPrompt(
|
|
|
90
112
|
}
|
|
91
113
|
};
|
|
92
114
|
|
|
115
|
+
const notifyTiming = (callback: (() => void) | undefined, label: string) => {
|
|
116
|
+
try {
|
|
117
|
+
callback?.();
|
|
118
|
+
} catch (exc) {
|
|
119
|
+
diagnostics.push(`${label} listener failed: ${errorMessage(exc)}`);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
93
123
|
const requestGracefulShutdown = () => {
|
|
94
124
|
const message = options.gracefulShutdownPrompt?.trim();
|
|
95
125
|
if (!message || finished || promptSettled || aborted) {
|
|
@@ -130,13 +160,18 @@ export async function runGuardedSessionPrompt(
|
|
|
130
160
|
}
|
|
131
161
|
timedOut = true;
|
|
132
162
|
diagnostics.push(`session prompt timed out after ${formatMilliseconds(timeoutMs(options.timeoutMs))}`);
|
|
133
|
-
requestGracefulShutdown();
|
|
134
163
|
|
|
135
164
|
const graceMs = nonNegativeMilliseconds(options.gracefulShutdownMs);
|
|
165
|
+
if (graceMs > 0) {
|
|
166
|
+
notifyTiming(() => options.onGracePeriodStart?.(graceMs), "grace-period progress");
|
|
167
|
+
}
|
|
168
|
+
requestGracefulShutdown();
|
|
169
|
+
|
|
136
170
|
const hardAbort = () => {
|
|
137
171
|
if (finished || promptSettled) {
|
|
138
172
|
return;
|
|
139
173
|
}
|
|
174
|
+
graceExpired = true;
|
|
140
175
|
abortSession(`session prompt exceeded ${formatMilliseconds(timeoutMs(options.timeoutMs))} timeout`);
|
|
141
176
|
resolveCompleted();
|
|
142
177
|
};
|
|
@@ -152,20 +187,35 @@ export async function runGuardedSessionPrompt(
|
|
|
152
187
|
if (finished || promptSettled) {
|
|
153
188
|
return;
|
|
154
189
|
}
|
|
155
|
-
|
|
190
|
+
cancelled = true;
|
|
191
|
+
abortSession(abortReason(options.abortSignal, "session prompt cancelled by outer signal"));
|
|
156
192
|
resolveCompleted();
|
|
157
193
|
};
|
|
158
194
|
|
|
159
195
|
try {
|
|
160
196
|
if (options.abortSignal?.aborted) {
|
|
161
197
|
aborted = true;
|
|
162
|
-
|
|
198
|
+
cancelled = true;
|
|
199
|
+
error = abortReason(options.abortSignal, "session prompt cancelled before start");
|
|
163
200
|
} else {
|
|
164
201
|
unsubscribe = session.subscribe((event: unknown) => {
|
|
165
202
|
events.push(event);
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
203
|
+
if (events.length > MAX_CAPTURED_SESSION_EVENTS) events.shift();
|
|
204
|
+
if (isAssistantMessageStart(event)) {
|
|
205
|
+
currentAssistantTextChunks = [];
|
|
206
|
+
assistantText = "";
|
|
207
|
+
}
|
|
208
|
+
const delta = assistantTextDeltaFromEvent(event);
|
|
209
|
+
if (delta !== undefined) {
|
|
210
|
+
if (delta) currentAssistantTextChunks.push(delta);
|
|
211
|
+
outputObserved ||= delta.trim().length > 0;
|
|
212
|
+
} else {
|
|
213
|
+
const text = assistantTextFromEvent(event);
|
|
214
|
+
if (text) {
|
|
215
|
+
currentAssistantTextChunks = [text];
|
|
216
|
+
assistantText = text;
|
|
217
|
+
outputObserved ||= text.trim().length > 0;
|
|
218
|
+
}
|
|
169
219
|
}
|
|
170
220
|
try {
|
|
171
221
|
options.onEvent?.(event);
|
|
@@ -192,6 +242,13 @@ export async function runGuardedSessionPrompt(
|
|
|
192
242
|
|
|
193
243
|
const limitMs = timeoutMs(options.timeoutMs);
|
|
194
244
|
if (limitMs > 0) {
|
|
245
|
+
for (const checkpoint of normalizedProgressCheckpoints(options.progressCheckpointsMs, limitMs)) {
|
|
246
|
+
schedule(() => {
|
|
247
|
+
if (!finished && !promptSettled && !timedOut && !aborted) {
|
|
248
|
+
notifyTiming(() => options.onProgressCheckpoint?.(checkpoint), "timing progress");
|
|
249
|
+
}
|
|
250
|
+
}, checkpoint);
|
|
251
|
+
}
|
|
195
252
|
schedule(triggerTimeout, limitMs);
|
|
196
253
|
}
|
|
197
254
|
|
|
@@ -205,7 +262,8 @@ export async function runGuardedSessionPrompt(
|
|
|
205
262
|
clearTimers();
|
|
206
263
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
207
264
|
unsubscribe?.();
|
|
208
|
-
assistantText = latestAssistantText(session, events, assistantText);
|
|
265
|
+
assistantText = latestAssistantText(session, events, currentAssistantTextChunks.join("") || assistantText);
|
|
266
|
+
outputObserved ||= assistantText.trim().length > 0 && assistantText !== assistantTextAtStart;
|
|
209
267
|
if (options.dispose !== false) {
|
|
210
268
|
try {
|
|
211
269
|
const disposeResult = (session.dispose as (() => unknown) | undefined)?.();
|
|
@@ -220,7 +278,20 @@ export async function runGuardedSessionPrompt(
|
|
|
220
278
|
}
|
|
221
279
|
}
|
|
222
280
|
|
|
223
|
-
return buildResult(
|
|
281
|
+
return buildResult(
|
|
282
|
+
session,
|
|
283
|
+
events,
|
|
284
|
+
assistantText,
|
|
285
|
+
timedOut,
|
|
286
|
+
timedOut && promptSettled && !graceExpired && !aborted && failure === undefined,
|
|
287
|
+
outputObserved,
|
|
288
|
+
graceExpired,
|
|
289
|
+
aborted,
|
|
290
|
+
cancelled,
|
|
291
|
+
error,
|
|
292
|
+
failure,
|
|
293
|
+
diagnostics,
|
|
294
|
+
);
|
|
224
295
|
}
|
|
225
296
|
|
|
226
297
|
function buildResult(
|
|
@@ -228,7 +299,11 @@ function buildResult(
|
|
|
228
299
|
events: unknown[],
|
|
229
300
|
assistantText: string,
|
|
230
301
|
timedOut: boolean,
|
|
302
|
+
completedDuringGrace: boolean,
|
|
303
|
+
outputObserved: boolean,
|
|
304
|
+
graceExpired: boolean,
|
|
231
305
|
aborted: boolean,
|
|
306
|
+
cancelled: boolean,
|
|
232
307
|
error: string | undefined,
|
|
233
308
|
failure: unknown,
|
|
234
309
|
diagnostics: string[],
|
|
@@ -236,7 +311,11 @@ function buildResult(
|
|
|
236
311
|
return {
|
|
237
312
|
assistantText: latestAssistantText(session, events, assistantText),
|
|
238
313
|
timedOut,
|
|
314
|
+
completedDuringGrace,
|
|
315
|
+
outputObserved,
|
|
316
|
+
graceExpired,
|
|
239
317
|
aborted,
|
|
318
|
+
cancelled,
|
|
240
319
|
error,
|
|
241
320
|
...(failure === undefined ? {} : { failure }),
|
|
242
321
|
diagnostics: [...diagnostics],
|
|
@@ -255,8 +334,10 @@ function latestAssistantText(session: WorkerSessionLike, events: unknown[], fall
|
|
|
255
334
|
if (fromMessages) {
|
|
256
335
|
return fromMessages;
|
|
257
336
|
}
|
|
258
|
-
|
|
259
|
-
|
|
337
|
+
if (fallback) {
|
|
338
|
+
return fallback;
|
|
339
|
+
}
|
|
340
|
+
return lastAssistantTextFromEvents(events);
|
|
260
341
|
}
|
|
261
342
|
|
|
262
343
|
function timeoutMs(value: number | undefined): number {
|
|
@@ -266,6 +347,16 @@ function timeoutMs(value: number | undefined): number {
|
|
|
266
347
|
return Math.max(0, value);
|
|
267
348
|
}
|
|
268
349
|
|
|
350
|
+
function normalizedProgressCheckpoints(values: readonly number[] | undefined, limitMs: number): number[] {
|
|
351
|
+
if (!values) {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
return [...new Set(values)]
|
|
355
|
+
.filter((value) => Number.isFinite(value) && value > 0 && value < limitMs)
|
|
356
|
+
.map((value) => Math.floor(value))
|
|
357
|
+
.sort((left, right) => left - right);
|
|
358
|
+
}
|
|
359
|
+
|
|
269
360
|
function nonNegativeMilliseconds(value: number | undefined): number {
|
|
270
361
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
271
362
|
return 0;
|
|
@@ -277,6 +368,26 @@ function formatMilliseconds(ms: number): string {
|
|
|
277
368
|
return `${(ms / 1000).toFixed(3)}s`;
|
|
278
369
|
}
|
|
279
370
|
|
|
371
|
+
function isAssistantMessageStart(event: unknown): boolean {
|
|
372
|
+
return (
|
|
373
|
+
isRecord(event) && event.type === "message_start" && isRecord(event.message) && event.message.role === "assistant"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function assistantTextDeltaFromEvent(event: unknown): string | undefined {
|
|
378
|
+
if (!isRecord(event) || event.type !== "message_update" || !isRecord(event.assistantMessageEvent)) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
382
|
+
return assistantEvent.type === "text_delta" && typeof assistantEvent.delta === "string"
|
|
383
|
+
? assistantEvent.delta
|
|
384
|
+
: undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
388
|
+
return typeof value === "object" && value !== null;
|
|
389
|
+
}
|
|
390
|
+
|
|
280
391
|
function abortReason(signal: AbortSignal | undefined, fallback: string): string {
|
|
281
392
|
const reason = signal?.reason;
|
|
282
393
|
if (reason === undefined) {
|
package/src/todo_generator.ts
CHANGED
|
@@ -133,7 +133,7 @@ export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: str
|
|
|
133
133
|
return markdown;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
let next =
|
|
136
|
+
let next = insertGlobalInstructions(markdown, goalInstructionLines(trimmedGoal));
|
|
137
137
|
const coverageGoal = parseCoverageGoal(trimmedGoal);
|
|
138
138
|
if (coverageGoal) {
|
|
139
139
|
next = appendCoverageVerificationToTasks(next, coverageGoalVerifyBullet(coverageGoal));
|
|
@@ -142,14 +142,33 @@ export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: str
|
|
|
142
142
|
return next;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function
|
|
145
|
+
export function applyWorkerCapabilityConstraintsToTodoMarkdown(
|
|
146
|
+
markdown: string,
|
|
147
|
+
constraints: readonly string[],
|
|
148
|
+
): string {
|
|
149
|
+
const additions = constraints
|
|
150
|
+
.map(oneLine)
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map((item) => `- ${item}`);
|
|
153
|
+
if (additions.length === 0) {
|
|
154
|
+
return markdown;
|
|
155
|
+
}
|
|
156
|
+
const next = insertGlobalInstructions(markdown, additions);
|
|
157
|
+
validateTodoMarkdown(next);
|
|
158
|
+
return next;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function goalInstructionLines(goal: string): string[] {
|
|
146
162
|
const coverageGoal = parseCoverageGoal(goal);
|
|
147
163
|
const additions = [`- Long task goal: ${goal}`];
|
|
148
164
|
if (coverageGoal) {
|
|
149
165
|
additions.push(`- Coverage goal: ${coverageGoalAction(coverageGoal)}`);
|
|
150
166
|
additions.push(`- Coverage verification: ${coverageGoalVerification(coverageGoal)}`);
|
|
151
167
|
}
|
|
168
|
+
return additions;
|
|
169
|
+
}
|
|
152
170
|
|
|
171
|
+
function insertGlobalInstructions(markdown: string, additions: readonly string[]): string {
|
|
153
172
|
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
154
173
|
const progressIndex = lines.findIndex((line) => /^##\s+Progress\s*$/i.test(line.trim()));
|
|
155
174
|
if (progressIndex < 0) {
|
|
@@ -229,9 +248,44 @@ function oneLine(value: string): string {
|
|
|
229
248
|
return value.replace(/\s+/g, " ").trim();
|
|
230
249
|
}
|
|
231
250
|
|
|
232
|
-
export function
|
|
251
|
+
export function todoPlanningOnlyPromptBlock(capabilityConstraints: readonly string[] = []): string {
|
|
252
|
+
const capabilityBlock = capabilityConstraints.length
|
|
253
|
+
? `\n\nWorker capability constraints (preserve these above ## Progress and in affected tasks):\n${capabilityConstraints
|
|
254
|
+
.map((constraint) => `- ${oneLine(constraint)}`)
|
|
255
|
+
.join("\n")}`
|
|
256
|
+
: "";
|
|
257
|
+
return `Planning-only boundary:
|
|
258
|
+
- Produce only a concise executable plan for future workers.
|
|
259
|
+
- Do not perform requested end work: do not implement or write code, execute research or report findings, create requested creative output (prose, stories, copy, designs, or assets), or produce any other final deliverable.
|
|
260
|
+
- Use future-worker action language; do not claim work is complete or invent results.
|
|
261
|
+
- Keep repeated task sections compact: use a one-sentence Goal and Done when, plus only the necessary Status and Verify bullets. Omit rationale, lengthy analysis, summaries, duplicated context, unrequested examples, and boilerplate.
|
|
262
|
+
- Minimize worker handoffs because each TODO starts another model assignment. Use the fewest tasks that safely preserve dependencies and explicit boundaries; combine tightly coupled implementation, tests, and documentation that use the same context. Do not create separate setup, audit, or final-verification tasks when that work belongs inside an implementation task.
|
|
263
|
+
- Preserve every instruction, constraint, required deliverable, and acceptance condition from the source request and supplied planning context. Put shared constraints above ## Progress and task-specific requirements in the relevant task.${capabilityBlock}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function buildTodoCreationPrompt(
|
|
267
|
+
rawInput: string,
|
|
268
|
+
goal?: string,
|
|
269
|
+
capabilityConstraints: readonly string[] = [],
|
|
270
|
+
): string {
|
|
233
271
|
const goalBlock = todoGoalPromptBlock(goal);
|
|
234
|
-
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown
|
|
272
|
+
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.
|
|
273
|
+
|
|
274
|
+
${todoPlanningOnlyPromptBlock(capabilityConstraints)}
|
|
275
|
+
|
|
276
|
+
Required format:
|
|
277
|
+
- Output only markdown, with no commentary and no code fence.
|
|
278
|
+
- Start with exactly: # Pi Long Task TODO
|
|
279
|
+
- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title
|
|
280
|
+
- Include a --- separator before task sections.
|
|
281
|
+
- Create sequential sections named ## TODO N — Title.
|
|
282
|
+
- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.
|
|
283
|
+
- Keep tasks focused and independently assignable to worker sessions.
|
|
284
|
+
${goalBlock}
|
|
285
|
+
Raw input:
|
|
286
|
+
|
|
287
|
+
${rawInput.trim()}
|
|
288
|
+
`;
|
|
235
289
|
}
|
|
236
290
|
|
|
237
291
|
export function buildTodoRepairPrompt(
|
|
@@ -239,9 +293,33 @@ export function buildTodoRepairPrompt(
|
|
|
239
293
|
invalidOutput: string,
|
|
240
294
|
validationError: string,
|
|
241
295
|
goal?: string,
|
|
296
|
+
capabilityConstraints: readonly string[] = [],
|
|
242
297
|
): string {
|
|
243
298
|
const goalBlock = todoGoalPromptBlock(goal);
|
|
244
|
-
return `
|
|
299
|
+
return `Repair the previous response into valid Pi Long Task TODO markdown. Correct its plan and format only; do not continue or perform any attempted end work.
|
|
300
|
+
|
|
301
|
+
${todoPlanningOnlyPromptBlock(capabilityConstraints)}
|
|
302
|
+
|
|
303
|
+
Validation/extraction error:
|
|
304
|
+
${validationError.trim() || "Unknown validation error."}
|
|
305
|
+
|
|
306
|
+
Required format:
|
|
307
|
+
- Output only corrected markdown, with no commentary and no code fence.
|
|
308
|
+
- Start with exactly: # Pi Long Task TODO
|
|
309
|
+
- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title
|
|
310
|
+
- Include a --- separator before task sections.
|
|
311
|
+
- Create sequential sections named ## TODO N — Title.
|
|
312
|
+
- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.
|
|
313
|
+
- Keep tasks focused and independently assignable to worker sessions.
|
|
314
|
+
${goalBlock}
|
|
315
|
+
Original raw input:
|
|
316
|
+
|
|
317
|
+
${rawInput.trim()}
|
|
318
|
+
|
|
319
|
+
Previous invalid output (repair its planning content; do not extend its end work):
|
|
320
|
+
|
|
321
|
+
${invalidOutput.trim()}
|
|
322
|
+
`;
|
|
245
323
|
}
|
|
246
324
|
|
|
247
325
|
function todoGoalPromptBlock(goal: string | undefined): string {
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Static } from "typebox";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
+
import { MAX_PLANNER_DURATION_MS, type PlannerBudget } from "./planner_config.ts";
|
|
4
5
|
import type { TaskProgressModel } from "./task_progress.ts";
|
|
6
|
+
import type { WorkerCapabilityWarning } from "./worker_capabilities.ts";
|
|
5
7
|
import type { SessionOutcome } from "./worker_session.ts";
|
|
6
8
|
|
|
7
9
|
const NetworkRecoveryParams = Type.Object(
|
|
@@ -51,6 +53,22 @@ export const PiLongTaskParams = Type.Object(
|
|
|
51
53
|
description: "Optional high-level goal or desired outcome for the long-task run.",
|
|
52
54
|
}),
|
|
53
55
|
),
|
|
56
|
+
todoTimeoutMs: Type.Optional(
|
|
57
|
+
Type.Integer({
|
|
58
|
+
minimum: 1,
|
|
59
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
60
|
+
description:
|
|
61
|
+
"Explicit TODO-planner timeout in milliseconds. When omitted, the 5-minute default adapts deterministically for explicit item counts, enumerated deliverables, and separately planned tasks, up to 15 minutes.",
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
todoGracefulShutdownMs: Type.Optional(
|
|
65
|
+
Type.Integer({
|
|
66
|
+
minimum: 0,
|
|
67
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
68
|
+
description:
|
|
69
|
+
"Grace period in milliseconds after the TODO-planner timeout. Defaults to 15000 (15 seconds); use 0 to disable the grace period.",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
54
72
|
networkRecovery: Type.Optional(NetworkRecoveryParams),
|
|
55
73
|
},
|
|
56
74
|
{ additionalProperties: false },
|
|
@@ -113,6 +131,22 @@ export const PiGoalTaskParams = Type.Object(
|
|
|
113
131
|
description: "Maximum bash command timeout in milliseconds allowed in worker sessions.",
|
|
114
132
|
}),
|
|
115
133
|
),
|
|
134
|
+
todoTimeoutMs: Type.Optional(
|
|
135
|
+
Type.Integer({
|
|
136
|
+
minimum: 1,
|
|
137
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
138
|
+
description:
|
|
139
|
+
"Explicit TODO-planner timeout in milliseconds for child long-task planning and plan revisions. When omitted, the 5-minute default adapts deterministically for explicit item counts, enumerated deliverables, and separately planned tasks, up to 15 minutes.",
|
|
140
|
+
}),
|
|
141
|
+
),
|
|
142
|
+
todoGracefulShutdownMs: Type.Optional(
|
|
143
|
+
Type.Integer({
|
|
144
|
+
minimum: 0,
|
|
145
|
+
maximum: MAX_PLANNER_DURATION_MS,
|
|
146
|
+
description:
|
|
147
|
+
"Grace period in milliseconds after a child TODO-planner timeout. Defaults to 15000 (15 seconds); use 0 to disable the grace period.",
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
116
150
|
networkRecovery: Type.Optional(NetworkRecoveryParams),
|
|
117
151
|
},
|
|
118
152
|
{ additionalProperties: false },
|
|
@@ -165,6 +199,8 @@ export interface PiLongTaskResult {
|
|
|
165
199
|
}>;
|
|
166
200
|
taskProgress: TaskProgressModel;
|
|
167
201
|
workerCostTotal: number;
|
|
202
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
203
|
+
capabilityWarnings?: readonly WorkerCapabilityWarning[];
|
|
168
204
|
commit: boolean;
|
|
169
205
|
goal?: string;
|
|
170
206
|
error?: string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface IsolatedWorkerCapabilities {
|
|
2
|
+
/** Tool names exposed directly to the worker session. */
|
|
3
|
+
tools: readonly string[];
|
|
4
|
+
/** Pi Long Task isolated sessions deliberately disable extension runtimes. */
|
|
5
|
+
extensionsEnabled: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type WorkerCapabilityWarningCode = "unavailable_browser_capability";
|
|
9
|
+
|
|
10
|
+
export interface WorkerCapabilityWarning {
|
|
11
|
+
code: WorkerCapabilityWarningCode;
|
|
12
|
+
requestedCapabilities: readonly string[];
|
|
13
|
+
availableTools: readonly string[];
|
|
14
|
+
message: string;
|
|
15
|
+
planningConstraint: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BROWSER_EXTENSION_REQUIREMENT_RE =
|
|
19
|
+
/\b(?:must(?:\s+use)?|need(?:s|ed)?(?:\s+to\s+use)?|require(?:s|d)?|rely(?:ing)?\s+on|use|using|via|through|with)\s+(?:the\s+|an?\s+)?(?:google\s+)?(?:chrome|chromium|firefox|edge|browser)(?:\s+(?:browser|devtools?))?\s+(?:extension|mcp|connector)\b/i;
|
|
20
|
+
const REQUIRED_NAMED_BROWSER_EXTENSION_RE =
|
|
21
|
+
/\b(?:chrome\s+devtools\s+(?:mcp|extension|tool)|(?:chrome|browser)\s+(?:mcp|extension(?:\s+tool)?|tool\s+extension))\s+(?:is\s+(?:required|needed)|must\s+be\s+used)\b/i;
|
|
22
|
+
const IMPERATIVE_CHROME_RE =
|
|
23
|
+
/\b(?:open|launch|control|drive|browse\s+with|inspect\s+(?:with|using)|scrape\s+(?:with|using)|fetch\s+(?:with|using)|test\s+(?:with|using))\s+(?:google\s+)?chrome\b/i;
|
|
24
|
+
const EXPLICIT_BROWSER_TOOL_RE =
|
|
25
|
+
/\b(?:must(?:\s+use)?|need(?:s|ed)?(?:\s+to\s+use)?|require(?:s|d)?|use|using|via|through|with)\s+(?:the\s+)?(browser|chrome|web[_-]?fetch|playwright|puppeteer)\s+tool\b/i;
|
|
26
|
+
const EXPLICIT_CHROME_RUNTIME_RE =
|
|
27
|
+
/(?:\b(?:must\s+use|need(?:s|ed)?\s+to\s+use|use|using)\s+(?:google\s+)?chrome(?:\s+devtools?)?(?=\s+(?:to|for)\b|\s*[.,;:]|\s*$)|\b(?:via|through)\s+(?:google\s+)?chrome\b|\b(?:google\s+)?chrome(?:\s+devtools?)?\s+(?:is\s+required|must\s+be\s+used)\b)/i;
|
|
28
|
+
const NEGATED_CAPABILITY_RE =
|
|
29
|
+
/\b(?:do\s+not|don't|dont|never|avoid|without)\s+(?:use|using|rely(?:ing)?\s+on|requiring?)?\s*(?:the\s+|an?\s+)?(?:google\s+)?(?:chrome(?:\s+(?:browser|devtools?))?|chromium|firefox|edge|browser|web[_-]?fetch|playwright|puppeteer)(?:\s+(?:extension|mcp|tool(?:ing)?|connector))?/gi;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect only explicit requests to invoke a browser capability. Merely asking
|
|
33
|
+
* workers to build or support a browser extension is implementation work and
|
|
34
|
+
* does not imply that the extension must be loaded during the run.
|
|
35
|
+
*/
|
|
36
|
+
export function detectUnavailableWorkerCapabilities(
|
|
37
|
+
requestText: string,
|
|
38
|
+
capabilities: Readonly<IsolatedWorkerCapabilities>,
|
|
39
|
+
): WorkerCapabilityWarning[] {
|
|
40
|
+
const text = requestText.replace(NEGATED_CAPABILITY_RE, " ");
|
|
41
|
+
const extensionRequested =
|
|
42
|
+
BROWSER_EXTENSION_REQUIREMENT_RE.test(text) ||
|
|
43
|
+
REQUIRED_NAMED_BROWSER_EXTENSION_RE.test(text) ||
|
|
44
|
+
IMPERATIVE_CHROME_RE.test(text);
|
|
45
|
+
const browserToolMatch = EXPLICIT_BROWSER_TOOL_RE.exec(text);
|
|
46
|
+
const requestedDirectTools = [
|
|
47
|
+
browserToolMatch?.[1],
|
|
48
|
+
EXPLICIT_CHROME_RUNTIME_RE.test(text) ? "chrome" : undefined,
|
|
49
|
+
].filter((item, index, all): item is string => Boolean(item) && all.indexOf(item) === index);
|
|
50
|
+
const unavailableDirectTools = requestedDirectTools.filter(
|
|
51
|
+
(requested) =>
|
|
52
|
+
!capabilities.tools.some((available) => canonicalToolName(available) === canonicalToolName(requested)),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if ((!extensionRequested || capabilities.extensionsEnabled) && unavailableDirectTools.length === 0) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const requestedCapabilities = [
|
|
60
|
+
extensionRequested && !capabilities.extensionsEnabled ? "Chrome/browser extension runtime" : undefined,
|
|
61
|
+
unavailableDirectTools.length > 0 ? `${unavailableDirectTools.join("/")} direct tool` : undefined,
|
|
62
|
+
].filter((item): item is string => Boolean(item));
|
|
63
|
+
const availableTools = [...capabilities.tools];
|
|
64
|
+
const toolList = availableTools.length > 0 ? availableTools.join(", ") : "none";
|
|
65
|
+
const alternatives = availableWorkerAlternatives(capabilities);
|
|
66
|
+
const alternativeText = alternatives.join(", or ");
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
code: "unavailable_browser_capability",
|
|
71
|
+
requestedCapabilities,
|
|
72
|
+
availableTools,
|
|
73
|
+
message:
|
|
74
|
+
`Worker capability warning: isolated Pi Long Task workers disable extensions and expose only these direct tools: ${toolList}. ` +
|
|
75
|
+
`They cannot silently use the requested ${requestedCapabilities.join(" or ")}. The run will continue, but it must use an available safe alternative when that satisfies the request: ${alternativeText}. ` +
|
|
76
|
+
"If the exact extension or browser tool is mandatory, the affected task must report blocked instead of claiming it used that capability.",
|
|
77
|
+
planningConstraint:
|
|
78
|
+
`Isolated-worker capability constraint: extensions are disabled and workers have only these direct tools: ${toolList}. ` +
|
|
79
|
+
`Do not create or execute tasks that assume the requested ${requestedCapabilities.join(" or ")} is available, and never claim it was used. ` +
|
|
80
|
+
`When equivalent, ${alternativeText}. If the exact unavailable capability is mandatory, make the affected task report blocked with the required user action.`,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function canonicalToolName(tool: string): string {
|
|
86
|
+
return tool.toLowerCase().replace(/[-_]/g, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function availableWorkerAlternatives(capabilities: Readonly<IsolatedWorkerCapabilities>): string[] {
|
|
90
|
+
const alternatives: string[] = [];
|
|
91
|
+
if (capabilities.tools.includes("bash")) {
|
|
92
|
+
alternatives.push(
|
|
93
|
+
"fetch public content through a supported command-line mechanism via bash or run project-provided browser automation via bash when available",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (capabilities.tools.includes("read")) {
|
|
97
|
+
alternatives.push("supply the page or source content to the run so workers can read it locally");
|
|
98
|
+
}
|
|
99
|
+
if (alternatives.length === 0) {
|
|
100
|
+
alternatives.push("supply the needed source content to the run");
|
|
101
|
+
}
|
|
102
|
+
return alternatives;
|
|
103
|
+
}
|