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 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. 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.
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) pendingResumePlan = null;
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
- const dir = buildSubagentSessionDir(root, currentSessionId, toolCallId, index);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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];