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 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. Starting another prompt from session_start races with it and
437
- // Pi correctly reports "agent is already processing". In that case we only
438
- // seed pendingResumePlan; before_agent_start injects the exact subagent
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) pendingResumePlan = null;
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
- const dir = buildSubagentSessionDir(root, currentSessionId, toolCallId, index);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
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(args: Record<string, any>, theme: { fg: ThemeFg; bold: (s: string) => string }): Text {
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 ${theme.fg("warning", "⏳")} ${theme.fg("accent", agent)}${theme.fg("dim", preview)}`;
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 (!result.model && msg.model) result.model = msg.model;
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];