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
package/src/worker_config.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NetworkRecoveryConfigError,
|
|
3
|
+
resolveNetworkRecoveryConfig,
|
|
4
|
+
type NetworkRecoveryConfigInput,
|
|
5
|
+
} from "./network_recovery_config.ts";
|
|
6
|
+
|
|
1
7
|
export interface ParsedWorkerRuntimeConfig {
|
|
2
8
|
modelName?: string;
|
|
3
9
|
maxAttemptsPerTask?: number;
|
|
@@ -5,6 +11,7 @@ export interface ParsedWorkerRuntimeConfig {
|
|
|
5
11
|
maxBashTimeoutMs?: number;
|
|
6
12
|
workerSessionReuseEnabled?: boolean;
|
|
7
13
|
workerSessionReuseContextThresholdPercent?: number;
|
|
14
|
+
networkRecovery?: NetworkRecoveryConfigInput;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
type MutableWorkerRuntimeConfig = ParsedWorkerRuntimeConfig & { provider?: string; model?: string };
|
|
@@ -36,6 +43,9 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
|
|
|
36
43
|
|
|
37
44
|
parseLineDirectives(text, state);
|
|
38
45
|
parseNaturalLanguageDirectives(text, state);
|
|
46
|
+
if (state.networkRecovery) {
|
|
47
|
+
resolveNetworkRecoveryConfig(state.networkRecovery);
|
|
48
|
+
}
|
|
39
49
|
|
|
40
50
|
const modelName = combineProviderAndModel(state.provider, state.model);
|
|
41
51
|
return {
|
|
@@ -49,6 +59,7 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
|
|
|
49
59
|
...(state.workerSessionReuseContextThresholdPercent !== undefined
|
|
50
60
|
? { workerSessionReuseContextThresholdPercent: state.workerSessionReuseContextThresholdPercent }
|
|
51
61
|
: {}),
|
|
62
|
+
...(state.networkRecovery ? { networkRecovery: { ...state.networkRecovery } } : {}),
|
|
52
63
|
};
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -159,6 +170,11 @@ function parseNaturalLanguageDirectives(text: string, state: MutableWorkerRuntim
|
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
function applyDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
|
|
173
|
+
if (/\bnetwork\b/.test(key) && /\brecover(?:y|ies)?\b/.test(key)) {
|
|
174
|
+
applyNetworkRecoveryDirective(key, value, state);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
if (/\breuse\b/.test(key) && /\b(?:threshold|context)\b/.test(key)) {
|
|
163
179
|
const threshold = percentageFromText(value);
|
|
164
180
|
if (threshold !== undefined) {
|
|
@@ -215,6 +231,64 @@ function applyDirective(key: string, value: string, state: MutableWorkerRuntimeC
|
|
|
215
231
|
}
|
|
216
232
|
}
|
|
217
233
|
|
|
234
|
+
function applyNetworkRecoveryDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
|
|
235
|
+
const recovery = (state.networkRecovery ??= {});
|
|
236
|
+
|
|
237
|
+
if (/\bbase\b/.test(key) && /\bdelay\b/.test(key)) {
|
|
238
|
+
recovery.baseDelayMs = requiredNetworkRecoveryDuration("base delay", value);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (/\bmax(?:imum)?\b/.test(key) && /\bdelay\b/.test(key)) {
|
|
242
|
+
recovery.maxDelayMs = requiredNetworkRecoveryDuration("maximum delay", value);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (/\b(?:outage|duration|wait)\b/.test(key)) {
|
|
246
|
+
const normalized = trimDirectiveValue(value)
|
|
247
|
+
.toLowerCase()
|
|
248
|
+
.replace(/[.!]+$/g, "")
|
|
249
|
+
.trim();
|
|
250
|
+
if (/^(?:unlimited|indefinite|indefinitely|until cancelled|until canceled)$/.test(normalized)) {
|
|
251
|
+
recovery.maxOutageMs = null;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
recovery.maxOutageMs = requiredNetworkRecoveryDuration("maximum outage", value);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (/\b(?:enabled?|enablement)\b/.test(key) || /\bnetwork recovery\b/.test(key)) {
|
|
258
|
+
const enabled = booleanSetting(value);
|
|
259
|
+
if (enabled === undefined) {
|
|
260
|
+
throw new NetworkRecoveryConfigError(
|
|
261
|
+
"Network recovery must be configured as enabled/disabled, on/off, true/false, or yes/no.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
recovery.enabled = enabled;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new NetworkRecoveryConfigError(`Unknown network recovery configuration directive: ${key}.`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function requiredNetworkRecoveryDuration(label: string, value: string): number {
|
|
272
|
+
const trimmed = trimDirectiveValue(value)
|
|
273
|
+
.replace(/[.!]+$/g, "")
|
|
274
|
+
.trim();
|
|
275
|
+
const match = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?$/i.exec(
|
|
276
|
+
trimmed,
|
|
277
|
+
);
|
|
278
|
+
if (!match) {
|
|
279
|
+
throw new NetworkRecoveryConfigError(
|
|
280
|
+
`Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const milliseconds = durationMsFromText(trimmed, { allowBareSeconds: true });
|
|
284
|
+
if (milliseconds === undefined) {
|
|
285
|
+
throw new NetworkRecoveryConfigError(
|
|
286
|
+
`Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return milliseconds;
|
|
290
|
+
}
|
|
291
|
+
|
|
218
292
|
function captureTokens(text: string, pattern: RegExp, apply: (token: string) => void): void {
|
|
219
293
|
for (const match of text.matchAll(pattern)) {
|
|
220
294
|
const token = modelToken(match[1] ?? "");
|
package/src/worker_session.ts
CHANGED
|
@@ -8,14 +8,25 @@ import {
|
|
|
8
8
|
parseCompleteTaskResult,
|
|
9
9
|
parseReportedStatus,
|
|
10
10
|
} from "./result_writer.ts";
|
|
11
|
+
import type { NetworkRecoveryConfig } from "./network_recovery_config.ts";
|
|
11
12
|
import type { Task } from "./todo_parser.ts";
|
|
12
13
|
|
|
14
|
+
export interface WorkerNetworkRecoveryContext {
|
|
15
|
+
/** One-based coordinator network retry count; this is not a task attempt. */
|
|
16
|
+
retryCount: number;
|
|
17
|
+
durableEvidencePath: string;
|
|
18
|
+
priorSessionId?: string;
|
|
19
|
+
failure: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export interface WorkerTaskPromptOptions {
|
|
14
23
|
todoPath: string;
|
|
15
24
|
task: Pick<Task, "taskId" | "title" | "section">;
|
|
16
25
|
attempt: number;
|
|
17
26
|
commitRequested: boolean;
|
|
18
27
|
previousAttempts?: string;
|
|
28
|
+
/** Continuity supplied only when a failed transport session is replaced. */
|
|
29
|
+
networkRecoveryContext?: WorkerNetworkRecoveryContext;
|
|
19
30
|
globalInstructions?: string;
|
|
20
31
|
goal?: string;
|
|
21
32
|
maxBashTimeoutSeconds: number;
|
|
@@ -43,6 +54,18 @@ Previous attempts for this same assigned task are below. Use them only as contin
|
|
|
43
54
|
\`\`\`text
|
|
44
55
|
${previousAttempts}
|
|
45
56
|
\`\`\`
|
|
57
|
+
`
|
|
58
|
+
: "";
|
|
59
|
+
|
|
60
|
+
const recovery = options.networkRecoveryContext;
|
|
61
|
+
const recoveryText = recovery
|
|
62
|
+
? `
|
|
63
|
+
Network recovery continuation (network retry ${recovery.retryCount}, still ordinary task attempt ${options.attempt}):
|
|
64
|
+
- The prior worker session ended only after Pi's bounded provider-request retries were exhausted: ${recovery.failure}
|
|
65
|
+
- Durable interruption evidence was recorded in \`${recovery.durableEvidencePath}\`${recovery.priorSessionId ? ` for session \`${recovery.priorSessionId}\`` : ""}.
|
|
66
|
+
- The prior session may already have completed tool calls and changed the working tree. Inspect the durable evidence and current files before acting.
|
|
67
|
+
- Continue this same TODO from its current state. Never blindly replay prior edits, commands, commits, external writes, or other side effects.
|
|
68
|
+
- Report one final TASK_RESULT for the assignment only after verifying what remains.
|
|
46
69
|
`
|
|
47
70
|
: "";
|
|
48
71
|
|
|
@@ -93,7 +116,7 @@ Rules:
|
|
|
93
116
|
- Do not run bash commands with timeout greater than ${options.maxBashTimeoutSeconds.toFixed(0)} seconds. For long full-suite checks, run once with a bounded timeout and report any timeout/failure in TASK_RESULT instead of continuing indefinitely.
|
|
94
117
|
- If TODO-file global instructions restrict scope, obey them strictly. If the task appears to require out-of-scope code changes, stop and report \`status: blocked\` instead of changing those files.
|
|
95
118
|
|
|
96
|
-
${globalText}Assigned task content only:
|
|
119
|
+
${globalText}${recoveryText}Assigned task content only:
|
|
97
120
|
|
|
98
121
|
\`\`\`markdown
|
|
99
122
|
${options.task.section.trimEnd()}
|
|
@@ -289,6 +312,8 @@ export type WorkerSessionFactory = (options: CreateWorkerSessionOptions) => Prom
|
|
|
289
312
|
export interface RunWorkerTaskOptions extends WorkerTaskPromptOptions, CreateWorkerSessionOptions {
|
|
290
313
|
taskTimeoutSeconds?: number;
|
|
291
314
|
gracefulShutdownSeconds?: number;
|
|
315
|
+
/** Normalized coordinator policy for resuming this operation after transient transport failure. */
|
|
316
|
+
networkRecovery?: Readonly<NetworkRecoveryConfig>;
|
|
292
317
|
abortSignal?: AbortSignal;
|
|
293
318
|
sessionFactory?: WorkerSessionFactory;
|
|
294
319
|
onEvent?: (event: CapturedWorkerEvent) => void;
|
|
@@ -353,6 +378,8 @@ export interface SessionOutcome {
|
|
|
353
378
|
timedOut: boolean;
|
|
354
379
|
aborted: boolean;
|
|
355
380
|
error?: string;
|
|
381
|
+
/** Original provider/transport failure retained for coordinator classification. */
|
|
382
|
+
failure?: unknown;
|
|
356
383
|
}
|
|
357
384
|
|
|
358
385
|
export function buildMissingTaskResultMessage(): string {
|
|
@@ -512,6 +539,7 @@ export async function runWorkerTaskAssignment(
|
|
|
512
539
|
let timedOut = false;
|
|
513
540
|
let aborted = false;
|
|
514
541
|
let error: string | undefined;
|
|
542
|
+
let failure: unknown;
|
|
515
543
|
let finished = false;
|
|
516
544
|
let turnCount = 0;
|
|
517
545
|
let messageUsageCostTotal = 0;
|
|
@@ -637,6 +665,7 @@ export async function runWorkerTaskAssignment(
|
|
|
637
665
|
},
|
|
638
666
|
(exc: unknown) => {
|
|
639
667
|
settled = true;
|
|
668
|
+
failure ??= exc;
|
|
640
669
|
error = error ?? errorMessage(exc);
|
|
641
670
|
resolvePromptWait?.();
|
|
642
671
|
resolvePromptWait = undefined;
|
|
@@ -773,6 +802,7 @@ export async function runWorkerTaskAssignment(
|
|
|
773
802
|
assistantText = latestInvocationAssistantText(session, assistantText, invocationMessageStart, !reusedAssignment);
|
|
774
803
|
}
|
|
775
804
|
} catch (exc) {
|
|
805
|
+
failure ??= exc;
|
|
776
806
|
error = error ?? errorMessage(exc);
|
|
777
807
|
} finally {
|
|
778
808
|
finished = true;
|
|
@@ -823,6 +853,7 @@ export async function runWorkerTaskAssignment(
|
|
|
823
853
|
timedOut,
|
|
824
854
|
aborted: aborted || cancelled,
|
|
825
855
|
error,
|
|
856
|
+
...(failure === undefined ? {} : { failure }),
|
|
826
857
|
};
|
|
827
858
|
}
|
|
828
859
|
|
|
@@ -872,6 +903,7 @@ export function buildWorkerSessionCreationFailureOutcome(
|
|
|
872
903
|
timedOut: false,
|
|
873
904
|
aborted: Boolean(options.abortSignal?.aborted),
|
|
874
905
|
error: message,
|
|
906
|
+
failure: error,
|
|
875
907
|
};
|
|
876
908
|
}
|
|
877
909
|
|