oira666_pi-subagent 0.2.4 → 0.2.6
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 +205 -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,69 @@ 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
|
+
trigger: "resumePrompt" | "nextRequest";
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function clearSyntheticResumeState(): void {
|
|
383
|
+
const state = getSyntheticResumeState();
|
|
384
|
+
state.plan = null;
|
|
385
|
+
state.phase = "tool";
|
|
386
|
+
state.trigger = "resumePrompt";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function getSyntheticResumeState(): SyntheticResumeState {
|
|
390
|
+
const g = globalThis as any;
|
|
391
|
+
if (!g[RESUME_STATE_KEY]) {
|
|
392
|
+
g[RESUME_STATE_KEY] = { plan: null, phase: "tool", trigger: "resumePrompt" } satisfies SyntheticResumeState;
|
|
393
|
+
}
|
|
394
|
+
return g[RESUME_STATE_KEY] as SyntheticResumeState;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function emptyModelUsage() {
|
|
398
|
+
return {
|
|
399
|
+
input: 0,
|
|
400
|
+
output: 0,
|
|
401
|
+
cacheRead: 0,
|
|
402
|
+
cacheWrite: 0,
|
|
403
|
+
totalTokens: 0,
|
|
404
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function getMessageText(message: any): string {
|
|
409
|
+
const content = message?.content;
|
|
410
|
+
if (typeof content === "string") return content;
|
|
411
|
+
if (!Array.isArray(content)) return "";
|
|
412
|
+
return content
|
|
413
|
+
.map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : ""))
|
|
414
|
+
.join("");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isSyntheticResumePrompt(context: any, taskCount: number): boolean {
|
|
418
|
+
const messages = Array.isArray(context?.messages) ? context.messages : [];
|
|
419
|
+
const lastUser = [...messages].reverse().find((message) => message?.role === "user");
|
|
420
|
+
return getMessageText(lastUser).trim() === `Resuming ${taskCount} subagents...`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function formatModelFlag(model: any): string | undefined {
|
|
424
|
+
if (!model?.id || !model?.provider) return undefined;
|
|
425
|
+
return `${model.provider}/${model.id}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
369
428
|
// ---------------------------------------------------------------------------
|
|
370
429
|
// Extension entry point
|
|
371
430
|
// ---------------------------------------------------------------------------
|
|
372
431
|
|
|
373
432
|
export default function (pi: ExtensionAPI) {
|
|
433
|
+
let resumeModelRegistry: any | undefined;
|
|
434
|
+
|
|
374
435
|
pi.registerFlag("subagent-max-depth", {
|
|
375
436
|
description: "Maximum allowed subagent delegation depth (default: 3).",
|
|
376
437
|
type: "string",
|
|
@@ -381,6 +442,100 @@ export default function (pi: ExtensionAPI) {
|
|
|
381
442
|
type: "boolean",
|
|
382
443
|
});
|
|
383
444
|
|
|
445
|
+
pi.registerProvider(RESUME_PROVIDER, {
|
|
446
|
+
baseUrl: "http://127.0.0.1/pi-subagent-resume",
|
|
447
|
+
api: "openai-responses",
|
|
448
|
+
apiKey: "pi-subagent-resume-noop-key",
|
|
449
|
+
streamSimple: async (model, context, options) => {
|
|
450
|
+
const stream = createAssistantMessageEventStream();
|
|
451
|
+
const state = getSyntheticResumeState();
|
|
452
|
+
const plan = state.plan;
|
|
453
|
+
const phase = state.phase;
|
|
454
|
+
const triggerMatches =
|
|
455
|
+
state.trigger === "nextRequest" ||
|
|
456
|
+
(plan ? isSyntheticResumePrompt(context, plan.tasks.length) : false);
|
|
457
|
+
if (plan && phase === "tool" && triggerMatches) {
|
|
458
|
+
state.phase = "final";
|
|
459
|
+
const toolCall = {
|
|
460
|
+
type: "toolCall" as const,
|
|
461
|
+
id: `resume_subagent_${Date.now()}`,
|
|
462
|
+
name: "subagent",
|
|
463
|
+
arguments: { tasks: plan.tasks },
|
|
464
|
+
};
|
|
465
|
+
const message = {
|
|
466
|
+
role: "assistant" as const,
|
|
467
|
+
content: [toolCall],
|
|
468
|
+
api: model.api,
|
|
469
|
+
provider: model.provider,
|
|
470
|
+
model: model.id,
|
|
471
|
+
usage: emptyModelUsage(),
|
|
472
|
+
stopReason: "toolUse" as const,
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
};
|
|
475
|
+
queueMicrotask(() => {
|
|
476
|
+
stream.push({ type: "start", partial: message });
|
|
477
|
+
stream.push({ type: "toolcall_start", contentIndex: 0, partial: message });
|
|
478
|
+
stream.push({ type: "toolcall_end", contentIndex: 0, toolCall, partial: message });
|
|
479
|
+
stream.push({ type: "done", reason: "toolUse", message });
|
|
480
|
+
stream.end(message);
|
|
481
|
+
});
|
|
482
|
+
return stream;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (phase === "final" && modelToRestoreAfterResume) {
|
|
486
|
+
const restore = modelToRestoreAfterResume;
|
|
487
|
+
const auth = resumeModelRegistry
|
|
488
|
+
? await resumeModelRegistry.getApiKeyAndHeaders(restore)
|
|
489
|
+
: { ok: true, apiKey: undefined, headers: undefined };
|
|
490
|
+
if (!auth.ok) {
|
|
491
|
+
throw new Error(auth.error);
|
|
492
|
+
}
|
|
493
|
+
return streamModelSimple(restore, context, {
|
|
494
|
+
...options,
|
|
495
|
+
apiKey: auth.apiKey,
|
|
496
|
+
headers: auth.headers,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!(plan && phase === "tool")) {
|
|
501
|
+
state.plan = null;
|
|
502
|
+
state.phase = "tool";
|
|
503
|
+
}
|
|
504
|
+
const message = {
|
|
505
|
+
role: "assistant" as const,
|
|
506
|
+
content: [],
|
|
507
|
+
api: model.api,
|
|
508
|
+
provider: model.provider,
|
|
509
|
+
model: model.id,
|
|
510
|
+
usage: emptyModelUsage(),
|
|
511
|
+
stopReason: "stop" as const,
|
|
512
|
+
timestamp: Date.now(),
|
|
513
|
+
};
|
|
514
|
+
queueMicrotask(() => {
|
|
515
|
+
stream.push({ type: "start", partial: message });
|
|
516
|
+
stream.push({ type: "done", reason: "stop", message });
|
|
517
|
+
stream.end(message);
|
|
518
|
+
});
|
|
519
|
+
return stream;
|
|
520
|
+
},
|
|
521
|
+
models: [
|
|
522
|
+
{
|
|
523
|
+
id: RESUME_MODEL_ID,
|
|
524
|
+
name: "Pi Subagent Resume",
|
|
525
|
+
api: "openai-responses",
|
|
526
|
+
reasoning: false,
|
|
527
|
+
input: ["text"],
|
|
528
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
529
|
+
// Keep this large so Pi's pre-prompt auto-compaction does not call the
|
|
530
|
+
// synthetic provider before the visible resume prompt is appended. That
|
|
531
|
+
// would consume the tool-call phase during compaction and the real
|
|
532
|
+
// resume turn would only see the final text message.
|
|
533
|
+
contextWindow: 1_000_000,
|
|
534
|
+
maxTokens: 16,
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
});
|
|
538
|
+
|
|
384
539
|
const depthConfig = resolveDelegationDepthConfig(pi);
|
|
385
540
|
const { currentDepth, maxDepth, canDelegate, ancestorAgentStack, preventCycles } =
|
|
386
541
|
depthConfig;
|
|
@@ -392,8 +547,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
547
|
let currentSessionId = "ephemeral";
|
|
393
548
|
let currentSubagentSessionRoot = "";
|
|
394
549
|
let pendingResumePlan: ResumableSubagentCall | null = null;
|
|
550
|
+
let modelToRestoreAfterResume: any | undefined;
|
|
395
551
|
const approvedProjectAgentDirsForSession = new Set<string>();
|
|
396
552
|
|
|
553
|
+
async function restoreModelAfterResumeFailure(ctx?: { ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }) {
|
|
554
|
+
const restore = modelToRestoreAfterResume;
|
|
555
|
+
modelToRestoreAfterResume = undefined;
|
|
556
|
+
pendingResumePlan = null;
|
|
557
|
+
clearSyntheticResumeState();
|
|
558
|
+
if (!restore) return;
|
|
559
|
+
try {
|
|
560
|
+
await pi.setModel(restore);
|
|
561
|
+
} catch (err) {
|
|
562
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
563
|
+
ctx?.ui?.notify?.(`Failed to restore model after subagent resume error: ${message}`, "error");
|
|
564
|
+
console.error("[pi-subagent] Failed to restore model after resume error:", err);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
397
568
|
// Auto-discover agents on session start
|
|
398
569
|
pi.on("session_start", async (event, ctx) => {
|
|
399
570
|
if (!canDelegate) return;
|
|
@@ -430,13 +601,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
430
601
|
if (!shouldResume) return;
|
|
431
602
|
|
|
432
603
|
pendingResumePlan = plan;
|
|
604
|
+
const resumeState = getSyntheticResumeState();
|
|
605
|
+
resumeState.plan = plan;
|
|
606
|
+
resumeState.phase = "tool";
|
|
607
|
+
resumeState.trigger = hasCliInitialPrompt(process.argv) ? "nextRequest" : "resumePrompt";
|
|
608
|
+
modelToRestoreAfterResume = ctx.model;
|
|
609
|
+
resumeModelRegistry = ctx.modelRegistry;
|
|
433
610
|
ensureSubagentToolActive(pi);
|
|
611
|
+
const resumeModel = ctx.modelRegistry.find(RESUME_PROVIDER, RESUME_MODEL_ID);
|
|
612
|
+
if (!resumeModel || !(await pi.setModel(resumeModel))) {
|
|
613
|
+
ctx.ui.notify("Failed to switch to synthetic subagent resume model.", "error");
|
|
614
|
+
await restoreModelAfterResumeFailure(ctx);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
434
617
|
|
|
435
618
|
// 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.
|
|
619
|
+
// to be sent. That prompt will be answered by the synthetic provider with
|
|
620
|
+
// a real assistant subagent tool call. In interactive mode, submit a short
|
|
621
|
+
// visible prompt that triggers the same synthetic provider path.
|
|
440
622
|
if (hasCliInitialPrompt(process.argv)) {
|
|
441
623
|
if (ctx.hasUI) ctx.ui.notify(`Resuming ${plan.tasks.length} subagents...`, "info");
|
|
442
624
|
} else {
|
|
@@ -444,9 +626,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
444
626
|
}
|
|
445
627
|
} catch (err) {
|
|
446
628
|
console.error("[pi-subagent] Error in session_start:", err);
|
|
629
|
+
await restoreModelAfterResumeFailure(ctx);
|
|
447
630
|
}
|
|
448
631
|
});
|
|
449
632
|
|
|
633
|
+
pi.on("agent_end", async () => {
|
|
634
|
+
await restoreModelAfterResumeFailure();
|
|
635
|
+
});
|
|
636
|
+
|
|
450
637
|
// Inject available agents into the system prompt
|
|
451
638
|
pi.on("before_agent_start", async (event) => {
|
|
452
639
|
try {
|
|
@@ -456,15 +643,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
456
643
|
const agentList = discoveredAgents
|
|
457
644
|
.map((a) => `- **${a.name}**: ${a.description}`)
|
|
458
645
|
.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
646
|
return {
|
|
465
647
|
systemPrompt:
|
|
466
648
|
event.systemPrompt +
|
|
467
|
-
resumeInstruction +
|
|
468
649
|
`\n\n## Available Subagents
|
|
469
650
|
|
|
470
651
|
The following subagents are available via the \`subagent\` tool:
|
|
@@ -630,7 +811,9 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
630
811
|
pendingResumePlan && sameTasks(pendingResumePlan.tasks, tasks)
|
|
631
812
|
? pendingResumePlan
|
|
632
813
|
: null;
|
|
633
|
-
if (resumePlan)
|
|
814
|
+
if (resumePlan) {
|
|
815
|
+
pendingResumePlan = null;
|
|
816
|
+
}
|
|
634
817
|
|
|
635
818
|
if (tasks.length === 1) {
|
|
636
819
|
const [task] = tasks;
|
|
@@ -646,6 +829,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
646
829
|
resumePlan?.details?.results[0],
|
|
647
830
|
getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, 0),
|
|
648
831
|
!!resumePlan,
|
|
832
|
+
formatModelFlag(modelToRestoreAfterResume),
|
|
649
833
|
);
|
|
650
834
|
}
|
|
651
835
|
|
|
@@ -659,6 +843,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
659
843
|
resumePlan?.details?.results,
|
|
660
844
|
(index) => getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, index),
|
|
661
845
|
!!resumePlan,
|
|
846
|
+
formatModelFlag(modelToRestoreAfterResume),
|
|
662
847
|
);
|
|
663
848
|
} catch (err) {
|
|
664
849
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -672,7 +857,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
672
857
|
|
|
673
858
|
},
|
|
674
859
|
|
|
675
|
-
renderCall: (args, theme) => renderCall(args, theme),
|
|
860
|
+
renderCall: (args, theme, context) => renderCall(args, theme, context),
|
|
676
861
|
renderResult: (result, { expanded }, theme) =>
|
|
677
862
|
renderResult(result, expanded, theme),
|
|
678
863
|
});
|
|
@@ -680,9 +865,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
680
865
|
|
|
681
866
|
function getSessionDirForTask(toolCallId: string, index: number): string {
|
|
682
867
|
const root = currentSubagentSessionRoot || pathlessSubagentRootFallback();
|
|
683
|
-
|
|
684
|
-
ensureDir(dir);
|
|
685
|
-
return dir;
|
|
868
|
+
return buildSubagentSessionDir(root, currentSessionId, toolCallId, index);
|
|
686
869
|
}
|
|
687
870
|
|
|
688
871
|
function pathlessSubagentRootFallback(): string {
|
|
@@ -705,6 +888,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
705
888
|
previousResult: SingleResult | undefined,
|
|
706
889
|
sessionDir: string,
|
|
707
890
|
resumeExistingSession: boolean,
|
|
891
|
+
fallbackModel?: string,
|
|
708
892
|
) {
|
|
709
893
|
if (previousResult && isFinishedResult(previousResult)) {
|
|
710
894
|
return {
|
|
@@ -732,8 +916,10 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
732
916
|
onUpdate,
|
|
733
917
|
makeDetails: makeDetails("single"),
|
|
734
918
|
sessionDir: previousResult?.sessionDir ?? sessionDir,
|
|
919
|
+
sessionRoot: currentSubagentSessionRoot,
|
|
735
920
|
resumeSession: resumeExistingSession,
|
|
736
921
|
initialResult: previousResult,
|
|
922
|
+
fallbackModel,
|
|
737
923
|
});
|
|
738
924
|
|
|
739
925
|
if (isResultError(result)) {
|
|
@@ -774,6 +960,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
774
960
|
resumeResults: SingleResult[] | undefined,
|
|
775
961
|
getSessionDir: (index: number) => string,
|
|
776
962
|
resumeExistingSessions: boolean,
|
|
963
|
+
fallbackModel?: string,
|
|
777
964
|
) {
|
|
778
965
|
return executeParallelSubprocess(
|
|
779
966
|
tasks,
|
|
@@ -789,6 +976,8 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
789
976
|
resumeResults,
|
|
790
977
|
(index) => getSessionDir(index),
|
|
791
978
|
resumeExistingSessions,
|
|
979
|
+
currentSubagentSessionRoot,
|
|
980
|
+
fallbackModel,
|
|
792
981
|
);
|
|
793
982
|
}
|
|
794
983
|
}
|
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];
|