pi-long-task 0.4.0 → 0.6.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 +34 -0
- package/README.md +139 -10
- package/package.json +2 -2
- package/src/coordinator.ts +1132 -45
- package/src/goal_discovery.ts +2 -0
- package/src/goal_loop.ts +76 -0
- package/src/goal_orchestrator.ts +87 -1
- package/src/goal_review.ts +206 -15
- package/src/goal_todo_execution.ts +3 -0
- package/src/goal_todo_generation.ts +96 -3
- package/src/index.ts +20 -5
- package/src/network_failure.ts +574 -0
- package/src/network_recovery.ts +395 -0
- package/src/network_recovery_config.ts +89 -0
- package/src/render.ts +2 -0
- package/src/session_guard.ts +8 -1
- package/src/todo_generator.ts +2 -2
- package/src/types.ts +32 -0
- package/src/worker_config.ts +137 -14
- package/src/worker_reuse_policy.ts +389 -0
- package/src/worker_session.ts +294 -34
|
@@ -3,9 +3,18 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { runCoordinator, type CoordinatorResult, type RunCoordinatorOptions } from "./coordinator.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
excludeNetworkOutageFromGoalDeadlines,
|
|
8
|
+
type GoalIterationState,
|
|
9
|
+
type GoalLoopState,
|
|
10
|
+
recordGeneratedTodo,
|
|
11
|
+
startGoalIteration,
|
|
12
|
+
} from "./goal_loop.ts";
|
|
7
13
|
import { GoalStateStore } from "./goal_state.ts";
|
|
8
14
|
import type { GoalSpecification } from "./goal_spec.ts";
|
|
15
|
+
import { classifyNetworkFailure } from "./network_failure.ts";
|
|
16
|
+
import { recoverNetworkOperation, type NetworkRecoveryEvent } from "./network_recovery.ts";
|
|
17
|
+
import type { NetworkRecoveryConfig } from "./network_recovery_config.ts";
|
|
9
18
|
import { parseTasks } from "./todo_parser.ts";
|
|
10
19
|
import {
|
|
11
20
|
applyGoalInstructionsToTodoMarkdown,
|
|
@@ -29,10 +38,12 @@ export interface GoalTodoGenerationOptions {
|
|
|
29
38
|
modelName?: string;
|
|
30
39
|
thinkingLevel?: string;
|
|
31
40
|
maxBashTimeoutMs?: number;
|
|
41
|
+
networkRecovery?: Readonly<NetworkRecoveryConfig>;
|
|
32
42
|
now?: () => Date;
|
|
33
43
|
additionalContext?: string;
|
|
34
44
|
outputPath?: string;
|
|
35
45
|
goalSpecification?: GoalSpecification;
|
|
46
|
+
onNetworkRecovery?: (event: NetworkRecoveryEvent) => void;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
export interface GoalTodoGenerationResult {
|
|
@@ -92,7 +103,14 @@ export async function runGoalTodoGenerationLongTask(
|
|
|
92
103
|
);
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
let excludedOutageMs = 0;
|
|
107
|
+
const captureOutage = (event: NetworkRecoveryEvent) => {
|
|
108
|
+
if (event.type === "cleanup") {
|
|
109
|
+
excludedOutageMs += event.state.elapsedMs;
|
|
110
|
+
}
|
|
111
|
+
options.onNetworkRecovery?.(event);
|
|
112
|
+
};
|
|
113
|
+
const childOptions: RunCoordinatorOptions = {
|
|
96
114
|
inputText: payload,
|
|
97
115
|
commit: false,
|
|
98
116
|
goal: state.goal,
|
|
@@ -104,7 +122,36 @@ export async function runGoalTodoGenerationLongTask(
|
|
|
104
122
|
taskThinking: options.thinkingLevel,
|
|
105
123
|
taskTimeoutMs: childTimeoutMs,
|
|
106
124
|
maxBashTimeoutMs: options.maxBashTimeoutMs,
|
|
107
|
-
|
|
125
|
+
networkRecovery: options.networkRecovery,
|
|
126
|
+
onNetworkRecovery: captureOutage,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let childResult: CoordinatorResult | undefined;
|
|
130
|
+
let childFailure: unknown;
|
|
131
|
+
try {
|
|
132
|
+
childResult = await runGoalPlannerWithNetworkRecovery(
|
|
133
|
+
options.longTaskRunner ?? runCoordinator,
|
|
134
|
+
childOptions,
|
|
135
|
+
options.networkRecovery,
|
|
136
|
+
options.abortSignal,
|
|
137
|
+
now,
|
|
138
|
+
captureOutage,
|
|
139
|
+
);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
childFailure = error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (excludedOutageMs > 0) {
|
|
145
|
+
state = excludeNetworkOutageFromGoalDeadlines(state, excludedOutageMs, "planner", { now: now() });
|
|
146
|
+
await persistStateChange(store, previousTraceLength, state);
|
|
147
|
+
previousTraceLength = state.trace.length;
|
|
148
|
+
}
|
|
149
|
+
if (childFailure !== undefined) {
|
|
150
|
+
throw childFailure;
|
|
151
|
+
}
|
|
152
|
+
if (!childResult) {
|
|
153
|
+
throw new GoalTodoGenerationError("TODO-generation planner ended without a coordinator result.");
|
|
154
|
+
}
|
|
108
155
|
|
|
109
156
|
throwIfAborted(options.abortSignal);
|
|
110
157
|
const rawOutput = await readGeneratedTodo(rawTodoPath, childResult);
|
|
@@ -259,6 +306,45 @@ async function persistStateChange(
|
|
|
259
306
|
await store.appendNewTraceEvents(previousTraceLength, state);
|
|
260
307
|
}
|
|
261
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Resume the TODO-generation child under its stable run ID. This child is
|
|
311
|
+
* constrained to one replaceable generated-plan artifact (never implementation
|
|
312
|
+
* work), while the default coordinator durably resumes its TODO evidence and
|
|
313
|
+
* rotates provider-failed sessions. Network probes therefore neither create a
|
|
314
|
+
* goal iteration nor consume a normal child task attempt.
|
|
315
|
+
*/
|
|
316
|
+
async function runGoalPlannerWithNetworkRecovery(
|
|
317
|
+
runner: GoalTodoGenerationLongTaskRunner,
|
|
318
|
+
childOptions: RunCoordinatorOptions,
|
|
319
|
+
networkRecovery: Readonly<NetworkRecoveryConfig> | undefined,
|
|
320
|
+
abortSignal: AbortSignal | undefined,
|
|
321
|
+
now: () => Date,
|
|
322
|
+
onRecoveryEvent: (event: NetworkRecoveryEvent) => void,
|
|
323
|
+
): Promise<CoordinatorResult> {
|
|
324
|
+
const run = (recoverySignal?: AbortSignal) =>
|
|
325
|
+
runner({
|
|
326
|
+
...childOptions,
|
|
327
|
+
abortSignal: combineAbortSignals(abortSignal, recoverySignal),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
return await run();
|
|
332
|
+
} catch (initialFailure) {
|
|
333
|
+
if (!networkRecovery?.enabled || !classifyNetworkFailure(initialFailure).recoverable) {
|
|
334
|
+
throw initialFailure;
|
|
335
|
+
}
|
|
336
|
+
const recovered = await recoverNetworkOperation({
|
|
337
|
+
initialFailure,
|
|
338
|
+
config: networkRecovery,
|
|
339
|
+
signal: abortSignal,
|
|
340
|
+
now: () => now().getTime(),
|
|
341
|
+
onEvent: onRecoveryEvent,
|
|
342
|
+
retry: ({ signal }) => run(signal),
|
|
343
|
+
});
|
|
344
|
+
return recovered.value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
262
348
|
async function readGeneratedTodo(rawTodoPath: string, childResult: CoordinatorResult): Promise<string> {
|
|
263
349
|
try {
|
|
264
350
|
return await readFile(rawTodoPath, "utf8");
|
|
@@ -538,6 +624,13 @@ function sha256(value: string): string {
|
|
|
538
624
|
return createHash("sha256").update(value).digest("hex");
|
|
539
625
|
}
|
|
540
626
|
|
|
627
|
+
function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
|
|
628
|
+
const available = signals.filter((signal): signal is AbortSignal => Boolean(signal));
|
|
629
|
+
if (available.length === 0) return undefined;
|
|
630
|
+
if (available.length === 1) return available[0];
|
|
631
|
+
return AbortSignal.any(available);
|
|
632
|
+
}
|
|
633
|
+
|
|
541
634
|
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
542
635
|
if (signal?.aborted) {
|
|
543
636
|
throw new GoalTodoGenerationError("TODO generation was aborted before producing a generated TODO.");
|
package/src/index.ts
CHANGED
|
@@ -276,10 +276,8 @@ function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
|
|
|
276
276
|
const progress = update.taskProgress;
|
|
277
277
|
const summary = progress?.summary;
|
|
278
278
|
const statusDetails = sidebarUpdateStateDetails(update);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
`${statusDetails.icon} ${statusDetails.label} · ${update.activeStatus ?? update.message}`,
|
|
282
|
-
];
|
|
279
|
+
const statusText = normalizeActiveStatus(update.activeStatus ?? update.message);
|
|
280
|
+
const lines = ["Pi Long Task", `${statusDetails.icon} ${statusDetails.label} · ${statusText}`];
|
|
283
281
|
if (summary) {
|
|
284
282
|
lines.push(
|
|
285
283
|
`Tasks: ${summary.completedTasks}/${summary.totalTasks} · ${summary.completedPercent}%` +
|
|
@@ -396,7 +394,8 @@ function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme:
|
|
|
396
394
|
rows.push(theme.fg("success", "No active task"));
|
|
397
395
|
}
|
|
398
396
|
|
|
399
|
-
const
|
|
397
|
+
const rawActiveStatus = update.activeStatus ?? normalizeMessageForSidebar(update.message, update);
|
|
398
|
+
const activeStatus = rawActiveStatus ? normalizeActiveStatus(rawActiveStatus) : undefined;
|
|
400
399
|
if (currentTask && activeStatus) {
|
|
401
400
|
rows.push("", sidebarHeading("Active status", theme));
|
|
402
401
|
rows.push(...wrapPlainText(activeStatus, width, 6).map((line) => theme.fg("accent", line)));
|
|
@@ -573,8 +572,12 @@ function sidebarUpdateStateDetails(update: CoordinatorProgressUpdate): {
|
|
|
573
572
|
return { icon: "✓", label: "Plan ready", color: "success" };
|
|
574
573
|
case "task_start":
|
|
575
574
|
return { icon: "▢", label: "Running task", color: "accent" };
|
|
575
|
+
case "worker_session":
|
|
576
|
+
return { icon: "↻", label: "Worker session", color: "accent" };
|
|
576
577
|
case "worker_tool":
|
|
577
578
|
return { icon: "+", label: "Worker tool", color: "warning" };
|
|
579
|
+
case "network_wait":
|
|
580
|
+
return { icon: "↻", label: "Waiting for connection", color: "warning" };
|
|
578
581
|
case "task_done":
|
|
579
582
|
return { icon: "✓", label: "Task complete", color: "success" };
|
|
580
583
|
case "task_blocked":
|
|
@@ -681,6 +684,18 @@ function normalizeMessageForSidebar(updateMessage: string, update: CoordinatorPr
|
|
|
681
684
|
return title && message.includes(title) && message.length <= title.length + 16 ? undefined : message;
|
|
682
685
|
}
|
|
683
686
|
|
|
687
|
+
function normalizeActiveStatus(status: string): string {
|
|
688
|
+
const normalized = status.trim();
|
|
689
|
+
const firstOutcome = /^(Finished|Failed):\s*/i.exec(normalized)?.[1];
|
|
690
|
+
if (!firstOutcome) {
|
|
691
|
+
return normalized;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const activity = normalized.replace(/^(?:(?:Finished|Failed):\s*)+/i, "");
|
|
695
|
+
const outcome = firstOutcome.toLowerCase() === "failed" ? "Failed" : "Finished";
|
|
696
|
+
return activity ? `${outcome}: ${activity}` : `${outcome}:`;
|
|
697
|
+
}
|
|
698
|
+
|
|
684
699
|
function wrapPlainText(text: string, width: number, limit?: number): string[] {
|
|
685
700
|
const safeWidth = Math.max(8, width);
|
|
686
701
|
const words = text.trim().split(/\s+/).filter(Boolean);
|