pi-long-task 0.3.7 → 0.3.9
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 +4 -2
- package/package.json +1 -1
- package/src/coordinator.ts +327 -30
- package/src/index.ts +76 -12
- package/src/session_guard.ts +287 -0
- package/src/todo_generator.ts +10 -0
- package/src/worker_config.ts +291 -0
- package/src/worker_session.ts +2 -2
package/README.md
CHANGED
|
@@ -18,11 +18,11 @@ Use it when a coding request is bigger than one focused interaction. Pi Long Tas
|
|
|
18
18
|
When you ask Pi to run a long task, Pi Long Task:
|
|
19
19
|
|
|
20
20
|
1. Recognizes natural-language requests like "run a long task with commits" and routes them to `pi_long_task`.
|
|
21
|
-
2. Creates or cleans up a TODO plan from your request.
|
|
21
|
+
2. Creates or cleans up a TODO plan from your request. Natural-language planning uses a bounded planner session; if generated TODO markdown is invalid, Pi Long Task asks the planner to repair it once before failing the run.
|
|
22
22
|
3. Works through each unfinished TODO task in order using isolated worker sessions.
|
|
23
23
|
4. Registers a Pi TUI sidebar/widget when UI support is available and updates it with the current task, inferred subtask progress, and full task timeline while the run is active.
|
|
24
24
|
5. Retries unfinished tasks up to the configured attempt limit.
|
|
25
|
-
6. Records progress, task artifacts, and final results under `tmp/pi-long-task/<run-id>/`.
|
|
25
|
+
6. Records progress, planner diagnostics, task artifacts, and final results under `tmp/pi-long-task/<run-id>/`.
|
|
26
26
|
7. Returns a summary with completed, failed, blocked, and remaining task counts, plus worker spend when available.
|
|
27
27
|
8. Optionally commits completed work after each task.
|
|
28
28
|
|
|
@@ -230,6 +230,8 @@ That smoke test creates disposable git repos and verifies both `commit: false` a
|
|
|
230
230
|
## Limitations and expectations
|
|
231
231
|
|
|
232
232
|
- Tasks run sequentially, one TODO at a time; Pi Long Task prioritizes isolation, progress tracking, and safe handoff over parallel execution.
|
|
233
|
+
- Natural-language TODO planning has a bounded time budget (five minutes by default, with a short graceful-shutdown request). If planning times out or is aborted before a valid plan exists, the run fails before worker tasks start and records planner diagnostics in `TASK_RESULT.md`.
|
|
234
|
+
- If the planner returns invalid TODO markdown, Pi Long Task makes one repair attempt. A second invalid response fails planning with diagnostics instead of guessing at a plan.
|
|
233
235
|
- Real runs require usable Pi model credentials, such as a working Pi login or API key for the selected model.
|
|
234
236
|
- Worker spend is added to the main Pi `$ spent` total as cost-only usage. Token counts are not merged into the main thread because worker sessions have separate context windows, and merging their token usage would corrupt the main conversation's context statistics.
|
|
235
237
|
- Run artifacts are written under `tmp/pi-long-task/<run-id>/`.
|
package/package.json
CHANGED
package/src/coordinator.ts
CHANGED
|
@@ -11,17 +11,20 @@ import type {
|
|
|
11
11
|
import { commitAfterSession, gitDirtyPaths, shouldCommitOutcome, type CommitAfterSessionResult } from "./git.ts";
|
|
12
12
|
import { formatCoordinatorResultMessage } from "./render.ts";
|
|
13
13
|
import { extractResultSummary } from "./result_writer.ts";
|
|
14
|
+
import { runGuardedSessionPrompt } from "./session_guard.ts";
|
|
15
|
+
import { parseWorkerRuntimeConfig } from "./worker_config.ts";
|
|
14
16
|
import { buildTaskProgressModel, type TaskProgressModel, type TaskProgressStatus } from "./task_progress.ts";
|
|
15
17
|
import {
|
|
16
18
|
buildTodoCreationPrompt,
|
|
17
|
-
|
|
19
|
+
buildTodoRepairPrompt,
|
|
20
|
+
extractAndValidateTodoMarkdown,
|
|
21
|
+
TodoGenerationError,
|
|
18
22
|
todoMarkdownFromString,
|
|
19
23
|
validateTodoMarkdown,
|
|
20
24
|
} from "./todo_generator.ts";
|
|
21
25
|
import { markTaskDone, parseTasks, todoGlobalInstructions, type Task } from "./todo_parser.ts";
|
|
22
26
|
import {
|
|
23
27
|
createIsolatedWorkerSession,
|
|
24
|
-
lastAssistantTextFromMessages,
|
|
25
28
|
runWorkerTask,
|
|
26
29
|
type RunWorkerTaskOptions,
|
|
27
30
|
type SessionOutcome,
|
|
@@ -34,6 +37,8 @@ export type { CoordinatorStatus } from "./types.ts";
|
|
|
34
37
|
export const DEFAULT_COORDINATOR_OPTIONS = {
|
|
35
38
|
maxAttemptsPerTask: 3,
|
|
36
39
|
taskTimeoutMs: 900_000,
|
|
40
|
+
todoTimeoutMs: 300_000,
|
|
41
|
+
todoGracefulShutdownMs: 15_000,
|
|
37
42
|
maxBashTimeoutMs: 300_000,
|
|
38
43
|
taskThinking: "high",
|
|
39
44
|
todoThinking: "xhigh",
|
|
@@ -50,6 +55,18 @@ export type CoordinatorProgressPhase =
|
|
|
50
55
|
| "task_failed"
|
|
51
56
|
| "complete";
|
|
52
57
|
|
|
58
|
+
export type PlannerDiagnosticKind = "timeout" | "abort" | "invalid_output" | "repair_attempt" | "failure";
|
|
59
|
+
|
|
60
|
+
export interface PlannerDiagnostic {
|
|
61
|
+
kind: PlannerDiagnosticKind;
|
|
62
|
+
message: string;
|
|
63
|
+
diagnostics?: string[];
|
|
64
|
+
sessionFile?: string;
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PlannerDiagnosticHandler = (diagnostic: PlannerDiagnostic) => void;
|
|
69
|
+
|
|
53
70
|
export type CoordinatorProgressItemStatus = "empty" | "in_progress" | "done" | "failed" | "blocked";
|
|
54
71
|
|
|
55
72
|
export interface CoordinatorProgressTask {
|
|
@@ -84,6 +101,10 @@ export interface CoordinatorProgressUpdate {
|
|
|
84
101
|
currentTask?: CoordinatorProgressTask;
|
|
85
102
|
subtasks?: CoordinatorProgressSubtask[];
|
|
86
103
|
taskProgress?: TaskProgressModel;
|
|
104
|
+
plannerDiagnostic?: PlannerDiagnosticKind;
|
|
105
|
+
plannerDiagnostics?: string[];
|
|
106
|
+
plannerSessionFile?: string;
|
|
107
|
+
plannerSessionId?: string;
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
export type CoordinatorProgressHandler = (update: CoordinatorProgressUpdate) => void;
|
|
@@ -97,8 +118,12 @@ export interface RunCoordinatorOptions extends PiLongTaskInput {
|
|
|
97
118
|
todoPlanner?: TodoPlanner;
|
|
98
119
|
workerSessionFactory?: WorkerSessionFactory;
|
|
99
120
|
todoSessionFactory?: WorkerSessionFactory;
|
|
121
|
+
workerModel?: unknown;
|
|
122
|
+
workerModelName?: string;
|
|
100
123
|
maxAttemptsPerTask?: number;
|
|
101
124
|
taskTimeoutMs?: number;
|
|
125
|
+
todoTimeoutMs?: number;
|
|
126
|
+
todoGracefulShutdownMs?: number;
|
|
102
127
|
maxBashTimeoutMs?: number;
|
|
103
128
|
taskThinking?: string;
|
|
104
129
|
todoThinking?: string;
|
|
@@ -111,8 +136,12 @@ export interface TodoPlannerOptions {
|
|
|
111
136
|
cwd: string;
|
|
112
137
|
runDir: string;
|
|
113
138
|
thinkingLevel: string;
|
|
139
|
+
model?: unknown;
|
|
114
140
|
abortSignal?: AbortSignal;
|
|
141
|
+
timeoutMs?: number;
|
|
142
|
+
gracefulShutdownMs?: number;
|
|
115
143
|
sessionFactory?: WorkerSessionFactory;
|
|
144
|
+
onDiagnostic?: PlannerDiagnosticHandler;
|
|
116
145
|
}
|
|
117
146
|
|
|
118
147
|
export interface TaskAttemptSummary {
|
|
@@ -167,8 +196,12 @@ interface RuntimeOptions {
|
|
|
167
196
|
maxAttemptsPerTask: number;
|
|
168
197
|
taskTimeoutSeconds: number;
|
|
169
198
|
maxBashTimeoutSeconds: number;
|
|
199
|
+
workerModel?: unknown;
|
|
200
|
+
workerModelName?: string;
|
|
170
201
|
taskThinking: string;
|
|
171
202
|
todoThinking: string;
|
|
203
|
+
todoTimeoutMs: number;
|
|
204
|
+
todoGracefulShutdownMs: number;
|
|
172
205
|
workerRunner: WorkerRunner;
|
|
173
206
|
todoPlanner: TodoPlanner;
|
|
174
207
|
abortSignal?: AbortSignal;
|
|
@@ -177,6 +210,7 @@ interface RuntimeOptions {
|
|
|
177
210
|
now: () => Date;
|
|
178
211
|
onProgress?: CoordinatorProgressHandler;
|
|
179
212
|
workerCostState: WorkerCostState;
|
|
213
|
+
plannerDiagnostics: PlannerDiagnostic[];
|
|
180
214
|
}
|
|
181
215
|
|
|
182
216
|
export async function runCoordinator(options: RunCoordinatorOptions): Promise<CoordinatorResult> {
|
|
@@ -187,11 +221,13 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
187
221
|
|
|
188
222
|
await mkdir(runtime.runDir, { recursive: true });
|
|
189
223
|
await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId), "utf8");
|
|
224
|
+
let planningComplete = false;
|
|
190
225
|
|
|
191
226
|
try {
|
|
192
227
|
emitProgress(runtime, "Creating TODO plan...", { phase: "planning" });
|
|
193
228
|
let todoMarkdown = await generateOrNormalizeTodoMarkdown(options.inputText, runtime);
|
|
194
229
|
validateTodoMarkdown(todoMarkdown);
|
|
230
|
+
planningComplete = true;
|
|
195
231
|
await writeFile(runtime.todoPath, todoMarkdown, "utf8");
|
|
196
232
|
const initialTasks = parseTasks(todoMarkdown);
|
|
197
233
|
emitProgress(runtime, `Created TODO plan with ${initialTasks.length} task(s).`, {
|
|
@@ -240,6 +276,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
240
276
|
globalInstructions: todoGlobalInstructions(todoMarkdown),
|
|
241
277
|
maxBashTimeoutSeconds: runtime.maxBashTimeoutSeconds,
|
|
242
278
|
taskTimeoutSeconds: runtime.taskTimeoutSeconds,
|
|
279
|
+
model: runtime.workerModel,
|
|
280
|
+
modelName: runtime.workerModelName,
|
|
243
281
|
thinkingLevel: runtime.taskThinking,
|
|
244
282
|
abortSignal: runtime.abortSignal,
|
|
245
283
|
sessionFactory: runtime.workerSessionFactory,
|
|
@@ -371,9 +409,18 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
371
409
|
return result;
|
|
372
410
|
} catch (error) {
|
|
373
411
|
const message = errorMessage(error);
|
|
374
|
-
|
|
412
|
+
if (!planningComplete) {
|
|
413
|
+
recordPlannerDiagnostic(runtime, {
|
|
414
|
+
kind: "failure",
|
|
415
|
+
message: `TODO planning failed: ${message}`,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const resultError = !planningComplete
|
|
419
|
+
? `${message} See ${runtime.taskResultPath} for planner diagnostics.`
|
|
420
|
+
: message;
|
|
421
|
+
const summary = `Pi Long Task failed: ${resultError}`;
|
|
375
422
|
try {
|
|
376
|
-
await
|
|
423
|
+
await appendFailureNote(runtime.taskResultPath, message, !planningComplete ? runtime.plannerDiagnostics : []);
|
|
377
424
|
} catch {
|
|
378
425
|
// Best effort only; the original error is returned below.
|
|
379
426
|
}
|
|
@@ -399,7 +446,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
399
446
|
taskProgress: buildTaskProgressModel({ tasks: [], attempts }),
|
|
400
447
|
workerCostTotal: runtime.workerCostState.total,
|
|
401
448
|
commit: options.commit,
|
|
402
|
-
error:
|
|
449
|
+
error: resultError,
|
|
403
450
|
};
|
|
404
451
|
result.message = formatCoordinatorResultMessage(result);
|
|
405
452
|
emitProgress(runtime, "Pi Long Task failed.", {
|
|
@@ -417,49 +464,242 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
|
|
|
417
464
|
return local;
|
|
418
465
|
}
|
|
419
466
|
|
|
420
|
-
const plannerText = await runtime
|
|
467
|
+
const plannerText = await requestTodoPlan(inputText, runtime);
|
|
468
|
+
return extractTodoMarkdownWithOneRepair(
|
|
469
|
+
inputText,
|
|
470
|
+
plannerText,
|
|
471
|
+
(repairPrompt) => requestTodoPlan(repairPrompt, runtime),
|
|
472
|
+
{
|
|
473
|
+
onInvalidOutput: (validationError) =>
|
|
474
|
+
recordPlannerDiagnostic(runtime, {
|
|
475
|
+
kind: "invalid_output",
|
|
476
|
+
message: `TODO planner returned invalid output: ${validationError}`,
|
|
477
|
+
}),
|
|
478
|
+
onRepairAttempt: (validationError) =>
|
|
479
|
+
recordPlannerDiagnostic(runtime, {
|
|
480
|
+
kind: "repair_attempt",
|
|
481
|
+
message: `Asking TODO planner to repair invalid output: ${validationError}`,
|
|
482
|
+
}),
|
|
483
|
+
onFailure: (validationError) =>
|
|
484
|
+
recordPlannerDiagnostic(runtime, {
|
|
485
|
+
kind: "failure",
|
|
486
|
+
message: `TODO planner repair failed: ${validationError}`,
|
|
487
|
+
}),
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
interface TodoExtractionRepairHooks {
|
|
493
|
+
onInvalidOutput?: (validationError: string) => void;
|
|
494
|
+
onRepairAttempt?: (validationError: string) => void;
|
|
495
|
+
onFailure?: (validationError: string) => void;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function extractTodoMarkdownWithOneRepair(
|
|
499
|
+
inputText: string,
|
|
500
|
+
plannerText: string,
|
|
501
|
+
requestRepair: (repairPrompt: string) => Promise<string>,
|
|
502
|
+
hooks: TodoExtractionRepairHooks = {},
|
|
503
|
+
): Promise<string> {
|
|
504
|
+
try {
|
|
505
|
+
return extractAndValidateTodoMarkdown(plannerText);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
const validationError = errorMessage(error);
|
|
508
|
+
hooks.onInvalidOutput?.(validationError);
|
|
509
|
+
hooks.onRepairAttempt?.(validationError);
|
|
510
|
+
const repairText = await requestRepair(buildTodoRepairPrompt(inputText, plannerText, validationError));
|
|
511
|
+
try {
|
|
512
|
+
return extractAndValidateTodoMarkdown(repairText);
|
|
513
|
+
} catch (repairError) {
|
|
514
|
+
const repairMessage = errorMessage(repairError);
|
|
515
|
+
hooks.onFailure?.(repairMessage);
|
|
516
|
+
throw new TodoGenerationError(
|
|
517
|
+
`TODO planner returned invalid TODO markdown after one repair attempt: ${repairMessage}`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Promise<string> {
|
|
524
|
+
return runtime.todoPlanner({
|
|
421
525
|
inputText,
|
|
422
526
|
cwd: runtime.cwd,
|
|
423
527
|
runDir: runtime.runDir,
|
|
424
528
|
thinkingLevel: runtime.todoThinking,
|
|
529
|
+
model: runtime.workerModel,
|
|
425
530
|
abortSignal: runtime.abortSignal,
|
|
531
|
+
timeoutMs: runtime.todoTimeoutMs,
|
|
532
|
+
gracefulShutdownMs: runtime.todoGracefulShutdownMs,
|
|
426
533
|
sessionFactory: runtime.todoSessionFactory,
|
|
534
|
+
onDiagnostic: (diagnostic) => recordPlannerDiagnostic(runtime, diagnostic),
|
|
427
535
|
});
|
|
428
|
-
return extractTodoMarkdown(plannerText);
|
|
429
536
|
}
|
|
430
537
|
|
|
538
|
+
// Planner/worker lifecycle differences are audited in docs/planner-worker-lifecycle-audit.md;
|
|
539
|
+
// keep this function's public contract stable while moving shared prompt guarding into a helper.
|
|
431
540
|
export async function runTodoPlanner(options: TodoPlannerOptions): Promise<string> {
|
|
432
|
-
|
|
541
|
+
const sessionFactory = options.sessionFactory ?? createIsolatedWorkerSession;
|
|
542
|
+
const result = await sessionFactory({
|
|
543
|
+
cwd: options.cwd,
|
|
544
|
+
tools: [],
|
|
545
|
+
model: options.model,
|
|
546
|
+
thinkingLevel: options.thinkingLevel,
|
|
547
|
+
});
|
|
548
|
+
const session = result.session;
|
|
549
|
+
const timeoutMs = positiveMilliseconds(options.timeoutMs, DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
|
|
550
|
+
const gracefulShutdownMs = options.gracefulShutdownMs ?? DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs;
|
|
551
|
+
|
|
552
|
+
let plannerMarkdown: string | undefined;
|
|
553
|
+
let plannerError: unknown;
|
|
554
|
+
|
|
433
555
|
try {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
556
|
+
const plannerText = await runTodoPlannerPrompt({
|
|
557
|
+
session,
|
|
558
|
+
prompt: buildTodoCreationPrompt(options.inputText),
|
|
559
|
+
abortSignal: options.abortSignal,
|
|
560
|
+
timeoutMs,
|
|
561
|
+
gracefulShutdownMs,
|
|
562
|
+
diagnostics: result.diagnostics,
|
|
563
|
+
onDiagnostic: options.onDiagnostic,
|
|
439
564
|
});
|
|
440
|
-
session = result.session;
|
|
441
565
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
566
|
+
plannerMarkdown = await extractTodoMarkdownWithOneRepair(
|
|
567
|
+
options.inputText,
|
|
568
|
+
plannerText,
|
|
569
|
+
(repairPrompt) =>
|
|
570
|
+
runTodoPlannerPrompt({
|
|
571
|
+
session,
|
|
572
|
+
prompt: repairPrompt,
|
|
573
|
+
abortSignal: options.abortSignal,
|
|
574
|
+
timeoutMs,
|
|
575
|
+
gracefulShutdownMs,
|
|
576
|
+
diagnostics: result.diagnostics,
|
|
577
|
+
onDiagnostic: options.onDiagnostic,
|
|
578
|
+
}),
|
|
579
|
+
{
|
|
580
|
+
onInvalidOutput: (validationError) =>
|
|
581
|
+
options.onDiagnostic?.({
|
|
582
|
+
kind: "invalid_output",
|
|
583
|
+
message: `TODO planner returned invalid output: ${validationError}`,
|
|
584
|
+
diagnostics: result.diagnostics,
|
|
585
|
+
sessionFile: session.sessionFile,
|
|
586
|
+
sessionId: session.sessionId,
|
|
587
|
+
}),
|
|
588
|
+
onRepairAttempt: (validationError) =>
|
|
589
|
+
options.onDiagnostic?.({
|
|
590
|
+
kind: "repair_attempt",
|
|
591
|
+
message: `Asking TODO planner to repair invalid output: ${validationError}`,
|
|
592
|
+
diagnostics: result.diagnostics,
|
|
593
|
+
sessionFile: session.sessionFile,
|
|
594
|
+
sessionId: session.sessionId,
|
|
595
|
+
}),
|
|
596
|
+
onFailure: (validationError) =>
|
|
597
|
+
options.onDiagnostic?.({
|
|
598
|
+
kind: "failure",
|
|
599
|
+
message: `TODO planner repair failed: ${validationError}`,
|
|
600
|
+
diagnostics: result.diagnostics,
|
|
601
|
+
sessionFile: session.sessionFile,
|
|
602
|
+
sessionId: session.sessionId,
|
|
603
|
+
}),
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
plannerError = error;
|
|
608
|
+
}
|
|
445
609
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
} finally {
|
|
455
|
-
session?.dispose?.();
|
|
610
|
+
try {
|
|
611
|
+
await Promise.resolve(session.dispose?.());
|
|
612
|
+
} catch (error) {
|
|
613
|
+
plannerError = plannerError ?? new TodoGenerationError(`TODO planner dispose failed: ${errorMessage(error)}`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (plannerError) {
|
|
617
|
+
throw plannerError;
|
|
456
618
|
}
|
|
619
|
+
if (!plannerMarkdown) {
|
|
620
|
+
throw new TodoGenerationError("TODO planner did not return valid TODO markdown.");
|
|
621
|
+
}
|
|
622
|
+
return plannerMarkdown;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function runTodoPlannerPrompt(options: {
|
|
626
|
+
session: WorkerSessionLike;
|
|
627
|
+
prompt: string;
|
|
628
|
+
abortSignal?: AbortSignal;
|
|
629
|
+
timeoutMs: number;
|
|
630
|
+
gracefulShutdownMs: number;
|
|
631
|
+
diagnostics?: string[];
|
|
632
|
+
onDiagnostic?: PlannerDiagnosticHandler;
|
|
633
|
+
}): Promise<string> {
|
|
634
|
+
const promptResult = await runGuardedSessionPrompt({
|
|
635
|
+
session: options.session,
|
|
636
|
+
prompt: options.prompt,
|
|
637
|
+
abortSignal: options.abortSignal,
|
|
638
|
+
timeoutMs: options.timeoutMs,
|
|
639
|
+
gracefulShutdownMs: options.gracefulShutdownMs,
|
|
640
|
+
gracefulShutdownPrompt: buildTodoPlanningShutdownMessage(),
|
|
641
|
+
diagnostics: options.diagnostics,
|
|
642
|
+
dispose: false,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (promptResult.timedOut) {
|
|
646
|
+
const message = `TODO planner timed out: ${promptResult.error ?? "time budget exceeded"}`;
|
|
647
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("timeout", message, promptResult));
|
|
648
|
+
throw new TodoGenerationError(message);
|
|
649
|
+
}
|
|
650
|
+
if (promptResult.aborted) {
|
|
651
|
+
const message = `TODO planner aborted: ${promptResult.error ?? "outer abort signal"}`;
|
|
652
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("abort", message, promptResult));
|
|
653
|
+
throw new TodoGenerationError(message);
|
|
654
|
+
}
|
|
655
|
+
if (promptResult.error) {
|
|
656
|
+
const message = `TODO planner failed: ${promptResult.error}`;
|
|
657
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("failure", message, promptResult));
|
|
658
|
+
throw new TodoGenerationError(message);
|
|
659
|
+
}
|
|
660
|
+
if (!promptResult.assistantText) {
|
|
661
|
+
const message = "TODO planner did not return assistant text.";
|
|
662
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("failure", message, promptResult));
|
|
663
|
+
throw new TodoGenerationError(message);
|
|
664
|
+
}
|
|
665
|
+
return promptResult.assistantText;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function plannerPromptDiagnostic(
|
|
669
|
+
kind: Extract<PlannerDiagnosticKind, "timeout" | "abort" | "failure">,
|
|
670
|
+
message: string,
|
|
671
|
+
promptResult: {
|
|
672
|
+
diagnostics: string[];
|
|
673
|
+
sessionFile?: string;
|
|
674
|
+
sessionId?: string;
|
|
675
|
+
},
|
|
676
|
+
): PlannerDiagnostic {
|
|
677
|
+
return {
|
|
678
|
+
kind,
|
|
679
|
+
message,
|
|
680
|
+
diagnostics: promptResult.diagnostics,
|
|
681
|
+
sessionFile: promptResult.sessionFile,
|
|
682
|
+
sessionId: promptResult.sessionId,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function buildTodoPlanningShutdownMessage(): string {
|
|
687
|
+
return `Pi Long Task notice: TODO planning has reached its time budget.
|
|
688
|
+
Return the best valid Pi Long Task TODO markdown you can produce now, or stop if that is not possible.`;
|
|
457
689
|
}
|
|
458
690
|
|
|
459
691
|
function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
460
692
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
461
693
|
const runId = sanitizeRunId(options.runId ?? defaultRunId(options.now?.() ?? new Date()));
|
|
462
694
|
const runDir = path.join(cwd, "tmp", "pi-long-task", runId);
|
|
695
|
+
const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText);
|
|
696
|
+
const configuredAttempts = options.maxAttemptsPerTask ?? parsedWorkerConfig.maxAttemptsPerTask;
|
|
697
|
+
const configuredTaskTimeoutMs = options.taskTimeoutMs ?? parsedWorkerConfig.taskTimeoutMs;
|
|
698
|
+
const configuredTodoTimeoutMs = options.todoTimeoutMs;
|
|
699
|
+
const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs;
|
|
700
|
+
const configuredMaxBashTimeoutMs = options.maxBashTimeoutMs ?? parsedWorkerConfig.maxBashTimeoutMs;
|
|
701
|
+
const workerModelName = options.workerModelName ?? parsedWorkerConfig.modelName;
|
|
702
|
+
const workerModel = workerModelName ? undefined : options.workerModel;
|
|
463
703
|
|
|
464
704
|
return {
|
|
465
705
|
cwd,
|
|
@@ -467,10 +707,17 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
467
707
|
runDir,
|
|
468
708
|
todoPath: path.join(runDir, "TODO.md"),
|
|
469
709
|
taskResultPath: path.join(runDir, "TASK_RESULT.md"),
|
|
470
|
-
maxAttemptsPerTask: positiveInteger(
|
|
471
|
-
taskTimeoutSeconds: positiveMilliseconds(
|
|
710
|
+
maxAttemptsPerTask: positiveInteger(configuredAttempts, DEFAULT_COORDINATOR_OPTIONS.maxAttemptsPerTask),
|
|
711
|
+
taskTimeoutSeconds: positiveMilliseconds(configuredTaskTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.taskTimeoutMs) / 1000,
|
|
712
|
+
todoTimeoutMs: positiveMilliseconds(configuredTodoTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs),
|
|
713
|
+
todoGracefulShutdownMs: positiveMilliseconds(
|
|
714
|
+
configuredTodoGracefulShutdownMs,
|
|
715
|
+
DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
|
|
716
|
+
),
|
|
472
717
|
maxBashTimeoutSeconds:
|
|
473
|
-
positiveMilliseconds(
|
|
718
|
+
positiveMilliseconds(configuredMaxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
|
|
719
|
+
workerModel,
|
|
720
|
+
workerModelName,
|
|
474
721
|
taskThinking: options.taskThinking ?? DEFAULT_COORDINATOR_OPTIONS.taskThinking,
|
|
475
722
|
todoThinking: options.todoThinking ?? DEFAULT_COORDINATOR_OPTIONS.todoThinking,
|
|
476
723
|
workerRunner: options.workerRunner ?? runWorkerTask,
|
|
@@ -481,6 +728,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
481
728
|
now: options.now ?? (() => new Date()),
|
|
482
729
|
onProgress: options.onProgress,
|
|
483
730
|
workerCostState: createWorkerCostState(),
|
|
731
|
+
plannerDiagnostics: [],
|
|
484
732
|
};
|
|
485
733
|
}
|
|
486
734
|
|
|
@@ -499,6 +747,31 @@ function emitProgress(
|
|
|
499
747
|
});
|
|
500
748
|
}
|
|
501
749
|
|
|
750
|
+
function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDiagnostic): void {
|
|
751
|
+
const normalized: PlannerDiagnostic = {
|
|
752
|
+
kind: diagnostic.kind,
|
|
753
|
+
message: diagnostic.message,
|
|
754
|
+
diagnostics: diagnostic.diagnostics?.filter(Boolean),
|
|
755
|
+
sessionFile: diagnostic.sessionFile,
|
|
756
|
+
sessionId: diagnostic.sessionId,
|
|
757
|
+
};
|
|
758
|
+
const last = runtime.plannerDiagnostics.at(-1);
|
|
759
|
+
if (last?.kind === normalized.kind && last.message === normalized.message) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
runtime.plannerDiagnostics.push(normalized);
|
|
763
|
+
emitProgress(runtime, normalized.message, {
|
|
764
|
+
phase: "planning",
|
|
765
|
+
status: normalized.kind,
|
|
766
|
+
isError: normalized.kind !== "repair_attempt",
|
|
767
|
+
plannerDiagnostic: normalized.kind,
|
|
768
|
+
plannerDiagnostics: normalized.diagnostics,
|
|
769
|
+
plannerSessionFile: normalized.sessionFile,
|
|
770
|
+
plannerSessionId: normalized.sessionId,
|
|
771
|
+
taskProgress: buildTaskProgressModel({ tasks: [] }),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
502
775
|
function createWorkerCostState(): WorkerCostState {
|
|
503
776
|
return {
|
|
504
777
|
total: 0,
|
|
@@ -753,6 +1026,30 @@ function initialTaskResultMarkdown(runId: string): string {
|
|
|
753
1026
|
return `# Pi Long Task TASK_RESULT\n\nRun: ${runId}\n`;
|
|
754
1027
|
}
|
|
755
1028
|
|
|
1029
|
+
async function appendFailureNote(
|
|
1030
|
+
pathname: string,
|
|
1031
|
+
message: string,
|
|
1032
|
+
plannerDiagnostics: readonly PlannerDiagnostic[],
|
|
1033
|
+
): Promise<void> {
|
|
1034
|
+
const lines = ["", "## Pi Long Task failure", "", message];
|
|
1035
|
+
if (plannerDiagnostics.length > 0) {
|
|
1036
|
+
lines.push("", "### Planner diagnostics");
|
|
1037
|
+
for (const diagnostic of plannerDiagnostics) {
|
|
1038
|
+
lines.push("", `- ${diagnostic.kind}: ${diagnostic.message}`);
|
|
1039
|
+
if (diagnostic.sessionId) {
|
|
1040
|
+
lines.push(` - Session ID: ${diagnostic.sessionId}`);
|
|
1041
|
+
}
|
|
1042
|
+
if (diagnostic.sessionFile) {
|
|
1043
|
+
lines.push(` - Session file: ${diagnostic.sessionFile}`);
|
|
1044
|
+
}
|
|
1045
|
+
for (const item of diagnostic.diagnostics ?? []) {
|
|
1046
|
+
lines.push(` - Diagnostic: ${item}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
await appendFile(pathname, `${lines.join("\n")}\n`, "utf8");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
756
1053
|
async function appendCommitNote(pathname: string, result: CommitAfterSessionResult): Promise<void> {
|
|
757
1054
|
const lines = ["", "### Commit note", ""];
|
|
758
1055
|
if (result.hash) {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { truncateToWidth, type Component, type OverlayHandle, type TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
|
|
5
5
|
import { runCoordinator, type CoordinatorProgressUpdate, type CoordinatorResult } from "./coordinator.ts";
|
|
6
6
|
import { longTaskInputTransform } from "./input_router.ts";
|
|
@@ -72,6 +72,7 @@ function toolDetails(result: CoordinatorResult) {
|
|
|
72
72
|
taskProgress: result.taskProgress,
|
|
73
73
|
workerCostTotal: result.workerCostTotal,
|
|
74
74
|
summary: result.summary,
|
|
75
|
+
error: result.error,
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -118,20 +119,24 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
let latestUpdate: CoordinatorProgressUpdate | undefined;
|
|
121
|
-
let
|
|
122
|
-
let
|
|
122
|
+
let widgetComponent: PiLongTaskSidebarComponent | undefined;
|
|
123
|
+
let widgetTui: TUI | undefined;
|
|
124
|
+
let overlayComponent: PiLongTaskSidebarComponent | undefined;
|
|
125
|
+
let overlayTui: TUI | undefined;
|
|
126
|
+
let overlayDone: ((result: undefined) => void) | undefined;
|
|
127
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
123
128
|
let closed = false;
|
|
124
129
|
|
|
125
130
|
if (supportsTuiWidget(ctx)) {
|
|
126
131
|
ctx.ui.setWidget(
|
|
127
132
|
LONG_TASK_WIDGET_KEY,
|
|
128
133
|
(tui, theme) => {
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
widgetTui = tui;
|
|
135
|
+
widgetComponent = new PiLongTaskSidebarComponent(theme, () => sidebarWidgetLineLimit(tui));
|
|
131
136
|
if (latestUpdate) {
|
|
132
|
-
|
|
137
|
+
widgetComponent.setUpdate(latestUpdate);
|
|
133
138
|
}
|
|
134
|
-
return
|
|
139
|
+
return widgetComponent;
|
|
135
140
|
},
|
|
136
141
|
{ placement: "aboveEditor" },
|
|
137
142
|
);
|
|
@@ -139,15 +144,59 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
139
144
|
ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, ["Pi Long Task: preparing sidebar..."], { placement: "aboveEditor" });
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
if (supportsTuiOverlay(ctx)) {
|
|
148
|
+
try {
|
|
149
|
+
const overlayPromise = ctx.ui.custom<undefined>(
|
|
150
|
+
(tui, theme, _keybindings, done) => {
|
|
151
|
+
overlayTui = tui;
|
|
152
|
+
overlayDone = done;
|
|
153
|
+
overlayComponent = new PiLongTaskSidebarComponent(theme);
|
|
154
|
+
if (latestUpdate) {
|
|
155
|
+
overlayComponent.setUpdate(latestUpdate);
|
|
156
|
+
}
|
|
157
|
+
if (closed) {
|
|
158
|
+
done(undefined);
|
|
159
|
+
}
|
|
160
|
+
return overlayComponent;
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
overlay: true,
|
|
164
|
+
overlayOptions: {
|
|
165
|
+
anchor: "right-center",
|
|
166
|
+
width: "34%",
|
|
167
|
+
minWidth: 32,
|
|
168
|
+
maxHeight: "85%",
|
|
169
|
+
margin: 1,
|
|
170
|
+
nonCapturing: true,
|
|
171
|
+
visible: (termWidth, termHeight) => termWidth >= 96 && termHeight >= 16,
|
|
172
|
+
},
|
|
173
|
+
onHandle: (handle) => {
|
|
174
|
+
overlayHandle = handle;
|
|
175
|
+
handle.unfocus();
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
void overlayPromise.catch(() => {
|
|
180
|
+
// The widget fallback remains active if overlay registration is unavailable.
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
// The widget fallback remains active if overlay registration is unavailable.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
142
187
|
return {
|
|
143
188
|
update(update: CoordinatorProgressUpdate): void {
|
|
144
189
|
if (closed) {
|
|
145
190
|
return;
|
|
146
191
|
}
|
|
147
192
|
latestUpdate = update;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
193
|
+
widgetComponent?.setUpdate(update);
|
|
194
|
+
overlayComponent?.setUpdate(update);
|
|
195
|
+
widgetTui?.requestRender();
|
|
196
|
+
if (overlayTui && overlayTui !== widgetTui) {
|
|
197
|
+
overlayTui.requestRender();
|
|
198
|
+
}
|
|
199
|
+
if (!widgetComponent) {
|
|
151
200
|
ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, renderSidebarWidgetLines(update), { placement: "aboveEditor" });
|
|
152
201
|
}
|
|
153
202
|
},
|
|
@@ -157,8 +206,17 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
|
|
|
157
206
|
}
|
|
158
207
|
closed = true;
|
|
159
208
|
ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, undefined);
|
|
160
|
-
|
|
161
|
-
|
|
209
|
+
if (overlayHandle) {
|
|
210
|
+
overlayHandle.hide();
|
|
211
|
+
} else {
|
|
212
|
+
overlayDone?.(undefined);
|
|
213
|
+
}
|
|
214
|
+
widgetComponent = undefined;
|
|
215
|
+
widgetTui = undefined;
|
|
216
|
+
overlayComponent = undefined;
|
|
217
|
+
overlayTui = undefined;
|
|
218
|
+
overlayDone = undefined;
|
|
219
|
+
overlayHandle = undefined;
|
|
162
220
|
},
|
|
163
221
|
};
|
|
164
222
|
}
|
|
@@ -168,6 +226,11 @@ function supportsTuiWidget(ctx: UiContext): boolean {
|
|
|
168
226
|
return mode === "tui" || mode === undefined;
|
|
169
227
|
}
|
|
170
228
|
|
|
229
|
+
function supportsTuiOverlay(ctx: UiContext): boolean {
|
|
230
|
+
const mode = (ctx as UiContext & { mode?: string }).mode;
|
|
231
|
+
return mode === "tui" || mode === undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
171
234
|
function sidebarWidgetLineLimit(tui: TUI): number {
|
|
172
235
|
const terminalRows = tui.terminal.rows;
|
|
173
236
|
const rows = Number.isFinite(terminalRows) ? Math.max(0, Math.floor(terminalRows)) : 24;
|
|
@@ -679,6 +742,7 @@ export default function registerPiLongTaskExtension(pi: ExtensionAPI) {
|
|
|
679
742
|
const result = await runCoordinator({
|
|
680
743
|
...params,
|
|
681
744
|
cwd: ctx?.cwd,
|
|
745
|
+
workerModel: ctx?.model,
|
|
682
746
|
abortSignal: signal,
|
|
683
747
|
onProgress: publishProgress,
|
|
684
748
|
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assistantTextFromEvent,
|
|
3
|
+
lastAssistantTextFromEvents,
|
|
4
|
+
lastAssistantTextFromMessages,
|
|
5
|
+
type WorkerSessionLike,
|
|
6
|
+
} from "./worker_session.ts";
|
|
7
|
+
|
|
8
|
+
export interface GuardedSessionPromptOptions {
|
|
9
|
+
session: WorkerSessionLike;
|
|
10
|
+
prompt: string;
|
|
11
|
+
promptOptions?: Record<string, unknown>;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
gracefulShutdownMs?: number;
|
|
15
|
+
gracefulShutdownPrompt?: string;
|
|
16
|
+
diagnostics?: string[];
|
|
17
|
+
onEvent?: (event: unknown) => void;
|
|
18
|
+
dispose?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GuardedSessionPromptResult {
|
|
22
|
+
assistantText: string;
|
|
23
|
+
timedOut: boolean;
|
|
24
|
+
aborted: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
diagnostics: string[];
|
|
27
|
+
events: unknown[];
|
|
28
|
+
sessionFile?: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runGuardedSessionPrompt(
|
|
33
|
+
options: GuardedSessionPromptOptions,
|
|
34
|
+
): Promise<GuardedSessionPromptResult> {
|
|
35
|
+
const session = options.session;
|
|
36
|
+
const diagnostics = [...(options.diagnostics ?? [])];
|
|
37
|
+
const events: unknown[] = [];
|
|
38
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
39
|
+
let assistantText = "";
|
|
40
|
+
let timedOut = false;
|
|
41
|
+
let aborted = false;
|
|
42
|
+
let error: string | undefined;
|
|
43
|
+
let promptSettled = false;
|
|
44
|
+
let finished = false;
|
|
45
|
+
let unsubscribe: (() => void) | undefined;
|
|
46
|
+
let complete: (() => void) | undefined;
|
|
47
|
+
|
|
48
|
+
const completed = new Promise<void>((resolve) => {
|
|
49
|
+
complete = resolve;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const resolveCompleted = () => {
|
|
53
|
+
complete?.();
|
|
54
|
+
complete = undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const clearTimers = () => {
|
|
58
|
+
for (const timer of timers) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
timers.clear();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const schedule = (fn: () => void, ms: number) => {
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
timers.delete(timer);
|
|
67
|
+
fn();
|
|
68
|
+
}, ms);
|
|
69
|
+
timers.add(timer);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const abortSession = (reason: string) => {
|
|
73
|
+
if (finished || aborted) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
aborted = true;
|
|
77
|
+
error = error ?? reason;
|
|
78
|
+
try {
|
|
79
|
+
const abortResult = session.abort?.();
|
|
80
|
+
if (isPromiseLike(abortResult)) {
|
|
81
|
+
void abortResult.catch((exc: unknown) => {
|
|
82
|
+
diagnostics.push(`session abort failed: ${errorMessage(exc)}`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} catch (exc) {
|
|
86
|
+
diagnostics.push(`session abort failed: ${errorMessage(exc)}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const requestGracefulShutdown = () => {
|
|
91
|
+
const message = options.gracefulShutdownPrompt?.trim();
|
|
92
|
+
if (!message || finished || promptSettled || aborted) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
if (session.isBashRunning && session.abortBash) {
|
|
98
|
+
session.abortBash();
|
|
99
|
+
diagnostics.push("aborted running bash before graceful shutdown request");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let request: Promise<unknown> | undefined;
|
|
103
|
+
if ((session.isStreaming || session.isBashRunning) && session.steer) {
|
|
104
|
+
request = session.steer(message);
|
|
105
|
+
} else if (session.followUp) {
|
|
106
|
+
request = session.followUp(message);
|
|
107
|
+
} else if (session.steer) {
|
|
108
|
+
request = session.steer(message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!request) {
|
|
112
|
+
diagnostics.push("graceful shutdown request skipped: session does not support steer/followUp");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
void request.catch((exc: unknown) => {
|
|
117
|
+
diagnostics.push(`graceful shutdown request failed: ${errorMessage(exc)}`);
|
|
118
|
+
});
|
|
119
|
+
} catch (exc) {
|
|
120
|
+
diagnostics.push(`graceful shutdown request failed: ${errorMessage(exc)}`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const triggerTimeout = () => {
|
|
125
|
+
if (finished || promptSettled || timedOut) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
timedOut = true;
|
|
129
|
+
diagnostics.push(`session prompt timed out after ${formatMilliseconds(timeoutMs(options.timeoutMs))}`);
|
|
130
|
+
requestGracefulShutdown();
|
|
131
|
+
|
|
132
|
+
const graceMs = nonNegativeMilliseconds(options.gracefulShutdownMs);
|
|
133
|
+
const hardAbort = () => {
|
|
134
|
+
if (finished || promptSettled) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
abortSession(`session prompt exceeded ${formatMilliseconds(timeoutMs(options.timeoutMs))} timeout`);
|
|
138
|
+
resolveCompleted();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (graceMs > 0) {
|
|
142
|
+
schedule(hardAbort, graceMs);
|
|
143
|
+
} else {
|
|
144
|
+
hardAbort();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const abortListener = () => {
|
|
149
|
+
if (finished || promptSettled) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
abortSession(abortReason(options.abortSignal, "session prompt aborted by outer signal"));
|
|
153
|
+
resolveCompleted();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
if (options.abortSignal?.aborted) {
|
|
158
|
+
aborted = true;
|
|
159
|
+
error = abortReason(options.abortSignal, "session prompt aborted before start");
|
|
160
|
+
} else {
|
|
161
|
+
unsubscribe = session.subscribe((event: unknown) => {
|
|
162
|
+
events.push(event);
|
|
163
|
+
const text = assistantTextFromEvent(event);
|
|
164
|
+
if (text) {
|
|
165
|
+
assistantText = text;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
options.onEvent?.(event);
|
|
169
|
+
} catch (exc) {
|
|
170
|
+
diagnostics.push(`event listener failed: ${errorMessage(exc)}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
options.abortSignal?.addEventListener("abort", abortListener, { once: true });
|
|
175
|
+
|
|
176
|
+
const promptPromise = session.prompt(options.prompt, options.promptOptions).then(
|
|
177
|
+
() => {
|
|
178
|
+
promptSettled = true;
|
|
179
|
+
resolveCompleted();
|
|
180
|
+
},
|
|
181
|
+
(exc: unknown) => {
|
|
182
|
+
promptSettled = true;
|
|
183
|
+
error = error ?? errorMessage(exc);
|
|
184
|
+
resolveCompleted();
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
void promptPromise;
|
|
188
|
+
|
|
189
|
+
const limitMs = timeoutMs(options.timeoutMs);
|
|
190
|
+
if (limitMs > 0) {
|
|
191
|
+
schedule(triggerTimeout, limitMs);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await completed;
|
|
195
|
+
}
|
|
196
|
+
} catch (exc) {
|
|
197
|
+
error = error ?? errorMessage(exc);
|
|
198
|
+
} finally {
|
|
199
|
+
finished = true;
|
|
200
|
+
clearTimers();
|
|
201
|
+
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
202
|
+
unsubscribe?.();
|
|
203
|
+
assistantText = latestAssistantText(session, events, assistantText);
|
|
204
|
+
if (options.dispose !== false) {
|
|
205
|
+
try {
|
|
206
|
+
const disposeResult = (session.dispose as (() => unknown) | undefined)?.();
|
|
207
|
+
if (isPromiseLike(disposeResult)) {
|
|
208
|
+
await disposeResult;
|
|
209
|
+
}
|
|
210
|
+
} catch (exc) {
|
|
211
|
+
const message = `session dispose failed: ${errorMessage(exc)}`;
|
|
212
|
+
diagnostics.push(message);
|
|
213
|
+
error = error ?? message;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return buildResult(session, events, assistantText, timedOut, aborted, error, diagnostics);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildResult(
|
|
222
|
+
session: WorkerSessionLike,
|
|
223
|
+
events: unknown[],
|
|
224
|
+
assistantText: string,
|
|
225
|
+
timedOut: boolean,
|
|
226
|
+
aborted: boolean,
|
|
227
|
+
error: string | undefined,
|
|
228
|
+
diagnostics: string[],
|
|
229
|
+
): GuardedSessionPromptResult {
|
|
230
|
+
return {
|
|
231
|
+
assistantText: latestAssistantText(session, events, assistantText),
|
|
232
|
+
timedOut,
|
|
233
|
+
aborted,
|
|
234
|
+
error,
|
|
235
|
+
diagnostics: [...diagnostics],
|
|
236
|
+
events: [...events],
|
|
237
|
+
sessionFile: session.sessionFile,
|
|
238
|
+
sessionId: session.sessionId,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function latestAssistantText(session: WorkerSessionLike, events: unknown[], fallback: string): string {
|
|
243
|
+
const direct = session.getLastAssistantText?.();
|
|
244
|
+
if (direct) {
|
|
245
|
+
return direct;
|
|
246
|
+
}
|
|
247
|
+
const fromMessages = lastAssistantTextFromMessages(session.messages);
|
|
248
|
+
if (fromMessages) {
|
|
249
|
+
return fromMessages;
|
|
250
|
+
}
|
|
251
|
+
const fromEvents = lastAssistantTextFromEvents(events);
|
|
252
|
+
return fromEvents || fallback;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function timeoutMs(value: number | undefined): number {
|
|
256
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
return Math.max(0, value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function nonNegativeMilliseconds(value: number | undefined): number {
|
|
263
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
return Math.max(0, value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatMilliseconds(ms: number): string {
|
|
270
|
+
return `${(ms / 1000).toFixed(3)}s`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function abortReason(signal: AbortSignal | undefined, fallback: string): string {
|
|
274
|
+
const reason = signal?.reason;
|
|
275
|
+
if (reason === undefined) {
|
|
276
|
+
return fallback;
|
|
277
|
+
}
|
|
278
|
+
return errorMessage(reason);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function errorMessage(error: unknown): string {
|
|
282
|
+
return error instanceof Error ? error.message : String(error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
286
|
+
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
287
|
+
}
|
package/src/todo_generator.ts
CHANGED
|
@@ -120,6 +120,16 @@ export function buildTodoCreationPrompt(rawInput: string): string {
|
|
|
120
120
|
return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.\n\nRequirements:\n- Output only markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n\nRaw input:\n\n${rawInput.trim()}\n`;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function buildTodoRepairPrompt(rawInput: string, invalidOutput: string, validationError: string): string {
|
|
124
|
+
return `Your previous response was not valid Pi Long Task TODO markdown. Correct it now.\n\nValidation/extraction error:\n${validationError.trim() || "Unknown validation error."}\n\nRequirements:\n- Output only corrected markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n\nOriginal raw input:\n\n${rawInput.trim()}\n\nPrevious invalid output:\n\n${invalidOutput.trim()}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function extractAndValidateTodoMarkdown(assistantText: string): string {
|
|
128
|
+
const markdown = extractTodoMarkdown(assistantText);
|
|
129
|
+
validateTodoMarkdown(markdown);
|
|
130
|
+
return markdown;
|
|
131
|
+
}
|
|
132
|
+
|
|
123
133
|
export function extractTodoMarkdown(assistantText: string): string {
|
|
124
134
|
for (const block of fencedMarkdownBlocks(assistantText)) {
|
|
125
135
|
const candidate = normalizeCandidate(block);
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
export interface ParsedWorkerRuntimeConfig {
|
|
2
|
+
modelName?: string;
|
|
3
|
+
maxAttemptsPerTask?: number;
|
|
4
|
+
taskTimeoutMs?: number;
|
|
5
|
+
maxBashTimeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const MODEL_TOKEN_RE = /[A-Za-z0-9][A-Za-z0-9._~:+/@-]*/;
|
|
9
|
+
const STOP_WORDS = new Set([
|
|
10
|
+
"and",
|
|
11
|
+
"as",
|
|
12
|
+
"for",
|
|
13
|
+
"from",
|
|
14
|
+
"in",
|
|
15
|
+
"is",
|
|
16
|
+
"of",
|
|
17
|
+
"on",
|
|
18
|
+
"per",
|
|
19
|
+
"please",
|
|
20
|
+
"task",
|
|
21
|
+
"tasks",
|
|
22
|
+
"the",
|
|
23
|
+
"to",
|
|
24
|
+
"use",
|
|
25
|
+
"with",
|
|
26
|
+
"worker",
|
|
27
|
+
"workers",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfig {
|
|
31
|
+
const state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string } = {};
|
|
32
|
+
|
|
33
|
+
parseLineDirectives(text, state);
|
|
34
|
+
parseNaturalLanguageDirectives(text, state);
|
|
35
|
+
|
|
36
|
+
const modelName = combineProviderAndModel(state.provider, state.model);
|
|
37
|
+
return {
|
|
38
|
+
...(modelName ? { modelName } : {}),
|
|
39
|
+
...(state.maxAttemptsPerTask !== undefined ? { maxAttemptsPerTask: state.maxAttemptsPerTask } : {}),
|
|
40
|
+
...(state.taskTimeoutMs !== undefined ? { taskTimeoutMs: state.taskTimeoutMs } : {}),
|
|
41
|
+
...(state.maxBashTimeoutMs !== undefined ? { maxBashTimeoutMs: state.maxBashTimeoutMs } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseLineDirectives(
|
|
46
|
+
text: string,
|
|
47
|
+
state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
|
|
48
|
+
): void {
|
|
49
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
50
|
+
const line = rawLine.replace(/^\s{0,3}>+\s?/, "").trim();
|
|
51
|
+
const match = line.match(
|
|
52
|
+
/^(?:[-*+]\s*)?(?:(pi\s+long\s+task|long\s+task|worker|workers?|task)\s+)?([a-z][a-z\s-]{0,40})\s*(?::|=)\s*(.+)$/i,
|
|
53
|
+
);
|
|
54
|
+
if (!match) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const prefix = normalizeWords(match[1] ?? "");
|
|
59
|
+
const key = normalizeWords(match[2] ?? "");
|
|
60
|
+
const fullKey = normalizeWords(`${prefix} ${key}`);
|
|
61
|
+
const value = match[3] ?? "";
|
|
62
|
+
|
|
63
|
+
applyDirective(fullKey, value, state);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseNaturalLanguageDirectives(
|
|
68
|
+
text: string,
|
|
69
|
+
state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
|
|
70
|
+
): void {
|
|
71
|
+
captureTokens(
|
|
72
|
+
text,
|
|
73
|
+
/\bworker\s+(?:model|provider\/model)\s*(?:is|=|:|to|as)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
|
|
74
|
+
(token) => {
|
|
75
|
+
state.model = token;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
captureTokens(
|
|
79
|
+
text,
|
|
80
|
+
/\b(?:use|using|with|set|run(?:ning)?)\s+(?:the\s+)?(?:worker\s+)?model\s*(?:to|as|is|=|:)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
|
|
81
|
+
(token) => {
|
|
82
|
+
state.model = token;
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
captureTokens(text, /\bmodel\s*(?:=|:)\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi, (token) => {
|
|
86
|
+
state.model = token;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
captureTokens(
|
|
90
|
+
text,
|
|
91
|
+
/\bworker\s+provider\s*(?:is|=|:|to|as)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
|
|
92
|
+
(token) => {
|
|
93
|
+
state.provider = token;
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
captureTokens(text, /\bprovider\s*(?:=|:)\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi, (token) => {
|
|
97
|
+
state.provider = token;
|
|
98
|
+
});
|
|
99
|
+
captureTokens(
|
|
100
|
+
text,
|
|
101
|
+
/\b(?:use|using|with|set)\s+(?:the\s+)?(?:worker\s+)?provider\s*(?:to|as|is|=|:)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
|
|
102
|
+
(token) => {
|
|
103
|
+
state.provider = token;
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
captureNumbers(
|
|
108
|
+
text,
|
|
109
|
+
/\b(?:max(?:imum)?\s+)?(?:worker\s+|task\s+)?attempts?\s*(?:per\s+task)?\s*(?:is|=|:|to|at)?\s*(\d+)/gi,
|
|
110
|
+
(value) => {
|
|
111
|
+
state.maxAttemptsPerTask = value;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
captureNumbers(text, /\b(\d+)\s+(?:worker\s+|task\s+)?attempts?\b/gi, (value) => {
|
|
115
|
+
state.maxAttemptsPerTask = value;
|
|
116
|
+
});
|
|
117
|
+
captureNumbers(text, /\btry\s+(?:each\s+task\s+)?(?:up\s+to\s+)?(\d+)\s+times\b/gi, (value) => {
|
|
118
|
+
state.maxAttemptsPerTask = value;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
captureDurations(
|
|
122
|
+
text,
|
|
123
|
+
/\b(?<!bash\s)(?<!max\s)(?:worker\s+|task\s+)?timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
124
|
+
(value) => {
|
|
125
|
+
state.taskTimeoutMs = value;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
captureDurations(
|
|
129
|
+
text,
|
|
130
|
+
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))\s+(?:worker\s+|task\s+)?timeout\b/gi,
|
|
131
|
+
(value) => {
|
|
132
|
+
state.taskTimeoutMs = value;
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
captureDurations(
|
|
136
|
+
text,
|
|
137
|
+
/\b(?:max\s+)?bash\s+timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
138
|
+
(value) => {
|
|
139
|
+
state.maxBashTimeoutMs = value;
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function applyDirective(
|
|
145
|
+
key: string,
|
|
146
|
+
value: string,
|
|
147
|
+
state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
|
|
148
|
+
): void {
|
|
149
|
+
if (/\bprovider\b/.test(key)) {
|
|
150
|
+
const token = modelToken(value);
|
|
151
|
+
if (token) {
|
|
152
|
+
state.provider = token;
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (/\bmodel\b/.test(key)) {
|
|
158
|
+
const token = modelToken(value);
|
|
159
|
+
if (token) {
|
|
160
|
+
state.model = token;
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (/\b(?:attempt|attempts|retry|retries)\b/.test(key)) {
|
|
166
|
+
const attempts = positiveIntegerFromText(value);
|
|
167
|
+
if (attempts !== undefined) {
|
|
168
|
+
state.maxAttemptsPerTask = attempts;
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (/\bbash\b/.test(key) && /\btimeout\b/.test(key)) {
|
|
174
|
+
const timeout = durationMsFromText(value, { allowBareSeconds: true });
|
|
175
|
+
if (timeout !== undefined) {
|
|
176
|
+
state.maxBashTimeoutMs = timeout;
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (/\btimeout\b/.test(key)) {
|
|
182
|
+
const timeout = durationMsFromText(value, { allowBareSeconds: true });
|
|
183
|
+
if (timeout !== undefined) {
|
|
184
|
+
state.taskTimeoutMs = timeout;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function captureTokens(text: string, pattern: RegExp, apply: (token: string) => void): void {
|
|
190
|
+
for (const match of text.matchAll(pattern)) {
|
|
191
|
+
const token = modelToken(match[1] ?? "");
|
|
192
|
+
if (token) {
|
|
193
|
+
apply(token);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function captureNumbers(text: string, pattern: RegExp, apply: (value: number) => void): void {
|
|
199
|
+
for (const match of text.matchAll(pattern)) {
|
|
200
|
+
const value = positiveIntegerFromText(match[1] ?? "");
|
|
201
|
+
if (value !== undefined) {
|
|
202
|
+
apply(value);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function captureDurations(text: string, pattern: RegExp, apply: (value: number) => void): void {
|
|
208
|
+
for (const match of text.matchAll(pattern)) {
|
|
209
|
+
const value = durationMsFromText(match[1] ?? "", { allowBareSeconds: true });
|
|
210
|
+
if (value !== undefined) {
|
|
211
|
+
apply(value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function combineProviderAndModel(provider: string | undefined, model: string | undefined): string | undefined {
|
|
217
|
+
if (!model) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
if (model.includes("/") || !provider) {
|
|
221
|
+
return model;
|
|
222
|
+
}
|
|
223
|
+
return `${provider}/${model}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function modelToken(value: string): string | undefined {
|
|
227
|
+
const trimmed = trimDirectiveValue(value);
|
|
228
|
+
const match = MODEL_TOKEN_RE.exec(trimmed);
|
|
229
|
+
if (!match) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const token = match[0].replace(/[.,;:]+$/g, "");
|
|
233
|
+
return token && !STOP_WORDS.has(token.toLowerCase()) ? token : undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function trimDirectiveValue(value: string): string {
|
|
237
|
+
return value
|
|
238
|
+
.trim()
|
|
239
|
+
.replace(/^['"`]+/, "")
|
|
240
|
+
.replace(/['"`]+$/, "")
|
|
241
|
+
.trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function positiveIntegerFromText(value: string): number | undefined {
|
|
245
|
+
const match = /\d+/.exec(value);
|
|
246
|
+
if (!match) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const parsed = Number.parseInt(match[0], 10);
|
|
250
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function durationMsFromText(value: string, options: { allowBareSeconds: boolean }): number | undefined {
|
|
254
|
+
const match = /(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?\b/i.exec(
|
|
255
|
+
value,
|
|
256
|
+
);
|
|
257
|
+
if (!match) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const amount = Number.parseFloat(match[1] ?? "");
|
|
262
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
267
|
+
if (!unit && !options.allowBareSeconds) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const multiplier = durationMultiplier(unit || "seconds");
|
|
272
|
+
const milliseconds = Math.round(amount * multiplier);
|
|
273
|
+
return Number.isSafeInteger(milliseconds) && milliseconds > 0 ? milliseconds : undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function durationMultiplier(unit: string): number {
|
|
277
|
+
if (unit === "ms" || unit.startsWith("millisecond") || unit.startsWith("msec")) {
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
if (unit === "h" || unit.startsWith("hour") || unit.startsWith("hr")) {
|
|
281
|
+
return 60 * 60 * 1000;
|
|
282
|
+
}
|
|
283
|
+
if (unit === "m" || unit.startsWith("minute") || unit.startsWith("min")) {
|
|
284
|
+
return 60 * 1000;
|
|
285
|
+
}
|
|
286
|
+
return 1000;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeWords(value: string): string {
|
|
290
|
+
return value.toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
291
|
+
}
|
package/src/worker_session.ts
CHANGED
|
@@ -186,7 +186,6 @@ export const extractAssistantTextFromEvent = assistantTextFromEvent;
|
|
|
186
186
|
export const extractLastAssistantTextFromEvents = lastAssistantTextFromEvents;
|
|
187
187
|
|
|
188
188
|
export const DEFAULT_WORKER_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
|
|
189
|
-
export const DEFAULT_WORKER_MODEL = "openai-codex/gpt-5.5";
|
|
190
189
|
export const DEFAULT_WORKER_THINKING_LEVEL = "high";
|
|
191
190
|
export const DEFAULT_TASK_TIMEOUT_SECONDS = 60 * 60;
|
|
192
191
|
export const DEFAULT_GRACEFUL_SHUTDOWN_SECONDS = 60;
|
|
@@ -309,7 +308,8 @@ export async function createIsolatedWorkerSession(
|
|
|
309
308
|
const resourceLoader = disableExtensionsForWorker(discoveredResourceLoader, () => pi.createExtensionRuntime());
|
|
310
309
|
await resourceLoader.reload();
|
|
311
310
|
|
|
312
|
-
const model =
|
|
311
|
+
const model =
|
|
312
|
+
options.model ?? (options.modelName ? await resolveWorkerModel(modelRegistry, options.modelName) : undefined);
|
|
313
313
|
const createOptions: Record<string, unknown> = {
|
|
314
314
|
cwd,
|
|
315
315
|
agentDir,
|