pi-subagents 0.12.2 → 0.12.4

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.
@@ -23,7 +23,7 @@ import { discoverAvailableSkills, normalizeSkillInput } from "./skills.js";
23
23
  import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.js";
24
24
  import { createForkContextResolver } from "./fork-context.js";
25
25
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
26
- import { getFinalOutput, mapConcurrent } from "./utils.js";
26
+ import { getSingleResultOutput, mapConcurrent } from "./utils.js";
27
27
  import {
28
28
  cleanupWorktrees,
29
29
  createWorktrees,
@@ -46,6 +46,8 @@ import {
46
46
  MAX_CONCURRENCY,
47
47
  MAX_PARALLEL,
48
48
  checkSubagentDepth,
49
+ resolveChildMaxSubagentDepth,
50
+ resolveCurrentMaxSubagentDepth,
49
51
  wrapForkTask,
50
52
  } from "./types.js";
51
53
 
@@ -362,6 +364,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
362
364
  }
363
365
  const id = randomUUID();
364
366
  const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
367
+ const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
365
368
 
366
369
  if (hasChain && params.chain) {
367
370
  const normalized = normalizeSkillInput(params.skill);
@@ -379,6 +382,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
379
382
  sessionRoot,
380
383
  chainSkills,
381
384
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
385
+ maxSubagentDepth: currentMaxSubagentDepth,
386
+ worktreeSetupHook: deps.config.worktreeSetupHook,
387
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
382
388
  });
383
389
  }
384
390
 
@@ -395,6 +401,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
395
401
  const effectiveOutput: string | false | undefined = rawOutput === true ? a.output : (rawOutput as string | false | undefined);
396
402
  const normalizedSkills = normalizeSkillInput(params.skill);
397
403
  const skills = normalizedSkills === false ? [] : normalizedSkills;
404
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
398
405
  return executeAsyncSingle(id, {
399
406
  agent: params.agent!,
400
407
  task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
@@ -409,6 +416,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
409
416
  sessionFile: sessionFileForIndex(0),
410
417
  skills,
411
418
  output: effectiveOutput,
419
+ maxSubagentDepth,
420
+ worktreeSetupHook: deps.config.worktreeSetupHook,
421
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
412
422
  });
413
423
  }
414
424
 
@@ -433,6 +443,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
433
443
  const normalized = normalizeSkillInput(params.skill);
434
444
  const chainSkills = normalized === false ? [] : (normalized ?? []);
435
445
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
446
+ const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
436
447
  const chainResult = await executeChain({
437
448
  chain,
438
449
  task: params.task,
@@ -451,6 +462,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
451
462
  onUpdate,
452
463
  chainSkills,
453
464
  chainDir: params.chainDir,
465
+ maxSubagentDepth: currentMaxSubagentDepth,
466
+ worktreeSetupHook: deps.config.worktreeSetupHook,
467
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
454
468
  });
455
469
 
456
470
  if (chainResult.requestedAsync) {
@@ -476,6 +490,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
476
490
  sessionRoot,
477
491
  chainSkills: chainResult.requestedAsync.chainSkills,
478
492
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
493
+ maxSubagentDepth: currentMaxSubagentDepth,
494
+ worktreeSetupHook: deps.config.worktreeSetupHook,
495
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
479
496
  });
480
497
  }
481
498
 
@@ -496,6 +513,7 @@ interface ForegroundParallelRunInput {
496
513
  artifactsDir: string;
497
514
  maxOutput?: MaxOutputConfig;
498
515
  paramsCwd?: string;
516
+ maxSubagentDepths: number[];
499
517
  modelOverrides: (string | undefined)[];
500
518
  skillOverrides: (string[] | false | undefined)[];
501
519
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
@@ -517,11 +535,20 @@ function createParallelWorktreeSetup(
517
535
  enabled: boolean | undefined,
518
536
  cwd: string,
519
537
  runId: string,
520
- taskCount: number,
538
+ tasks: TaskParam[],
539
+ setupHook: ExtensionConfig["worktreeSetupHook"],
540
+ setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
521
541
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
522
542
  if (!enabled) return {};
523
543
  try {
524
- return { setup: createWorktrees(cwd, runId, taskCount) };
544
+ return {
545
+ setup: createWorktrees(cwd, runId, tasks.length, {
546
+ agents: tasks.map((task) => task.agent),
547
+ setupHook: setupHook
548
+ ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
549
+ : undefined,
550
+ }),
551
+ };
525
552
  } catch (error) {
526
553
  const message = error instanceof Error ? error.message : String(error);
527
554
  return { errorResult: buildParallelModeError(message) };
@@ -587,6 +614,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
587
614
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
588
615
  artifactConfig: input.artifactConfig,
589
616
  maxOutput: input.maxOutput,
617
+ maxSubagentDepth: input.maxSubagentDepths[index],
590
618
  modelOverride: input.modelOverrides[index],
591
619
  skills: effectiveSkills === false ? [] : effectiveSkills,
592
620
  onUpdate: input.onUpdate
@@ -652,6 +680,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
652
680
  agentConfigs.push(config);
653
681
  }
654
682
 
683
+ const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
684
+ const maxSubagentDepths = agentConfigs.map((config) =>
685
+ resolveChildMaxSubagentDepth(currentMaxSubagentDepth, config.maxSubagentDepth),
686
+ );
687
+
655
688
  const effectiveCwd = params.cwd ?? ctx.cwd;
656
689
  if (params.worktree) {
657
690
  const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(tasks, effectiveCwd);
@@ -733,6 +766,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
733
766
  sessionRoot,
734
767
  chainSkills: [],
735
768
  sessionFilesByFlatIndex: tasks.map((_, index) => sessionFileForIndex(index)),
769
+ maxSubagentDepth: currentMaxSubagentDepth,
770
+ worktreeSetupHook: deps.config.worktreeSetupHook,
771
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
736
772
  });
737
773
  }
