oira666_pi-subagent 0.2.10 → 0.2.12
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 +52 -42
- package/package.json +1 -1
- package/render.ts +3 -2
- package/resume.ts +15 -21
- package/runner.ts +54 -12
- 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,11 +364,9 @@ 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
|
-
const RESUME_INTERACTIVE_DELAY_MS =
|
|
369
|
+
const RESUME_INTERACTIVE_DELAY_MS = 50;
|
|
377
370
|
|
|
378
371
|
type SyntheticResumeState = {
|
|
379
372
|
plan: ResumableSubagentCall | null;
|
|
@@ -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];
|
|
@@ -472,6 +457,7 @@ function getRestorableModel(ctx: any): any | undefined {
|
|
|
472
457
|
export default function (pi: ExtensionAPI) {
|
|
473
458
|
let resumeModelRegistry: any | undefined;
|
|
474
459
|
let lastRestorableModel: any | undefined;
|
|
460
|
+
let pendingInteractiveResumePrompt: string | null = null;
|
|
475
461
|
|
|
476
462
|
async function streamWithRealModelFallback(context: any, options: any, fallback: any) {
|
|
477
463
|
if (!fallback) return null;
|
|
@@ -661,7 +647,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
661
647
|
`The resumed session has an unfinished subagent call (${plan.tasks.length} task${plan.tasks.length === 1 ? "" : "s"}). Resume it from saved subagent sessions?`,
|
|
662
648
|
);
|
|
663
649
|
}
|
|
664
|
-
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
|
+
}
|
|
665
671
|
|
|
666
672
|
pendingResumePlan = plan;
|
|
667
673
|
const resumeState = getSyntheticResumeState();
|
|
@@ -685,20 +691,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
685
691
|
if (hasCliInitialPrompt(process.argv)) {
|
|
686
692
|
if (ctx.hasUI) ctx.ui.notify(`Resuming ${plan.tasks.length} subagents...`, "info");
|
|
687
693
|
} else {
|
|
688
|
-
// Do not start the synthetic resume turn
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
setTimeout(() => {
|
|
695
|
-
try {
|
|
696
|
-
pi.sendUserMessage(`Resuming ${plan.tasks.length} subagents...`);
|
|
697
|
-
} catch (err) {
|
|
698
|
-
console.error("[pi-subagent] Failed to start deferred resume turn:", err);
|
|
699
|
-
void restoreModelAfterResumeFailure(ctx);
|
|
700
|
-
}
|
|
701
|
-
}, RESUME_INTERACTIVE_DELAY_MS);
|
|
694
|
+
// Do not start the synthetic resume turn from session_start. Pi renders
|
|
695
|
+
// the resumed chat only after session_start/resources_discover complete;
|
|
696
|
+
// starting now lets that render wipe out the live tool component, so no
|
|
697
|
+
// real-time updates appear. Queue it for resources_discover instead,
|
|
698
|
+
// which is the last extension hook before the initial chat render.
|
|
699
|
+
pendingInteractiveResumePrompt = `Resuming ${plan.tasks.length} subagents...`;
|
|
702
700
|
}
|
|
703
701
|
} catch (err) {
|
|
704
702
|
console.error("[pi-subagent] Error in session_start:", err);
|
|
@@ -710,6 +708,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
710
708
|
await restoreModelAfterResumeFailure();
|
|
711
709
|
});
|
|
712
710
|
|
|
711
|
+
pi.on("resources_discover", (_event, ctx) => {
|
|
712
|
+
const prompt = pendingInteractiveResumePrompt;
|
|
713
|
+
if (!prompt) return;
|
|
714
|
+
pendingInteractiveResumePrompt = null;
|
|
715
|
+
setTimeout(() => {
|
|
716
|
+
try {
|
|
717
|
+
pi.sendUserMessage(prompt);
|
|
718
|
+
} catch (err) {
|
|
719
|
+
console.error("[pi-subagent] Failed to start deferred resume turn:", err);
|
|
720
|
+
void restoreModelAfterResumeFailure(ctx);
|
|
721
|
+
}
|
|
722
|
+
}, RESUME_INTERACTIVE_DELAY_MS);
|
|
723
|
+
});
|
|
724
|
+
|
|
713
725
|
// Inject available agents into the system prompt
|
|
714
726
|
pi.on("before_agent_start", async (event) => {
|
|
715
727
|
try {
|
|
@@ -940,12 +952,10 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
940
952
|
}
|
|
941
953
|
|
|
942
954
|
function getSessionDirForTask(toolCallId: string, index: number): string {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
function pathlessSubagentRootFallback(): string {
|
|
948
|
-
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);
|
|
949
959
|
}
|
|
950
960
|
|
|
951
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,26 @@ 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
|
+
|
|
295
314
|
export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
296
315
|
if (!line.trim()) return false;
|
|
297
316
|
|
|
@@ -307,6 +326,7 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
307
326
|
|
|
308
327
|
if (event.type === "message_end" && event.message) {
|
|
309
328
|
const msg = event.message as Message;
|
|
329
|
+
if (hasMessage(result, msg)) return true;
|
|
310
330
|
result.messages.push(msg);
|
|
311
331
|
|
|
312
332
|
if (msg.role === "assistant") {
|
|
@@ -328,7 +348,8 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
328
348
|
}
|
|
329
349
|
|
|
330
350
|
if (event.type === "tool_result_end" && event.message) {
|
|
331
|
-
|
|
351
|
+
const msg = event.message as Message;
|
|
352
|
+
if (!hasMessage(result, msg)) result.messages.push(msg);
|
|
332
353
|
return true;
|
|
333
354
|
}
|
|
334
355
|
|
|
@@ -515,6 +536,27 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
515
536
|
};
|
|
516
537
|
}
|
|
517
538
|
|
|
539
|
+
if (resumeSession && sessionDir && (!fs.existsSync(sessionDir) || !fs.statSync(sessionDir).isDirectory())) {
|
|
540
|
+
const errorMessage = `Cannot resume subagent session: session directory does not exist: ${sessionDir}`;
|
|
541
|
+
return {
|
|
542
|
+
agent: agentName,
|
|
543
|
+
agentSource: agent.source,
|
|
544
|
+
task,
|
|
545
|
+
exitCode: 1,
|
|
546
|
+
messages: initialResult?.messages ? [...initialResult.messages] : [],
|
|
547
|
+
stderr: errorMessage,
|
|
548
|
+
usage: initialResult?.usage ? { ...initialResult.usage } : emptyUsage(),
|
|
549
|
+
toolCalls: initialResult?.toolCalls ? { ...initialResult.toolCalls } : {},
|
|
550
|
+
model: initialResult?.model ?? agent.model,
|
|
551
|
+
stopReason: "error",
|
|
552
|
+
errorMessage,
|
|
553
|
+
completedTurns: initialResult?.completedTurns ?? 0,
|
|
554
|
+
turnInProgress: false,
|
|
555
|
+
liveLog: initialResult?.liveLog ? [...initialResult.liveLog] : [],
|
|
556
|
+
sessionDir,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
518
560
|
const result: SingleResult = {
|
|
519
561
|
agent: agentName,
|
|
520
562
|
agentSource: agent.source,
|
|
@@ -743,7 +785,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
743
785
|
if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
|
|
744
786
|
}
|
|
745
787
|
|
|
746
|
-
if (result.exitCode === 0 &&
|
|
788
|
+
if (result.exitCode === 0 && endedWithSyntheticResumeFailure(result.messages)) {
|
|
747
789
|
result.exitCode = 1;
|
|
748
790
|
result.stopReason = "error";
|
|
749
791
|
result.errorMessage = "Subagent resume failed before the real model continued.";
|
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;
|