oira666_pi-subagent 0.2.11 → 0.2.13
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/index.ts +30 -27
- package/package.json +1 -1
- package/render.ts +3 -2
- package/resume.ts +15 -21
- package/runner.ts +80 -16
- package/shared.ts +11 -0
package/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { runAgentSubprocess, executeParallelSubprocess } from "./runner.js";
|
|
|
20
20
|
import {
|
|
21
21
|
SUBAGENT_RESUME_DISABLE_ENV,
|
|
22
22
|
SUBAGENT_RESUME_PROMPT_ENV,
|
|
23
|
+
branchEntries,
|
|
23
24
|
buildSubagentSessionDir,
|
|
24
25
|
findLatestResumableSubagentCall,
|
|
25
26
|
getDefaultSubagentSessionRoot,
|
|
@@ -30,7 +31,10 @@ import {
|
|
|
30
31
|
} from "./resume.js";
|
|
31
32
|
import {
|
|
32
33
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
34
|
+
RESUME_MODEL_ID,
|
|
35
|
+
RESUME_PROVIDER,
|
|
33
36
|
SUBAGENT_MAX_PARALLEL_TASKS_ENV,
|
|
37
|
+
parseBoolean,
|
|
34
38
|
parseNonNegativeInt,
|
|
35
39
|
} from "./shared.js";
|
|
36
40
|
|
|
@@ -97,15 +101,6 @@ interface DelegationDepthConfig {
|
|
|
97
101
|
preventCycles: boolean;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
function parseBoolean(raw: unknown): boolean | null {
|
|
101
|
-
if (typeof raw === "boolean") return raw;
|
|
102
|
-
if (typeof raw !== "string") return null;
|
|
103
|
-
const normalized = raw.trim().toLowerCase();
|
|
104
|
-
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
105
|
-
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
104
|
function parseProjectAgentConfirmationSetting(
|
|
110
105
|
raw: unknown,
|
|
111
106
|
): ProjectAgentConfirmationSetting | null {
|
|
@@ -369,8 +364,6 @@ function hasCliInitialPrompt(argv: string[]): boolean {
|
|
|
369
364
|
return false;
|
|
370
365
|
}
|
|
371
366
|
|
|
372
|
-
const RESUME_PROVIDER = "pi-subagent-resume";
|
|
373
|
-
const RESUME_MODEL_ID = "synthetic-tool-call";
|
|
374
367
|
const RESUME_STATE_KEY = "__piSubagentResumeState";
|
|
375
368
|
const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
|
|
376
369
|
const RESUME_INTERACTIVE_DELAY_MS = 50;
|
|
@@ -428,15 +421,7 @@ function formatModelFlag(model: any): string | undefined {
|
|
|
428
421
|
}
|
|
429
422
|
|
|
430
423
|
function findLastNonResumeModel(ctx: any): any | undefined {
|
|
431
|
-
const entries = (
|
|
432
|
-
const leafId = ctx.sessionManager?.getLeafId?.();
|
|
433
|
-
if (leafId) {
|
|
434
|
-
const branch = ctx.sessionManager?.getBranch?.(leafId);
|
|
435
|
-
if (Array.isArray(branch)) return branch;
|
|
436
|
-
}
|
|
437
|
-
const all = ctx.sessionManager?.getEntries?.();
|
|
438
|
-
return Array.isArray(all) ? all : [];
|
|
439
|
-
})();
|
|
424
|
+
const entries = branchEntries(ctx);
|
|
440
425
|
|
|
441
426
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
442
427
|
const entry = entries[i];
|
|
@@ -662,7 +647,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
662
647
|
`The resumed session has an unfinished subagent call (${plan.tasks.length} task${plan.tasks.length === 1 ? "" : "s"}). Resume it from saved subagent sessions?`,
|
|
663
648
|
);
|
|
664
649
|
}
|
|
665
|
-
if (!shouldResume)
|
|
650
|
+
if (!shouldResume) {
|
|
651
|
+
if (ctx.model?.provider === RESUME_PROVIDER) {
|
|
652
|
+
if (restorableModel) {
|
|
653
|
+
await pi.setModel(restorableModel);
|
|
654
|
+
} else {
|
|
655
|
+
ctx.ui.notify(
|
|
656
|
+
`Subagent resume was declined, but the current model is the synthetic resume model and no real fallback model is available. Select a real model before continuing, or set ${SUBAGENT_FALLBACK_MODEL_ENV}=provider/model.`,
|
|
657
|
+
"error",
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!restorableModel && ctx.model?.provider === RESUME_PROVIDER) {
|
|
665
|
+
ctx.ui.notify(
|
|
666
|
+
`Cannot resume subagents while on the synthetic resume model because no real fallback model is available. Select a real model or set ${SUBAGENT_FALLBACK_MODEL_ENV}=provider/model.`,
|
|
667
|
+
"error",
|
|
668
|
+
);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
666
671
|
|
|
667
672
|
pendingResumePlan = plan;
|
|
668
673
|
const resumeState = getSyntheticResumeState();
|
|
@@ -947,12 +952,10 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
947
952
|
}
|
|
948
953
|
|
|
949
954
|
function getSessionDirForTask(toolCallId: string, index: number): string {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
function pathlessSubagentRootFallback(): string {
|
|
955
|
-
return `${process.env.HOME ?? "."}/.pi/agent/sessions-subagents`;
|
|
955
|
+
if (!currentSubagentSessionRoot) {
|
|
956
|
+
throw new Error("Cannot create subagent session dir: subagent session root is not initialized.");
|
|
957
|
+
}
|
|
958
|
+
return buildSubagentSessionDir(currentSubagentSessionRoot, currentSessionId, toolCallId, index);
|
|
956
959
|
}
|
|
957
960
|
|
|
958
961
|
// -----------------------------------------------------------------------
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -44,7 +44,7 @@ interface TreeCounts {
|
|
|
44
44
|
|
|
45
45
|
interface PendingSubagentCall {
|
|
46
46
|
toolCallId: string;
|
|
47
|
-
tasks: Array<{ agent: string; task?: string }>;
|
|
47
|
+
tasks: Array<{ agent: string; task?: string; cwd?: string }>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
@@ -151,6 +151,7 @@ function extractPendingSubagentCalls(messages: SingleResult["messages"]): Pendin
|
|
|
151
151
|
.map((task: any) => ({
|
|
152
152
|
agent: task.agent,
|
|
153
153
|
task: typeof task.task === "string" ? task.task : undefined,
|
|
154
|
+
cwd: typeof task.cwd === "string" ? task.cwd : undefined,
|
|
154
155
|
}))
|
|
155
156
|
: [];
|
|
156
157
|
calls.push({
|
|
@@ -181,7 +182,7 @@ function buildNodesFromNestedResult(nested: NestedSubagentResult): TreeNode[] {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
function subagentCallSignature(call: PendingSubagentCall): string {
|
|
184
|
-
return JSON.stringify(call.tasks.map((task) => ({ agent: task.agent, task: task.task ?? "" })));
|
|
185
|
+
return JSON.stringify(call.tasks.map((task) => ({ agent: task.agent, task: task.task ?? "", cwd: task.cwd ?? "" })));
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
function nestedResultIsHealthy(nested: NestedSubagentResult | undefined): boolean {
|
package/resume.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
1
|
import * as path from "node:path";
|
|
4
2
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { parseBoolean } from "./shared.js";
|
|
5
4
|
import { isResultError, isSubagentDetails, type SingleResult, type SubagentDetails } from "./types.js";
|
|
6
5
|
|
|
7
6
|
export const SUBAGENT_RESUME_PROMPT_ENV = "PI_SUBAGENT_RESUME_PROMPT";
|
|
@@ -16,14 +15,7 @@ export interface ResumableSubagentCall {
|
|
|
16
15
|
details?: SubagentDetails;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
export
|
|
20
|
-
if (typeof raw === "boolean") return raw;
|
|
21
|
-
if (typeof raw !== "string") return null;
|
|
22
|
-
const normalized = raw.trim().toLowerCase();
|
|
23
|
-
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
24
|
-
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
18
|
+
export const parseBooleanEnv = parseBoolean;
|
|
27
19
|
|
|
28
20
|
export function getDefaultSubagentSessionRoot(ctx: ExtensionContext): string {
|
|
29
21
|
const inheritedRoot = process.env[SUBAGENT_SESSION_ROOT_ENV];
|
|
@@ -33,7 +25,8 @@ export function getDefaultSubagentSessionRoot(ctx: ExtensionContext): string {
|
|
|
33
25
|
if (typeof mainSessionDir === "string" && mainSessionDir.length > 0) {
|
|
34
26
|
return path.join(path.dirname(mainSessionDir), "sessions-subagents");
|
|
35
27
|
}
|
|
36
|
-
|
|
28
|
+
|
|
29
|
+
throw new Error("Cannot determine subagent session root: sessionManager.getSessionDir() is unavailable.");
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
export function buildSubagentSessionDir(
|
|
@@ -47,11 +40,7 @@ export function buildSubagentSessionDir(
|
|
|
47
40
|
return path.join(root, safeParent, safeTool, String(index));
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
export function
|
|
51
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function branchEntries(ctx: ExtensionContext): SessionEntry[] {
|
|
43
|
+
export function branchEntries(ctx: ExtensionContext): SessionEntry[] {
|
|
55
44
|
const leafId = ctx.sessionManager.getLeafId?.();
|
|
56
45
|
if (leafId) {
|
|
57
46
|
const branch = ctx.sessionManager.getBranch?.(leafId);
|
|
@@ -87,9 +76,10 @@ function normalizeTasks(args: any): Array<{ agent: string; task: string; cwd?: s
|
|
|
87
76
|
return tasks;
|
|
88
77
|
}
|
|
89
78
|
|
|
90
|
-
function hasUnfinishedResults(details: SubagentDetails | undefined): boolean {
|
|
79
|
+
function hasUnfinishedResults(details: SubagentDetails | undefined, expectedTaskCount: number): boolean {
|
|
91
80
|
if (!details) return true;
|
|
92
|
-
|
|
81
|
+
if (details.results.length < expectedTaskCount) return true;
|
|
82
|
+
return details.results.slice(0, expectedTaskCount).some((result) => result.exitCode === -1 || isResultError(result));
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
function messageHasNonEmptyText(message: any): boolean {
|
|
@@ -155,7 +145,7 @@ export function findLatestResumableSubagentCall(ctx: ExtensionContext): Resumabl
|
|
|
155
145
|
const candidates: Array<ResumableSubagentCall & { activityOrder: number }> = [];
|
|
156
146
|
for (const [toolCallId, call] of calls) {
|
|
157
147
|
const result = results.get(toolCallId);
|
|
158
|
-
const unfinished = !result || result.isError || hasUnfinishedResults(result.details);
|
|
148
|
+
const unfinished = !result || result.isError || hasUnfinishedResults(result.details, call.tasks.length);
|
|
159
149
|
if (!unfinished) continue;
|
|
160
150
|
candidates.push({
|
|
161
151
|
previousToolCallId: toolCallId,
|
|
@@ -165,7 +155,7 @@ export function findLatestResumableSubagentCall(ctx: ExtensionContext): Resumabl
|
|
|
165
155
|
});
|
|
166
156
|
}
|
|
167
157
|
|
|
168
|
-
const latest = candidates.at(-1);
|
|
158
|
+
const latest = candidates.sort((a, b) => a.activityOrder - b.activityOrder).at(-1);
|
|
169
159
|
if (!latest || !hasOnlyIgnorableTrailingEntries(entries, latest.activityOrder)) return null;
|
|
170
160
|
return latest;
|
|
171
161
|
}
|
|
@@ -174,7 +164,11 @@ export function sameTasks(
|
|
|
174
164
|
a: Array<{ agent: string; task: string; cwd?: string }>,
|
|
175
165
|
b: Array<{ agent: string; task: string; cwd?: string }>,
|
|
176
166
|
): boolean {
|
|
177
|
-
|
|
167
|
+
if (a.length !== b.length) return false;
|
|
168
|
+
return a.every((task, index) => {
|
|
169
|
+
const other = b[index];
|
|
170
|
+
return task.agent === other.agent && task.task === other.task && (task.cwd ?? undefined) === (other.cwd ?? undefined);
|
|
171
|
+
});
|
|
178
172
|
}
|
|
179
173
|
|
|
180
174
|
export function isFinishedResult(result: SingleResult | undefined): boolean {
|
package/runner.ts
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
27
27
|
DEFAULT_MAX_CONCURRENCY,
|
|
28
28
|
PARALLEL_HEARTBEAT_MS,
|
|
29
|
+
RESUME_MODEL_ID,
|
|
30
|
+
RESUME_PROVIDER,
|
|
29
31
|
SUBAGENT_MAX_PARALLEL_TASKS_ENV,
|
|
30
32
|
SUBAGENT_MAX_CONCURRENCY_ENV,
|
|
31
33
|
parseNonNegativeInt,
|
|
@@ -49,23 +51,20 @@ function isTerminalStopReason(reason: string | undefined): boolean {
|
|
|
49
51
|
return reason !== undefined && TERMINAL_STOP_REASONS.has(reason);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
function
|
|
54
|
+
function endedWithSyntheticResumeFailure(messages: Message[]): boolean {
|
|
53
55
|
const lastAssistant = [...messages].reverse().find((message: any) => message?.role === "assistant") as any;
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
lastAssistant.content.length === 0
|
|
60
|
-
);
|
|
56
|
+
if (lastAssistant?.provider !== RESUME_PROVIDER || lastAssistant?.model !== RESUME_MODEL_ID) return false;
|
|
57
|
+
const content = Array.isArray(lastAssistant.content) ? lastAssistant.content : [];
|
|
58
|
+
const handedOffToRealModel = messages.some((message: any) => message?.role === "assistant" && message.provider !== RESUME_PROVIDER);
|
|
59
|
+
const hasToolCall = content.some((part: any) => part?.type === "toolCall");
|
|
60
|
+
return !handedOffToRealModel && !hasToolCall;
|
|
61
61
|
}
|
|
62
62
|
const SUBAGENT_DEPTH_ENV = "PI_SUBAGENT_DEPTH";
|
|
63
63
|
const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
64
64
|
const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
65
65
|
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
66
66
|
const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
|
|
67
|
-
|
|
68
|
-
const RESUME_MODEL_ID = "synthetic-tool-call";
|
|
67
|
+
|
|
69
68
|
// PI_OFFLINE intentionally removed: setting it on child processes blocks all API
|
|
70
69
|
// calls and renders subagents unable to do any LLM work. Children inherit the
|
|
71
70
|
// parent's PI_OFFLINE value via process.env spread if needed.
|
|
@@ -292,6 +291,40 @@ function pushLiveLog(result: SingleResult, entry: LiveLogEntry): void {
|
|
|
292
291
|
if (result.liveLog.length > MAX_LIVE_LOG_ENTRIES) result.liveLog.shift();
|
|
293
292
|
}
|
|
294
293
|
|
|
294
|
+
function messageDedupKey(message: Message): string {
|
|
295
|
+
const anyMessage = message as any;
|
|
296
|
+
if (typeof anyMessage.id === "string") return `id:${anyMessage.id}`;
|
|
297
|
+
return JSON.stringify({
|
|
298
|
+
role: anyMessage.role,
|
|
299
|
+
provider: anyMessage.provider,
|
|
300
|
+
model: anyMessage.model,
|
|
301
|
+
stopReason: anyMessage.stopReason,
|
|
302
|
+
toolCallId: anyMessage.toolCallId,
|
|
303
|
+
toolName: anyMessage.toolName,
|
|
304
|
+
content: anyMessage.content,
|
|
305
|
+
usage: anyMessage.usage,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function hasMessage(result: SingleResult, message: Message): boolean {
|
|
310
|
+
const key = messageDedupKey(message);
|
|
311
|
+
return result.messages.some((existing) => messageDedupKey(existing) === key);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function resultHasStarted(result: SingleResult | undefined): boolean {
|
|
315
|
+
if (!result) return false;
|
|
316
|
+
return result.messages.length > 0 || result.completedTurns > 0 || result.liveLog.length > 0 || Object.keys(result.toolCalls).length > 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function sessionDirExists(dir: string | undefined): boolean {
|
|
320
|
+
if (!dir) return false;
|
|
321
|
+
try {
|
|
322
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
295
328
|
export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
296
329
|
if (!line.trim()) return false;
|
|
297
330
|
|
|
@@ -307,6 +340,7 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
307
340
|
|
|
308
341
|
if (event.type === "message_end" && event.message) {
|
|
309
342
|
const msg = event.message as Message;
|
|
343
|
+
if (hasMessage(result, msg)) return true;
|
|
310
344
|
result.messages.push(msg);
|
|
311
345
|
|
|
312
346
|
if (msg.role === "assistant") {
|
|
@@ -328,7 +362,8 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
328
362
|
}
|
|
329
363
|
|
|
330
364
|
if (event.type === "tool_result_end" && event.message) {
|
|
331
|
-
|
|
365
|
+
const msg = event.message as Message;
|
|
366
|
+
if (!hasMessage(result, msg)) result.messages.push(msg);
|
|
332
367
|
return true;
|
|
333
368
|
}
|
|
334
369
|
|
|
@@ -515,6 +550,31 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
515
550
|
};
|
|
516
551
|
}
|
|
517
552
|
|
|
553
|
+
let shouldContinueSession = resumeSession;
|
|
554
|
+
if (resumeSession && sessionDir && !sessionDirExists(sessionDir)) {
|
|
555
|
+
if (resultHasStarted(initialResult)) {
|
|
556
|
+
const errorMessage = `Cannot resume subagent session: session directory does not exist: ${sessionDir}`;
|
|
557
|
+
return {
|
|
558
|
+
agent: agentName,
|
|
559
|
+
agentSource: agent.source,
|
|
560
|
+
task,
|
|
561
|
+
exitCode: 1,
|
|
562
|
+
messages: initialResult?.messages ? [...initialResult.messages] : [],
|
|
563
|
+
stderr: errorMessage,
|
|
564
|
+
usage: initialResult?.usage ? { ...initialResult.usage } : emptyUsage(),
|
|
565
|
+
toolCalls: initialResult?.toolCalls ? { ...initialResult.toolCalls } : {},
|
|
566
|
+
model: initialResult?.model ?? agent.model,
|
|
567
|
+
stopReason: "error",
|
|
568
|
+
errorMessage,
|
|
569
|
+
completedTurns: initialResult?.completedTurns ?? 0,
|
|
570
|
+
turnInProgress: false,
|
|
571
|
+
liveLog: initialResult?.liveLog ? [...initialResult.liveLog] : [],
|
|
572
|
+
sessionDir,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
shouldContinueSession = false;
|
|
576
|
+
}
|
|
577
|
+
|
|
518
578
|
const result: SingleResult = {
|
|
519
579
|
agent: agentName,
|
|
520
580
|
agentSource: agent.source,
|
|
@@ -561,7 +621,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
561
621
|
promptTmpPath,
|
|
562
622
|
task,
|
|
563
623
|
sessionDir,
|
|
564
|
-
|
|
624
|
+
shouldContinueSession,
|
|
565
625
|
fallbackModel,
|
|
566
626
|
);
|
|
567
627
|
let wasAborted = false;
|
|
@@ -743,7 +803,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
743
803
|
if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
|
|
744
804
|
}
|
|
745
805
|
|
|
746
|
-
if (result.exitCode === 0 &&
|
|
806
|
+
if (result.exitCode === 0 && endedWithSyntheticResumeFailure(result.messages)) {
|
|
747
807
|
result.exitCode = 1;
|
|
748
808
|
result.stopReason = "error";
|
|
749
809
|
result.errorMessage = "Subagent resume failed before the real model continued.";
|
|
@@ -841,7 +901,6 @@ export async function executeParallelSubprocess(
|
|
|
841
901
|
completedTurns: 0,
|
|
842
902
|
turnInProgress: false,
|
|
843
903
|
liveLog: [],
|
|
844
|
-
sessionDir: getSessionDir?.(index, t),
|
|
845
904
|
}));
|
|
846
905
|
|
|
847
906
|
const emitProgress = () => {
|
|
@@ -876,7 +935,12 @@ export async function executeParallelSubprocess(
|
|
|
876
935
|
emitProgress();
|
|
877
936
|
return previousResult;
|
|
878
937
|
}
|
|
879
|
-
const
|
|
938
|
+
const savedSessionDir = previousResult?.sessionDir;
|
|
939
|
+
const savedSessionDirExists = sessionDirExists(savedSessionDir);
|
|
940
|
+
const shouldResumeThisSession = resumeExistingSessions && (!previousResult || !savedSessionDir || savedSessionDirExists);
|
|
941
|
+
const sessionDir = shouldResumeThisSession && savedSessionDirExists
|
|
942
|
+
? savedSessionDir
|
|
943
|
+
: getSessionDir?.(index, t);
|
|
880
944
|
const result = await runAgentSubprocess({
|
|
881
945
|
cwd: defaultCwd,
|
|
882
946
|
agents,
|
|
@@ -890,7 +954,7 @@ export async function executeParallelSubprocess(
|
|
|
890
954
|
signal,
|
|
891
955
|
sessionDir,
|
|
892
956
|
sessionRoot,
|
|
893
|
-
resumeSession:
|
|
957
|
+
resumeSession: shouldResumeThisSession && !!sessionDir,
|
|
894
958
|
initialResult: previousResult,
|
|
895
959
|
fallbackModel,
|
|
896
960
|
onUpdate: (partial) => {
|
package/shared.ts
CHANGED
|
@@ -13,11 +13,22 @@ export const DEFAULT_MAX_CONCURRENCY = 8;
|
|
|
13
13
|
export const PARALLEL_HEARTBEAT_MS = 1000;
|
|
14
14
|
export const SUBAGENT_MAX_PARALLEL_TASKS_ENV = "PI_SUBAGENT_MAX_PARALLEL_TASKS";
|
|
15
15
|
export const SUBAGENT_MAX_CONCURRENCY_ENV = "PI_SUBAGENT_MAX_CONCURRENCY";
|
|
16
|
+
export const RESUME_PROVIDER = "pi-subagent-resume";
|
|
17
|
+
export const RESUME_MODEL_ID = "synthetic-tool-call";
|
|
16
18
|
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
// Shared helpers
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
|
|
23
|
+
export function parseBoolean(raw: unknown): boolean | null {
|
|
24
|
+
if (typeof raw === "boolean") return raw;
|
|
25
|
+
if (typeof raw !== "string") return null;
|
|
26
|
+
const normalized = raw.trim().toLowerCase();
|
|
27
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
28
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
/** Parse a string into a non-negative safe integer, or null on failure. */
|
|
22
33
|
export function parseNonNegativeInt(raw: unknown): number | null {
|
|
23
34
|
if (typeof raw !== "string") return null;
|