pi-long-task 0.6.0 → 0.7.0
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 +22 -0
- package/README.md +72 -5
- package/package.json +1 -1
- package/src/coordinator.ts +433 -50
- 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 +113 -7
- package/src/todo_generator.ts +82 -5
- package/src/types.ts +36 -0
- package/src/worker_capabilities.ts +103 -0
- package/src/worker_config.ts +74 -7
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,
|
|
@@ -84,11 +104,11 @@ export type { CoordinatorStatus } from "./types.ts";
|
|
|
84
104
|
export const DEFAULT_COORDINATOR_OPTIONS = {
|
|
85
105
|
maxAttemptsPerTask: 3,
|
|
86
106
|
taskTimeoutMs: 900_000,
|
|
87
|
-
todoTimeoutMs:
|
|
107
|
+
todoTimeoutMs: DEFAULT_PLANNER_TIMEOUT_MS,
|
|
88
108
|
todoGracefulShutdownMs: 15_000,
|
|
89
109
|
maxBashTimeoutMs: 300_000,
|
|
90
110
|
taskThinking: "high",
|
|
91
|
-
todoThinking:
|
|
111
|
+
todoThinking: DEFAULT_PLANNER_THINKING_LEVEL,
|
|
92
112
|
workerSessionReuse: DEFAULT_WORKER_SESSION_REUSE_ENABLED,
|
|
93
113
|
workerSessionReuseContextThresholdPercent: DEFAULT_WORKER_SESSION_REUSE_CONTEXT_THRESHOLD_PERCENT,
|
|
94
114
|
networkRecovery: DEFAULT_NETWORK_RECOVERY_CONFIG,
|
|
@@ -96,6 +116,7 @@ export const DEFAULT_COORDINATOR_OPTIONS = {
|
|
|
96
116
|
|
|
97
117
|
export type WorkerRunner = (options: RunWorkerTaskOptions) => Promise<SessionOutcome>;
|
|
98
118
|
export type CoordinatorProgressPhase =
|
|
119
|
+
| "capability_warning"
|
|
99
120
|
| "planning"
|
|
100
121
|
| "planned"
|
|
101
122
|
| "task_start"
|
|
@@ -108,14 +129,30 @@ export type CoordinatorProgressPhase =
|
|
|
108
129
|
| "task_obsolete"
|
|
109
130
|
| "complete";
|
|
110
131
|
|
|
111
|
-
export type PlannerDiagnosticKind =
|
|
132
|
+
export type PlannerDiagnosticKind =
|
|
133
|
+
| "timeout"
|
|
134
|
+
| "cancelled"
|
|
135
|
+
/** @deprecated Planner cancellation is now reported as `cancelled`. */
|
|
136
|
+
| "abort"
|
|
137
|
+
| "network_recovery"
|
|
138
|
+
| "network_failure"
|
|
139
|
+
| "invalid_output"
|
|
140
|
+
| "repair_attempt"
|
|
141
|
+
| "failure";
|
|
112
142
|
|
|
113
143
|
export interface PlannerDiagnostic {
|
|
114
144
|
kind: PlannerDiagnosticKind;
|
|
115
145
|
message: string;
|
|
146
|
+
/** Present whenever output presence is known; partial content itself is deliberately omitted. */
|
|
147
|
+
partialOutputObserved?: boolean;
|
|
116
148
|
diagnostics?: string[];
|
|
117
149
|
sessionFile?: string;
|
|
118
150
|
sessionId?: string;
|
|
151
|
+
/** Network lifecycle data is separate from timeout/cancellation classification. */
|
|
152
|
+
networkRecoveryEvent?: NetworkRecoveryEventType;
|
|
153
|
+
networkFailureReason?: string;
|
|
154
|
+
networkRetryCount?: number;
|
|
155
|
+
networkOutageElapsedMs?: number;
|
|
119
156
|
}
|
|
120
157
|
|
|
121
158
|
export type PlannerDiagnosticHandler = (diagnostic: PlannerDiagnostic) => void;
|
|
@@ -158,8 +195,18 @@ export interface CoordinatorProgressUpdate {
|
|
|
158
195
|
taskProgress?: TaskProgressModel;
|
|
159
196
|
plannerDiagnostic?: PlannerDiagnosticKind;
|
|
160
197
|
plannerDiagnostics?: string[];
|
|
198
|
+
plannerPartialOutputObserved?: boolean;
|
|
161
199
|
plannerSessionFile?: string;
|
|
162
200
|
plannerSessionId?: string;
|
|
201
|
+
/** Deterministic deadline selection used by planner calls in this run. */
|
|
202
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
203
|
+
/** Human-facing state with exact millisecond values retained for integrations. */
|
|
204
|
+
plannerProgressState?: PlannerProgressState;
|
|
205
|
+
plannerElapsedMs?: number;
|
|
206
|
+
plannerRemainingMs?: number;
|
|
207
|
+
plannerGracePeriodMs?: number;
|
|
208
|
+
plannerGraceRemainingMs?: number;
|
|
209
|
+
capabilityWarning?: Readonly<WorkerCapabilityWarning>;
|
|
163
210
|
workerSessionEvent?: WorkerSessionDiagnostic["event"];
|
|
164
211
|
workerSessionReason?: string;
|
|
165
212
|
workerSessionContextUsagePercent?: number;
|
|
@@ -170,6 +217,10 @@ export interface CoordinatorProgressUpdate {
|
|
|
170
217
|
networkNextRetryAtMs?: number;
|
|
171
218
|
networkNextRetryInMs?: number;
|
|
172
219
|
networkFailureReason?: string;
|
|
220
|
+
/** Identifies whether recovery belongs to planning or worker execution. */
|
|
221
|
+
networkOperation?: "planner" | "worker";
|
|
222
|
+
/** Planner recovery never mutates the configured per-attempt deadline. */
|
|
223
|
+
plannerDeadlinePolicy?: "per_attempt_excludes_network_wait";
|
|
173
224
|
}
|
|
174
225
|
|
|
175
226
|
export type CoordinatorProgressHandler = (update: CoordinatorProgressUpdate) => void;
|
|
@@ -210,20 +261,31 @@ export interface TodoPlannerOptions {
|
|
|
210
261
|
inputText: string;
|
|
211
262
|
cwd: string;
|
|
212
263
|
runDir: string;
|
|
213
|
-
|
|
264
|
+
/** Defaults to the planner-only balanced level; explicit values are forwarded unchanged. */
|
|
265
|
+
thinkingLevel?: string;
|
|
214
266
|
model?: unknown;
|
|
215
267
|
abortSignal?: AbortSignal;
|
|
216
268
|
timeoutMs?: number;
|
|
217
269
|
gracefulShutdownMs?: number;
|
|
270
|
+
/** Structured record of explicit/default/adaptive deadline selection. */
|
|
271
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
218
272
|
sessionFactory?: WorkerSessionFactory;
|
|
219
273
|
onDiagnostic?: PlannerDiagnosticHandler;
|
|
274
|
+
/** Shared human-readable timing events for CLI, TUI, and headless integrations. */
|
|
275
|
+
onProgress?: PlannerProgressHandler;
|
|
220
276
|
goal?: string;
|
|
221
277
|
/** Exact prompt for revision planners; bypasses the initial TODO-creation wrapper. */
|
|
222
278
|
plannerPrompt?: string;
|
|
223
279
|
/** Structured revision context supplied alongside plannerPrompt. */
|
|
224
280
|
planRevision?: Readonly<PlanRevisionRequest>;
|
|
225
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* Normalized coordinator recovery policy. Recovery wait is accounted on its
|
|
283
|
+
* own outage clock; every replay receives the same configured per-attempt
|
|
284
|
+
* planner deadline, so recovery settings never replace that deadline.
|
|
285
|
+
*/
|
|
226
286
|
networkRecovery?: Readonly<NetworkRecoveryConfig>;
|
|
287
|
+
/** Run-level constraints derived from capabilities unavailable to isolated workers. */
|
|
288
|
+
capabilityConstraints?: readonly string[];
|
|
227
289
|
}
|
|
228
290
|
|
|
229
291
|
export interface TaskAttemptSummary {
|
|
@@ -276,6 +338,10 @@ export interface CoordinatorResult {
|
|
|
276
338
|
workerUsageTotal?: WorkerUsageTotals;
|
|
277
339
|
/** Additive lifecycle counters for adaptive worker-session reuse. */
|
|
278
340
|
workerSessionMetrics?: WorkerSessionMetrics;
|
|
341
|
+
/** Deterministic deadline selection used by planner calls in this run. */
|
|
342
|
+
plannerBudget?: Readonly<PlannerBudget>;
|
|
343
|
+
/** Explicit, non-fatal warnings for requested capabilities unavailable to isolated workers. */
|
|
344
|
+
capabilityWarnings?: readonly WorkerCapabilityWarning[];
|
|
279
345
|
commit: boolean;
|
|
280
346
|
goal?: string;
|
|
281
347
|
error?: string;
|
|
@@ -307,6 +373,7 @@ interface RuntimeOptions {
|
|
|
307
373
|
networkRecovery: NetworkRecoveryConfig;
|
|
308
374
|
todoTimeoutMs: number;
|
|
309
375
|
todoGracefulShutdownMs: number;
|
|
376
|
+
plannerBudget: PlannerBudget;
|
|
310
377
|
workerRunner: WorkerRunner;
|
|
311
378
|
useRetainedWorkerLifecycle: boolean;
|
|
312
379
|
todoPlanner: TodoPlanner;
|
|
@@ -320,6 +387,7 @@ interface RuntimeOptions {
|
|
|
320
387
|
workerTextByWorker: Map<string, string>;
|
|
321
388
|
workerTextPublishedLengthByWorker: Map<string, number>;
|
|
322
389
|
plannerDiagnostics: PlannerDiagnostic[];
|
|
390
|
+
capabilityWarnings: WorkerCapabilityWarning[];
|
|
323
391
|
workerSessionMetrics: WorkerSessionMetrics;
|
|
324
392
|
steeringQueue?: SerializedSteeringQueue;
|
|
325
393
|
onPlanRevisionAccepted?: (revision: GeneratedPlanRevision) => void | Promise<void>;
|
|
@@ -327,7 +395,12 @@ interface RuntimeOptions {
|
|
|
327
395
|
lastProgress?: CoordinatorProgressUpdate;
|
|
328
396
|
progressClosed: boolean;
|
|
329
397
|
networkRecoverySequence: number;
|
|
330
|
-
activeNetworkRecoveries: Map<number,
|
|
398
|
+
activeNetworkRecoveries: Map<number, ActiveNetworkRecovery>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
interface ActiveNetworkRecovery {
|
|
402
|
+
event: NetworkRecoveryEvent;
|
|
403
|
+
operation: "planner" | "worker";
|
|
331
404
|
}
|
|
332
405
|
|
|
333
406
|
type RetainedWorkerReuseScope = "sequential_task" | "partial_continuation";
|
|
@@ -956,7 +1029,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
956
1029
|
const commits: CoordinatorCommitSummary[] = [];
|
|
957
1030
|
|
|
958
1031
|
await mkdir(runtime.runDir, { recursive: true });
|
|
959
|
-
await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId), "utf8");
|
|
1032
|
+
await writeFile(runtime.taskResultPath, initialTaskResultMarkdown(runtime.runId, runtime.capabilityWarnings), "utf8");
|
|
960
1033
|
let planningComplete = false;
|
|
961
1034
|
let latestTodoMarkdown: string | undefined;
|
|
962
1035
|
let latestTasks: Task[] = [];
|
|
@@ -972,7 +1045,14 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
972
1045
|
const protectedDirtyPathsByTask = new Map<string, Set<string>>();
|
|
973
1046
|
|
|
974
1047
|
try {
|
|
975
|
-
|
|
1048
|
+
for (const warning of runtime.capabilityWarnings) {
|
|
1049
|
+
emitProgress(runtime, warning.message, {
|
|
1050
|
+
phase: "capability_warning",
|
|
1051
|
+
status: "warning",
|
|
1052
|
+
capabilityWarning: warning,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
emitPlannerProgress(runtime, createPlannerStartedProgress(runtime.plannerBudget, runtime.todoGracefulShutdownMs));
|
|
976
1056
|
let todoMarkdown = await generateOrNormalizeTodoMarkdown(inputText, runtime);
|
|
977
1057
|
validateTodoMarkdown(todoMarkdown);
|
|
978
1058
|
planningComplete = true;
|
|
@@ -1374,6 +1454,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1374
1454
|
workerCostTotal: runtime.workerCostState.total,
|
|
1375
1455
|
workerUsageTotal: aggregateWorkerUsage(outcomes),
|
|
1376
1456
|
workerSessionMetrics: snapshotWorkerSessionMetrics(runtime.workerSessionMetrics),
|
|
1457
|
+
plannerBudget: runtime.plannerBudget,
|
|
1458
|
+
capabilityWarnings: runtime.capabilityWarnings,
|
|
1377
1459
|
commit: options.commit,
|
|
1378
1460
|
goal: runtime.goal,
|
|
1379
1461
|
error: failure,
|
|
@@ -1388,10 +1470,12 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1388
1470
|
return result;
|
|
1389
1471
|
} catch (error) {
|
|
1390
1472
|
const message = errorMessage(error);
|
|
1391
|
-
if (!planningComplete) {
|
|
1473
|
+
if (!planningComplete && !hasTerminalPlannerDiagnostic(runtime.plannerDiagnostics)) {
|
|
1392
1474
|
recordPlannerDiagnostic(runtime, {
|
|
1393
|
-
kind: "failure",
|
|
1394
|
-
message:
|
|
1475
|
+
kind: runtime.abortSignal?.aborted ? "cancelled" : "failure",
|
|
1476
|
+
message: runtime.abortSignal?.aborted
|
|
1477
|
+
? `TODO planning cancelled: ${abortSignalReason(runtime.abortSignal)}`
|
|
1478
|
+
: `TODO planning failed: ${message}`,
|
|
1395
1479
|
});
|
|
1396
1480
|
}
|
|
1397
1481
|
const resultError = !planningComplete
|
|
@@ -1457,6 +1541,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1457
1541
|
workerCostTotal: runtime.workerCostState.total,
|
|
1458
1542
|
workerUsageTotal: aggregateWorkerUsage(outcomes),
|
|
1459
1543
|
workerSessionMetrics: snapshotWorkerSessionMetrics(runtime.workerSessionMetrics),
|
|
1544
|
+
plannerBudget: runtime.plannerBudget,
|
|
1545
|
+
capabilityWarnings: runtime.capabilityWarnings,
|
|
1460
1546
|
commit: options.commit,
|
|
1461
1547
|
goal: runtime.goal,
|
|
1462
1548
|
error: resultError,
|
|
@@ -1478,9 +1564,10 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1478
1564
|
}
|
|
1479
1565
|
|
|
1480
1566
|
async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: RuntimeOptions): Promise<string> {
|
|
1567
|
+
const capabilityConstraints = runtime.capabilityWarnings.map((warning) => warning.planningConstraint);
|
|
1481
1568
|
const local = todoMarkdownFromString(inputText, runtime.goal);
|
|
1482
1569
|
if (local) {
|
|
1483
|
-
return local;
|
|
1570
|
+
return applyWorkerCapabilityConstraintsToTodoMarkdown(local, capabilityConstraints);
|
|
1484
1571
|
}
|
|
1485
1572
|
|
|
1486
1573
|
const plannerText = await requestTodoPlan(inputText, runtime);
|
|
@@ -1489,6 +1576,7 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
|
|
|
1489
1576
|
plannerText,
|
|
1490
1577
|
(repairPrompt) => requestTodoPlan(repairPrompt, runtime),
|
|
1491
1578
|
runtime.goal,
|
|
1579
|
+
capabilityConstraints,
|
|
1492
1580
|
{
|
|
1493
1581
|
onInvalidOutput: (validationError) =>
|
|
1494
1582
|
recordPlannerDiagnostic(runtime, {
|
|
@@ -1507,7 +1595,8 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
|
|
|
1507
1595
|
}),
|
|
1508
1596
|
},
|
|
1509
1597
|
);
|
|
1510
|
-
|
|
1598
|
+
const withGoal = applyGoalInstructionsToTodoMarkdown(planned, runtime.goal);
|
|
1599
|
+
return applyWorkerCapabilityConstraintsToTodoMarkdown(withGoal, capabilityConstraints);
|
|
1511
1600
|
}
|
|
1512
1601
|
|
|
1513
1602
|
interface TodoExtractionRepairHooks {
|
|
@@ -1521,6 +1610,7 @@ async function extractTodoMarkdownWithOneRepair(
|
|
|
1521
1610
|
plannerText: string,
|
|
1522
1611
|
requestRepair: (repairPrompt: string) => Promise<string>,
|
|
1523
1612
|
goal?: string,
|
|
1613
|
+
capabilityConstraints: readonly string[] = [],
|
|
1524
1614
|
hooks: TodoExtractionRepairHooks = {},
|
|
1525
1615
|
): Promise<string> {
|
|
1526
1616
|
try {
|
|
@@ -1529,7 +1619,9 @@ async function extractTodoMarkdownWithOneRepair(
|
|
|
1529
1619
|
const validationError = errorMessage(error);
|
|
1530
1620
|
hooks.onInvalidOutput?.(validationError);
|
|
1531
1621
|
hooks.onRepairAttempt?.(validationError);
|
|
1532
|
-
const repairText = await requestRepair(
|
|
1622
|
+
const repairText = await requestRepair(
|
|
1623
|
+
buildTodoRepairPrompt(inputText, plannerText, validationError, goal, capabilityConstraints),
|
|
1624
|
+
);
|
|
1533
1625
|
try {
|
|
1534
1626
|
return extractAndValidateTodoMarkdown(repairText);
|
|
1535
1627
|
} catch (repairError) {
|
|
@@ -1553,9 +1645,16 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
|
|
|
1553
1645
|
abortSignal: runtime.abortSignal,
|
|
1554
1646
|
timeoutMs: runtime.todoTimeoutMs,
|
|
1555
1647
|
gracefulShutdownMs: runtime.todoGracefulShutdownMs,
|
|
1648
|
+
plannerBudget: runtime.plannerBudget,
|
|
1556
1649
|
sessionFactory: runtime.todoSessionFactory,
|
|
1557
1650
|
networkRecovery: runtime.networkRecovery,
|
|
1651
|
+
capabilityConstraints: runtime.capabilityWarnings.map((warning) => warning.planningConstraint),
|
|
1558
1652
|
onDiagnostic: (diagnostic) => recordPlannerDiagnostic(runtime, diagnostic),
|
|
1653
|
+
onProgress: (event) => {
|
|
1654
|
+
if (event.state !== "started") {
|
|
1655
|
+
emitPlannerProgress(runtime, event);
|
|
1656
|
+
}
|
|
1657
|
+
},
|
|
1559
1658
|
goal: runtime.goal,
|
|
1560
1659
|
},
|
|
1561
1660
|
runtime,
|
|
@@ -1567,8 +1666,10 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
|
|
|
1567
1666
|
* failed. The default planner disables tools and disposes every session before
|
|
1568
1667
|
* rejecting, so each retry rotates unsafe conversation state while replaying
|
|
1569
1668
|
* only the complete immutable planning context. Recovery owns no planner
|
|
1570
|
-
* repair/attempt counter, and each fresh call retains the
|
|
1571
|
-
*
|
|
1669
|
+
* repair/attempt counter, and each fresh call retains the exact configured
|
|
1670
|
+
* per-attempt planner timeout. Network wait is intentionally excluded and is
|
|
1671
|
+
* governed solely by the separate outage deadline; recovery cannot reset,
|
|
1672
|
+
* extend, or replace the timeout attached to any individual planner call.
|
|
1572
1673
|
*/
|
|
1573
1674
|
async function runPlannerOperationWithNetworkRecovery(
|
|
1574
1675
|
plannerOptions: TodoPlannerOptions,
|
|
@@ -1577,6 +1678,8 @@ async function runPlannerOperationWithNetworkRecovery(
|
|
|
1577
1678
|
const run = (recoverySignal?: AbortSignal) =>
|
|
1578
1679
|
runtime.todoPlanner({
|
|
1579
1680
|
...plannerOptions,
|
|
1681
|
+
// The recovery signal carries cancellation/outage expiry only. It does
|
|
1682
|
+
// not alter timeoutMs, which remains authoritative for every replay.
|
|
1580
1683
|
abortSignal: combineAbortSignals(plannerOptions.abortSignal, recoverySignal),
|
|
1581
1684
|
});
|
|
1582
1685
|
|
|
@@ -1584,17 +1687,73 @@ async function runPlannerOperationWithNetworkRecovery(
|
|
|
1584
1687
|
return await run();
|
|
1585
1688
|
} catch (initialFailure) {
|
|
1586
1689
|
const classification = classifyNetworkFailure(initialFailure);
|
|
1690
|
+
if (plannerOptions.abortSignal?.aborted || classification.reason === "cancelled") {
|
|
1691
|
+
recordPlannerCancellation(runtime, plannerOptions.abortSignal, initialFailure);
|
|
1692
|
+
throw plannerCancellationError(plannerOptions.abortSignal, initialFailure);
|
|
1693
|
+
}
|
|
1587
1694
|
if (!runtime.networkRecovery.enabled || !classification.recoverable) {
|
|
1588
1695
|
throw initialFailure;
|
|
1589
1696
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1697
|
+
|
|
1698
|
+
const publishRecovery = createNetworkRecoveryProgressHandler(runtime, "planner");
|
|
1699
|
+
let recoveryStarted = false;
|
|
1700
|
+
let lastRecoveryEvent: NetworkRecoveryEvent | undefined;
|
|
1701
|
+
const onRecoveryEvent = (event: NetworkRecoveryEvent) => {
|
|
1702
|
+
if (event.type !== "cleanup") lastRecoveryEvent = event;
|
|
1703
|
+
if (event.type === "outage_started" && !recoveryStarted) {
|
|
1704
|
+
recoveryStarted = true;
|
|
1705
|
+
recordPlannerDiagnostic(
|
|
1706
|
+
runtime,
|
|
1707
|
+
plannerNetworkDiagnostic(
|
|
1708
|
+
event,
|
|
1709
|
+
plannerOptions.timeoutMs,
|
|
1710
|
+
latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
1711
|
+
),
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
publishRecovery(event);
|
|
1715
|
+
if (event.type === "recovered") {
|
|
1716
|
+
recordPlannerDiagnostic(
|
|
1717
|
+
runtime,
|
|
1718
|
+
plannerNetworkDiagnostic(
|
|
1719
|
+
event,
|
|
1720
|
+
plannerOptions.timeoutMs,
|
|
1721
|
+
latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
1722
|
+
),
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
try {
|
|
1728
|
+
const recovered = await recoverNetworkOperation({
|
|
1729
|
+
initialFailure,
|
|
1730
|
+
config: runtime.networkRecovery,
|
|
1731
|
+
signal: plannerOptions.abortSignal,
|
|
1732
|
+
onEvent: onRecoveryEvent,
|
|
1733
|
+
retry: ({ signal }) => run(signal),
|
|
1734
|
+
});
|
|
1735
|
+
return recovered.value;
|
|
1736
|
+
} catch (recoveryFailure) {
|
|
1737
|
+
if (plannerOptions.abortSignal?.aborted) {
|
|
1738
|
+
recordPlannerCancellation(runtime, plannerOptions.abortSignal, recoveryFailure);
|
|
1739
|
+
throw plannerCancellationError(plannerOptions.abortSignal, recoveryFailure);
|
|
1740
|
+
} else if (!hasTerminalPlannerDiagnosticAfterLatestRecovery(runtime.plannerDiagnostics)) {
|
|
1741
|
+
const finalClassification = classifyNetworkFailure(recoveryFailure);
|
|
1742
|
+
recordPlannerDiagnostic(runtime, {
|
|
1743
|
+
kind: "network_failure",
|
|
1744
|
+
message: `TODO planner network recovery ended before planning completed: ${errorMessage(recoveryFailure)}`,
|
|
1745
|
+
partialOutputObserved: latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
1746
|
+
networkRecoveryEvent:
|
|
1747
|
+
lastRecoveryEvent?.type === "outage_expired" || lastRecoveryEvent?.type === "failed"
|
|
1748
|
+
? lastRecoveryEvent.type
|
|
1749
|
+
: "failed",
|
|
1750
|
+
networkFailureReason: lastRecoveryEvent?.state.lastFailure.reason ?? finalClassification.reason,
|
|
1751
|
+
networkRetryCount: lastRecoveryEvent?.state.retryCount,
|
|
1752
|
+
networkOutageElapsedMs: lastRecoveryEvent?.state.elapsedMs,
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
throw recoveryFailure;
|
|
1756
|
+
}
|
|
1598
1757
|
}
|
|
1599
1758
|
}
|
|
1600
1759
|
|
|
@@ -1640,9 +1799,12 @@ async function generateSteeringPlanRevision(options: {
|
|
|
1640
1799
|
abortSignal: options.runtime.abortSignal,
|
|
1641
1800
|
timeoutMs: options.runtime.todoTimeoutMs,
|
|
1642
1801
|
gracefulShutdownMs: options.runtime.todoGracefulShutdownMs,
|
|
1802
|
+
plannerBudget: options.runtime.plannerBudget,
|
|
1643
1803
|
sessionFactory: options.runtime.todoSessionFactory,
|
|
1644
1804
|
networkRecovery: options.runtime.networkRecovery,
|
|
1805
|
+
capabilityConstraints: options.runtime.capabilityWarnings.map((warning) => warning.planningConstraint),
|
|
1645
1806
|
onDiagnostic: (diagnostic) => recordPlannerDiagnostic(options.runtime, diagnostic),
|
|
1807
|
+
onProgress: (event) => emitPlannerProgress(options.runtime, event),
|
|
1646
1808
|
goal: options.runtime.goal,
|
|
1647
1809
|
},
|
|
1648
1810
|
options.runtime,
|
|
@@ -1751,16 +1913,41 @@ function relevantPlanRevisionResults(
|
|
|
1751
1913
|
// Planner/worker lifecycle differences are audited in docs/planner-worker-lifecycle-audit.md;
|
|
1752
1914
|
// keep this function's public contract stable while moving shared prompt guarding into a helper.
|
|
1753
1915
|
export async function runTodoPlanner(options: TodoPlannerOptions): Promise<string> {
|
|
1916
|
+
const capabilityConstraints =
|
|
1917
|
+
options.capabilityConstraints ??
|
|
1918
|
+
capabilityWarningsForRequest(options.inputText, options.goal).map((warning) => warning.planningConstraint);
|
|
1919
|
+
const plannerBudget =
|
|
1920
|
+
options.plannerBudget ??
|
|
1921
|
+
resolvePlannerBudget({
|
|
1922
|
+
inputText: options.inputText,
|
|
1923
|
+
explicitTimeoutMs: options.timeoutMs,
|
|
1924
|
+
defaultTimeoutMs: DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs,
|
|
1925
|
+
});
|
|
1926
|
+
const timeoutMs = resolvePlannerTimeoutMs(options.timeoutMs, plannerBudget.timeoutMs);
|
|
1927
|
+
const gracefulShutdownMs = resolvePlannerGracefulShutdownMs(
|
|
1928
|
+
options.gracefulShutdownMs,
|
|
1929
|
+
DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
|
|
1930
|
+
);
|
|
1931
|
+
const effectivePlannerBudget: PlannerBudget =
|
|
1932
|
+
timeoutMs === plannerBudget.timeoutMs
|
|
1933
|
+
? plannerBudget
|
|
1934
|
+
: {
|
|
1935
|
+
...plannerBudget,
|
|
1936
|
+
timeoutMs,
|
|
1937
|
+
extensionApplied: false,
|
|
1938
|
+
extensionMs: 0,
|
|
1939
|
+
source: "explicit",
|
|
1940
|
+
trigger: undefined,
|
|
1941
|
+
};
|
|
1942
|
+
notifyPlannerProgress(options.onProgress, createPlannerStartedProgress(effectivePlannerBudget, gracefulShutdownMs));
|
|
1754
1943
|
const sessionFactory = options.sessionFactory ?? createIsolatedWorkerSession;
|
|
1755
1944
|
const result = await sessionFactory({
|
|
1756
1945
|
cwd: options.cwd,
|
|
1757
1946
|
tools: [],
|
|
1758
1947
|
model: options.model,
|
|
1759
|
-
thinkingLevel: options.thinkingLevel,
|
|
1948
|
+
thinkingLevel: options.thinkingLevel ?? DEFAULT_PLANNER_THINKING_LEVEL,
|
|
1760
1949
|
});
|
|
1761
1950
|
const session = result.session;
|
|
1762
|
-
const timeoutMs = positiveMilliseconds(options.timeoutMs, DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
|
|
1763
|
-
const gracefulShutdownMs = options.gracefulShutdownMs ?? DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs;
|
|
1764
1951
|
|
|
1765
1952
|
let plannerMarkdown: string | undefined;
|
|
1766
1953
|
let plannerError: unknown;
|
|
@@ -1768,12 +1955,14 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
1768
1955
|
try {
|
|
1769
1956
|
const plannerText = await runTodoPlannerPrompt({
|
|
1770
1957
|
session,
|
|
1771
|
-
prompt: options.plannerPrompt ?? buildTodoCreationPrompt(options.inputText, options.goal),
|
|
1958
|
+
prompt: options.plannerPrompt ?? buildTodoCreationPrompt(options.inputText, options.goal, capabilityConstraints),
|
|
1772
1959
|
abortSignal: options.abortSignal,
|
|
1773
1960
|
timeoutMs,
|
|
1774
1961
|
gracefulShutdownMs,
|
|
1775
1962
|
diagnostics: result.diagnostics,
|
|
1776
1963
|
onDiagnostic: options.onDiagnostic,
|
|
1964
|
+
plannerBudget: effectivePlannerBudget,
|
|
1965
|
+
onProgress: options.onProgress,
|
|
1777
1966
|
});
|
|
1778
1967
|
|
|
1779
1968
|
plannerMarkdown = await extractTodoMarkdownWithOneRepair(
|
|
@@ -1788,8 +1977,11 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
1788
1977
|
gracefulShutdownMs,
|
|
1789
1978
|
diagnostics: result.diagnostics,
|
|
1790
1979
|
onDiagnostic: options.onDiagnostic,
|
|
1980
|
+
plannerBudget: effectivePlannerBudget,
|
|
1981
|
+
onProgress: options.onProgress,
|
|
1791
1982
|
}),
|
|
1792
1983
|
options.goal,
|
|
1984
|
+
capabilityConstraints,
|
|
1793
1985
|
{
|
|
1794
1986
|
onInvalidOutput: (validationError) =>
|
|
1795
1987
|
options.onDiagnostic?.({
|
|
@@ -1833,7 +2025,8 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
1833
2025
|
if (!plannerMarkdown) {
|
|
1834
2026
|
throw new TodoGenerationError("TODO planner did not return valid TODO markdown.");
|
|
1835
2027
|
}
|
|
1836
|
-
|
|
2028
|
+
const withGoal = applyGoalInstructionsToTodoMarkdown(plannerMarkdown, options.goal);
|
|
2029
|
+
return applyWorkerCapabilityConstraintsToTodoMarkdown(withGoal, capabilityConstraints);
|
|
1837
2030
|
}
|
|
1838
2031
|
|
|
1839
2032
|
async function runTodoPlannerPrompt(options: {
|
|
@@ -1844,6 +2037,8 @@ async function runTodoPlannerPrompt(options: {
|
|
|
1844
2037
|
gracefulShutdownMs: number;
|
|
1845
2038
|
diagnostics?: string[];
|
|
1846
2039
|
onDiagnostic?: PlannerDiagnosticHandler;
|
|
2040
|
+
plannerBudget: Readonly<PlannerBudget>;
|
|
2041
|
+
onProgress?: PlannerProgressHandler;
|
|
1847
2042
|
}): Promise<string> {
|
|
1848
2043
|
const promptResult = await runGuardedSessionPrompt({
|
|
1849
2044
|
session: options.session,
|
|
@@ -1853,17 +2048,47 @@ async function runTodoPlannerPrompt(options: {
|
|
|
1853
2048
|
gracefulShutdownMs: options.gracefulShutdownMs,
|
|
1854
2049
|
gracefulShutdownPrompt: buildTodoPlanningShutdownMessage(),
|
|
1855
2050
|
diagnostics: options.diagnostics,
|
|
2051
|
+
progressCheckpointsMs: plannerProgressCheckpoints(options.timeoutMs),
|
|
2052
|
+
onProgressCheckpoint: (elapsedMs) =>
|
|
2053
|
+
notifyPlannerProgress(
|
|
2054
|
+
options.onProgress,
|
|
2055
|
+
createPlannerActiveProgress(options.plannerBudget, options.gracefulShutdownMs, elapsedMs),
|
|
2056
|
+
),
|
|
2057
|
+
onGracePeriodStart: (gracePeriodMs) =>
|
|
2058
|
+
notifyPlannerProgress(options.onProgress, createPlannerGraceProgress(options.plannerBudget, gracePeriodMs)),
|
|
1856
2059
|
dispose: false,
|
|
1857
2060
|
});
|
|
1858
2061
|
|
|
2062
|
+
// Caller cancellation wins even when it arrives during grace. A hard abort
|
|
2063
|
+
// caused by grace expiry has cancelled=false and remains a timeout.
|
|
2064
|
+
if (promptResult.cancelled) {
|
|
2065
|
+
const outputState = promptResult.outputObserved
|
|
2066
|
+
? "partial output observed; content omitted"
|
|
2067
|
+
: "no planner output observed";
|
|
2068
|
+
const message = `TODO planner cancelled (${outputState}): ${promptResult.error ?? "caller cancellation"}`;
|
|
2069
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("cancelled", message, promptResult));
|
|
2070
|
+
throw new TodoGenerationError(message);
|
|
2071
|
+
}
|
|
1859
2072
|
if (promptResult.timedOut) {
|
|
1860
|
-
|
|
2073
|
+
if (
|
|
2074
|
+
promptResult.completedDuringGrace &&
|
|
2075
|
+
promptResult.outputObserved &&
|
|
2076
|
+
!promptResult.error &&
|
|
2077
|
+
isSafeCompletedTodoPlannerOutput(promptResult.assistantText)
|
|
2078
|
+
) {
|
|
2079
|
+
return promptResult.assistantText;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const outputState = promptResult.outputObserved
|
|
2083
|
+
? "partial output observed; content omitted"
|
|
2084
|
+
: "no planner output observed";
|
|
2085
|
+
const message = `TODO planner timed out (${outputState}): ${promptResult.error ?? "time budget exceeded"}`;
|
|
1861
2086
|
options.onDiagnostic?.(plannerPromptDiagnostic("timeout", message, promptResult));
|
|
1862
2087
|
throw new TodoGenerationError(message);
|
|
1863
2088
|
}
|
|
1864
2089
|
if (promptResult.aborted) {
|
|
1865
|
-
const message = `TODO planner
|
|
1866
|
-
options.onDiagnostic?.(plannerPromptDiagnostic("
|
|
2090
|
+
const message = `TODO planner stopped: ${promptResult.error ?? "session abort"}`;
|
|
2091
|
+
options.onDiagnostic?.(plannerPromptDiagnostic("failure", message, promptResult));
|
|
1867
2092
|
throw new TodoGenerationError(message);
|
|
1868
2093
|
}
|
|
1869
2094
|
if (promptResult.error) {
|
|
@@ -1883,9 +2108,10 @@ async function runTodoPlannerPrompt(options: {
|
|
|
1883
2108
|
}
|
|
1884
2109
|
|
|
1885
2110
|
function plannerPromptDiagnostic(
|
|
1886
|
-
kind: Extract<PlannerDiagnosticKind, "timeout" | "abort" | "failure">,
|
|
2111
|
+
kind: Extract<PlannerDiagnosticKind, "timeout" | "cancelled" | "abort" | "failure">,
|
|
1887
2112
|
message: string,
|
|
1888
2113
|
promptResult: {
|
|
2114
|
+
outputObserved: boolean;
|
|
1889
2115
|
diagnostics: string[];
|
|
1890
2116
|
sessionFile?: string;
|
|
1891
2117
|
sessionId?: string;
|
|
@@ -1894,12 +2120,25 @@ function plannerPromptDiagnostic(
|
|
|
1894
2120
|
return {
|
|
1895
2121
|
kind,
|
|
1896
2122
|
message,
|
|
2123
|
+
partialOutputObserved: promptResult.outputObserved,
|
|
1897
2124
|
diagnostics: promptResult.diagnostics,
|
|
1898
2125
|
sessionFile: promptResult.sessionFile,
|
|
1899
2126
|
sessionId: promptResult.sessionId,
|
|
1900
2127
|
};
|
|
1901
2128
|
}
|
|
1902
2129
|
|
|
2130
|
+
function isSafeCompletedTodoPlannerOutput(text: string): boolean {
|
|
2131
|
+
if (!text.trim()) {
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
extractAndValidateTodoMarkdown(text);
|
|
2136
|
+
return true;
|
|
2137
|
+
} catch {
|
|
2138
|
+
return false;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1903
2142
|
function buildTodoPlanningShutdownMessage(): string {
|
|
1904
2143
|
return `Pi Long Task notice: TODO planning has reached its time budget.
|
|
1905
2144
|
Return the best valid Pi Long Task TODO markdown you can produce now, or stop if that is not possible.`;
|
|
@@ -1912,8 +2151,13 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1912
2151
|
const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText ?? "");
|
|
1913
2152
|
const configuredAttempts = options.maxAttemptsPerTask ?? parsedWorkerConfig.maxAttemptsPerTask;
|
|
1914
2153
|
const configuredTaskTimeoutMs = options.taskTimeoutMs ?? parsedWorkerConfig.taskTimeoutMs;
|
|
1915
|
-
const configuredTodoTimeoutMs = options.todoTimeoutMs;
|
|
1916
|
-
const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs;
|
|
2154
|
+
const configuredTodoTimeoutMs = options.todoTimeoutMs ?? parsedWorkerConfig.todoTimeoutMs;
|
|
2155
|
+
const configuredTodoGracefulShutdownMs = options.todoGracefulShutdownMs ?? parsedWorkerConfig.todoGracefulShutdownMs;
|
|
2156
|
+
const plannerBudget = resolvePlannerBudget({
|
|
2157
|
+
inputText: coordinatorInputText(options),
|
|
2158
|
+
explicitTimeoutMs: configuredTodoTimeoutMs,
|
|
2159
|
+
defaultTimeoutMs: DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs,
|
|
2160
|
+
});
|
|
1917
2161
|
const configuredMaxBashTimeoutMs = options.maxBashTimeoutMs ?? parsedWorkerConfig.maxBashTimeoutMs;
|
|
1918
2162
|
const workerModelName = options.workerModelName ?? parsedWorkerConfig.modelName;
|
|
1919
2163
|
const workerModel = workerModelName ? undefined : options.workerModel;
|
|
@@ -1927,6 +2171,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1927
2171
|
...parsedWorkerConfig.networkRecovery,
|
|
1928
2172
|
...options.networkRecovery,
|
|
1929
2173
|
});
|
|
2174
|
+
const capabilityWarnings = capabilityWarningsForRequest(options.inputText, options.goal);
|
|
1930
2175
|
|
|
1931
2176
|
return {
|
|
1932
2177
|
cwd,
|
|
@@ -1936,11 +2181,12 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1936
2181
|
taskResultPath: path.join(runDir, "TASK_RESULT.md"),
|
|
1937
2182
|
maxAttemptsPerTask: positiveInteger(configuredAttempts, DEFAULT_COORDINATOR_OPTIONS.maxAttemptsPerTask),
|
|
1938
2183
|
taskTimeoutSeconds: positiveMilliseconds(configuredTaskTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.taskTimeoutMs) / 1000,
|
|
1939
|
-
todoTimeoutMs:
|
|
1940
|
-
todoGracefulShutdownMs:
|
|
2184
|
+
todoTimeoutMs: plannerBudget.timeoutMs,
|
|
2185
|
+
todoGracefulShutdownMs: resolvePlannerGracefulShutdownMs(
|
|
1941
2186
|
configuredTodoGracefulShutdownMs,
|
|
1942
2187
|
DEFAULT_COORDINATOR_OPTIONS.todoGracefulShutdownMs,
|
|
1943
2188
|
),
|
|
2189
|
+
plannerBudget,
|
|
1944
2190
|
maxBashTimeoutSeconds:
|
|
1945
2191
|
positiveMilliseconds(configuredMaxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
|
|
1946
2192
|
workerModel,
|
|
@@ -1964,6 +2210,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1964
2210
|
workerTextByWorker: new Map(),
|
|
1965
2211
|
workerTextPublishedLengthByWorker: new Map(),
|
|
1966
2212
|
plannerDiagnostics: [],
|
|
2213
|
+
capabilityWarnings,
|
|
1967
2214
|
workerSessionMetrics: createWorkerSessionMetrics(),
|
|
1968
2215
|
steeringQueue: options.steeringQueue,
|
|
1969
2216
|
onPlanRevisionAccepted: options.onPlanRevisionAccepted,
|
|
@@ -1974,6 +2221,23 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
1974
2221
|
};
|
|
1975
2222
|
}
|
|
1976
2223
|
|
|
2224
|
+
function emitPlannerProgress(runtime: RuntimeOptions, event: Readonly<PlannerProgressEvent>): void {
|
|
2225
|
+
emitProgress(runtime, event.message, {
|
|
2226
|
+
phase: "planning",
|
|
2227
|
+
activeStatus: event.message,
|
|
2228
|
+
plannerBudget: event.budget,
|
|
2229
|
+
plannerProgressState: event.state,
|
|
2230
|
+
plannerElapsedMs: event.elapsedMs,
|
|
2231
|
+
plannerRemainingMs: event.remainingMs,
|
|
2232
|
+
plannerGracePeriodMs: event.gracePeriodMs,
|
|
2233
|
+
...(event.graceRemainingMs === undefined ? {} : { plannerGraceRemainingMs: event.graceRemainingMs }),
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function notifyPlannerProgress(handler: PlannerProgressHandler | undefined, event: PlannerProgressEvent): void {
|
|
2238
|
+
handler?.(event);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
1977
2241
|
function emitProgress(
|
|
1978
2242
|
runtime: RuntimeOptions,
|
|
1979
2243
|
message: string,
|
|
@@ -1992,7 +2256,7 @@ function emitProgress(
|
|
|
1992
2256
|
runtime.lastProgress = progress;
|
|
1993
2257
|
const activeRecovery = latestNetworkRecovery(runtime.activeNetworkRecoveries);
|
|
1994
2258
|
if (activeRecovery) {
|
|
1995
|
-
publishNetworkRecoveryProgress(runtime, activeRecovery);
|
|
2259
|
+
publishNetworkRecoveryProgress(runtime, activeRecovery.event, activeRecovery.operation);
|
|
1996
2260
|
} else {
|
|
1997
2261
|
runtime.onProgress?.(progress);
|
|
1998
2262
|
}
|
|
@@ -2005,7 +2269,10 @@ function emitProgress(
|
|
|
2005
2269
|
* status path. Operation IDs prevent an older concurrent recovery from
|
|
2006
2270
|
* repainting a newer outage or completion.
|
|
2007
2271
|
*/
|
|
2008
|
-
function createNetworkRecoveryProgressHandler(
|
|
2272
|
+
function createNetworkRecoveryProgressHandler(
|
|
2273
|
+
runtime: RuntimeOptions,
|
|
2274
|
+
operation: "planner" | "worker" = "worker",
|
|
2275
|
+
): (event: NetworkRecoveryEvent) => void {
|
|
2009
2276
|
const operationId = ++runtime.networkRecoverySequence;
|
|
2010
2277
|
let cleaned = false;
|
|
2011
2278
|
|
|
@@ -2024,7 +2291,7 @@ function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event:
|
|
|
2024
2291
|
if (event.type === "recovered") {
|
|
2025
2292
|
const active = latestNetworkRecovery(runtime.activeNetworkRecoveries);
|
|
2026
2293
|
if (active) {
|
|
2027
|
-
publishNetworkRecoveryProgress(runtime, active);
|
|
2294
|
+
publishNetworkRecoveryProgress(runtime, active.event, active.operation);
|
|
2028
2295
|
} else if (runtime.lastProgress) {
|
|
2029
2296
|
runtime.onProgress?.({ ...runtime.lastProgress, workerCostTotal: runtime.workerCostState.total });
|
|
2030
2297
|
}
|
|
@@ -2032,18 +2299,26 @@ function createNetworkRecoveryProgressHandler(runtime: RuntimeOptions): (event:
|
|
|
2032
2299
|
return;
|
|
2033
2300
|
}
|
|
2034
2301
|
|
|
2035
|
-
runtime.activeNetworkRecoveries.set(operationId, event);
|
|
2302
|
+
runtime.activeNetworkRecoveries.set(operationId, { event, operation });
|
|
2036
2303
|
if (operationId === latestNetworkRecoveryId(runtime.activeNetworkRecoveries)) {
|
|
2037
|
-
publishNetworkRecoveryProgress(runtime, event);
|
|
2304
|
+
publishNetworkRecoveryProgress(runtime, event, operation);
|
|
2038
2305
|
}
|
|
2039
2306
|
};
|
|
2040
2307
|
}
|
|
2041
2308
|
|
|
2042
|
-
function publishNetworkRecoveryProgress(
|
|
2309
|
+
function publishNetworkRecoveryProgress(
|
|
2310
|
+
runtime: RuntimeOptions,
|
|
2311
|
+
event: NetworkRecoveryEvent,
|
|
2312
|
+
operation: "planner" | "worker" = "worker",
|
|
2313
|
+
): void {
|
|
2043
2314
|
if (runtime.progressClosed) return;
|
|
2044
2315
|
const stable = runtime.lastProgress;
|
|
2045
2316
|
const nowMs = event.state.outageStartedAtMs + event.state.elapsedMs;
|
|
2046
|
-
const
|
|
2317
|
+
const recoveryStatus = formatNetworkRecoveryStatus(event);
|
|
2318
|
+
const message =
|
|
2319
|
+
operation === "planner"
|
|
2320
|
+
? `TODO planner network recovery: ${recoveryStatus} The ${formatFriendlyDuration(runtime.todoTimeoutMs)} planning deadline remains unchanged for each provider attempt.`
|
|
2321
|
+
: recoveryStatus;
|
|
2047
2322
|
runtime.onProgress?.({
|
|
2048
2323
|
message,
|
|
2049
2324
|
phase: "network_wait",
|
|
@@ -2067,17 +2342,19 @@ function publishNetworkRecoveryProgress(runtime: RuntimeOptions, event: NetworkR
|
|
|
2067
2342
|
networkNextRetryInMs:
|
|
2068
2343
|
event.state.nextRetryAtMs === undefined ? undefined : Math.max(0, event.state.nextRetryAtMs - nowMs),
|
|
2069
2344
|
networkFailureReason: event.state.lastFailure.reason,
|
|
2345
|
+
networkOperation: operation,
|
|
2346
|
+
...(operation === "planner" ? { plannerDeadlinePolicy: "per_attempt_excludes_network_wait" as const } : {}),
|
|
2070
2347
|
});
|
|
2071
2348
|
}
|
|
2072
2349
|
|
|
2073
2350
|
function latestNetworkRecovery(
|
|
2074
|
-
recoveries: ReadonlyMap<number,
|
|
2075
|
-
):
|
|
2351
|
+
recoveries: ReadonlyMap<number, ActiveNetworkRecovery>,
|
|
2352
|
+
): ActiveNetworkRecovery | undefined {
|
|
2076
2353
|
const id = latestNetworkRecoveryId(recoveries);
|
|
2077
2354
|
return id === undefined ? undefined : recoveries.get(id);
|
|
2078
2355
|
}
|
|
2079
2356
|
|
|
2080
|
-
function latestNetworkRecoveryId(recoveries: ReadonlyMap<number,
|
|
2357
|
+
function latestNetworkRecoveryId(recoveries: ReadonlyMap<number, ActiveNetworkRecovery>): number | undefined {
|
|
2081
2358
|
let latest: number | undefined;
|
|
2082
2359
|
for (const id of recoveries.keys()) {
|
|
2083
2360
|
if (latest === undefined || id > latest) latest = id;
|
|
@@ -2089,13 +2366,79 @@ function isTerminalNetworkRecoveryEvent(type: NetworkRecoveryEventType): boolean
|
|
|
2089
2366
|
return type === "recovered" || type === "failed" || type === "cancelled" || type === "outage_expired";
|
|
2090
2367
|
}
|
|
2091
2368
|
|
|
2369
|
+
function plannerNetworkDiagnostic(
|
|
2370
|
+
event: NetworkRecoveryEvent,
|
|
2371
|
+
timeoutMs: number | undefined,
|
|
2372
|
+
partialOutputObserved: boolean | undefined,
|
|
2373
|
+
): PlannerDiagnostic {
|
|
2374
|
+
const recovered = event.type === "recovered";
|
|
2375
|
+
const budget = formatFriendlyDuration(timeoutMs ?? DEFAULT_COORDINATOR_OPTIONS.todoTimeoutMs);
|
|
2376
|
+
return {
|
|
2377
|
+
kind: "network_recovery",
|
|
2378
|
+
message: recovered
|
|
2379
|
+
? `TODO planner network recovery succeeded after ${formatFriendlyDuration(event.state.elapsedMs)}; planning continues with its unchanged ${budget} per-attempt deadline.`
|
|
2380
|
+
: `TODO planner network recovery started (${event.state.lastFailure.reason}); its outage clock is separate and the ${budget} per-attempt planning deadline remains unchanged.`,
|
|
2381
|
+
partialOutputObserved,
|
|
2382
|
+
networkRecoveryEvent: event.type,
|
|
2383
|
+
networkFailureReason: event.state.lastFailure.reason,
|
|
2384
|
+
networkRetryCount: event.state.retryCount,
|
|
2385
|
+
networkOutageElapsedMs: event.state.elapsedMs,
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function latestPlannerPartialOutput(diagnostics: readonly PlannerDiagnostic[]): boolean | undefined {
|
|
2390
|
+
return [...diagnostics].reverse().find((diagnostic) => diagnostic.partialOutputObserved !== undefined)
|
|
2391
|
+
?.partialOutputObserved;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function hasTerminalPlannerDiagnostic(diagnostics: readonly PlannerDiagnostic[]): boolean {
|
|
2395
|
+
const kind = diagnostics.at(-1)?.kind;
|
|
2396
|
+
return kind !== undefined && ["timeout", "cancelled", "abort", "network_failure", "failure"].includes(kind);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function hasTerminalPlannerDiagnosticAfterLatestRecovery(diagnostics: readonly PlannerDiagnostic[]): boolean {
|
|
2400
|
+
let recoveryIndex = -1;
|
|
2401
|
+
for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
|
|
2402
|
+
if (diagnostics[index]?.kind === "network_recovery") {
|
|
2403
|
+
recoveryIndex = index;
|
|
2404
|
+
break;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return diagnostics
|
|
2408
|
+
.slice(recoveryIndex + 1)
|
|
2409
|
+
.some((diagnostic) => ["timeout", "cancelled", "abort", "network_failure"].includes(diagnostic.kind));
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function recordPlannerCancellation(runtime: RuntimeOptions, signal: AbortSignal | undefined, cause: unknown): void {
|
|
2413
|
+
if (runtime.plannerDiagnostics.some((diagnostic) => diagnostic.kind === "cancelled")) return;
|
|
2414
|
+
recordPlannerDiagnostic(runtime, {
|
|
2415
|
+
kind: "cancelled",
|
|
2416
|
+
message: `TODO planning cancelled: ${signal?.aborted ? abortSignalReason(signal) : errorMessage(cause)}`,
|
|
2417
|
+
partialOutputObserved: latestPlannerPartialOutput(runtime.plannerDiagnostics),
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function abortSignalReason(signal: AbortSignal): string {
|
|
2422
|
+
return signal.reason === undefined ? "cancelled by caller" : errorMessage(signal.reason);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
function plannerCancellationError(signal: AbortSignal | undefined, cause: unknown): TodoGenerationError {
|
|
2426
|
+
const reason = signal?.aborted ? abortSignalReason(signal) : errorMessage(cause);
|
|
2427
|
+
return new TodoGenerationError(`TODO planning cancelled: ${reason}`, { cause });
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2092
2430
|
function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDiagnostic): void {
|
|
2093
2431
|
const normalized: PlannerDiagnostic = {
|
|
2094
2432
|
kind: diagnostic.kind,
|
|
2095
2433
|
message: diagnostic.message,
|
|
2434
|
+
partialOutputObserved: diagnostic.partialOutputObserved,
|
|
2096
2435
|
diagnostics: diagnostic.diagnostics?.filter(Boolean),
|
|
2097
2436
|
sessionFile: diagnostic.sessionFile,
|
|
2098
2437
|
sessionId: diagnostic.sessionId,
|
|
2438
|
+
networkRecoveryEvent: diagnostic.networkRecoveryEvent,
|
|
2439
|
+
networkFailureReason: diagnostic.networkFailureReason,
|
|
2440
|
+
networkRetryCount: diagnostic.networkRetryCount,
|
|
2441
|
+
networkOutageElapsedMs: diagnostic.networkOutageElapsedMs,
|
|
2099
2442
|
};
|
|
2100
2443
|
const last = runtime.plannerDiagnostics.at(-1);
|
|
2101
2444
|
if (last?.kind === normalized.kind && last.message === normalized.message) {
|
|
@@ -2105,11 +2448,22 @@ function recordPlannerDiagnostic(runtime: RuntimeOptions, diagnostic: PlannerDia
|
|
|
2105
2448
|
emitProgress(runtime, normalized.message, {
|
|
2106
2449
|
phase: "planning",
|
|
2107
2450
|
status: normalized.kind,
|
|
2108
|
-
isError:
|
|
2451
|
+
isError: !["repair_attempt", "network_recovery"].includes(normalized.kind),
|
|
2109
2452
|
plannerDiagnostic: normalized.kind,
|
|
2110
2453
|
plannerDiagnostics: normalized.diagnostics,
|
|
2454
|
+
plannerPartialOutputObserved: normalized.partialOutputObserved,
|
|
2111
2455
|
plannerSessionFile: normalized.sessionFile,
|
|
2112
2456
|
plannerSessionId: normalized.sessionId,
|
|
2457
|
+
networkRecoveryEvent: normalized.networkRecoveryEvent,
|
|
2458
|
+
networkRetryCount: normalized.networkRetryCount,
|
|
2459
|
+
networkOutageElapsedMs: normalized.networkOutageElapsedMs,
|
|
2460
|
+
networkFailureReason: normalized.networkFailureReason,
|
|
2461
|
+
networkOperation:
|
|
2462
|
+
normalized.kind === "network_recovery" || normalized.kind === "network_failure" ? "planner" : undefined,
|
|
2463
|
+
plannerDeadlinePolicy:
|
|
2464
|
+
normalized.kind === "network_recovery" || normalized.kind === "network_failure"
|
|
2465
|
+
? "per_attempt_excludes_network_wait"
|
|
2466
|
+
: undefined,
|
|
2113
2467
|
taskProgress: buildTaskProgressModel({ tasks: [] }),
|
|
2114
2468
|
});
|
|
2115
2469
|
}
|
|
@@ -2582,8 +2936,11 @@ function outcomeProgressItemStatus(
|
|
|
2582
2936
|
return "failed";
|
|
2583
2937
|
}
|
|
2584
2938
|
|
|
2585
|
-
function initialTaskResultMarkdown(runId: string): string {
|
|
2586
|
-
|
|
2939
|
+
function initialTaskResultMarkdown(runId: string, capabilityWarnings: readonly WorkerCapabilityWarning[] = []): string {
|
|
2940
|
+
const warningBlock = capabilityWarnings.length
|
|
2941
|
+
? `\n\n## Worker capability warnings\n\n${capabilityWarnings.map((warning) => `- ${warning.message}`).join("\n")}`
|
|
2942
|
+
: "";
|
|
2943
|
+
return `# Pi Long Task TASK_RESULT\n\nRun: ${runId}${warningBlock}\n`;
|
|
2587
2944
|
}
|
|
2588
2945
|
|
|
2589
2946
|
async function appendFailureNote(
|
|
@@ -2596,6 +2953,21 @@ async function appendFailureNote(
|
|
|
2596
2953
|
lines.push("", "### Planner diagnostics");
|
|
2597
2954
|
for (const diagnostic of plannerDiagnostics) {
|
|
2598
2955
|
lines.push("", `- ${diagnostic.kind}: ${diagnostic.message}`);
|
|
2956
|
+
if (diagnostic.partialOutputObserved !== undefined) {
|
|
2957
|
+
lines.push(` - Partial output observed: ${diagnostic.partialOutputObserved ? "yes" : "no"}`);
|
|
2958
|
+
}
|
|
2959
|
+
if (diagnostic.networkRecoveryEvent) {
|
|
2960
|
+
lines.push(` - Network recovery event: ${diagnostic.networkRecoveryEvent}`);
|
|
2961
|
+
}
|
|
2962
|
+
if (diagnostic.networkFailureReason) {
|
|
2963
|
+
lines.push(` - Network failure reason: ${diagnostic.networkFailureReason}`);
|
|
2964
|
+
}
|
|
2965
|
+
if (diagnostic.networkRetryCount !== undefined) {
|
|
2966
|
+
lines.push(` - Network retry count: ${diagnostic.networkRetryCount}`);
|
|
2967
|
+
}
|
|
2968
|
+
if (diagnostic.networkOutageElapsedMs !== undefined) {
|
|
2969
|
+
lines.push(` - Network outage elapsed: ${formatFriendlyDuration(diagnostic.networkOutageElapsedMs)}`);
|
|
2970
|
+
}
|
|
2599
2971
|
if (diagnostic.sessionId) {
|
|
2600
2972
|
lines.push(` - Session ID: ${diagnostic.sessionId}`);
|
|
2601
2973
|
}
|
|
@@ -2794,6 +3166,17 @@ function coordinatorInputText(options: RunCoordinatorOptions): string {
|
|
|
2794
3166
|
return normalizeOptionalText(options.inputText) ?? normalizeOptionalText(options.goal) ?? "";
|
|
2795
3167
|
}
|
|
2796
3168
|
|
|
3169
|
+
function capabilityWarningsForRequest(inputText?: string, goal?: string): WorkerCapabilityWarning[] {
|
|
3170
|
+
const requestText = [normalizeOptionalText(inputText), normalizeOptionalText(goal)]
|
|
3171
|
+
.filter((item): item is string => Boolean(item))
|
|
3172
|
+
.filter((item, index, all) => all.indexOf(item) === index)
|
|
3173
|
+
.join("\n");
|
|
3174
|
+
return detectUnavailableWorkerCapabilities(requestText, {
|
|
3175
|
+
tools: DEFAULT_WORKER_TOOLS,
|
|
3176
|
+
extensionsEnabled: false,
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2797
3180
|
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
2798
3181
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
2799
3182
|
return Math.floor(value);
|