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/worker_config.ts
CHANGED
|
@@ -3,11 +3,14 @@ import {
|
|
|
3
3
|
resolveNetworkRecoveryConfig,
|
|
4
4
|
type NetworkRecoveryConfigInput,
|
|
5
5
|
} from "./network_recovery_config.ts";
|
|
6
|
+
import { MAX_PLANNER_DURATION_MS, PlannerDurationConfigError } from "./planner_config.ts";
|
|
6
7
|
|
|
7
8
|
export interface ParsedWorkerRuntimeConfig {
|
|
8
9
|
modelName?: string;
|
|
9
10
|
maxAttemptsPerTask?: number;
|
|
10
11
|
taskTimeoutMs?: number;
|
|
12
|
+
todoTimeoutMs?: number;
|
|
13
|
+
todoGracefulShutdownMs?: number;
|
|
11
14
|
maxBashTimeoutMs?: number;
|
|
12
15
|
workerSessionReuseEnabled?: boolean;
|
|
13
16
|
workerSessionReuseContextThresholdPercent?: number;
|
|
@@ -52,6 +55,8 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
|
|
|
52
55
|
...(modelName ? { modelName } : {}),
|
|
53
56
|
...(state.maxAttemptsPerTask !== undefined ? { maxAttemptsPerTask: state.maxAttemptsPerTask } : {}),
|
|
54
57
|
...(state.taskTimeoutMs !== undefined ? { taskTimeoutMs: state.taskTimeoutMs } : {}),
|
|
58
|
+
...(state.todoTimeoutMs !== undefined ? { todoTimeoutMs: state.todoTimeoutMs } : {}),
|
|
59
|
+
...(state.todoGracefulShutdownMs !== undefined ? { todoGracefulShutdownMs: state.todoGracefulShutdownMs } : {}),
|
|
55
60
|
...(state.maxBashTimeoutMs !== undefined ? { maxBashTimeoutMs: state.maxBashTimeoutMs } : {}),
|
|
56
61
|
...(state.workerSessionReuseEnabled !== undefined
|
|
57
62
|
? { workerSessionReuseEnabled: state.workerSessionReuseEnabled }
|
|
@@ -135,14 +140,36 @@ function parseNaturalLanguageDirectives(text: string, state: MutableWorkerRuntim
|
|
|
135
140
|
|
|
136
141
|
captureDurations(
|
|
137
142
|
text,
|
|
138
|
-
/\b(
|
|
143
|
+
/\b(?:todo\s+)?(?:planner|planning)\s+timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
144
|
+
(value) => {
|
|
145
|
+
state.todoTimeoutMs = value;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
captureDurations(
|
|
149
|
+
text,
|
|
150
|
+
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))\s+(?:todo\s+)?(?:planner|planning)\s+timeout\b/gi,
|
|
151
|
+
(value) => {
|
|
152
|
+
state.todoTimeoutMs = value;
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
captureDurations(
|
|
156
|
+
text,
|
|
157
|
+
/\b(?:todo\s+)?(?:planner|planning)\s+(?:grace(?:ful)?(?:\s+shutdown|\s+period)?|shutdown\s+grace(?:\s+period)?)\s*(?:duration\s*)?(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
158
|
+
(value) => {
|
|
159
|
+
state.todoGracefulShutdownMs = value;
|
|
160
|
+
},
|
|
161
|
+
{ allowZero: true },
|
|
162
|
+
);
|
|
163
|
+
captureDurations(
|
|
164
|
+
text,
|
|
165
|
+
/\b(?<!bash\s)(?<!max\s)(?<!planner\s)(?<!planning\s)(?:worker\s+|task\s+)?timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
|
|
139
166
|
(value) => {
|
|
140
167
|
state.taskTimeoutMs = value;
|
|
141
168
|
},
|
|
142
169
|
);
|
|
143
170
|
captureDurations(
|
|
144
171
|
text,
|
|
145
|
-
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))\
|
|
172
|
+
/\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))[ \t]+(?:worker[ \t]+|task[ \t]+)?timeout\b/gi,
|
|
146
173
|
(value) => {
|
|
147
174
|
state.taskTimeoutMs = value;
|
|
148
175
|
},
|
|
@@ -215,6 +242,16 @@ function applyDirective(key: string, value: string, state: MutableWorkerRuntimeC
|
|
|
215
242
|
return;
|
|
216
243
|
}
|
|
217
244
|
|
|
245
|
+
if (/\b(?:planner|planning)\b/.test(key) && /\b(?:grace|graceful|shutdown)\b/.test(key)) {
|
|
246
|
+
state.todoGracefulShutdownMs = requiredPlannerDuration("graceful-shutdown duration", value, true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (/\b(?:planner|planning)\b/.test(key) && /\btimeout\b/.test(key)) {
|
|
251
|
+
state.todoTimeoutMs = requiredPlannerDuration("timeout", value, false);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
218
255
|
if (/\bbash\b/.test(key) && /\btimeout\b/.test(key)) {
|
|
219
256
|
const timeout = durationMsFromText(value, { allowBareSeconds: true });
|
|
220
257
|
if (timeout !== undefined) {
|
|
@@ -268,6 +305,23 @@ function applyNetworkRecoveryDirective(key: string, value: string, state: Mutabl
|
|
|
268
305
|
throw new NetworkRecoveryConfigError(`Unknown network recovery configuration directive: ${key}.`);
|
|
269
306
|
}
|
|
270
307
|
|
|
308
|
+
function requiredPlannerDuration(label: string, value: string, allowZero: boolean): number {
|
|
309
|
+
const trimmed = trimDirectiveValue(value)
|
|
310
|
+
.replace(/[.!]+$/g, "")
|
|
311
|
+
.trim();
|
|
312
|
+
const match = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?$/i.exec(
|
|
313
|
+
trimmed,
|
|
314
|
+
);
|
|
315
|
+
const milliseconds = match ? durationMsFromText(trimmed, { allowBareSeconds: true, allowZero }) : undefined;
|
|
316
|
+
if (milliseconds === undefined || milliseconds > MAX_PLANNER_DURATION_MS) {
|
|
317
|
+
const minimum = allowZero ? "non-negative" : "positive";
|
|
318
|
+
throw new PlannerDurationConfigError(
|
|
319
|
+
`TODO planner ${label} must be a ${minimum} finite duration no greater than about 24.9 days (${MAX_PLANNER_DURATION_MS} milliseconds), for example 30s or 5m.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return milliseconds;
|
|
323
|
+
}
|
|
324
|
+
|
|
271
325
|
function requiredNetworkRecoveryDuration(label: string, value: string): number {
|
|
272
326
|
const trimmed = trimDirectiveValue(value)
|
|
273
327
|
.replace(/[.!]+$/g, "")
|
|
@@ -307,9 +361,17 @@ function captureNumbers(text: string, pattern: RegExp, apply: (value: number) =>
|
|
|
307
361
|
}
|
|
308
362
|
}
|
|
309
363
|
|
|
310
|
-
function captureDurations(
|
|
364
|
+
function captureDurations(
|
|
365
|
+
text: string,
|
|
366
|
+
pattern: RegExp,
|
|
367
|
+
apply: (value: number) => void,
|
|
368
|
+
options: { allowZero?: boolean } = {},
|
|
369
|
+
): void {
|
|
311
370
|
for (const match of text.matchAll(pattern)) {
|
|
312
|
-
const value = durationMsFromText(match[1] ?? "", {
|
|
371
|
+
const value = durationMsFromText(match[1] ?? "", {
|
|
372
|
+
allowBareSeconds: true,
|
|
373
|
+
allowZero: options.allowZero,
|
|
374
|
+
});
|
|
313
375
|
if (value !== undefined) {
|
|
314
376
|
apply(value);
|
|
315
377
|
}
|
|
@@ -373,7 +435,10 @@ function booleanSetting(value: string): boolean | undefined {
|
|
|
373
435
|
return undefined;
|
|
374
436
|
}
|
|
375
437
|
|
|
376
|
-
function durationMsFromText(
|
|
438
|
+
function durationMsFromText(
|
|
439
|
+
value: string,
|
|
440
|
+
options: { allowBareSeconds: boolean; allowZero?: boolean },
|
|
441
|
+
): number | undefined {
|
|
377
442
|
const match = /(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?\b/i.exec(
|
|
378
443
|
value,
|
|
379
444
|
);
|
|
@@ -382,7 +447,7 @@ function durationMsFromText(value: string, options: { allowBareSeconds: boolean
|
|
|
382
447
|
}
|
|
383
448
|
|
|
384
449
|
const amount = Number.parseFloat(match[1] ?? "");
|
|
385
|
-
if (!Number.isFinite(amount) || amount
|
|
450
|
+
if (!Number.isFinite(amount) || amount < 0 || (!options.allowZero && amount === 0)) {
|
|
386
451
|
return undefined;
|
|
387
452
|
}
|
|
388
453
|
|
|
@@ -393,7 +458,9 @@ function durationMsFromText(value: string, options: { allowBareSeconds: boolean
|
|
|
393
458
|
|
|
394
459
|
const multiplier = durationMultiplier(unit || "seconds");
|
|
395
460
|
const milliseconds = Math.round(amount * multiplier);
|
|
396
|
-
return Number.isSafeInteger(milliseconds) && milliseconds
|
|
461
|
+
return Number.isSafeInteger(milliseconds) && (options.allowZero ? milliseconds >= 0 : milliseconds > 0)
|
|
462
|
+
? milliseconds
|
|
463
|
+
: undefined;
|
|
397
464
|
}
|
|
398
465
|
|
|
399
466
|
function durationMultiplier(unit: string): number {
|
package/src/worker_session.ts
CHANGED
|
@@ -113,6 +113,7 @@ Rules:
|
|
|
113
113
|
- If you need to stop because context is high or the work is blocked, leave the repository in a safe state and report \`status: partial\` or \`status: blocked\`.
|
|
114
114
|
- Use the repository's AGENTS.md/project instructions.
|
|
115
115
|
- Run focused verification commands when practical.
|
|
116
|
+
- Minimize latency: batch independent reads and searches when possible, avoid repeated repository-wide scans, and prefer focused checks over full suites unless the task requires a full suite.
|
|
116
117
|
- 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.
|
|
117
118
|
- 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.
|
|
118
119
|
|
|
@@ -253,6 +254,9 @@ export const DEFAULT_WORKER_TOOLS = ["read", "bash", "edit", "write", "grep", "f
|
|
|
253
254
|
export const DEFAULT_WORKER_THINKING_LEVEL = "high";
|
|
254
255
|
export const DEFAULT_TASK_TIMEOUT_SECONDS = 60 * 60;
|
|
255
256
|
export const DEFAULT_GRACEFUL_SHUTDOWN_SECONDS = 60;
|
|
257
|
+
/** Keep streamed diagnostics useful without returning thousands of token-delta objects. */
|
|
258
|
+
export const MAX_CAPTURED_WORKER_EVENTS = 256;
|
|
259
|
+
export const MAX_CAPTURED_STREAM_TEXT_CHARS = 4_096;
|
|
256
260
|
|
|
257
261
|
export interface WorkerSessionLike {
|
|
258
262
|
prompt(text: string, options?: Record<string, unknown>): Promise<void>;
|
|
@@ -532,7 +536,7 @@ export async function runWorkerTaskAssignment(
|
|
|
532
536
|
const compactionEvents: string[] = [];
|
|
533
537
|
const events: CapturedWorkerEvent[] = [];
|
|
534
538
|
let assistantText = "";
|
|
535
|
-
let
|
|
539
|
+
let currentAssistantTextChunks: string[] = [];
|
|
536
540
|
let sessionFile: string | undefined;
|
|
537
541
|
let sessionId: string | undefined;
|
|
538
542
|
let shutdownRequested = false;
|
|
@@ -561,7 +565,7 @@ export async function runWorkerTaskAssignment(
|
|
|
561
565
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
562
566
|
|
|
563
567
|
const capture = (event: CapturedWorkerEvent) => {
|
|
564
|
-
events
|
|
568
|
+
retainCapturedWorkerEvent(events, event);
|
|
565
569
|
options.onEvent?.(event);
|
|
566
570
|
};
|
|
567
571
|
|
|
@@ -711,7 +715,8 @@ export async function runWorkerTaskAssignment(
|
|
|
711
715
|
case "message_start": {
|
|
712
716
|
const message = event.message;
|
|
713
717
|
if (isRecord(message) && message.role === "assistant") {
|
|
714
|
-
|
|
718
|
+
currentAssistantTextChunks = [];
|
|
719
|
+
assistantText = "";
|
|
715
720
|
}
|
|
716
721
|
break;
|
|
717
722
|
}
|
|
@@ -719,8 +724,7 @@ export async function runWorkerTaskAssignment(
|
|
|
719
724
|
const assistantEvent = event.assistantMessageEvent;
|
|
720
725
|
if (isRecord(assistantEvent) && assistantEvent.type === "text_delta") {
|
|
721
726
|
const delta = typeof assistantEvent.delta === "string" ? assistantEvent.delta : "";
|
|
722
|
-
|
|
723
|
-
assistantText = currentAssistantText || assistantText;
|
|
727
|
+
if (delta) currentAssistantTextChunks.push(delta);
|
|
724
728
|
}
|
|
725
729
|
break;
|
|
726
730
|
}
|
|
@@ -728,6 +732,7 @@ export async function runWorkerTaskAssignment(
|
|
|
728
732
|
const messageText = assistantMessageText(event.message);
|
|
729
733
|
if (messageText) {
|
|
730
734
|
assistantText = messageText;
|
|
735
|
+
currentAssistantTextChunks = [messageText];
|
|
731
736
|
}
|
|
732
737
|
recordWorkerUsageCost(workerUsageCostFromEvent(event), workerUsageCostKeyFromEvent(event));
|
|
733
738
|
break;
|
|
@@ -777,7 +782,8 @@ export async function runWorkerTaskAssignment(
|
|
|
777
782
|
|
|
778
783
|
if (taskTimeoutSeconds > 0) {
|
|
779
784
|
schedule(() => {
|
|
780
|
-
|
|
785
|
+
const currentAssistantText = currentAssistantTextChunks.join("") || assistantText;
|
|
786
|
+
if (finished || hasCompleteTaskResult(currentAssistantText)) {
|
|
781
787
|
return;
|
|
782
788
|
}
|
|
783
789
|
timedOut = true;
|
|
@@ -792,14 +798,24 @@ export async function runWorkerTaskAssignment(
|
|
|
792
798
|
}
|
|
793
799
|
|
|
794
800
|
await waitForPrompt(prompt);
|
|
795
|
-
assistantText = latestInvocationAssistantText(
|
|
801
|
+
assistantText = latestInvocationAssistantText(
|
|
802
|
+
session,
|
|
803
|
+
currentAssistantTextChunks.join("") || assistantText,
|
|
804
|
+
invocationMessageStart,
|
|
805
|
+
!reusedAssignment,
|
|
806
|
+
);
|
|
796
807
|
|
|
797
808
|
if (!hasCompleteTaskResult(assistantText) && !error && !aborted && !timedOut && !options.abortSignal?.aborted) {
|
|
798
809
|
contextObservations.push(
|
|
799
810
|
"missing TASK_RESULT status after initial prompt, or required fields were incomplete; requested required block once",
|
|
800
811
|
);
|
|
801
812
|
await waitForPrompt(buildMissingTaskResultMessage());
|
|
802
|
-
assistantText = latestInvocationAssistantText(
|
|
813
|
+
assistantText = latestInvocationAssistantText(
|
|
814
|
+
session,
|
|
815
|
+
currentAssistantTextChunks.join("") || assistantText,
|
|
816
|
+
invocationMessageStart,
|
|
817
|
+
!reusedAssignment,
|
|
818
|
+
);
|
|
803
819
|
}
|
|
804
820
|
} catch (exc) {
|
|
805
821
|
failure ??= exc;
|
|
@@ -809,7 +825,12 @@ export async function runWorkerTaskAssignment(
|
|
|
809
825
|
clearTimers();
|
|
810
826
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
811
827
|
unsubscribe?.();
|
|
812
|
-
assistantText = latestInvocationAssistantText(
|
|
828
|
+
assistantText = latestInvocationAssistantText(
|
|
829
|
+
session,
|
|
830
|
+
currentAssistantTextChunks.join("") || assistantText,
|
|
831
|
+
invocationMessageStart,
|
|
832
|
+
!reusedAssignment,
|
|
833
|
+
);
|
|
813
834
|
sessionFile = session.sessionFile ?? sessionFile;
|
|
814
835
|
sessionId = session.sessionId ?? sessionId;
|
|
815
836
|
sessionStatsEnd = await workerSessionStatsSnapshot(session);
|
|
@@ -907,6 +928,23 @@ export function buildWorkerSessionCreationFailureOutcome(
|
|
|
907
928
|
};
|
|
908
929
|
}
|
|
909
930
|
|
|
931
|
+
function retainCapturedWorkerEvent(events: CapturedWorkerEvent[], event: CapturedWorkerEvent): void {
|
|
932
|
+
if (event.type === "message_update" && event.textDelta !== undefined) {
|
|
933
|
+
const previous = events.at(-1);
|
|
934
|
+
if (previous?.type === "message_update" && previous.textDelta !== undefined) {
|
|
935
|
+
previous.textDelta = `${previous.textDelta}${event.textDelta}`.slice(-MAX_CAPTURED_STREAM_TEXT_CHARS);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
events.push(event);
|
|
941
|
+
if (events.length <= MAX_CAPTURED_WORKER_EVENTS) return;
|
|
942
|
+
|
|
943
|
+
// Prefer evicting token-stream diagnostics over lifecycle/tool evidence.
|
|
944
|
+
const streamedIndex = events.findIndex((item) => item.type === "message_update");
|
|
945
|
+
events.splice(streamedIndex >= 0 ? streamedIndex : 0, 1);
|
|
946
|
+
}
|
|
947
|
+
|
|
910
948
|
function textFromContentPart(item: unknown): string {
|
|
911
949
|
if (!isRecord(item)) {
|
|
912
950
|
return "";
|