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.
@@ -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(?<!bash\s)(?<!max\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,
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))\s+(?:worker\s+|task\s+)?timeout\b/gi,
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(text: string, pattern: RegExp, apply: (value: number) => void): void {
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] ?? "", { allowBareSeconds: true });
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(value: string, options: { allowBareSeconds: boolean }): number | undefined {
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 <= 0) {
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 > 0 ? milliseconds : undefined;
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 {
@@ -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 currentAssistantText = "";
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.push(event);
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
- currentAssistantText = "";
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
- currentAssistantText += delta;
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
- if (finished || hasCompleteTaskResult(assistantText)) {
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(session, assistantText, invocationMessageStart, !reusedAssignment);
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(session, assistantText, invocationMessageStart, !reusedAssignment);
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(session, assistantText, invocationMessageStart, !reusedAssignment);
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 "";