oira666_pi-subagent 0.2.4 → 0.2.5
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 +199 -16
- package/package.json +1 -1
- package/render.ts +11 -2
- package/resume.ts +4 -0
- package/runner.ts +18 -2
package/index.ts
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
createAssistantMessageEventStream,
|
|
14
|
+
streamSimple as streamModelSimple,
|
|
15
|
+
} from "@mariozechner/pi-ai";
|
|
12
16
|
import { Type } from "@sinclair/typebox";
|
|
13
17
|
import { type AgentConfig, discoverAgents } from "./agents.js";
|
|
14
18
|
import { renderCall, renderResult } from "./render.js";
|
|
@@ -17,7 +21,6 @@ import {
|
|
|
17
21
|
SUBAGENT_RESUME_DISABLE_ENV,
|
|
18
22
|
SUBAGENT_RESUME_PROMPT_ENV,
|
|
19
23
|
buildSubagentSessionDir,
|
|
20
|
-
ensureDir,
|
|
21
24
|
findLatestResumableSubagentCall,
|
|
22
25
|
getDefaultSubagentSessionRoot,
|
|
23
26
|
isFinishedResult,
|
|
@@ -366,11 +369,67 @@ function hasCliInitialPrompt(argv: string[]): boolean {
|
|
|
366
369
|
return false;
|
|
367
370
|
}
|
|
368
371
|
|
|
372
|
+
const RESUME_PROVIDER = "pi-subagent-resume";
|
|
373
|
+
const RESUME_MODEL_ID = "synthetic-tool-call";
|
|
374
|
+
const RESUME_STATE_KEY = "__piSubagentResumeState";
|
|
375
|
+
|
|
376
|
+
type SyntheticResumeState = {
|
|
377
|
+
plan: ResumableSubagentCall | null;
|
|
378
|
+
phase: "tool" | "final";
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
function clearSyntheticResumeState(): void {
|
|
382
|
+
const state = getSyntheticResumeState();
|
|
383
|
+
state.plan = null;
|
|
384
|
+
state.phase = "tool";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getSyntheticResumeState(): SyntheticResumeState {
|
|
388
|
+
const g = globalThis as any;
|
|
389
|
+
if (!g[RESUME_STATE_KEY]) {
|
|
390
|
+
g[RESUME_STATE_KEY] = { plan: null, phase: "tool" } satisfies SyntheticResumeState;
|
|
391
|
+
}
|
|
392
|
+
return g[RESUME_STATE_KEY] as SyntheticResumeState;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function emptyModelUsage() {
|
|
396
|
+
return {
|
|
397
|
+
input: 0,
|
|
398
|
+
output: 0,
|
|
399
|
+
cacheRead: 0,
|
|
400
|
+
cacheWrite: 0,
|
|
401
|
+
totalTokens: 0,
|
|
402
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function getMessageText(message: any): string {
|
|
407
|
+
const content = message?.content;
|
|
408
|
+
if (typeof content === "string") return content;
|
|
409
|
+
if (!Array.isArray(content)) return "";
|
|
410
|
+
return content
|
|
411
|
+
.map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : ""))
|
|
412
|
+
.join("");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isSyntheticResumePrompt(context: any, taskCount: number): boolean {
|
|
416
|
+
const messages = Array.isArray(context?.messages) ? context.messages : [];
|
|
417
|
+
const lastUser = [...messages].reverse().find((message) => message?.role === "user");
|
|
418
|
+
return getMessageText(lastUser).trim() === `Resuming ${taskCount} subagents...`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function formatModelFlag(model: any): string | undefined {
|
|
422
|
+
if (!model?.id || !model?.provider) return undefined;
|
|
423
|
+
return `${model.provider}/${model.id}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
369
426
|
// ---------------------------------------------------------------------------
|
|
370
427
|
// Extension entry point
|
|
371
428
|
// ---------------------------------------------------------------------------
|
|
372
429
|
|
|
373
430
|
export default function (pi: ExtensionAPI) {
|
|
431
|
+
let resumeModelRegistry: any | undefined;
|
|
432
|
+
|
|
374
433
|
pi.registerFlag("subagent-max-depth", {
|
|
375
434
|
description: "Maximum allowed subagent delegation depth (default: 3).",
|
|
376
435
|
type: "string",
|
|
@@ -381,6 +440,97 @@ export default function (pi: ExtensionAPI) {
|
|
|
381
440
|
type: "boolean",
|
|
382
441
|
});
|
|
383
442
|
|
|
443
|
+
pi.registerProvider(RESUME_PROVIDER, {
|
|
444
|
+
baseUrl: "http://127.0.0.1/pi-subagent-resume",
|
|
445
|
+
api: "openai-responses",
|
|
446
|
+
apiKey: "pi-subagent-resume-noop-key",
|
|
447
|
+
streamSimple: async (model, context, options) => {
|
|
448
|
+
const stream = createAssistantMessageEventStream();
|
|
449
|
+
const state = getSyntheticResumeState();
|
|
450
|
+
const plan = state.plan;
|
|
451
|
+
const phase = state.phase;
|
|
452
|
+
if (plan && phase === "tool" && isSyntheticResumePrompt(context, plan.tasks.length)) {
|
|
453
|
+
state.phase = "final";
|
|
454
|
+
const toolCall = {
|
|
455
|
+
type: "toolCall" as const,
|
|
456
|
+
id: `resume_subagent_${Date.now()}`,
|
|
457
|
+
name: "subagent",
|
|
458
|
+
arguments: { tasks: plan.tasks },
|
|
459
|
+
};
|
|
460
|
+
const message = {
|
|
461
|
+
role: "assistant" as const,
|
|
462
|
+
content: [toolCall],
|
|
463
|
+
api: model.api,
|
|
464
|
+
provider: model.provider,
|
|
465
|
+
model: model.id,
|
|
466
|
+
usage: emptyModelUsage(),
|
|
467
|
+
stopReason: "toolUse" as const,
|
|
468
|
+
timestamp: Date.now(),
|
|
469
|
+
};
|
|
470
|
+
queueMicrotask(() => {
|
|
471
|
+
stream.push({ type: "start", partial: message });
|
|
472
|
+
stream.push({ type: "toolcall_start", contentIndex: 0, partial: message });
|
|
473
|
+
stream.push({ type: "toolcall_end", contentIndex: 0, toolCall, partial: message });
|
|
474
|
+
stream.push({ type: "done", reason: "toolUse", message });
|
|
475
|
+
stream.end(message);
|
|
476
|
+
});
|
|
477
|
+
return stream;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (phase === "final" && modelToRestoreAfterResume) {
|
|
481
|
+
const restore = modelToRestoreAfterResume;
|
|
482
|
+
const auth = resumeModelRegistry
|
|
483
|
+
? await resumeModelRegistry.getApiKeyAndHeaders(restore)
|
|
484
|
+
: { ok: true, apiKey: undefined, headers: undefined };
|
|
485
|
+
if (!auth.ok) {
|
|
486
|
+
throw new Error(auth.error);
|
|
487
|
+
}
|
|
488
|
+
return streamModelSimple(restore, context, {
|
|
489
|
+
...options,
|
|
490
|
+
apiKey: auth.apiKey,
|
|
491
|
+
headers: auth.headers,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!(plan && phase === "tool")) {
|
|
496
|
+
state.plan = null;
|
|
497
|
+
state.phase = "tool";
|
|
498
|
+
}
|
|
499
|
+
const message = {
|
|
500
|
+
role: "assistant" as const,
|
|
501
|
+
content: [],
|
|
502
|
+
api: model.api,
|
|
503
|
+
provider: model.provider,
|
|
504
|
+
model: model.id,
|
|
505
|
+
usage: emptyModelUsage(),
|
|
506
|
+
stopReason: "stop" as const,
|
|
507
|
+
timestamp: Date.now(),
|
|
508
|
+
};
|
|
509
|
+
queueMicrotask(() => {
|
|
510
|
+
stream.push({ type: "start", partial: message });
|
|
511
|
+
stream.push({ type: "done", reason: "stop", message });
|
|
512
|
+
stream.end(message);
|
|
513
|
+
});
|
|
514
|
+
return stream;
|
|
515
|
+
},
|
|
516
|
+
models: [
|
|
517
|
+
{
|
|
518
|
+
id: RESUME_MODEL_ID,
|
|
519
|
+
name: "Pi Subagent Resume",
|
|
520
|
+
api: "openai-responses",
|
|
521
|
+
reasoning: false,
|
|
522
|
+
input: ["text"],
|
|
523
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
524
|
+
// Keep this large so Pi's pre-prompt auto-compaction does not call the
|
|
525
|
+
// synthetic provider before the visible resume prompt is appended. That
|
|
526
|
+
// would consume the tool-call phase during compaction and the real
|
|
527
|
+
// resume turn would only see the final text message.
|
|
528
|
+
contextWindow: 1_000_000,
|
|
529
|
+
maxTokens: 16,
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
});
|
|
533
|
+
|
|
384
534
|
const depthConfig = resolveDelegationDepthConfig(pi);
|
|
385
535
|
const { currentDepth, maxDepth, canDelegate, ancestorAgentStack, preventCycles } =
|
|
386
536
|
depthConfig;
|
|
@@ -392,8 +542,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
542
|
let currentSessionId = "ephemeral";
|
|
393
543
|
let currentSubagentSessionRoot = "";
|
|
394
544
|
let pendingResumePlan: ResumableSubagentCall | null = null;
|
|
545
|
+
let modelToRestoreAfterResume: any | undefined;
|
|
395
546
|
const approvedProjectAgentDirsForSession = new Set<string>();
|
|
396
547
|
|
|
548
|
+
async function restoreModelAfterResumeFailure(ctx?: { ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }) {
|
|
549
|
+
const restore = modelToRestoreAfterResume;
|
|
550
|
+
modelToRestoreAfterResume = undefined;
|
|
551
|
+
pendingResumePlan = null;
|
|
552
|
+
clearSyntheticResumeState();
|
|
553
|
+
if (!restore) return;
|
|
554
|
+
try {
|
|
555
|
+
await pi.setModel(restore);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
558
|
+
ctx?.ui?.notify?.(`Failed to restore model after subagent resume error: ${message}`, "error");
|
|
559
|
+
console.error("[pi-subagent] Failed to restore model after resume error:", err);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
397
563
|
// Auto-discover agents on session start
|
|
398
564
|
pi.on("session_start", async (event, ctx) => {
|
|
399
565
|
if (!canDelegate) return;
|
|
@@ -430,13 +596,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
430
596
|
if (!shouldResume) return;
|
|
431
597
|
|
|
432
598
|
pendingResumePlan = plan;
|
|
599
|
+
const resumeState = getSyntheticResumeState();
|
|
600
|
+
resumeState.plan = plan;
|
|
601
|
+
resumeState.phase = "tool";
|
|
602
|
+
modelToRestoreAfterResume = ctx.model;
|
|
603
|
+
resumeModelRegistry = ctx.modelRegistry;
|
|
433
604
|
ensureSubagentToolActive(pi);
|
|
605
|
+
const resumeModel = ctx.modelRegistry.find(RESUME_PROVIDER, RESUME_MODEL_ID);
|
|
606
|
+
if (!resumeModel || !(await pi.setModel(resumeModel))) {
|
|
607
|
+
ctx.ui.notify("Failed to switch to synthetic subagent resume model.", "error");
|
|
608
|
+
await restoreModelAfterResumeFailure(ctx);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
434
611
|
|
|
435
612
|
// In print/json subprocesses there is already an initial CLI prompt about
|
|
436
|
-
// to be sent.
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
// call instruction into the upcoming turn.
|
|
613
|
+
// to be sent. That prompt will be answered by the synthetic provider with
|
|
614
|
+
// a real assistant subagent tool call. In interactive mode, submit a short
|
|
615
|
+
// visible prompt that triggers the same synthetic provider path.
|
|
440
616
|
if (hasCliInitialPrompt(process.argv)) {
|
|
441
617
|
if (ctx.hasUI) ctx.ui.notify(`Resuming ${plan.tasks.length} subagents...`, "info");
|
|
442
618
|
} else {
|
|
@@ -444,9 +620,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
444
620
|
}
|
|
445
621
|
} catch (err) {
|
|
446
622
|
console.error("[pi-subagent] Error in session_start:", err);
|
|
623
|
+
await restoreModelAfterResumeFailure(ctx);
|
|
447
624
|
}
|
|
448
625
|
});
|
|
449
626
|
|
|
627
|
+
pi.on("agent_end", async () => {
|
|
628
|
+
await restoreModelAfterResumeFailure();
|
|
629
|
+
});
|
|
630
|
+
|
|
450
631
|
// Inject available agents into the system prompt
|
|
451
632
|
pi.on("before_agent_start", async (event) => {
|
|
452
633
|
try {
|
|
@@ -456,15 +637,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
456
637
|
const agentList = discoveredAgents
|
|
457
638
|
.map((a) => `- **${a.name}**: ${a.description}`)
|
|
458
639
|
.join("\n");
|
|
459
|
-
if (pendingResumePlan) ensureSubagentToolActive(pi);
|
|
460
|
-
const resumeInstruction = pendingResumePlan
|
|
461
|
-
? `\n\n## Interrupted Subagent Resume\n\nThe user approved resuming an interrupted subagent delegation. The \`subagent\` tool has been enabled for this turn. You MUST call the \`subagent\` tool exactly once now with these exact arguments and no other tool calls first:\n\n\`\`\`json\n${JSON.stringify({ tasks: pendingResumePlan.tasks }, null, 2)}\n\`\`\`\n`
|
|
462
|
-
: "";
|
|
463
|
-
|
|
464
640
|
return {
|
|
465
641
|
systemPrompt:
|
|
466
642
|
event.systemPrompt +
|
|
467
|
-
resumeInstruction +
|
|
468
643
|
`\n\n## Available Subagents
|
|
469
644
|
|
|
470
645
|
The following subagents are available via the \`subagent\` tool:
|
|
@@ -630,7 +805,9 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
630
805
|
pendingResumePlan && sameTasks(pendingResumePlan.tasks, tasks)
|
|
631
806
|
? pendingResumePlan
|
|
632
807
|
: null;
|
|
633
|
-
if (resumePlan)
|
|
808
|
+
if (resumePlan) {
|
|
809
|
+
pendingResumePlan = null;
|
|
810
|
+
}
|
|
634
811
|
|
|
635
812
|
if (tasks.length === 1) {
|
|
636
813
|
const [task] = tasks;
|
|
@@ -646,6 +823,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
646
823
|
resumePlan?.details?.results[0],
|
|
647
824
|
getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, 0),
|
|
648
825
|
!!resumePlan,
|
|
826
|
+
formatModelFlag(modelToRestoreAfterResume),
|
|
649
827
|
);
|
|
650
828
|
}
|
|
651
829
|
|
|
@@ -659,6 +837,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
659
837
|
resumePlan?.details?.results,
|
|
660
838
|
(index) => getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, index),
|
|
661
839
|
!!resumePlan,
|
|
840
|
+
formatModelFlag(modelToRestoreAfterResume),
|
|
662
841
|
);
|
|
663
842
|
} catch (err) {
|
|
664
843
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -672,7 +851,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
672
851
|
|
|
673
852
|
},
|
|
674
853
|
|
|
675
|
-
renderCall: (args, theme) => renderCall(args, theme),
|
|
854
|
+
renderCall: (args, theme, context) => renderCall(args, theme, context),
|
|
676
855
|
renderResult: (result, { expanded }, theme) =>
|
|
677
856
|
renderResult(result, expanded, theme),
|
|
678
857
|
});
|
|
@@ -680,9 +859,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
680
859
|
|
|
681
860
|
function getSessionDirForTask(toolCallId: string, index: number): string {
|
|
682
861
|
const root = currentSubagentSessionRoot || pathlessSubagentRootFallback();
|
|
683
|
-
|
|
684
|
-
ensureDir(dir);
|
|
685
|
-
return dir;
|
|
862
|
+
return buildSubagentSessionDir(root, currentSessionId, toolCallId, index);
|
|
686
863
|
}
|
|
687
864
|
|
|
688
865
|
function pathlessSubagentRootFallback(): string {
|
|
@@ -705,6 +882,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
705
882
|
previousResult: SingleResult | undefined,
|
|
706
883
|
sessionDir: string,
|
|
707
884
|
resumeExistingSession: boolean,
|
|
885
|
+
fallbackModel?: string,
|
|
708
886
|
) {
|
|
709
887
|
if (previousResult && isFinishedResult(previousResult)) {
|
|
710
888
|
return {
|
|
@@ -732,8 +910,10 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
732
910
|
onUpdate,
|
|
733
911
|
makeDetails: makeDetails("single"),
|
|
734
912
|
sessionDir: previousResult?.sessionDir ?? sessionDir,
|
|
913
|
+
sessionRoot: currentSubagentSessionRoot,
|
|
735
914
|
resumeSession: resumeExistingSession,
|
|
736
915
|
initialResult: previousResult,
|
|
916
|
+
fallbackModel,
|
|
737
917
|
});
|
|
738
918
|
|
|
739
919
|
if (isResultError(result)) {
|
|
@@ -774,6 +954,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
774
954
|
resumeResults: SingleResult[] | undefined,
|
|
775
955
|
getSessionDir: (index: number) => string,
|
|
776
956
|
resumeExistingSessions: boolean,
|
|
957
|
+
fallbackModel?: string,
|
|
777
958
|
) {
|
|
778
959
|
return executeParallelSubprocess(
|
|
779
960
|
tasks,
|
|
@@ -789,6 +970,8 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
789
970
|
resumeResults,
|
|
790
971
|
(index) => getSessionDir(index),
|
|
791
972
|
resumeExistingSessions,
|
|
973
|
+
currentSubagentSessionRoot,
|
|
974
|
+
fallbackModel,
|
|
792
975
|
);
|
|
793
976
|
}
|
|
794
977
|
}
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -400,14 +400,23 @@ function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
|
|
|
400
400
|
// renderCall — shown while the tool is being invoked
|
|
401
401
|
// ---------------------------------------------------------------------------
|
|
402
402
|
|
|
403
|
-
export function renderCall(
|
|
403
|
+
export function renderCall(
|
|
404
|
+
args: Record<string, any>,
|
|
405
|
+
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
406
|
+
context?: { isPartial?: boolean; isError?: boolean },
|
|
407
|
+
): Text {
|
|
404
408
|
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
405
409
|
const count = tasks.length;
|
|
410
|
+
const icon = context?.isPartial === false
|
|
411
|
+
? context.isError
|
|
412
|
+
? theme.fg("error", "❌")
|
|
413
|
+
: theme.fg("success", "✅")
|
|
414
|
+
: theme.fg("warning", "⏳");
|
|
406
415
|
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `${count} task${count === 1 ? "" : "s"}`)}`;
|
|
407
416
|
for (const task of tasks.slice(0, 6)) {
|
|
408
417
|
const agent = typeof task?.agent === "string" ? task.agent : "...";
|
|
409
418
|
const preview = typeof task?.task === "string" ? ` ${truncate(task.task, 56)}` : "";
|
|
410
|
-
text += `\n ${
|
|
419
|
+
text += `\n ${icon} ${theme.fg("accent", agent)}${theme.fg("dim", preview)}`;
|
|
411
420
|
}
|
|
412
421
|
if (tasks.length > 6) text += `\n ${theme.fg("muted", `... +${tasks.length - 6} more`)}`;
|
|
413
422
|
return new Text(text, 0, 0);
|
package/resume.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { isResultError, isSubagentDetails, type SingleResult, type SubagentDetai
|
|
|
6
6
|
|
|
7
7
|
export const SUBAGENT_RESUME_PROMPT_ENV = "PI_SUBAGENT_RESUME_PROMPT";
|
|
8
8
|
export const SUBAGENT_RESUME_DISABLE_ENV = "PI_SUBAGENT_DISABLE_RESUME";
|
|
9
|
+
export const SUBAGENT_SESSION_ROOT_ENV = "PI_SUBAGENT_SESSION_ROOT";
|
|
9
10
|
|
|
10
11
|
type SessionEntry = ReturnType<ExtensionContext["sessionManager"]["getEntries"]>[number];
|
|
11
12
|
|
|
@@ -25,6 +26,9 @@ export function parseBooleanEnv(raw: unknown): boolean | null {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function getDefaultSubagentSessionRoot(ctx: ExtensionContext): string {
|
|
29
|
+
const inheritedRoot = process.env[SUBAGENT_SESSION_ROOT_ENV];
|
|
30
|
+
if (inheritedRoot) return inheritedRoot;
|
|
31
|
+
|
|
28
32
|
const mainSessionDir = ctx.sessionManager.getSessionDir?.();
|
|
29
33
|
if (typeof mainSessionDir === "string" && mainSessionDir.length > 0) {
|
|
30
34
|
return path.join(path.dirname(mainSessionDir), "sessions-subagents");
|
package/runner.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getFinalOutput,
|
|
22
22
|
getNestedSubagentErrorSummary,
|
|
23
23
|
} from "./types.js";
|
|
24
|
+
import { SUBAGENT_SESSION_ROOT_ENV } from "./resume.js";
|
|
24
25
|
import {
|
|
25
26
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
26
27
|
DEFAULT_MAX_CONCURRENCY,
|
|
@@ -51,6 +52,7 @@ const SUBAGENT_DEPTH_ENV = "PI_SUBAGENT_DEPTH";
|
|
|
51
52
|
const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
52
53
|
const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
53
54
|
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
55
|
+
const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
|
|
54
56
|
// PI_OFFLINE intentionally removed: setting it on child processes blocks all API
|
|
55
57
|
// calls and renders subagents unable to do any LLM work. Children inherit the
|
|
56
58
|
// parent's PI_OFFLINE value via process.env spread if needed.
|
|
@@ -305,7 +307,7 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
305
307
|
result.usage.cost += usage.cost?.total || 0;
|
|
306
308
|
result.usage.contextTokens = usage.totalTokens || 0;
|
|
307
309
|
}
|
|
308
|
-
if (
|
|
310
|
+
if (msg.model && msg.model !== "synthetic-tool-call") result.model = msg.model;
|
|
309
311
|
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
310
312
|
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
311
313
|
}
|
|
@@ -367,6 +369,7 @@ function buildPiArgs(
|
|
|
367
369
|
task: string,
|
|
368
370
|
sessionDir: string | undefined,
|
|
369
371
|
resumeSession: boolean,
|
|
372
|
+
fallbackModelOverride?: string,
|
|
370
373
|
): string[] {
|
|
371
374
|
const args: string[] = [
|
|
372
375
|
"--mode",
|
|
@@ -380,7 +383,7 @@ function buildPiArgs(
|
|
|
380
383
|
args.push("-p");
|
|
381
384
|
|
|
382
385
|
// Agent config takes priority; fall back to parent CLI value
|
|
383
|
-
const model = agent.model ?? _inheritedCliArgs.fallbackModel;
|
|
386
|
+
const model = agent.model ?? fallbackModelOverride ?? process.env[SUBAGENT_FALLBACK_MODEL_ENV] ?? _inheritedCliArgs.fallbackModel;
|
|
384
387
|
if (model) args.push("--model", model);
|
|
385
388
|
|
|
386
389
|
const thinking = agent.thinking ?? _inheritedCliArgs.fallbackThinking;
|
|
@@ -444,10 +447,14 @@ export interface RunAgentOptions {
|
|
|
444
447
|
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
445
448
|
/** Dedicated session directory for this subagent process. */
|
|
446
449
|
sessionDir?: string;
|
|
450
|
+
/** Top-level root for all subagent session directories in this delegation tree. */
|
|
451
|
+
sessionRoot?: string;
|
|
447
452
|
/** Continue the most recent session in sessionDir instead of creating a new one. */
|
|
448
453
|
resumeSession?: boolean;
|
|
449
454
|
/** Previously captured state for this same subagent, used to render resumed nested trees. */
|
|
450
455
|
initialResult?: SingleResult;
|
|
456
|
+
/** Fallback model to use when the agent config does not pin one. */
|
|
457
|
+
fallbackModel?: string;
|
|
451
458
|
}
|
|
452
459
|
|
|
453
460
|
/**
|
|
@@ -470,8 +477,10 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
470
477
|
onUpdate,
|
|
471
478
|
makeDetails,
|
|
472
479
|
sessionDir,
|
|
480
|
+
sessionRoot,
|
|
473
481
|
resumeSession = false,
|
|
474
482
|
initialResult,
|
|
483
|
+
fallbackModel,
|
|
475
484
|
} = opts;
|
|
476
485
|
|
|
477
486
|
const agent = agents.find((a) => a.name === agentName);
|
|
@@ -540,6 +549,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
540
549
|
task,
|
|
541
550
|
sessionDir,
|
|
542
551
|
resumeSession,
|
|
552
|
+
fallbackModel,
|
|
543
553
|
);
|
|
544
554
|
let wasAborted = false;
|
|
545
555
|
|
|
@@ -564,6 +574,8 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
564
574
|
[SUBAGENT_MAX_DEPTH_ENV]: String(propagatedMaxDepth),
|
|
565
575
|
[SUBAGENT_STACK_ENV]: JSON.stringify(propagatedStack),
|
|
566
576
|
[SUBAGENT_PREVENT_CYCLES_ENV]: preventCycles ? "1" : "0",
|
|
577
|
+
...(sessionRoot ? { [SUBAGENT_SESSION_ROOT_ENV]: sessionRoot } : {}),
|
|
578
|
+
...(fallbackModel ? { [SUBAGENT_FALLBACK_MODEL_ENV]: fallbackModel } : {}),
|
|
567
579
|
// PI_OFFLINE is NOT forced here — see explanation near PI_OFFLINE_ENV.
|
|
568
580
|
},
|
|
569
581
|
});
|
|
@@ -761,6 +773,8 @@ export async function executeParallelSubprocess(
|
|
|
761
773
|
resumeResults?: SingleResult[],
|
|
762
774
|
getSessionDir?: (index: number, task: { agent: string; task: string; cwd?: string }) => string | undefined,
|
|
763
775
|
resumeExistingSessions = false,
|
|
776
|
+
sessionRoot?: string,
|
|
777
|
+
fallbackModel?: string,
|
|
764
778
|
): Promise<{
|
|
765
779
|
content: Array<{ type: "text"; text: string }>;
|
|
766
780
|
details: SubagentDetails;
|
|
@@ -855,8 +869,10 @@ export async function executeParallelSubprocess(
|
|
|
855
869
|
preventCycles,
|
|
856
870
|
signal,
|
|
857
871
|
sessionDir,
|
|
872
|
+
sessionRoot,
|
|
858
873
|
resumeSession: resumeExistingSessions && !!sessionDir,
|
|
859
874
|
initialResult: previousResult,
|
|
875
|
+
fallbackModel,
|
|
860
876
|
onUpdate: (partial) => {
|
|
861
877
|
if (partial.details?.results[0]) {
|
|
862
878
|
allResults[index] = partial.details.results[0];
|