738
774
  }
@@ -744,7 +780,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
744
780
  params.worktree,
745
781
  effectiveCwd,
746
782
  runId,
747
- tasks.length,
783
+ tasks,
784
+ deps.config.worktreeSetupHook,
785
+ deps.config.worktreeSetupHookTimeoutMs,
748
786
  );
749
787
  if (errorResult) return errorResult;
750
788
 
@@ -772,6 +810,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
772
810
  modelOverrides,
773
811
  skillOverrides,
774
812
  behaviors,
813
+ maxSubagentDepths,
775
814
  liveResults,
776
815
  liveProgress,
777
816
  onUpdate,
@@ -793,7 +832,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
793
832
  const aggregatedOutput = aggregateParallelOutputs(
794
833
  results.map((result) => ({
795
834
  agent: result.agent,
796
- output: result.truncation?.text || getFinalOutput(result.messages),
835
+ output: result.truncation?.text || getSingleResultOutput(result),
797
836
  exitCode: result.exitCode,
798
837
  error: result.error,
799
838
  })),
@@ -850,6 +889,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
850
889
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
851
890
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
852
891
  let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : (rawOutput as string | false | undefined);
892
+ const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
893
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
853
894
 
854
895
  if (params.clarify === true && ctx.hasUI) {
855
896
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
@@ -912,6 +953,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
912
953
  sessionFile: sessionFileForIndex(0),
913
954
  skills: skillOverride === false ? [] : skillOverride,
914
955
  output: effectiveOutput,
956
+ maxSubagentDepth,
957
+ worktreeSetupHook: deps.config.worktreeSetupHook,
958
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
915
959
  });
916
960
  }
917
961
  }
@@ -940,6 +984,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
940
984
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
941
985
  artifactConfig,
942
986
  maxOutput: params.maxOutput,
987
+ outputPath,
988
+ maxSubagentDepth,
943
989
  onUpdate,
944
990
  modelOverride,
945
991
  skills: effectiveSkills,
@@ -949,12 +995,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
949
995
  if (r.progress) allProgress.push(r.progress);
950
996
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
951
997
 
952
- const fullOutput = getFinalOutput(r.messages);
998
+ const fullOutput = getSingleResultOutput(r);
953
999
  const finalizedOutput = finalizeSingleOutput({
954
1000
  fullOutput,
955
1001
  truncatedOutput: r.truncation?.text,
956
1002
  outputPath,
957
1003
  exitCode: r.exitCode,
1004
+ savedPath: r.savedOutputPath,
1005
+ saveError: r.outputSaveError,
958
1006
  });
959
1007
 
960
1008
  if (r.exitCode !== 0)
@@ -1010,7 +1058,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1010
1058
  return handleManagementAction(params.action, params, ctx);
1011
1059
  }
1012
1060
 
