pi-long-task 0.5.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 +18 -0
- package/README.md +85 -2
- package/package.json +1 -1
- package/src/coordinator.ts +450 -34
- 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 +2 -0
- 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 +74 -0
- package/src/worker_session.ts +33 -1
|
@@ -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
|
@@ -576,6 +576,8 @@ function sidebarUpdateStateDetails(update: CoordinatorProgressUpdate): {
|
|
|
576
576
|
return { icon: "↻", label: "Worker session", color: "accent" };
|
|
577
577
|
case "worker_tool":
|
|
578
578
|
return { icon: "+", label: "Worker tool", color: "warning" };
|
|
579
|
+
case "network_wait":
|
|
580
|
+
return { icon: "↻", label: "Waiting for connection", color: "warning" };
|
|
579
581
|
case "task_done":
|
|
580
582
|
return { icon: "✓", label: "Task complete", color: "success" };
|
|
581
583
|
case "task_blocked":
|