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/coordinator.ts
CHANGED
|
@@ -32,6 +32,24 @@ import {
|
|
|
32
32
|
type PlanRevisionRelevantResult,
|
|
33
33
|
} from "./plan_revision_generation.ts";
|
|
34
34
|
import { taskSemanticFingerprint, type PlanTaskState } from "./plan_revision.ts";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_PLANNER_THINKING_LEVEL,
|
|
37
|
+
DEFAULT_PLANNER_TIMEOUT_MS,
|
|
38
|
+
resolvePlannerBudget,
|
|
39
|
+
resolvePlannerGracefulShutdownMs,
|
|
40
|
+
resolvePlannerTimeoutMs,
|
|
41
|
+
type PlannerBudget,
|
|
42
|
+
} from "./planner_config.ts";
|
|
43
|
+
import {
|
|
44
|
+
createPlannerActiveProgress,
|
|
45
|
+
createPlannerGraceProgress,
|
|
46
|
+
createPlannerStartedProgress,
|
|
47
|
+
formatFriendlyDuration,
|
|
48
|
+
plannerProgressCheckpoints,
|
|
49
|
+
type PlannerProgressEvent,
|
|
50
|
+
type PlannerProgressHandler,
|
|
51
|
+
type PlannerProgressState,
|
|
52
|
+
} from "./planner_progress.ts";
|
|
35
53
|
import {
|
|
36
54
|
PersistentTodoPlanStore,
|
|
37
55
|
planTaskReference,
|
|
@@ -53,6 +71,7 @@ import {
|
|
|
53
71
|
import { buildTaskProgressModel, type TaskProgressModel, type TaskProgressStatus } from "./task_progress.ts";
|
|
54
72
|
import {
|
|
55
73
|
applyGoalInstructionsToTodoMarkdown,
|
|
74
|
+
applyWorkerCapabilityConstraintsToTodoMarkdown,
|
|
56
75
|
buildTodoCreationPrompt,
|
|
57
76
|
buildTodoRepairPrompt,
|
|
58
77
|
extractAndValidateTodoMarkdown,
|
|
@@ -61,6 +80,7 @@ import {
|
|
|
61
80
|
validateTodoMarkdown,
|
|
62
81
|
} from "./todo_generator.ts";
|
|
63
82
|
import { parseTasks, todoGlobalInstructions, type Task } from "./todo_parser.ts";
|
|
83
|
+
import { detectUnavailableWorkerCapabilities, type WorkerCapabilityWarning } from "./worker_capabilities.ts";
|
|
64
84
|
import {
|
|
65
85
|
buildWorkerSessionCreationFailureOutcome,
|
|
66
86
|
createIsolatedWorkerSession,
|
|
@@ -81,14 +101,19 @@ import {
|
|
|
81
101
|
|
|
82
102
|
export type { CoordinatorStatus } from "./types.ts";
|
|
83
103
|
|
|
104
|
+
const WORKER_PROGRESS_MAX_BUFFER_CHARS = 2_048;
|
|
105
|
+
const WORKER_PROGRESS_MAX_STATUS_CHARS = 800;
|
|
106
|
+
const WORKER_PROGRESS_MIN_CHARACTER_DELTA = 256;
|
|
107
|
+
const WORKER_PROGRESS_MIN_INTERVAL_MS = 100;
|
|
108
|
+
|
|
84
109
|
export const DEFAULT_COORDINATOR_OPTIONS = {
|
|
85
110
|
maxAttemptsPerTask: 3,
|
|
86
111
|
taskTimeoutMs: 900_000,
|
|
87
|
-
todoTimeoutMs:
|
|
112
|
+
todoTimeoutMs: DEFAULT_PLANNER_TIMEOUT_MS,
|
|
88
113
|
todoGracefulShutdownMs: 15_000,
|
|
89
114
|
maxBashTimeoutMs: 300_000,
|
|
90
115
|
taskThinking: "high",
|
|
91
|
-
todoThinking:
|
|
116
|
+
todoThinking: DEFAULT_PLANNER_THINKING_LEVEL,
|
|
92
117
|
workerSessionReuse: DEFAULT_WORKER_SESSION_REUSE_ENABLED,
|
|
93
118
|
workerSessionReuseContextThresholdPercent: DEFAULT_WORKER_SESSION_REUSE_CONTEXT_THRESHOLD_PERCENT,
|
|
94
119
|
networkRecovery: DEFAULT_NETWORK_RECOVERY_CONFIG,
|
|
@@ -96,6 +121,7 @@ export const DEFAULT_COORDINATOR_OPTIONS = {
|
|
|
96
121
|
|
|
97
122
|
export type WorkerRunner = (options: RunWorkerTaskOptions) => Promise<SessionOutcome>;
|
|
98
123
|
export type CoordinatorProgressPhase =
|
|
124
|
+
| "capability_warning"
|
|
99
125
|
| "planning"
|
|
100
126
|
| "planned"
|
|
101
127
|
| "task_start"
|
|
@@ -108,14 +134,30 @@ export type CoordinatorProgressPhase =
|
|
|
108
134
|
| "task_obsolete"
|
|
109
135
|
| "complete";
|
|
110
136
|
|
|
111
|
-
export type PlannerDiagnosticKind =
|
|
137
|
+
export type PlannerDiagnosticKind =
|
|
138
|
+
| "timeout"
|
|
139
|
+
| "cancelled"
|
|
140
|
+
/** @deprecated Planner cancellation is now reported as `cancelled`. */
|
|
141
|
+
| "abort"
|
|
142
|
+
| "network_recovery"
|
|
143
|
+
| "network_failure"
|
|
144
|
+
| "invalid_output"
|
|
145
|
+
| "repair_attempt"
|
|
146
|
+
| "failure";
|
|
112
147
|
|
|
113
148
|
export interface PlannerDiagnostic {
|
|
114
149
|
kind: PlannerDiagnosticKind;
|
|
115
150
|
message: string;
|
|
151
|
+
/** Present whenever output presence is known; partial content itself is deliberately omitted. */
|
|
152
|
+
partialOutputObserved?: boolean;
|
|
116
153
|
diagnostics?: string[];
|
|
117
154
|
sessionFile?: string;
|
|
118
155
|
sessionId?: string;
|
|
156
|
+
/** Network lifecycle data is separate from timeout/cancellation classification. */
|
|
157
|
+
networkRecoveryEvent?: NetworkRecoveryEventType;
|
|
158
|
+
networkFailureReason?: string;
|
|
159
|
+
networkRetryCount?: number;
|
|
160
|
+
networkOutageElapsedMs?: number;
|
|
119
161
|
}
|
|
120
162
|
|
|
121
163
|
export type PlannerDiagnosticHandler = (diagnostic: PlannerDiagnostic) => void;
|
|
@@ -158,8 +200,18 @@ export interface CoordinatorProgressUpdate {
|
|
|
158
200
|
taskProgress?: TaskProgressModel;
|
|
159
201
|
plannerDiagnostic?: PlannerDiagnosticKind;
|
|
160
202
|
plannerDiagnostics?: string[];
|
|
203
|
+
plannerPartialOutputObserved?: boolean;
|
|
161
204
|
plannerSessionFile?: string;
|
|
162
205
|
plannerSessionId?: string;
|
|
206
|
+
/** Deterministic deadline selection used by planner calls in this run. */
|
|
207
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
208
|
+
/** Human-facing state with exact millisecond values retained for integrations. */
|
|
209
|
+
plannerProgressState?: PlannerProgressState;
|
|
210
|
+
plannerElapsedMs?: number;
|
|
211
|
+
plannerRemainingMs?: number;
|
|
212
|
+
plannerGracePeriodMs?: number;
|
|
213
|
+
plannerGraceRemainingMs?: number;
|
|
214
|
+
capabilityWarning?: Readonly<WorkerCapabilityWarning>;
|
|
163
215
|
workerSessionEvent?: WorkerSessionDiagnostic["event"];
|
|
164
216
|
workerSessionReason?: string;
|
|
165
217
|
workerSessionContextUsagePercent?: number;
|
|
@@ -170,6 +222,10 @@ export interface CoordinatorProgressUpdate {
|
|
|
170
222
|
networkNextRetryAtMs?: number;
|
|
171
223
|
networkNextRetryInMs?: number;
|
|
172
224
|
networkFailureReason?: string;
|
|
225
|
+
/** Identifies whether recovery belongs to planning or worker execution. */
|
|
226
|
+
networkOperation?: "planner" | "worker";
|
|
227
|
+
/** Planner recovery never mutates the configured per-attempt deadline. */
|
|
228
|
+
plannerDeadlinePolicy?: "per_attempt_excludes_network_wait";
|
|
173
229
|
}
|
|
174
230
|
|
|
175
231
|
export type CoordinatorProgressHandler = (update: CoordinatorProgressUpdate) => void;
|
|
@@ -210,20 +266,31 @@ export interface TodoPlannerOptions {
|
|
|
210
266
|
inputText: string;
|
|
211
267
|
cwd: string;
|
|
212
268
|
runDir: string;
|
|
213
|
-
|
|
269
|
+
/** Defaults to the planner-only balanced level; explicit values are forwarded unchanged. */
|
|
270
|
+
thinkingLevel?: string;
|
|
214
271
|
model?: unknown;
|
|
215
272
|
abortSignal?: AbortSignal;
|
|
216
273
|
timeoutMs?: number;
|
|
217
274
|
gracefulShutdownMs?: number;
|
|
275
|
+
/** Structured record of explicit/default/adaptive deadline selection. */
|
|
276
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
218
277
|
sessionFactory?: WorkerSessionFactory;
|
|
219
278
|
onDiagnostic?: PlannerDiagnosticHandler;
|
|
279
|
+
/** Shared human-readable timing events for CLI, TUI, and headless integrations. */
|
|
280
|
+
onProgress?: PlannerProgressHandler;
|
|
220
281
|
goal?: string;
|
|
221
282
|
/** Exact prompt for revision planners; bypasses the initial TODO-creation wrapper. */
|
|
222
283
|
plannerPrompt?: string;
|
|
223
284
|
/** Structured revision context supplied alongside plannerPrompt. */
|
|
224
285
|
planRevision?: Readonly<PlanRevisionRequest>;
|
|
225
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Normalized coordinator recovery policy. Recovery wait is accounted on its
|
|
288
|
+
* own outage clock; every replay receives the same configured per-attempt
|
|
289
|
+
* planner deadline, so recovery settings never replace that deadline.
|
|
290
|
+
*/
|
|
226
291
|
networkRecovery?: Readonly<NetworkRecoveryConfig>;
|
|
292
|
+
/** Run-level constraints derived from capabilities unavailable to isolated workers. */
|
|
293
|
+
capabilityConstraints?: readonly string[];
|
|
227
294
|
}
|
|
228
295
|
|
|
229
296
|
export interface TaskAttemptSummary {
|
|
@@ -276,6 +343,10 @@ export interface CoordinatorResult {
|
|
|
276
343
|
workerUsageTotal?: WorkerUsageTotals;
|
|
277
344
|
/** Additive lifecycle counters for adaptive worker-session reuse. */
|
|
278
345
|
workerSessionMetrics?: WorkerSessionMetrics;
|
|
346
|
+
/** Deterministic deadline selection used by planner calls in this run. */
|
|
347
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
348
|
+
/** Explicit, non-fatal warnings for requested capabilities unavailable to isolated workers. */
|
|
349
|
+
capabilityWarnings?: readonly WorkerCapabilityWarning[];
|
|
279
350
|
commit: boolean;
|
|
280
351
|
goal?: string;
|
|
281
352
|
error?: string;
|
|
@@ -307,6 +378,7 @@ interface RuntimeOptions {
|
|
|
307
378
|
networkRecovery: NetworkRecoveryConfig;
|
|
308
379
|
todoTimeoutMs: number;
|
|
309
380
|
todoGracefulShutdownMs: number;
|
|
381
|
+
plannerBudget: PlannerBudget;
|
|
310
382
|
workerRunner: WorkerRunner;
|
|
311
383
|
useRetainedWorkerLifecycle: boolean;
|
|
312
384
|
todoPlanner: TodoPlanner;
|
|
@@ -318,8 +390,11 @@ interface RuntimeOptions {
|
|
|
318
390
|
workerCostState: WorkerCostState;
|
|
319
391
|
workerActivityByWorker: Map<string, string>;
|
|
320
392
|
workerTextByWorker: Map<string, string>;
|
|
393
|
+
workerTextLengthByWorker: Map<string, number>;
|
|
321
394
|
workerTextPublishedLengthByWorker: Map<string, number>;
|
|
395
|
+
workerTextPublishedAtByWorker: Map<string, number>;
|
|
322
396
|
plannerDiagnostics: PlannerDiagnostic[];
|
|
397
|
+
capabilityWarnings: WorkerCapabilityWarning[];
|
|
323
398
|
workerSessionMetrics: WorkerSessionMetrics;
|
|
324
399
|
steeringQueue?: SerializedSteeringQueue;
|
|
325
400
|
onPlanRevisionAccepted?: (revision: GeneratedPlanRevision) => void | Promise<void>;
|
|
@@ -327,7 +402,12 @@ interface RuntimeOptions {
|
|
|
327
402
|
lastProgress?: CoordinatorProgressUpdate;
|
|
328
403
|
progressClosed: boolean;
|
|
329
404
|
networkRecoverySequence: number;
|
|
330
|
-
activeNetworkRecoveries: Map<number,
|
|
405
|
+
activeNetworkRecoveries: Map<number, ActiveNetworkRecovery>;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface ActiveNetworkRecovery {
|
|
409
|
+
event: NetworkRecoveryEvent;
|
|
410
|
+
operation: "planner" | "worker";
|
|
331
411
|
}
|
|
332
412
|
|
|
333
413
|
type RetainedWorkerReuseScope = "sequential_task" | "partial_continuation";
|
|
@@ -956,7 +1036,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
956
1036
|
const commits: CoordinatorCommitSummary[] = [];
|
|
957
1037
|
|
|
958
1038
|
await mkdir(runtime.runDir, { recursive: true });
|
|
959
|
-
await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId), "utf8");
|
|
1039
|
+
await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId, runtime.capabilityWarnings), "utf8");
|
|
960
1040
|
let planningComplete = false;
|
|
961
1041
|
let latestTodoMarkdown: string | undefined;
|
|
962
1042
|
let latestTasks: Task[] = [];
|
|
@@ -972,7 +1052,14 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
972
1052
|
const protectedDirtyPathsByTask = new Map<string, Set<string>>();
|
|
973
1053
|
|
|
974
1054
|
try {
|
|
975
|
-
|
|
1055
|
+
for (const warning of runtime.capabilityWarnings) {
|
|
1056
|
+
emitProgress(runtime, warning.message, {
|
|
1057
|
+
phase: "capability_warning",
|
|
1058
|
+
status: "warning",
|
|
1059
|
+
capabilityWarning: warning,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
emitPlannerProgress(runtime, createPlannerStartedProgress(runtime.plannerBudget, runtime.todoGracefulShutdownMs));
|
|
976
1063
|
let todoMarkdown = await generateOrNormalizeTodoMarkdown(inputText, runtime);
|
|
977
1064
|
validateTodoMarkdown(todoMarkdown);
|
|
978
1065
|
planningComplete = true;
|
|
@@ -1103,7 +1190,9 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1103
1190
|
activeTaskReference = taskPlanReference;
|
|
1104
1191
|
runtime.workerActivityByWorker.set(worker, initialActivity);
|
|
1105
1192
|
runtime.workerTextByWorker.delete(worker);
|
|
1193
|
+
runtime.workerTextLengthByWorker.delete(worker);
|
|
1106
1194
|
runtime.workerTextPublishedLengthByWorker.delete(worker);
|
|
1195
|
+
runtime.workerTextPublishedAtByWorker.delete(worker);
|
|
1107
1196
|
emitProgress(
|
|
1108
1197
|
runtime,
|
|
1109
1198
|
`Running TODO ${nextTask.taskId} — ${nextTask.title}${attempt > 1 ? ` (attempt ${attempt})` : ""}...`,
|
|
@@ -1374,6 +1463,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1374
1463
|
workerCostTotal: runtime.workerCostState.total,
|
|
1375
1464
|
workerUsageTotal: aggregateWorkerUsage(outcomes),
|
|
1376
1465
|
workerSessionMetrics: snapshotWorkerSessionMetrics(runtime.workerSessionMetrics),
|
|
1466
|
+
plannerBudget: runtime.plannerBudget,
|
|
1467
|
+
capabilityWarnings: runtime.capabilityWarnings,
|
|
1377
1468
|
commit: options.commit,
|
|
1378
1469
|
goal: runtime.goal,
|
|
1379
1470
|
error: failure,
|
|
@@ -1388,10 +1479,12 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1388
1479
|
return result;
|
|
1389
1480
|
} catch (error) {
|
|
1390
1481
|
const message = errorMessage(error);
|
|
1391
|
-
if (!planningComplete) {
|
|
1482
|
+
if (!planningComplete && !hasTerminalPlannerDiagnostic(runtime.plannerDiagnostics)) {
|
|
1392
1483
|
recordPlannerDiagnostic(runtime, {
|
|
1393
|
-
kind: "failure",
|
|
1394
|
-
message:
|
|
1484
|
+
kind: runtime.abortSignal?.aborted ? "cancelled" : "failure",
|
|
1485
|
+
message: runtime.abortSignal?.aborted
|
|
1486
|
+
? `TODO planning cancelled: ${abortSignalReason(runtime.abortSignal)}`
|
|
1487
|
+
: `TODO planning failed: ${message}`,
|
|
1395
1488
|
});
|
|
1396
1489
|
}
|
|
1397
1490
|
const resultError = !planningComplete
|
|
@@ -1457,6 +1550,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1457
1550
|
workerCostTotal: runtime.workerCostState.total,
|
|
1458
1551
|
workerUsageTotal: aggregateWorkerUsage(outcomes),
|
|
1459
1552
|
workerSessionMetrics: snapshotWorkerSessionMetrics(runtime.workerSessionMetrics),
|
|
1553
|
+
plannerBudget: runtime.plannerBudget,
|
|
1554
|
+
capabilityWarnings: runtime.capabilityWarnings,
|
|
1460
1555
|
commit: options.commit,
|
|
1461
1556
|
goal: runtime.goal,
|
|
1462
1557
|
error: resultError,
|
|
@@ -1478,9 +1573,10 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1478
1573
|
}
|
|
1479
1574
|
|
|
1480
1575
|
async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: RuntimeOptions): Promise<string> {
|
|
1576
|
+
const capabilityConstraints = runtime.capabilityWarnings.map((warning) => warning.planningConstraint);
|
|
1481
1577
|
const local = todoMarkdownFromString(inputText, runtime.goal);
|
|
1482
1578
|
if (local) {
|
|
1483
|
-
return local;
|
|
1579
|
+
return applyWorkerCapabilityConstraintsToTodoMarkdown(local, capabilityConstraints);
|
|
1484
1580
|
}
|
|
1485
1581
|
|
|
1486
1582
|
const plannerText = await requestTodoPlan(inputText, runtime);
|
|
@@ -1489,6 +1585,7 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
|
|
|
1489
1585
|
plannerText,
|
|
1490
1586
|
(repairPrompt) => requestTodoPlan(repairPrompt, runtime),
|
|
1491
1587
|
runtime.goal,
|
|
1588
|
+
capabilityConstraints,
|
|
1492
1589
|
{
|
|
1493
1590
|
onInvalidOutput: (validationError) =>
|
|
1494
1591
|
recordPlannerDiagnostic(runtime, {
|
|
@@ -1507,7 +1604,8 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
|
|
|
1507
1604
|
}),
|
|
1508
1605
|
},
|
|
1509
1606
|
);
|
|
1510
|
-
|
|
1607
|
+
const withGoal = applyGoalInstructionsToTodoMarkdown(planned, runtime.goal);
|
|
1608
|
+
return applyWorkerCapabilityConstraintsToTodoMarkdown(withGoal, capabilityConstraints);
|
|
1511
1609
|
}
|
|
1512
1610
|
|
|
1513
1611
|
interface TodoExtractionRepairHooks {
|
|
@@ -1521,6 +1619,7 @@ async function extractTodoMarkdownWithOneRepair(
|
|
|
1521
1619
|
plannerText: string,
|
|
1522
1620
|
requestRepair: (repairPrompt: string) => Promise<string>,
|
|
1523
1621
|
goal?: string,
|
|
1622
|
+
capabilityConstraints: readonly string[] = [],
|
|
1524
1623
|
hooks: TodoExtractionRepairHooks = {},
|
|
1525
1624
|
): Promise<string> {
|
|
1526
1625
|
try {
|
|
@@ -1529,7 +1628,9 @@ async function extractTodoMarkdownWithOneRepair(
|
|
|
1529
1628
|
const validationError = errorMessage(error);
|
|
1530
1629
|
hooks.onInvalidOutput?.(validationError);
|
|
1531
1630
|
hooks.onRepairAttempt?.(validationError);
|
|
1532
|
-
const repairText = await requestRepair(
|
|
1631
|
+
const repairText = await requestRepair(
|
|
1632
|
+
buildTodoRepairPrompt(inputText, plannerText, validationError, goal, capabilityConstraints),
|
|
1633
|
+
);
|
|
1533
1634
|
try {
|
|
1534
1635
|
return extractAndValidateTodoMarkdown(repairText);
|
|
1535
1636
|
} catch (repairError) {
|
|
@@ -1553,9 +1654,16 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
|
|
|
1553
1654
|
abortSignal: runtime.abortSignal,
|
|
1554
1655
|
timeoutMs: runtime.todoTimeoutMs,
|
|
1555
1656
|
gracefulShutdownMs: runtime.todoGracefulShutdownMs,
|
|
1657
|
+
plannerBudget: runtime.plannerBudget,
|
|
1556
1658
|
sessionFactory: runtime.todoSessionFactory,
|
|
1557
1659
|
networkRecovery: runtime.networkRecovery,
|
|
1660
|
+
capabilityConstraints: runtime.capabilityWarnings.map((warning) => warning.planningConstraint),
|
|
1558
1661
|
onDiagnostic: (diagnostic) => recordPlannerDiagnostic(runtime, diagnostic),
|
|
1662
|
+
onProgress: (event) => {
|
|
1663
|
+
if (event.state !== "started") {
|
|
1664
|
+
emitPlannerProgress(runtime, event);
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1559
1667
|
goal: runtime.goal,
|
|
1560
1668
|
},
|
|
1561
1669
|
runtime,
|
|
@@ -1567,8 +1675,10 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
|
|
|
1567
1675
|
* failed. The default planner disables tools and disposes every session before
|
|
1568
1676
|
* rejecting, so each retry rotates unsafe conversation state while replaying
|
|
1569
1677
|
* only the complete immutable planning context. Recovery owns no planner
|
|
1570
|
-
* repair/attempt counter, and each fresh call retains the
|
|
1571
|
-
*
|
|
1678
|
+
* repair/attempt counter, and each fresh call retains the exact configured
|
|
1679
|
+
* per-attempt planner timeout. Network wait is intentionally excluded and is
|
|
1680
|
+
* governed solely by the separate outage deadline; recovery cannot reset,
|
|
1681
|
+
* extend, or replace the timeout attached to any individual planner call.
|
|
1572
1682
|
*/
|
|
1573
1683
|
async function runPlannerOperationWithNetworkRecovery(
|
|
1574
1684
|
plannerOptions: TodoPlannerOptions,
|
|
@@ -1577,6 +1687,8 @@ async function runPlannerOperationWithNetworkRecovery(
|
|
|
1577
1687
|
const run = (recoverySignal?: AbortSignal) =>
|
|
1578
1688
|
runtime.todoPlanner({
|
|
1579
1689
|
...plannerOptions,
|
|
1690
|
+
// The recovery signal carries cancellation/outage expiry only. It does
|
|
1691
|
+
// not alter timeoutMs, which remains authoritative for every replay.
|
|
1580
1692
|
abortSignal: combineAbortSignals(plannerOptions.abortSignal, recoverySignal),
|
|
1581
1693
|
});
|
|
1582
1694
|
|
|
@@ -1584,17 +1696,73 @@ async function runPlannerOperationWithNetworkRecovery(
|
|
|
1584
1696
|
return await run();
|
|
1585
1697
|
} catch (initialFailure) {
|
|
1586
1698
|
const classification = classifyNetworkFailure(initialFailure);
|
|
1699
|
+
if (plannerOptions.abortSignal?.aborted || classification.reason === "cancelled") {
|
|
1700
|
+
recordPlannerCancellation(runtime, plannerOptions.abortSignal, initialFailure);
|
|
1701
|
+
throw plannerCancellationError(plannerOptions.abortSignal, initialFailure);
|
|
1702
|
+
}
|
|
1587
1703
|
if (!runtime.networkRecovery.enabled || !classification.recoverable) {
|
|
1588
1704
|
throw initialFailure;
|
|
1589
1705
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1706
|
+
|
|
1707
|
+
const publishRecovery = createNetworkRecoveryProgressHandler(runtime, "planner");
|
|
1708
|
+
let recoveryStarted = false;
|
|
1709
|
+
let lastRecoveryEvent: NetworkRecoveryEvent | undefined;
|
|
1710
|
+
const onRecoveryEvent = (event: NetworkRecoveryEvent) => {
|
|
1711
|
+
if (event.type !== "cleanup") lastRecoveryEvent = event;
|
|
1712
|
+
if (event.type === "outage_started" && !recoveryStarted) {
|
|
1713
|
+
recoveryStarted = true;
|
|
1714
|
+
recordPlannerDiagnostic(
|
|
1715
|
+
runtime,
|
|
1716
|
+
plannerNetworkDiagnostic(
|
|
1717
|
+
event,
|
|
1718
|
+
plannerOptions.timeoutMs,
|
|
1719
|
+
latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
1720
|
+
),
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
publishRecovery(event);
|
|
1724
|
+
if (event.type === "recovered") {
|
|
1725
|
+
recordPlannerDiagnostic(
|
|
1726
|
+
runtime,
|
|
1727
|
+
plannerNetworkDiagnostic(
|
|
1728
|
+
event,
|
|
1729
|
+
plannerOptions.timeoutMs,
|
|
1730
|
+
latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
1731
|
+
),
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
try {
|
|
1737
|
+
const recovered = await recoverNetworkOperation({
|
|
1738
|
+
initialFailure,
|
|
1739
|
+
config: runtime.networkRecovery,
|
|
1740
|
+
signal: plannerOptions.abortSignal,
|
|
1741
|
+
onEvent: onRecoveryEvent,
|
|
1742
|
+
retry: ({ signal }) => run(signal),
|
|
1743
|
+
});
|
|
1744
|
+
return recovered.value;
|
|
1745
|
+
} catch (recoveryFailure) {
|
|
1746
|
+
if (plannerOptions.abortSignal?.aborted) {
|
|
1747
|
+
recordPlannerCancellation(runtime, plannerOptions.abortSignal, recoveryFailure);
|
|
1748
|
+
throw plannerCancellationError(plannerOptions.abortSignal, recoveryFailure);
|
|
1749
|
+
} else if (!hasTerminalPlannerDiagnosticAfterLatestRecovery(runtime.plannerDiagnostics)) {
|
|
1750
|
+
const finalClassification = classifyNetworkFailure(recoveryFailure);
|
|
1751
|
+
recordPlannerDiagnostic(runtime, {
|
|
1752
|
+
kind: "network_failure",
|
|
1753
|
+
message: `TODO planner network recovery ended before planning completed: ${errorMessage(recoveryFailure)}`,
|
|
1754
|
+
partialOutputObserved: latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
1755
|
+
networkRecoveryEvent:
|
|
1756
|
+
lastRecoveryEvent?.type === "outage_expired" || lastRecoveryEvent?.type === "failed"
|
|
1757
|
+
? lastRecoveryEvent.type
|
|
1758
|
+
: "failed",
|
|
1759
|
+
networkFailureReason: lastRecoveryEvent?.state.lastFailure.reason ?? finalClassification.reason,
|
|
1760
|
+
networkRetryCount: lastRecoveryEvent?.state.retryCount,
|
|
1761
|
+
networkOutageElapsedMs: lastRecoveryEvent?.state.elapsedMs,
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
throw recoveryFailure;
|
|
1765
|
+
}
|
|
1598
1766
|
}
|
|
1599
1767
|
}
|
|
1600
1768
|
|
|
@@ -1640,9 +1808,12 @@ async function generateSteeringPlanRevision(options: {
|
|
|
1640
1808
|
abortSignal: options.runtime.abortSignal,
|
|
1641
1809
|
timeoutMs: options.runtime.todoTimeoutMs,
|
|
1642
1810
|
gracefulShutdownMs: options.runtime.todoGracefulShutdownMs,
|
|
1811
|
+
plannerBudget: options.runtime.plannerBudget,
|
|
1643
1812
|
sessionFactory: options.runtime.todoSessionFactory,
|
|
1644
1813
|
networkRecovery: options.runtime.networkRecovery,
|
|
1814
|
+
capabilityConstraints: options.runtime.capabilityWarnings.map((warning) => warning.planningConstraint),
|
|
1645
1815
|
onDiagnostic: (diagnostic) => recordPlannerDiagnostic(options.runtime, diagnostic),
|
|
1816
|
+
onProgress: (event) => emitPlannerProgress(options.runtime, event),
|
|
1646
1817
|
goal: options.runtime.goal,
|
|
1647
1818
|
},
|
|
1648
1819
|
options.runtime,
|
|
@@ -1751,16 +1922,41 @@ function relevantPlanRevisionResults(
|
|
|
1751
1922
|
// Planner/worker lifecycle differences are audited in docs/planner-worker-lifecycle-audit.md;
|
|
1752
1923
|
// keep this function's public contract stable while moving shared prompt guarding into a helper.
|
|
1753
1924
|
export async function runTodoPlanner(options: TodoPlannerOptions): Promise<string> {
|
|
1925
|
+
const capabilityConstraints =
|
|
1926
|
+
options.capabilityConstraints ??
|
|
1927
|
+
capabilityWarningsForRequest(options.inputText, options.goal).map((warning) => warning.planningConstraint);
|
|
1928
|
+
const plannerBudget =
|
|
1929
|
+
options.plannerBudget ??
|
|
1930
|
+
resolvePlannerBudget({
|
|
1931
|
+
inputText: options.inputText,
|
|
1932
|
+
explicitTimeoutMs: options.timeoutMs,
|
|
1933
|
+
defaultTimeoutMs: DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs,
|
|
1934
|
+
});
|
|
1935
|
+
const timeoutMs = resolvePlannerTimeoutMs(options.timeoutMs, plannerBudget.timeoutMs);
|
|
1936
|
+
const gracefulShutdownMs = resolvePlannerGracefulShutdownMs(
|
|
1937
|
+
options.gracefulShutdownMs,
|
|
1938
|
+
DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
|
|
1939
|
+
);
|
|
1940
|
+
const effectivePlannerBudget: PlannerBudget =
|
|
1941
|
+
timeoutMs === plannerBudget.timeoutMs
|
|
1942
|
+
? plannerBudget
|
|
1943
|
+
: {
|
|
1944
|
+
...plannerBudget,
|
|
1945
|
+
timeoutMs,
|
|
1946
|
+
extensionApplied: false,
|
|
1947
|
+
extensionMs: 0,
|
|
1948
|
+
source: "explicit",
|
|
1949
|
+
trigger: undefined,
|
|
1950
|
+
};
|
|
1951
|
+
notifyPlannerProgress(options.onProgress, createPlannerStartedProgress(effectivePlannerBudget, gracefulShutdownMs));
|
|
1754
1952
|
const sessionFactory = options.sessionFactory ?? createIsolatedWorkerSession;
|
|
1755
1953
|
const result = await sessionFactory({
|
|
1756
1954
|
cwd: options.cwd,
|
|
1757
1955
|
tools: [],
|
|
1758
1956
|
model: options.model,
|
|
1759
|
-
thinkingLevel: options.thinkingLevel,
|
|
1957
|
+
thinkingLevel: options.thinkingLevel ?? DEFAULT_PLANNER_THINKING_LEVEL,
|
|
1760
1958
|
});
|
|
1761
1959
|
const session = result.session;
|
|
1762
|
-
const timeoutMs = positiveMilliseconds(options.timeoutMs, DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
|
|
1763
|
-
const gracefulShutdownMs = options.gracefulShutdownMs ?? DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs;
|
|
1764
1960
|
|
|
1765
1961
|
let plannerMarkdown: string | undefined;
|
|
1766
1962
|
let plannerError: unknown;
|
|
@@ -1768,12 +1964,14 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
1768
1964
|
try {
|
|
1769
1965
|
const plannerText = await runTodoPlannerPrompt({
|
|
1770
1966
|
session,
|
|
1771
|
-
prompt: options.plannerPrompt ?? buildTodoCreationPrompt(options.inputText, options.goal),
|
|
1967
|
+
prompt: options.plannerPrompt ?? buildTodoCreationPrompt(options.inputText, options.goal, capabilityConstraints),
|
|
1772
1968
|
abortSignal: options.abortSignal,
|
|
1773
1969
|
timeoutMs,
|
|
1774
1970
|
gracefulShutdownMs,
|
|
1775
1971
|
diagnostics: result.diagnostics,
|
|
1776
1972
|
onDiagnostic: options.onDiagnostic,
|
|
1973
|
+
plannerBudget: effectivePlannerBudget,
|
|
1974
|
+
onProgress: options.onProgress,
|
|
1777
1975
|
});
|
|
1778
1976
|
|
|
1779
1977
|
plannerMarkdown = await extractTodoMarkdownWithOneRepair(
|
|
@@ -1788,8 +1986,11 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
1788
1986
|
gracefulShutdownMs,
|
|
1789
1987
|
diagnostics: result.diagnostics,
|
|
1790
1988
|
onDiagnostic: options.onDiagnostic,
|
|
1989
|
+
plannerBudget: effectivePlannerBudget,
|
|
1990
|
+
onProgress: options.onProgress,
|
|
1791
1991
|
}),
|
|
1792
1992
|
options.goal,
|
|
1993
|
+
capabilityConstraints,
|
|
1793
1994
|
{
|
|
1794
1995
|
onInvalidOutput: (validationError) =>
|
|
1795
1996
|
options.onDiagnostic?.({
|
|
@@ -1833,7 +2034,8 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
1833
2034
|
if (!plannerMarkdown) {
|
|
1834
2035
|
throw new TodoGenerationError("TODO planner did not return valid TODO markdown.");
|
|
1835
2036
|
}
|
|
1836
|
-
|
|
2037
|
+
const withGoal = applyGoalInstructionsToTodoMarkdown(plannerMarkdown, options.goal);
|
|
2038
|
+
return applyWorkerCapabilityConstraintsToTodoMarkdown(withGoal, capabilityConstraints);
|
|
1837
2039
|
}
|
|
1838
2040
|
|
|
1839
2041
|
async function runTodoPlannerPrompt(options: {
|
|
@@ -1844,6 +2046,8 @@ async function runTodoPlannerPrompt(options: {
|
|
|
1844
2046
|
gracefulShutdownMs: number;
|
|
1845
2047
|
diagnostics?: string[];
|
|
1846
2048
|
onDiagnostic?: PlannerDiagnosticHandler;
|
|
2049
|
+
plannerBudget: Readonly<PlannerBudget>;
|
|
2050
|
+
onProgress?: PlannerProgressHandler;
|
|
1847
2051
|
}): Promise<string> {
|
|
1848
2052
|
const promptResult = await runGuardedSessionPrompt({
|
|
1849
2053
|
session: options.session,
|
|
@@ -1853,17 +2057,47 @@ async function runTodoPlannerPrompt(options: {
|
|
|
1853
2057
|
gracefulShutdownMs: options.gracefulShutdownMs,
|
|
1854
2058
|
gracefulShutdownPrompt: buildTodoPlanningShutdownMessage(),
|
|
1855
2059
|
diagnostics: options.diagnostics,
|
|
2060
|
+
progressCheckpointsMs: plannerProgressCheckpoints(options.timeoutMs),
|
|
2061
|
+
onProgressCheckpoint: (elapsedMs) =>
|
|
2062
|
+
notifyPlannerProgress(
|
|
2063
|
+
options.onProgress,
|
|
2064
|
+
createPlannerActiveProgress(options.plannerBudget, options.gracefulShutdownMs, elapsedMs),
|
|
2065
|
+
),
|
|
2066
|
+
onGracePeriodStart: (gracePeriodMs) =>
|
|
2067
|
+
notifyPlannerProgress(options.onProgress, createPlannerGraceProgress(options.plannerBudget, gracePeriodMs)),
|
|
1856
2068
|
dispose: false,
|
|
1857
2069
|
});
|
|
1858
2070
|
|
|
2071
|
+
// Caller cancellation wins even when it arrives during grace. A hard abort
|
|
2072
|
+
// caused by grace expiry has cancelled=false and remains a timeout.
|
|
2073
|
+
if (promptResult.cancelled) {
|
|
2074
|
+
const outputState = promptResult.outputObserved
|
|
2075
|
+
? "partial output observed; content omitted"
|
|
2076
|
+
: "no planner output observed";
|
|
2077
|
+
const message = `TODO planner cancelled (${outputState}): ${promptResult.error ?? "caller cancellation"}`;
|
|
2078
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("cancelled", message, promptResult));
|
|
2079
|
+
throw new TodoGenerationError(message);
|
|
2080
|
+
}
|
|
1859
2081
|
if (promptResult.timedOut) {
|
|
1860
|
-
|
|
2082
|
+
if (
|
|
2083
|
+
promptResult.completedDuringGrace &&
|
|
2084
|
+
promptResult.outputObserved &&
|
|
2085
|
+
!promptResult.error &&
|
|
2086
|
+
isSafeCompletedTodoPlannerOutput(promptResult.assistantText)
|
|
2087
|
+
) {
|
|
2088
|
+
return promptResult.assistantText;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
const outputState = promptResult.outputObserved
|
|
2092
|
+
? "partial output observed; content omitted"
|
|
2093
|
+
: "no planner output observed";
|
|
2094
|
+
const message = `TODO planner timed out (${outputState}): ${promptResult.error ?? "time budget exceeded"}`;
|
|
1861
2095
|
options.onDiagnostic?.(plannerPromptDiagnostic("timeout", message, promptResult));
|
|
1862
2096
|
throw new TodoGenerationError(message);
|
|
1863
2097
|
}
|
|
1864
2098
|
if (promptResult.aborted) {
|
|
1865
|
-
const message = `TODO planner
|
|
1866
|
-
options.onDiagnostic?.(plannerPromptDiagnostic("
|
|
2099
|
+
const message = `TODO planner stopped: ${promptResult.error ?? "session abort"}`;
|
|
2100
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("failure", message, promptResult));
|
|
1867
2101
|
throw new TodoGenerationError(message);
|
|
1868
2102
|
}
|
|
1869
2103
|
if (promptResult.error) {
|
|
@@ -1883,9 +2117,10 @@ async function runTodoPlannerPrompt(options: {
|
|
|
1883
2117
|
}
|
|
1884
2118
|
|
|
1885
2119
|
function plannerPromptDiagnostic(
|
|
1886
|
-
kind: Extract<PlannerDiagnosticKind, "timeout" | "abort" | "failure">,
|
|
2120
|
+
kind: Extract<PlannerDiagnosticKind, "timeout" | "cancelled" | "abort" | "failure">,
|
|
1887
2121
|
message: string,
|
|
1888
2122
|
promptResult: {
|
|
2123
|
+
outputObserved: boolean;
|
|
1889
2124
|
diagnostics: string[];
|
|
1890
2125
|
sessionFile?: string;
|
|
1891
2126
|
sessionId?: string;
|
|
@@ -1894,12 +2129,25 @@ function plannerPromptDiagnostic(
|
|
|
1894
2129
|
return {
|
|
1895
2130
|
kind,
|
|
1896
2131
|
message,
|
|
2132
|
+
partialOutputObserved: promptResult.outputObserved,
|
|
1897
2133
|
diagnostics: promptResult.diagnostics,
|
|
1898
2134
|
sessionFile: promptResult.sessionFile,
|
|
1899
2135
|
sessionId: promptResult.sessionId,
|
|
1900
2136
|
};
|
|
1901
2137
|
}
|
|
1902
2138
|
|
|
2139
|
+
function isSafeCompletedTodoPlannerOutput(text: string): boolean {
|
|
2140
|
+
if (!text.trim()) {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
try {
|
|
2144
|
+
extractAndValidateTodoMarkdown(text);
|
|
2145
|
+
return true;
|
|
2146
|
+
} catch {
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
1903
2151
|
function buildTodoPlanningShutdownMessage(): string {
|
|
1904
2152
|
return `Pi Long Task notice: TODO planning has reached its time budget.
|
|
1905
2153
|
Return the best valid Pi Long Task TODO markdown you can produce now, or stop if that is not possible.`;
|
|
@@ -1912,8 +2160,13 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1912
2160
|
const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText ?? "");
|
|
1913
2161
|
const configuredAttempts = options.maxAttemptsPerTask ?? parsedWorkerConfig.maxAttemptsPerTask;
|
|
1914
2162
|
const configuredTaskTimeoutMs = options.taskTimeoutMs ?? parsedWorkerConfig.taskTimeoutMs;
|
|
1915
|
-
const configuredTodoTimeoutMs = options.todoTimeoutMs;
|
|
1916
|
-
const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs;
|
|
2163
|
+
const configuredTodoTimeoutMs = options.todoTimeoutMs ?? parsedWorkerConfig.todoTimeoutMs;
|
|
2164
|
+
const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs ?? parsedWorkerConfig.todoGracefulShutdownMs;
|
|
2165
|
+
const plannerBudget = resolvePlannerBudget({
|
|
2166
|
+
inputText: coordinatorInputText(options),
|
|
2167
|
+
explicitTimeoutMs: configuredTodoTimeoutMs,
|
|
2168
|
+
defaultTimeoutMs: DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs,
|
|
2169
|
+
});
|
|
1917
2170
|
const configuredMaxBashTimeoutMs = options.maxBashTimeoutMs ?? parsedWorkerConfig.maxBashTimeoutMs;
|
|
1918
2171
|
const workerModelName = options.workerModelName ?? parsedWorkerConfig.modelName;
|
|
1919
2172
|
const workerModel = workerModelName ? undefined : options.workerModel;
|
|
@@ -1927,6 +2180,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1927
2180
|
...parsedWorkerConfig.networkRecovery,
|
|
1928
2181
|
...options.networkRecovery,
|
|
1929
2182
|
});
|
|
2183
|
+
const capabilityWarnings = capabilityWarningsForRequest(options.inputText, options.goal);
|
|
1930
2184
|
|
|
1931
2185
|
return {
|
|
1932
2186
|
cwd,
|
|
@@ -1936,11 +2190,12 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1936
2190
|
taskResultPath: path.join(runDir, "TASK_RESULT.md"),
|
|
1937
2191
|
maxAttemptsPerTask: positiveInteger(configuredAttempts, DEFAULT_COORDINATOR_OPTIONS.maxAttemptsPerTask),
|
|
1938
2192
|
taskTimeoutSeconds: positiveMilliseconds(configuredTaskTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.taskTimeoutMs) / 1000,
|
|
1939
|
-
todoTimeoutMs:
|
|
1940
|
-
todoGracefulShutdownMs:
|
|
2193
|
+
todoTimeoutMs: plannerBudget.timeoutMs,
|
|
2194
|
+
todoGracefulShutdownMs: resolvePlannerGracefulShutdownMs(
|
|
1941
2195
|
configuredTodoGracefulShutdownMs,
|
|
1942
2196
|
DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
|
|
1943
2197
|
),
|
|
2198
|
+
plannerBudget,
|
|
1944
2199
|
maxBashTimeoutSeconds:
|
|
1945
2200
|
positiveMilliseconds(configuredMaxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
|
|
1946
2201
|
workerModel,
|
|
@@ -1962,8 +2217,11 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1962
2217
|
workerCostState: createWorkerCostState(),
|
|
1963
2218
|
workerActivityByWorker: new Map(),
|
|
1964
2219
|
workerTextByWorker: new Map(),
|
|
2220
|
+
workerTextLengthByWorker: new Map(),
|
|
1965
2221
|
workerTextPublishedLengthByWorker: new Map(),
|
|
2222
|
+
workerTextPublishedAtByWorker: new Map(),
|
|
1966
2223
|
plannerDiagnostics: [],
|
|
2224
|
+
capabilityWarnings,
|
|
1967
2225
|
workerSessionMetrics: createWorkerSessionMetrics(),
|
|
1968
2226
|
steeringQueue: options.steeringQueue,
|
|
1969
2227
|
onPlanRevisionAccepted: options.onPlanRevisionAccepted,
|
|
@@ -1974,6 +2232,23 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1974
2232
|
};
|
|
1975
2233
|
}
|
|
1976
2234
|
|
|
2235
|
+
function emitPlannerProgress(runtime: RuntimeOptions, event: Readonly<PlannerProgressEvent>): void {
|
|
2236
|
+
emitProgress(runtime, event.message, {
|
|
2237
|
+
phase: "planning",
|
|
2238
|
+
activeStatus: event.message,
|
|
2239
|
+
plannerBudget: event.budget,
|
|
2240
|
+
plannerProgressState: event.state,
|
|
2241
|
+
plannerElapsedMs: event.elapsedMs,
|
|
2242
|
+
plannerRemainingMs: event.remainingMs,
|
|
2243
|
+
plannerGracePeriodMs: event.gracePeriodMs,
|
|
2244
|
+
...(event.graceRemainingMs === undefined ? {} : { plannerGraceRemainingMs: event.graceRemainingMs }),
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function notifyPlannerProgress(handler: PlannerProgressHandler | undefined, event: PlannerProgressEvent): void {
|
|
2249
|
+
handler?.(event);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1977
2252
|
function emitProgress(
|
|
1978
2253
|
runtime: RuntimeOptions,
|
|
1979
2254
|
message: string,
|
|
@@ -1992,7 +2267,7 @@ function emitProgress(
|
|
|
1992
2267
|
runtime.lastProgress = progress;
|
|
1993
2268
|
const activeRecovery = latestNetworkRecovery(runtime.activeNetworkRecoveries);
|
|
1994
2269
|
if (activeRecovery) {
|
|
1995
|
-
publishNetworkRecoveryProgress(runtime, activeRecovery);
|
|
2270
|
+
publishNetworkRecoveryProgress(runtime, activeRecovery.event, activeRecovery.operation);
|
|
1996
2271
|
} else {
|
|
1997
2272
|
runtime.onProgress?.(progress);
|
|
1998
2273
|
}
|
|
@@ -2005,7 +2280,10 @@ function emitProgress(
|
|
|
2005
2280
|
* status path. Operation IDs prevent an older concurrent recovery from
|
|
2006
2281
|
* repainting a newer outage or completion.
|
|
2007
2282
|
*/
|
|
2008
|
-
function createNetworkRecoveryProgressHandler(
|
|
2283
|
+
function createNetworkRecoveryProgressHandler(
|
|
2284
|
+
runtime: RuntimeOptions,
|
|
2285
|
+
operation: "planner" | "worker" = "worker",
|
|
2286
|
+
): (event: NetworkRecoveryEvent) => void {
|
|
2009
2287
|
const operationId = ++runtime.networkRecoverySequence;
|
|
2010
2288
|
let cleaned = false;
|
|
2011
2289
|
|
|
@@ -2024,7 +2302,7 @@ function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event:
|
|
|
2024
2302
|
if (event.type === "recovered") {
|
|
2025
2303
|
const active = latestNetworkRecovery(runtime.activeNetworkRecoveries);
|
|
2026
2304
|
if (active) {
|
|
2027
|
-
publishNetworkRecoveryProgress(runtime, active);
|
|
2305
|
+
publishNetworkRecoveryProgress(runtime, active.event, active.operation);
|
|
2028
2306
|
} else if (runtime.lastProgress) {
|
|
2029
2307
|
runtime.onProgress?.({ ...runtime.lastProgress, workerCostTotal: runtime.workerCostState.total });
|
|
2030
2308
|
}
|
|
@@ -2032,18 +2310,26 @@ function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event:
|
|
|
2032
2310
|
return;
|
|
2033
2311
|
}
|
|
2034
2312
|
|
|
2035
|
-
runtime.activeNetworkRecoveries.set(operationId, event);
|
|
2313
|
+
runtime.activeNetworkRecoveries.set(operationId, { event, operation });
|
|
2036
2314
|
if (operationId === latestNetworkRecoveryId(runtime.activeNetworkRecoveries)) {
|
|
2037
|
-
publishNetworkRecoveryProgress(runtime, event);
|
|
2315
|
+
publishNetworkRecoveryProgress(runtime, event, operation);
|
|
2038
2316
|
}
|
|
2039
2317
|
};
|
|
2040
2318
|
}
|
|
2041
2319
|
|
|
2042
|
-
function publishNetworkRecoveryProgress(
|
|
2320
|
+
function publishNetworkRecoveryProgress(
|
|
2321
|
+
runtime: RuntimeOptions,
|
|
2322
|
+
event: NetworkRecoveryEvent,
|
|
2323
|
+
operation: "planner" | "worker" = "worker",
|
|
2324
|
+
): void {
|
|
2043
2325
|
if (runtime.progressClosed) return;
|
|
2044
2326
|
const stable = runtime.lastProgress;
|
|
2045
2327
|
const nowMs = event.state.outageStartedAtMs + event.state.elapsedMs;
|
|
2046
|
-
const
|
|
2328
|
+
const recoveryStatus = formatNetworkRecoveryStatus(event);
|
|
2329
|
+
const message =
|
|
2330
|
+
operation === "planner"
|
|
2331
|
+
? `TODO planner network recovery: ${recoveryStatus} The ${formatFriendlyDuration(runtime.todoTimeoutMs)} planning deadline remains unchanged for each provider attempt.`
|
|
2332
|
+
: recoveryStatus;
|
|
2047
2333
|
runtime.onProgress?.({
|
|
2048
2334
|
message,
|
|
2049
2335
|
phase: "network_wait",
|
|
@@ -2067,17 +2353,19 @@ function publishNetworkRecoveryProgress(runtime: RuntimeOptions, event: NetworkR
|
|
|
2067
2353
|
networkNextRetryInMs:
|
|
2068
2354
|
event.state.nextRetryAtMs === undefined ? undefined : Math.max(0, event.state.nextRetryAtMs - nowMs),
|
|
2069
2355
|
networkFailureReason: event.state.lastFailure.reason,
|
|
2356
|
+
networkOperation: operation,
|
|
2357
|
+
...(operation === "planner" ? { plannerDeadlinePolicy: "per_attempt_excludes_network_wait" as const } : {}),
|
|
2070
2358
|
});
|
|
2071
2359
|
}
|
|
2072
2360
|
|
|
2073
2361
|
function latestNetworkRecovery(
|
|
2074
|
-
recoveries: ReadonlyMap<number,
|
|
2075
|
-
):
|
|
2362
|
+
recoveries: ReadonlyMap<number, ActiveNetworkRecovery>,
|
|
2363
|
+
): ActiveNetworkRecovery | undefined {
|
|
2076
2364
|
const id = latestNetworkRecoveryId(recoveries);
|
|
2077
2365
|
return id === undefined ? undefined : recoveries.get(id);
|
|
2078
2366
|
}
|
|
2079
2367
|
|
|
2080
|
-
function latestNetworkRecoveryId(recoveries: ReadonlyMap<number,
|
|
2368
|
+
function latestNetworkRecoveryId(recoveries: ReadonlyMap<number, ActiveNetworkRecovery>): number | undefined {
|
|
2081
2369
|
let latest: number | undefined;
|
|
2082
2370
|
for (const id of recoveries.keys()) {
|
|
2083
2371
|
if (latest === undefined || id > latest) latest = id;
|
|
@@ -2089,13 +2377,79 @@ function isTerminalNetworkRecoveryEvent(type: NetworkRecoveryEventType): boolean
|
|
|
2089
2377
|
return type === "recovered" || type === "failed" || type === "cancelled" || type === "outage_expired";
|
|
2090
2378
|
}
|
|
2091
2379
|
|
|
2380
|
+
function plannerNetworkDiagnostic(
|
|
2381
|
+
event: NetworkRecoveryEvent,
|
|
2382
|
+
timeoutMs: number | undefined,
|
|
2383
|
+
partialOutputObserved: boolean | undefined,
|
|
2384
|
+
): PlannerDiagnostic {
|
|
2385
|
+
const recovered = event.type === "recovered";
|
|
2386
|
+
const budget = formatFriendlyDuration(timeoutMs ?? DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
|
|
2387
|
+
return {
|
|
2388
|
+
kind: "network_recovery",
|
|
2389
|
+
message: recovered
|
|
2390
|
+
? `TODO planner network recovery succeeded after ${formatFriendlyDuration(event.state.elapsedMs)}; planning continues with its unchanged ${budget} per-attempt deadline.`
|
|
2391
|
+
: `TODO planner network recovery started (${event.state.lastFailure.reason}); its outage clock is separate and the ${budget} per-attempt planning deadline remains unchanged.`,
|
|
2392
|
+
partialOutputObserved,
|
|
2393
|
+
networkRecoveryEvent: event.type,
|
|
2394
|
+
networkFailureReason: event.state.lastFailure.reason,
|
|
2395
|
+
networkRetryCount: event.state.retryCount,
|
|
2396
|
+
networkOutageElapsedMs: event.state.elapsedMs,
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function latestPlannerPartialOutput(diagnostics: readonly PlannerDiagnostic[]): boolean | undefined {
|
|
2401
|
+
return [...diagnostics].reverse().find((diagnostic) => diagnostic.partialOutputObserved !== undefined)
|
|
2402
|
+
?.partialOutputObserved;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
function hasTerminalPlannerDiagnostic(diagnostics: readonly PlannerDiagnostic[]): boolean {
|
|
2406
|
+
const kind = diagnostics.at(-1)?.kind;
|
|
2407
|
+
return kind !== undefined && ["timeout", "cancelled", "abort", "network_failure", "failure"].includes(kind);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function hasTerminalPlannerDiagnosticAfterLatestRecovery(diagnostics: readonly PlannerDiagnostic[]): boolean {
|
|
2411
|
+
let recoveryIndex = -1;
|
|
2412
|
+
for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
|
|
2413
|
+
if (diagnostics[index]?.kind === "network_recovery") {
|
|
2414
|
+
recoveryIndex = index;
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return diagnostics
|
|
2419
|
+
.slice(recoveryIndex + 1)
|
|
2420
|
+
.some((diagnostic) => ["timeout", "cancelled", "abort", "network_failure"].includes(diagnostic.kind));
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function recordPlannerCancellation(runtime: RuntimeOptions, signal: AbortSignal | undefined, cause: unknown): void {
|
|
2424
|
+
if (runtime.plannerDiagnostics.some((diagnostic) => diagnostic.kind === "cancelled")) return;
|
|
2425
|
+
recordPlannerDiagnostic(runtime, {
|
|
2426
|
+
kind: "cancelled",
|
|
2427
|
+
message: `TODO planning cancelled: ${signal?.aborted ? abortSignalReason(signal) : errorMessage(cause)}`,
|
|
2428
|
+
partialOutputObserved: latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function abortSignalReason(signal: AbortSignal): string {
|
|
2433
|
+
return signal.reason === undefined ? "cancelled by caller" : errorMessage(signal.reason);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function plannerCancellationError(signal: AbortSignal | undefined, cause: unknown): TodoGenerationError {
|
|
2437
|
+
const reason = signal?.aborted ? abortSignalReason(signal) : errorMessage(cause);
|
|
2438
|
+
return new TodoGenerationError(`TODO planning cancelled: ${reason}`, { cause });
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2092
2441
|
function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDiagnostic): void {
|
|
2093
2442
|
const normalized: PlannerDiagnostic = {
|
|
2094
2443
|
kind: diagnostic.kind,
|
|
2095
2444
|
message: diagnostic.message,
|
|
2445
|
+
partialOutputObserved: diagnostic.partialOutputObserved,
|
|
2096
2446
|
diagnostics: diagnostic.diagnostics?.filter(Boolean),
|
|
2097
2447
|
sessionFile: diagnostic.sessionFile,
|
|
2098
2448
|
sessionId: diagnostic.sessionId,
|
|
2449
|
+
networkRecoveryEvent: diagnostic.networkRecoveryEvent,
|
|
2450
|
+
networkFailureReason: diagnostic.networkFailureReason,
|
|
2451
|
+
networkRetryCount: diagnostic.networkRetryCount,
|
|
2452
|
+
networkOutageElapsedMs: diagnostic.networkOutageElapsedMs,
|
|
2099
2453
|
};
|
|
2100
2454
|
const last = runtime.plannerDiagnostics.at(-1);
|
|
2101
2455
|
if (last?.kind === normalized.kind && last.message === normalized.message) {
|
|
@@ -2105,11 +2459,22 @@ function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDia
|
|
|
2105
2459
|
emitProgress(runtime, normalized.message, {
|
|
2106
2460
|
phase: "planning",
|
|
2107
2461
|
status: normalized.kind,
|
|
2108
|
-
isError:
|
|
2462
|
+
isError: !["repair_attempt", "network_recovery"].includes(normalized.kind),
|
|
2109
2463
|
plannerDiagnostic: normalized.kind,
|
|
2110
2464
|
plannerDiagnostics: normalized.diagnostics,
|
|
2465
|
+
plannerPartialOutputObserved: normalized.partialOutputObserved,
|
|
2111
2466
|
plannerSessionFile: normalized.sessionFile,
|
|
2112
2467
|
plannerSessionId: normalized.sessionId,
|
|
2468
|
+
networkRecoveryEvent: normalized.networkRecoveryEvent,
|
|
2469
|
+
networkRetryCount: normalized.networkRetryCount,
|
|
2470
|
+
networkOutageElapsedMs: normalized.networkOutageElapsedMs,
|
|
2471
|
+
networkFailureReason: normalized.networkFailureReason,
|
|
2472
|
+
networkOperation:
|
|
2473
|
+
normalized.kind === "network_recovery" || normalized.kind === "network_failure" ? "planner" : undefined,
|
|
2474
|
+
plannerDeadlinePolicy:
|
|
2475
|
+
normalized.kind === "network_recovery" || normalized.kind === "network_failure"
|
|
2476
|
+
? "per_attempt_excludes_network_wait"
|
|
2477
|
+
: undefined,
|
|
2113
2478
|
taskProgress: buildTaskProgressModel({ tasks: [] }),
|
|
2114
2479
|
});
|
|
2115
2480
|
}
|
|
@@ -2314,15 +2679,23 @@ function emitWorkerEventProgress(
|
|
|
2314
2679
|
let activeStatus = runtime.workerActivityByWorker.get(worker);
|
|
2315
2680
|
|
|
2316
2681
|
if (event.type === "message_update" && event.textDelta) {
|
|
2317
|
-
const
|
|
2682
|
+
const previousText = runtime.workerTextByWorker.get(worker) ?? "";
|
|
2683
|
+
const workerText = `${previousText}${event.textDelta}`.slice(-WORKER_PROGRESS_MAX_BUFFER_CHARS);
|
|
2684
|
+
const receivedLength = (runtime.workerTextLengthByWorker.get(worker) ?? 0) + event.textDelta.length;
|
|
2318
2685
|
runtime.workerTextByWorker.set(worker, workerText);
|
|
2686
|
+
runtime.workerTextLengthByWorker.set(worker, receivedLength);
|
|
2319
2687
|
const streamedStatus = activeStatusFromWorkerText(workerText);
|
|
2320
2688
|
const publishedLength = runtime.workerTextPublishedLengthByWorker.get(worker) ?? 0;
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2689
|
+
const publishedAt = runtime.workerTextPublishedAtByWorker.get(worker);
|
|
2690
|
+
const nowMs = runtime.now().getTime();
|
|
2691
|
+
const sentenceBoundary = /[\n.!?:]\s*$/.test(event.textDelta);
|
|
2692
|
+
const enoughTimeElapsed = publishedAt === undefined || nowMs - publishedAt >= WORKER_PROGRESS_MIN_INTERVAL_MS;
|
|
2693
|
+
const enoughNewText = receivedLength - publishedLength >= WORKER_PROGRESS_MIN_CHARACTER_DELTA;
|
|
2694
|
+
if (streamedStatus && ((sentenceBoundary && enoughTimeElapsed) || enoughNewText)) {
|
|
2323
2695
|
activeStatus = streamedStatus;
|
|
2324
2696
|
runtime.workerActivityByWorker.set(worker, activeStatus);
|
|
2325
|
-
runtime.workerTextPublishedLengthByWorker.set(worker,
|
|
2697
|
+
runtime.workerTextPublishedLengthByWorker.set(worker, receivedLength);
|
|
2698
|
+
runtime.workerTextPublishedAtByWorker.set(worker, nowMs);
|
|
2326
2699
|
emitProgress(runtime, activeStatus, {
|
|
2327
2700
|
phase: "worker_tool",
|
|
2328
2701
|
taskId: task.taskId,
|
|
@@ -2340,7 +2713,9 @@ function emitWorkerEventProgress(
|
|
|
2340
2713
|
|
|
2341
2714
|
if (event.type === "message_end") {
|
|
2342
2715
|
runtime.workerTextByWorker.delete(worker);
|
|
2716
|
+
runtime.workerTextLengthByWorker.delete(worker);
|
|
2343
2717
|
runtime.workerTextPublishedLengthByWorker.delete(worker);
|
|
2718
|
+
runtime.workerTextPublishedAtByWorker.delete(worker);
|
|
2344
2719
|
}
|
|
2345
2720
|
|
|
2346
2721
|
if (event.activity) {
|
|
@@ -2426,7 +2801,9 @@ function stripToolOutcomePrefix(activity: string): string {
|
|
|
2426
2801
|
|
|
2427
2802
|
function activeStatusFromWorkerText(text: string): string {
|
|
2428
2803
|
const taskResultIndex = text.indexOf("TASK_RESULT:");
|
|
2429
|
-
|
|
2804
|
+
const normalized = (taskResultIndex >= 0 ? text.slice(0, taskResultIndex) : text).replace(/\s+/g, " ").trim();
|
|
2805
|
+
if (normalized.length <= WORKER_PROGRESS_MAX_STATUS_CHARS) return normalized;
|
|
2806
|
+
return `… ${normalized.slice(-WORKER_PROGRESS_MAX_STATUS_CHARS + 2)}`;
|
|
2430
2807
|
}
|
|
2431
2808
|
|
|
2432
2809
|
function emitObsoleteTaskOutcomeProgress(
|
|
@@ -2582,8 +2959,11 @@ function outcomeProgressItemStatus(
|
|
|
2582
2959
|
return "failed";
|
|
2583
2960
|
}
|
|
2584
2961
|
|
|
2585
|
-
function initialTaskResultMarkdown(runId: string): string {
|
|
2586
|
-
|
|
2962
|
+
function initialTaskResultMarkdown(runId: string, capabilityWarnings: readonly WorkerCapabilityWarning[] = []): string {
|
|
2963
|
+
const warningBlock = capabilityWarnings.length
|
|
2964
|
+
? `\n\n## Worker capability warnings\n\n${capabilityWarnings.map((warning) => `- ${warning.message}`).join("\n")}`
|
|
2965
|
+
: "";
|
|
2966
|
+
return `# Pi Long Task TASK_RESULT\n\nRun: ${runId}${warningBlock}\n`;
|
|
2587
2967
|
}
|
|
2588
2968
|
|
|
2589
2969
|
async function appendFailureNote(
|
|
@@ -2596,6 +2976,21 @@ async function appendFailureNote(
|
|
|
2596
2976
|
lines.push("", "### Planner diagnostics");
|
|
2597
2977
|
for (const diagnostic of plannerDiagnostics) {
|
|
2598
2978
|
lines.push("", `- ${diagnostic.kind}: ${diagnostic.message}`);
|
|
2979
|
+
if (diagnostic.partialOutputObserved !== undefined) {
|
|
2980
|
+
lines.push(` - Partial output observed: ${diagnostic.partialOutputObserved ? "yes" : "no"}`);
|
|
2981
|
+
}
|
|
2982
|
+
if (diagnostic.networkRecoveryEvent) {
|
|
2983
|
+
lines.push(` - Network recovery event: ${diagnostic.networkRecoveryEvent}`);
|
|
2984
|
+
}
|
|
2985
|
+
if (diagnostic.networkFailureReason) {
|
|
2986
|
+
lines.push(` - Network failure reason: ${diagnostic.networkFailureReason}`);
|
|
2987
|
+
}
|
|
2988
|
+
if (diagnostic.networkRetryCount !== undefined) {
|
|
2989
|
+
lines.push(` - Network retry count: ${diagnostic.networkRetryCount}`);
|
|
2990
|
+
}
|
|
2991
|
+
if (diagnostic.networkOutageElapsedMs !== undefined) {
|
|
2992
|
+
lines.push(` - Network outage elapsed: ${formatFriendlyDuration(diagnostic.networkOutageElapsedMs)}`);
|
|
2993
|
+
}
|
|
2599
2994
|
if (diagnostic.sessionId) {
|
|
2600
2995
|
lines.push(` - Session ID: ${diagnostic.sessionId}`);
|
|
2601
2996
|
}
|
|
@@ -2794,6 +3189,17 @@ function coordinatorInputText(options: RunCoordinatorOptions): string {
|
|
|
2794
3189
|
return normalizeOptionalText(options.inputText) ?? normalizeOptionalText(options.goal) ?? "";
|
|
2795
3190
|
}
|
|
2796
3191
|
|
|
3192
|
+
function capabilityWarningsForRequest(inputText?: string, goal?: string): WorkerCapabilityWarning[] {
|
|
3193
|
+
const requestText = [normalizeOptionalText(inputText), normalizeOptionalText(goal)]
|
|
3194
|
+
.filter((item): item is string => Boolean(item))
|
|
3195
|
+
.filter((item, index, all) => all.indexOf(item) === index)
|
|
3196
|
+
.join("\n");
|
|
3197
|
+
return detectUnavailableWorkerCapabilities(requestText, {
|
|
3198
|
+
tools: DEFAULT_WORKER_TOOLS,
|
|
3199
|
+
extensionsEnabled: false,
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
|
|
2797
3203
|
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
2798
3204
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
2799
3205
|
return Math.floor(value);
|