1013
- const { blocked, depth, maxDepth } = checkSubagentDepth();
1061
+ const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
1014
1062
  if (blocked) {
1015
1063
  return {
1016
1064
  content: [
@@ -3,9 +3,9 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
- import { appendJsonl, getArtifactPaths } from "./artifacts.js";
7
- import { getPiSpawnCommand } from "./pi-spawn.js";
8
- import { persistSingleOutput } from "./single-output.js";
6
+ import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
7
+ import { getPiSpawnCommand } from "./pi-spawn.ts";
8
+ import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
9
9
  import {
10
10
  type ArtifactConfig,
11
11
  type ArtifactPaths,
@@ -13,7 +13,7 @@ import {
13
13
  type MaxOutputConfig,
14
14
  truncateOutput,
15
15
  getSubagentDepthEnv,
16
- } from "./types.js";
16
+ } from "./types.ts";
17
17
  import {
18
18
  type RunnerSubagentStep as SubagentStep,
19
19
  type RunnerStep,
@@ -22,8 +22,8 @@ import {
22
22
  mapConcurrent,
23
23
  aggregateParallelOutputs,
24
24
  MAX_PARALLEL_CONCURRENCY,
25
- } from "./parallel-utils.js";
26
- import { buildPiArgs, cleanupTempDir } from "./pi-args.js";
25
+ } from "./parallel-utils.ts";
26
+ import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
27
27
  import {
28
28
  cleanupWorktrees,
29
29
  createWorktrees,
@@ -32,7 +32,7 @@ import {
32
32
  formatWorktreeDiffSummary,
33
33
  formatWorktreeTaskCwdConflict,
34
34
  type WorktreeSetup,
35
- } from "./worktree.js";
35
+ } from "./worktree.ts";
36
36
 
37
37
  interface SubagentRunConfig {
38
38
  id: string;
@@ -50,6 +50,8 @@ interface SubagentRunConfig {
50
50
  asyncDir: string;
51
51
  sessionId?: string | null;
52
52
  piPackageRoot?: string;
53
+ worktreeSetupHook?: string;
54
+ worktreeSetupHookTimeoutMs?: number;
53
55
  }
54
56
 
55
57
  interface StepResult {
@@ -116,10 +118,11 @@ function runPiStreaming(
116
118
  outputFile: string,
117
119
  env?: Record<string, string | undefined>,
118
120
  piPackageRoot?: string,
121
+ maxSubagentDepth?: number,
119
122
  ): Promise<{ stdout: string; exitCode: number | null }> {
120
123
  return new Promise((resolve) => {
121
124
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
122
- const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv() };
125
+ const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
123
126
  const spawnSpec = getPiSpawnCommand(args, piPackageRoot ? { piPackageRoot } : undefined);
124
127
  const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
125
128
  let stdout = "";
@@ -307,6 +310,7 @@ async function runSingleStep(
307
310
  const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
308
311
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
309
312
  const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
313
+ const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
310
314
  const { args, env, tempDir } = buildPiArgs({
311
315
  baseArgs: ["-p"],
312
316
  task,
@@ -332,22 +336,23 @@ async function runSingleStep(
332
336
  }
333
337
  }
334
338
 
335
- const result = await runPiStreaming(args, step.cwd ?? ctx.cwd, ctx.outputFile, env, ctx.piPackageRoot);
339
+ const result = await runPiStreaming(args, step.cwd ?? ctx.cwd, ctx.outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
336
340
  cleanupTempDir(tempDir);
337
341
 
338
- const output = (result.stdout || "").trim();
342
+ const rawOutput = (result.stdout || "").trim();
343
+ const resolvedOutput = step.outputPath && result.exitCode === 0
344
+ ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
345
+ : { fullOutput: rawOutput };
346
+ const output = resolvedOutput.fullOutput;
339
347
  let outputForSummary = output;
340
- if (step.outputPath && result.exitCode === 0) {
341
- const persisted = persistSingleOutput(step.outputPath, output);
342
- if (persisted.savedPath) {
343
- outputForSummary = output
344
- ? `${output}\n\nšŸ“„ Output saved to: ${persisted.savedPath}`
345
- : `šŸ“„ Output saved to: ${persisted.savedPath}`;
346
- } else if (persisted.error) {
347
- outputForSummary = output
348
- ? `${output}\n\nāš ļø Failed to save output to: ${step.outputPath}\n${persisted.error}`
349
- : `āš ļø Failed to save output to: ${step.outputPath}\n${persisted.error}`;
350
- }
348
+ if (resolvedOutput.savedPath) {
349
+ outputForSummary = output
350
+ ? `${output}\n\nšŸ“„ Output saved to: ${resolvedOutput.savedPath}`
351
+ : `šŸ“„ Output saved to: ${resolvedOutput.savedPath}`;
352
+ } else if (resolvedOutput.saveError && step.outputPath && result.exitCode === 0) {
353
+ outputForSummary = output
354
+ ? `${output}\n\nāš ļø Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
355
+ : `āš ļø Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`;
351
356
  }
352
357
 
353
358
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
@@ -580,7 +585,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
580
585
  break;
581
586
  }
582
587
  try {
583
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length);
588
+ worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, {
589
+ agents: group.parallel.map((task) => task.agent),
590
+ setupHook: config.worktreeSetupHook
591
+ ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
592
+ : undefined,
593
+ });
584
594
  } catch (error) {
585
595
  const setupError = error instanceof Error ? error.message : String(error);
586
596
  const failedAt = Date.now();
package/types.ts CHANGED
@@ -97,6 +97,9 @@ export interface SingleResult {
97
97
  progressSummary?: ProgressSummary;
98
98
  artifactPaths?: ArtifactPaths;
99
99
  truncation?: TruncationResult;
100
+ finalOutput?: string;
101
+ savedOutputPath?: string;
102
+ outputSaveError?: string;
100
103
  }
101
104
 
102
105
  export interface Details {
@@ -230,6 +233,8 @@ export interface RunSyncOptions {
230
233
  sessionDir?: string;
231
234
  sessionFile?: string;
232
235
  share?: boolean;
236
+ outputPath?: string;
237
+ maxSubagentDepth?: number;
233
238
  /** Override the agent's default model (format: "provider/id" or just "id") */
234
239
  modelOverride?: string;
235
240
  /** Skills to inject (overrides agent default if provided) */
@@ -239,6 +244,9 @@ export interface RunSyncOptions {
239
244
  export interface ExtensionConfig {
240
245
  asyncByDefault?: boolean;
241
246
  defaultSessionDir?: string;
247
+ maxSubagentDepth?: number;
248
+ worktreeSetupHook?: string;
249
+ worktreeSetupHookTimeoutMs?: number;
242
250
  }
243
251
 
244
252
  // ============================================================================
@@ -291,19 +299,37 @@ export function wrapForkTask(task: string, preamble?: string | false): string {
291
299
  // Recursion Depth Guard
292
300
  // ============================================================================
293
301
 
294
- export function checkSubagentDepth(): { blocked: boolean; depth: number; maxDepth: number } {
302
+ export function normalizeMaxSubagentDepth(value: unknown): number | undefined {
303
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
304
+ if (!Number.isInteger(parsed) || parsed < 0) return undefined;
305
+ return parsed;
306
+ }
307
+
308
+ export function resolveCurrentMaxSubagentDepth(configMaxDepth?: number): number {
309
+ return normalizeMaxSubagentDepth(process.env.PI_SUBAGENT_MAX_DEPTH)
310
+ ?? normalizeMaxSubagentDepth(configMaxDepth)
311
+ ?? DEFAULT_SUBAGENT_MAX_DEPTH;
312
+ }
313
+
314
+ export function resolveChildMaxSubagentDepth(parentMaxDepth: number, agentMaxDepth?: number): number {
315
+ const normalizedParent = normalizeMaxSubagentDepth(parentMaxDepth) ?? DEFAULT_SUBAGENT_MAX_DEPTH;
316
+ const normalizedAgent = normalizeMaxSubagentDepth(agentMaxDepth);
317
+ return normalizedAgent === undefined ? normalizedParent : Math.min(normalizedParent, normalizedAgent);
318
+ }
319
+
320
+ export function checkSubagentDepth(configMaxDepth?: number): { blocked: boolean; depth: number; maxDepth: number } {
295
321
  const depth = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
296
- const maxDepth = Number(process.env.PI_SUBAGENT_MAX_DEPTH ?? String(DEFAULT_SUBAGENT_MAX_DEPTH));
297
- const blocked = Number.isFinite(depth) && Number.isFinite(maxDepth) && depth >= maxDepth;
322
+ const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth);
323
+ const blocked = Number.isFinite(depth) && depth >= maxDepth;
298
324
  return { blocked, depth, maxDepth };
299
325
  }
300
326
 
301
- export function getSubagentDepthEnv(): Record<string, string> {
327
+ export function getSubagentDepthEnv(maxDepth?: number): Record<string, string> {
302
328
  const parentDepth = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
303
329
  const nextDepth = Number.isFinite(parentDepth) ? parentDepth + 1 : 1;
304
330
  return {
305
331
  PI_SUBAGENT_DEPTH: String(nextDepth),
306
- PI_SUBAGENT_MAX_DEPTH: process.env.PI_SUBAGENT_MAX_DEPTH ?? String(DEFAULT_SUBAGENT_MAX_DEPTH),
332
+ PI_SUBAGENT_MAX_DEPTH: String(normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth()),
307
333
  };
308
334
  }
309
335
 
package/utils.ts CHANGED
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import type { Message } from "@mariozechner/pi-ai";
9
- import type { AsyncStatus, DisplayItem, ErrorInfo } from "./types.js";
9
+ import type { AsyncStatus, DisplayItem, ErrorInfo, SingleResult } from "./types.ts";
10
10
 
11
11
  // ============================================================================
12
12
  // File System Utilities
@@ -200,6 +200,10 @@ export function getFinalOutput(messages: Message[]): string {
200
200
  return "";
201
201
  }
202
202
 
203
+ export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" | "messages">): string {
204
+ return result.finalOutput ?? getFinalOutput(result.messages);
205
+ }
206
+
203
207
  /**
204
208
  * Extract display items (text and tool calls) from messages
205
209
  */