@ferris1225/pi-subagents 4.1.1 → 4.1.3
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/README.md +461 -381
- package/agents/cleaner.md +16 -6
- package/agents/documenter.md +46 -0
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +8 -4
- package/agents/worker.md +9 -4
- package/package.json +55 -55
- package/src/agents.ts +53 -0
- package/src/announcements.ts +18 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +43 -13
- package/src/dispatch.ts +233 -303
- package/src/fixloop.ts +259 -62
- package/src/index.ts +3 -3
- package/src/models.ts +189 -189
- package/src/monitor.ts +101 -22
- package/src/prompt.ts +47 -12
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +29 -10
- package/src/runtime.ts +13 -7
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +162 -130
- package/src/spawn.ts +53 -13
- package/src/thread-lifecycle.ts +240 -54
- package/src/tools.ts +65 -37
- package/src/widget.ts +68 -22
- package/src/worktree.ts +27 -4
package/src/spawn.ts
CHANGED
|
@@ -49,8 +49,23 @@ export type { SubagentLiveEvent, UsageStats };
|
|
|
49
49
|
export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
50
50
|
/** 0 disables the watchdog; dispatch supplies the configured timeout. */
|
|
51
51
|
export const SUBAGENT_DEFAULT_IDLE_TIMEOUT_MS = 0;
|
|
52
|
-
|
|
52
|
+
/** Base delays cover Pi's stale-lock window and leave headroom beyond the
|
|
53
|
+
* default four-way launch fan-out. Additive jitter below reduces the chance
|
|
54
|
+
* that contenders retry in the same lockstep waves. */
|
|
55
|
+
export const SUBAGENT_STARTUP_RETRY_DELAYS_MS = [250, 750, 1500, 3000, 6000] as const;
|
|
53
56
|
export const MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS = 2000;
|
|
57
|
+
export const MAX_SUBAGENT_STARTUP_RETRY_JITTER_MS = 1000;
|
|
58
|
+
|
|
59
|
+
function normalizeStartupRetryDelay(delayMs: number): number {
|
|
60
|
+
return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function addStartupRetryJitter(delayMs: number, randomValue = Math.random()): number {
|
|
64
|
+
const baseDelay = Math.floor(normalizeStartupRetryDelay(delayMs));
|
|
65
|
+
if (baseDelay === 0) return 0;
|
|
66
|
+
const boundedRandom = Number.isFinite(randomValue) ? Math.max(0, Math.min(1, randomValue)) : 0;
|
|
67
|
+
return baseDelay + Math.floor(Math.min(baseDelay, MAX_SUBAGENT_STARTUP_RETRY_JITTER_MS) * boundedRandom);
|
|
68
|
+
}
|
|
54
69
|
|
|
55
70
|
export interface SingleResult extends RpcSingleResult {}
|
|
56
71
|
|
|
@@ -204,10 +219,17 @@ export function isModelLevelFailure(result: SingleResult): boolean {
|
|
|
204
219
|
if (result.stopReason === "aborted") return false;
|
|
205
220
|
if (result.dispatchFailed) return false;
|
|
206
221
|
if (result.rpcStartupFailed) return false;
|
|
222
|
+
// A negative RPC response proves Pi rejected the prompt before execution. It
|
|
223
|
+
// remains safe to hand off even though dispatch was attempted; local write,
|
|
224
|
+
// close, timeout, and lost-ACK failures never set this explicit flag.
|
|
225
|
+
if (result.rpcPromptRejected) return true;
|
|
226
|
+
// The prompt write completed but its ACK never arrived. With no later activity
|
|
227
|
+
// we cannot know whether Pi started the model or tools, so neither startup
|
|
228
|
+
// retry nor selected→main fallback may replay this objective.
|
|
229
|
+
if (result.rpcPromptDispatched && !result.rpcPromptAccepted && !result.rpcActivity) return false;
|
|
207
230
|
if (isRpcCommandTimeoutError(result.errorMessage)) return false;
|
|
208
231
|
if (result.integrationStatus === "retained") return false;
|
|
209
232
|
if (result.errorMessage?.includes("idle timeout")) return true;
|
|
210
|
-
if (result.rpcPromptRejected) return true;
|
|
211
233
|
|
|
212
234
|
// Classification belongs to the final assistant turn, not the whole attempt.
|
|
213
235
|
// Earlier useful text or failed tool calls are retained session history and
|
|
@@ -236,7 +258,7 @@ export function isRetryableStartupFailure(result: SingleResult, durationMs: numb
|
|
|
236
258
|
if (result.exitCode === 0) return false;
|
|
237
259
|
if (result.stopReason === "aborted") return false;
|
|
238
260
|
if (result.dispatchFailed) return false;
|
|
239
|
-
if (result.rpcPromptAccepted || result.rpcActivity) return false;
|
|
261
|
+
if (result.rpcPromptDispatched || result.rpcPromptAccepted || result.rpcActivity) return false;
|
|
240
262
|
if (result.errorMessage?.includes("idle timeout")) return false;
|
|
241
263
|
if (getFinalOutput(result.messages)) return false;
|
|
242
264
|
if (result.messages.length > 0) return false;
|
|
@@ -250,14 +272,15 @@ export function isRetryableStartupFailure(result: SingleResult, durationMs: numb
|
|
|
250
272
|
}
|
|
251
273
|
|
|
252
274
|
export function formatStartupRetryExhaustedError(model: string, attempts: number): string {
|
|
253
|
-
return `Subagent failed to start after ${attempts} attempt${attempts === 1 ? "" : "s"} on ${model}: the child
|
|
275
|
+
return `Subagent failed to start after ${attempts} attempt${attempts === 1 ? "" : "s"} on ${model}: the child failed before its initial RPC prompt was dispatched and produced no model, tool, output, or usage activity. This is typically a concurrent pi startup race (several sub-agents starting at once). Retry the dispatch, or temporarily lower maxConcurrency in /subagents-setup.`;
|
|
254
276
|
}
|
|
255
277
|
|
|
256
278
|
export async function waitForStartupRetry(delayMs: number, signal?: AbortSignal): Promise<boolean> {
|
|
257
|
-
|
|
279
|
+
const normalizedDelay = normalizeStartupRetryDelay(delayMs);
|
|
280
|
+
if (normalizedDelay === 0) return !signal?.aborted;
|
|
258
281
|
if (!signal) {
|
|
259
282
|
return new Promise<boolean>((resolve) => {
|
|
260
|
-
const timer = setTimeout(() => resolve(true),
|
|
283
|
+
const timer = setTimeout(() => resolve(true), normalizedDelay);
|
|
261
284
|
if (typeof timer.unref === "function") timer.unref();
|
|
262
285
|
});
|
|
263
286
|
}
|
|
@@ -272,7 +295,7 @@ export async function waitForStartupRetry(delayMs: number, signal?: AbortSignal)
|
|
|
272
295
|
resolve(shouldRetry);
|
|
273
296
|
};
|
|
274
297
|
const onAbort = (): void => finish(false);
|
|
275
|
-
const timer = setTimeout(() => finish(true),
|
|
298
|
+
const timer = setTimeout(() => finish(true), normalizedDelay);
|
|
276
299
|
if (typeof timer.unref === "function") timer.unref();
|
|
277
300
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
278
301
|
});
|
|
@@ -283,7 +306,7 @@ async function waitForControlledRetry(
|
|
|
283
306
|
signal: AbortSignal | undefined,
|
|
284
307
|
control: RpcRunControl | undefined,
|
|
285
308
|
): Promise<boolean> {
|
|
286
|
-
let remaining = delayMs;
|
|
309
|
+
let remaining = normalizeStartupRetryDelay(delayMs);
|
|
287
310
|
while (remaining > 0) {
|
|
288
311
|
if (control?.isParkRequested() || control?.isStopRequested()) return false;
|
|
289
312
|
const slice = Math.min(remaining, 50);
|
|
@@ -304,7 +327,7 @@ export function getResultOutput(result: SingleResult): string {
|
|
|
304
327
|
}
|
|
305
328
|
|
|
306
329
|
export function buildResumePrompt(task: string, reason: string): string {
|
|
307
|
-
return `You are resuming an earlier sub-agent session after ${reason}. Your earlier work — searches, reads, edits, and reasoning — is preserved in this session's history above; review it before acting.
|
|
330
|
+
return `You are resuming an earlier sub-agent session after ${reason}. Your earlier work — searches, reads, edits, and reasoning — is preserved in this session's history above; review it before acting. Current objective: ${task}. Pick up exactly where you left off and finish it. Do NOT redo searches, reads, or edits you already completed unless a step clearly failed. Continue now.`;
|
|
308
331
|
}
|
|
309
332
|
|
|
310
333
|
export function buildFallbackResumeReason(fromModel?: string): string {
|
|
@@ -328,6 +351,9 @@ export interface RunSingleOptions {
|
|
|
328
351
|
sessionId?: string;
|
|
329
352
|
/** Initial RPC prompt. Kept under the old name to limit caller churn. */
|
|
330
353
|
stdinText?: string;
|
|
354
|
+
/** Refresh parent-derived tools immediately before every startup retry and
|
|
355
|
+
* selected-to-main fallback process is spawned. */
|
|
356
|
+
resolveAgentForAttempt?: (agent: AgentConfig) => AgentConfig;
|
|
331
357
|
signal?: AbortSignal;
|
|
332
358
|
onLive?: (event: SubagentLiveEvent) => void;
|
|
333
359
|
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
@@ -368,6 +394,15 @@ function controlledDisposition(options: RunSingleOptions, base?: SingleResult):
|
|
|
368
394
|
return result;
|
|
369
395
|
}
|
|
370
396
|
|
|
397
|
+
function signalAbortDisposition(options: RunSingleOptions, base: SingleResult): SingleResult | undefined {
|
|
398
|
+
if (!options.signal?.aborted) return undefined;
|
|
399
|
+
base.parked = undefined;
|
|
400
|
+
base.exitCode = 1;
|
|
401
|
+
base.stopReason = "aborted";
|
|
402
|
+
base.errorMessage = "Subagent was aborted";
|
|
403
|
+
return base;
|
|
404
|
+
}
|
|
405
|
+
|
|
371
406
|
/** Spawn one RPC attempt and wait for stable settlement. */
|
|
372
407
|
export async function runSingleAgent(options: RunSingleOptions): Promise<SingleResult> {
|
|
373
408
|
const {
|
|
@@ -420,7 +455,8 @@ export async function runSingleAgentWithMainFallback(
|
|
|
420
455
|
): Promise<SingleResult> {
|
|
421
456
|
const agent = options.agent;
|
|
422
457
|
const launchedRef = agent?.model;
|
|
423
|
-
const
|
|
458
|
+
const customStartupDelays = options.startupRetryDelaysMs;
|
|
459
|
+
const startupDelays = customStartupDelays ?? SUBAGENT_STARTUP_RETRY_DELAYS_MS;
|
|
424
460
|
|
|
425
461
|
const sessionId = options.sessionId ?? randomUUID();
|
|
426
462
|
const sessionDir = options.sessionDir ?? (await mkdtemp(join(tmpdir(), "pi-subagent-session-")));
|
|
@@ -463,7 +499,10 @@ export async function runSingleAgentWithMainFallback(
|
|
|
463
499
|
}
|
|
464
500
|
const start = Date.now();
|
|
465
501
|
try {
|
|
466
|
-
|
|
502
|
+
const attemptOptions = opts.resolveAgentForAttempt
|
|
503
|
+
? { ...opts, agent: opts.resolveAgentForAttempt(opts.agent) }
|
|
504
|
+
: opts;
|
|
505
|
+
lastResult = await runSingleAgent(attemptOptions);
|
|
467
506
|
} catch (error) {
|
|
468
507
|
const failed = await dispatchFailure(error);
|
|
469
508
|
return controlledDisposition(opts, failed) ?? failed;
|
|
@@ -492,8 +531,9 @@ export async function runSingleAgentWithMainFallback(
|
|
|
492
531
|
} catch {
|
|
493
532
|
/* never throw from event handling */
|
|
494
533
|
}
|
|
495
|
-
|
|
496
|
-
|
|
534
|
+
const retryDelay = customStartupDelays ? delay : addStartupRetryJitter(delay);
|
|
535
|
+
if (!(await waitForControlledRetry(retryDelay, opts.signal, opts.control))) {
|
|
536
|
+
return controlledDisposition(opts, lastResult) ?? signalAbortDisposition(opts, lastResult) ?? lastResult;
|
|
497
537
|
}
|
|
498
538
|
retries++;
|
|
499
539
|
}
|