pi-subagents 0.21.2 → 0.21.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.
@@ -7,7 +7,7 @@ import type { Message } from "@mariozechner/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
8
  import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
9
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
10
- import { captureSingleOutputSnapshot, resolveSingleOutput } from "../shared/single-output.ts";
10
+ import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput } from "../shared/single-output.ts";
11
11
  import {
12
12
  type ActivityState,
13
13
  type ArtifactConfig,
@@ -662,19 +662,21 @@ async function runSingleStep(
662
662
  ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
663
663
  : { fullOutput: rawOutput };
664
664
  const output = resolvedOutput.fullOutput;
665
+ const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
665
666
  let outputForSummary = output;
666
667
  if (attemptNotes.length > 0) {
667
668
  outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
668
669
  }
669
- if (resolvedOutput.savedPath) {
670
- outputForSummary = outputForSummary
671
- ? `${outputForSummary}\n\nOutput saved to: ${resolvedOutput.savedPath}`
672
- : `Output saved to: ${resolvedOutput.savedPath}`;
673
- } else if (resolvedOutput.saveError && step.outputPath && finalResult?.exitCode === 0) {
674
- outputForSummary = outputForSummary
675
- ? `${outputForSummary}\n\nFailed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
676
- : `Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`;
677
- }
670
+ const finalizedOutput = finalizeSingleOutput({
671
+ fullOutput: outputForSummary,
672
+ outputPath: step.outputPath,
673
+ outputMode: step.outputMode,
674
+ exitCode: finalResult?.exitCode ?? 1,
675
+ savedPath: resolvedOutput.savedPath,
676
+ outputReference,
677
+ saveError: resolvedOutput.saveError,
678
+ });
679
+ outputForSummary = finalizedOutput.displayOutput;
678
680
 
679
681
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
680
682
  if (ctx.artifactConfig?.includeOutput !== false) {
@@ -240,6 +240,7 @@ export class ChainClarifyComponent implements Component {
240
240
 
241
241
  return {
242
242
  output: override.output !== undefined ? override.output : base.output,
243
+ outputMode: base.outputMode,
243
244
  reads: override.reads !== undefined ? override.reads : base.reads,
244
245
  progress: override.progress !== undefined ? override.progress : base.progress,
245
246
  skills: override.skills !== undefined ? override.skills : base.skills,
@@ -277,6 +278,7 @@ export class ChainClarifyComponent implements Component {
277
278
  const template = this.templates[i] ?? "";
278
279
  const step: ChainStepConfig = { agent: agent.name, task: template };
279
280
  if (override?.output !== undefined) step.output = behavior.output;
281
+ if (behavior.outputMode !== "inline") step.outputMode = behavior.outputMode;
280
282
  if (override?.reads !== undefined) step.reads = behavior.reads;
281
283
  if (override?.model !== undefined) step.model = behavior.model;
282
284
  if (override?.skills !== undefined) step.skills = behavior.skills;
@@ -55,6 +55,7 @@ import {
55
55
  resolveChildMaxSubagentDepth,
56
56
  } from "../../shared/types.ts";
57
57
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
58
+ import { validateFileOnlyOutputMode } from "../shared/single-output.ts";
58
59
 
59
60
  interface ChainExecutionDetailsInput {
60
61
  results: SingleResult[];
@@ -234,6 +235,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
234
235
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
235
236
  artifactConfig: input.artifactConfig,
236
237
  outputPath,
238
+ outputMode: behavior.outputMode,
237
239
  maxSubagentDepth,
238
240
  controlConfig: input.controlConfig,
239
241
  onControlEvent: input.onControlEvent,
@@ -412,6 +414,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
412
414
 
413
415
  const stepOverrides: StepOverrides[] = seqSteps.map((step) => ({
414
416
  output: step.output,
417
+ outputMode: step.outputMode,
415
418
  reads: step.reads,
416
419
  progress: step.progress,
417
420
  skills: normalizeSkillInput(step.skill),
@@ -462,6 +465,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
462
465
  task: result.templates[i]!,
463
466
  ...(override?.model ? { model: override.model } : {}),
464
467
  ...(override?.output !== undefined ? { output: override.output } : {}),
468
+ ...("outputMode" in step && step.outputMode !== undefined ? { outputMode: step.outputMode } : {}),
465
469
  ...(override?.reads !== undefined ? { reads: override.reads } : {}),
466
470
  ...(override?.progress !== undefined ? { progress: override.progress } : {}),
467
471
  ...(override?.skills !== undefined ? { skill: override.skills } : {}),
@@ -533,6 +537,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
533
537
  try {
534
538
  const agentNames = step.parallel.map((task) => task.agent);
535
539
  const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills);
540
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
541
+ const behavior = parallelBehaviors[taskIndex]!;
542
+ const outputPath = typeof behavior.output === "string"
543
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
544
+ : undefined;
545
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
546
+ if (validationError) return buildChainExecutionErrorResult(validationError, {
547
+ results,
548
+ includeProgress,
549
+ allProgress,
550
+ allArtifactPaths,
551
+ artifactsDir,
552
+ chainAgents,
553
+ totalSteps,
554
+ currentStepIndex: stepIndex,
555
+ });
556
+ }
536
557
  progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
537
558
  createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
538
559
 
@@ -664,6 +685,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
664
685
  const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
665
686
  const stepOverride: StepOverrides = {
666
687
  output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
688
+ outputMode: seqStep.outputMode,
667
689
  reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
668
690
  progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
669
691
  skills:
@@ -701,6 +723,19 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
701
723
  const outputPath = typeof behavior.output === "string"
702
724
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
703
725
  : undefined;
726
+ const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
727
+ if (validationError) {
728
+ return buildChainExecutionErrorResult(validationError, {
729
+ results,
730
+ includeProgress,
731
+ allProgress,
732
+ allArtifactPaths,
733
+ artifactsDir,
734
+ chainAgents,
735
+ totalSteps,
736
+ currentStepIndex: stepIndex,
737
+ });
738
+ }
704
739
  const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
705
740
  const interruptController = new AbortController();
706
741
  if (foregroundControl) {
@@ -731,6 +766,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
731
766
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
732
767
  artifactConfig,
733
768
  outputPath,
769
+ outputMode: behavior.outputMode,
734
770
  maxSubagentDepth,
735
771
  controlConfig,
736
772
  onControlEvent,
@@ -46,7 +46,7 @@ import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
46
46
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
47
47
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
48
48
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
49
- import { captureSingleOutputSnapshot, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
49
+ import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
50
50
  import {
51
51
  buildModelCandidates,
52
52
  formatModelAttemptNote,
@@ -64,6 +64,8 @@ import {
64
64
  summarizeRecentMutatingFailures,
65
65
  } from "../shared/long-running-guard.ts";
66
66
 
67
+ const artifactOutputByResult = new WeakMap<SingleResult, string>();
68
+
67
69
  function emptyUsage(): Usage {
68
70
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
69
71
  }
@@ -97,7 +99,7 @@ function snapshotProgress(progress: AgentProgress): AgentProgress {
97
99
  function snapshotResult(result: SingleResult, progress: AgentProgress): SingleResult {
98
100
  return {
99
101
  ...result,
100
- messages: result.messages ? [...result.messages] : undefined,
102
+ messages: result.outputMode === "file-only" && result.savedOutputPath ? undefined : result.messages ? [...result.messages] : undefined,
101
103
  usage: { ...result.usage },
102
104
  skills: result.skills ? [...result.skills] : undefined,
103
105
  attemptedModels: result.attemptedModels ? [...result.attemptedModels] : undefined,
@@ -112,6 +114,7 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
112
114
  progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
113
115
  artifactPaths: result.artifactPaths ? { ...result.artifactPaths } : undefined,
114
116
  truncation: result.truncation ? { ...result.truncation } : undefined,
117
+ outputReference: result.outputReference ? { ...result.outputReference } : undefined,
115
118
  };
116
119
  }
117
120
 
@@ -676,8 +679,15 @@ async function runSingleAttempt(
676
679
  fullOutput = resolvedOutput.fullOutput;
677
680
  result.savedOutputPath = resolvedOutput.savedPath;
678
681
  result.outputSaveError = resolvedOutput.saveError;
682
+ if (resolvedOutput.savedPath) {
683
+ result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
684
+ }
679
685
  }
680
- result.finalOutput = fullOutput;
686
+ artifactOutputByResult.set(result, fullOutput);
687
+ result.outputMode = options.outputMode ?? "inline";
688
+ result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
689
+ ? result.outputReference.message
690
+ : fullOutput;
681
691
  result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
682
692
  if (options.onUpdate) {
683
693
  const finalText = result.finalOutput || result.error || "(no output)";
@@ -717,6 +727,18 @@ export async function runSync(
717
727
  error: `Unknown agent: ${agentName}`,
718
728
  };
719
729
  }
730
+ const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
731
+ if (outputModeValidationError) {
732
+ return {
733
+ agent: agentName,
734
+ task,
735
+ exitCode: 1,
736
+ messages: [],
737
+ usage: emptyUsage(),
738
+ outputMode: options.outputMode,
739
+ error: outputModeValidationError,
740
+ };
741
+ }
720
742
 
721
743
  const shareEnabled = options.share === true;
722
744
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
@@ -829,7 +851,7 @@ export async function runSync(
829
851
  if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
830
852
  result.artifactPaths = artifactPathsResult;
831
853
  if (options.artifactConfig?.includeOutput !== false) {
832
- writeArtifact(artifactPathsResult.outputPath, result.finalOutput ?? "");
854
+ writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
833
855
  }
834
856
  if (options.artifactConfig?.includeMetadata !== false) {
835
857
  writeMetadata(artifactPathsResult.metadataPath, {
@@ -10,6 +10,7 @@ import { executeChain } from "./chain-execution.ts";
10
10
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
11
11
  import { handleManagementAction } from "../../agents/agent-management.ts";
12
12
  import { buildDoctorReport } from "../../extension/doctor.ts";
13
+ import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
13
14
  import { runSync } from "./execution.ts";
14
15
  import { resolveModelCandidate } from "../shared/model-fallback.ts";
15
16
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
@@ -30,7 +31,7 @@ import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "../back
30
31
  import { createForkContextResolver } from "../../shared/fork-context.ts";
31
32
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
32
33
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
33
- import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "../shared/single-output.ts";
34
+ import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
34
35
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
35
36
  import {
36
37
  buildSubagentResultIntercomPayload,
@@ -85,6 +86,7 @@ interface TaskParam {
85
86
  cwd?: string;
86
87
  count?: number;
87
88
  output?: string | boolean;
89
+ outputMode?: "inline" | "file-only";
88
90
  reads?: string[] | boolean;
89
91
  progress?: boolean;
90
92
  model?: string;
@@ -117,6 +119,7 @@ export interface SubagentParamsLike {
117
119
  model?: string;
118
120
  skill?: string | string[] | boolean;
119
121
  output?: string | boolean;
122
+ outputMode?: "inline" | "file-only";
120
123
  agentScope?: unknown;
121
124
  chainDir?: string;
122
125
  }
@@ -800,6 +803,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
800
803
  ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
801
804
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
802
805
  ...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}),
806
+ ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
803
807
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
804
808
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
805
809
  }));
@@ -867,6 +871,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
867
871
  }
868
872
  const rawOutput = params.output !== undefined ? params.output : a.output;
869
873
  const effectiveOutput: string | false | undefined = rawOutput === true ? a.output : (rawOutput as string | false | undefined);
874
+ const effectiveOutputMode = params.outputMode ?? "inline";
870
875
  const normalizedSkills = normalizeSkillInput(params.skill);
871
876
  const skills = normalizedSkills === false ? [] : normalizedSkills;
872
877
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
@@ -886,6 +891,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
886
891
  sessionFile: sessionFileForIndex(0),
887
892
  skills,
888
893
  output: effectiveOutput,
894
+ outputMode: effectiveOutputMode,
889
895
  modelOverride,
890
896
  maxSubagentDepth,
891
897
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1190,6 +1196,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1190
1196
  artifactConfig: input.artifactConfig,
1191
1197
  maxOutput: input.maxOutput,
1192
1198
  outputPath,
1199
+ outputMode: behavior?.outputMode,
1193
1200
  maxSubagentDepth: input.maxSubagentDepths[index],
1194
1201
  controlConfig: input.controlConfig,
1195
1202
  onControlEvent: input.onControlEvent,
@@ -1309,6 +1316,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1309
1316
  );
1310
1317
  const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
1311
1318
  ...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}),
1319
+ ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
1312
1320
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1313
1321
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
1314
1322
  ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
@@ -1384,6 +1392,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1384
1392
  ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
1385
1393
  ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
1386
1394
  ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
1395
+ ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1387
1396
  ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1388
1397
  ...(behaviorOverrides[i]?.progress !== undefined ? { progress: behaviorOverrides[i]!.progress } : {}),
1389
1398
  }));
@@ -1435,6 +1444,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1435
1444
  worktreeSetup,
1436
1445
  });
1437
1446
  if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
1447
+ for (let index = 0; index < tasks.length; index++) {
1448
+ const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
1449
+ const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd);
1450
+ const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, outputPath, `Parallel task ${index + 1} (${tasks[index]!.agent})`);
1451
+ if (validationError) return buildParallelModeError(validationError);
1452
+ }
1438
1453
 
1439
1454
  const parallelProgressPrecreated = firstProgressIndex !== -1;
1440
1455
  if (parallelProgressPrecreated) writeInitialProgressFile(effectiveCwd);
@@ -1585,6 +1600,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1585
1600
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
1586
1601
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
1587
1602
  let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : (rawOutput as string | false | undefined);
1603
+ const effectiveOutputMode = params.outputMode ?? "inline";
1588
1604
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1589
1605
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
1590
1606
 
@@ -1650,6 +1666,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1650
1666
  sessionFile: sessionFileForIndex(0),
1651
1667
  skills: skillOverride === false ? [] : skillOverride,
1652
1668
  output: effectiveOutput,
1669
+ outputMode: effectiveOutputMode,
1653
1670
  modelOverride,
1654
1671
  maxSubagentDepth,
1655
1672
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1666,6 +1683,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1666
1683
  }
1667
1684
  const cleanTask = task;
1668
1685
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd);
1686
+ const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
1687
+ if (validationError) {
1688
+ return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
1689
+ }
1669
1690
  task = injectSingleOutputInstruction(task, outputPath);
1670
1691
 
1671
1692
  let effectiveSkills: string[] | undefined;
@@ -1724,6 +1745,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1724
1745
  artifactConfig,
1725
1746
  maxOutput: params.maxOutput,
1726
1747
  outputPath,
1748
+ outputMode: effectiveOutputMode,
1727
1749
  maxSubagentDepth,
1728
1750
  onUpdate: forwardSingleUpdate,
1729
1751
  controlConfig,
@@ -1757,8 +1779,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1757
1779
  fullOutput,
1758
1780
  truncatedOutput: r.truncation?.text,
1759
1781
  outputPath,
1782
+ outputMode: r.outputMode,
1760
1783
  exitCode: r.exitCode,
1761
1784
  savedPath: r.savedOutputPath,
1785
+ outputReference: r.outputReference,
1762
1786
  saveError: r.outputSaveError,
1763
1787
  });
1764
1788
  const details = compactForegroundDetails({
@@ -2067,6 +2091,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2067
2091
  return toExecutionErrorResult(effectiveParams, error);
2068
2092
  } finally {
2069
2093
  if (foregroundControl) {
2094
+ clearPendingForegroundControlNotices(deps.state, runId);
2070
2095
  deps.state.foregroundControls.delete(runId);
2071
2096
  if (deps.state.lastForegroundControlId === runId) {
2072
2097
  deps.state.lastForegroundControlId = null;
@@ -13,6 +13,7 @@ export interface RunnerSubagentStep {
13
13
  inheritSkills: boolean;
14
14
  skills?: string[];
15
15
  outputPath?: string;
16
+ outputMode?: "inline" | "file-only";
16
17
  sessionFile?: string;
17
18
  maxSubagentDepth?: number;
18
19
  }
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import type { OutputMode, SavedOutputReference } from "../../shared/types.ts";
3
4
 
4
5
  export interface SingleOutputSnapshot {
5
6
  exists: boolean;
@@ -25,6 +26,43 @@ export function injectSingleOutputInstruction(task: string, outputPath: string |
25
26
  return `${task}\n\n---\n**Output:** Write your findings to: ${outputPath}`;
26
27
  }
27
28
 
29
+ function countLines(text: string): number {
30
+ if (!text) return 0;
31
+ const newlineMatches = text.match(/\r\n|\r|\n/g);
32
+ return (newlineMatches?.length ?? 0) + (/[\r\n]$/.test(text) ? 0 : 1);
33
+ }
34
+
35
+ function formatByteSize(bytes: number): string {
36
+ if (bytes < 1024) return `${bytes} B`;
37
+ const units = ["KB", "MB", "GB", "TB"];
38
+ let value = bytes / 1024;
39
+ let unitIndex = 0;
40
+ while (value >= 1024 && unitIndex < units.length - 1) {
41
+ value /= 1024;
42
+ unitIndex++;
43
+ }
44
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
45
+ }
46
+
47
+ export function formatSavedOutputReference(savedPath: string, fullOutput: string): SavedOutputReference {
48
+ const absolutePath = path.resolve(savedPath);
49
+ const bytes = Buffer.byteLength(fullOutput, "utf-8");
50
+ const lines = countLines(fullOutput);
51
+ return {
52
+ path: absolutePath,
53
+ bytes,
54
+ lines,
55
+ message: `Output saved to: ${absolutePath} (${formatByteSize(bytes)}, ${lines} ${lines === 1 ? "line" : "lines"}). Read this file if needed.`,
56
+ };
57
+ }
58
+
59
+ export function validateFileOnlyOutputMode(outputMode: OutputMode | undefined, outputPath: string | undefined, context: string): string | undefined {
60
+ if (outputMode === "file-only" && !outputPath) {
61
+ return `${context} sets outputMode: "file-only" but does not configure an output file. Set output to a path or use outputMode: "inline".`;
62
+ }
63
+ return undefined;
64
+ }
65
+
28
66
  export function captureSingleOutputSnapshot(outputPath: string | undefined): SingleOutputSnapshot | undefined {
29
67
  if (!outputPath) return undefined;
30
68
  try {
@@ -78,14 +116,20 @@ export function finalizeSingleOutput(params: {
78
116
  fullOutput: string;
79
117
  truncatedOutput?: string;
80
118
  outputPath?: string;
119
+ outputMode?: OutputMode;
81
120
  exitCode: number;
82
121
  savedPath?: string;
122
+ outputReference?: SavedOutputReference;
83
123
  saveError?: string;
84
- }): { displayOutput: string; savedPath?: string; saveError?: string } {
124
+ }): { displayOutput: string; savedPath?: string; outputReference?: SavedOutputReference; saveError?: string } {
85
125
  let displayOutput = params.truncatedOutput || params.fullOutput;
86
126
  if (params.exitCode === 0 && params.savedPath) {
87
- displayOutput += `\n\nOutput saved to: ${params.savedPath}`;
88
- return { displayOutput, savedPath: params.savedPath };
127
+ const outputReference = params.outputReference ?? formatSavedOutputReference(params.savedPath, params.fullOutput);
128
+ if (params.outputMode === "file-only") {
129
+ return { displayOutput: outputReference.message, savedPath: params.savedPath, outputReference };
130
+ }
131
+ displayOutput += `\n\n${outputReference.message}`;
132
+ return { displayOutput, savedPath: params.savedPath, outputReference };
89
133
  }
90
134
  if (params.exitCode === 0 && params.saveError && params.outputPath) {
91
135
  displayOutput += `\n\nFailed to save output to: ${params.outputPath}\n${params.saveError}`;
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { AgentConfig } from "../agents/agents.ts";
8
8
  import { normalizeSkillInput } from "../agents/skills.ts";
9
- import { CHAIN_RUNS_DIR } from "./types.ts";
9
+ import { CHAIN_RUNS_DIR, type OutputMode } from "./types.ts";
10
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
11
11
  const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
12
12
 
@@ -16,6 +16,7 @@ const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tas
16
16
 
17
17
  export interface ResolvedStepBehavior {
18
18
  output: string | false;
19
+ outputMode: OutputMode;
19
20
  reads: string[] | false;
20
21
  progress: boolean;
21
22
  skills: string[] | false;
@@ -24,6 +25,7 @@ export interface ResolvedStepBehavior {
24
25
 
25
26
  export interface StepOverrides {
26
27
  output?: string | false;
28
+ outputMode?: OutputMode;
27
29
  reads?: string[] | false;
28
30
  progress?: boolean;
29
31
  skills?: string[] | false;
@@ -44,6 +46,7 @@ export interface SequentialStep {
44
46
  task?: string;
45
47
  cwd?: string;
46
48
  output?: string | false;
49
+ outputMode?: OutputMode;
47
50
  reads?: string[] | false;
48
51
  progress?: boolean;
49
52
  skill?: string | string[] | false;
@@ -57,6 +60,7 @@ interface ParallelTaskItem {
57
60
  cwd?: string;
58
61
  count?: number;
59
62
  output?: string | false;
63
+ outputMode?: OutputMode;
60
64
  reads?: string[] | false;
61
65
  progress?: boolean;
62
66
  skill?: string | string[] | false;
@@ -211,8 +215,9 @@ export function resolveStepBehavior(
211
215
  }
212
216
  }
213
217
 
218
+ const outputMode = stepOverrides.outputMode ?? "inline";
214
219
  const model = stepOverrides.model ?? agentConfig.model;
215
- return { output, reads, progress, skills, model };
220
+ return { output, outputMode, reads, progress, skills, model };
216
221
  }
217
222
 
218
223
  // =============================================================================
@@ -348,8 +353,9 @@ export function resolveParallelBehaviors(
348
353
  }
349
354
  }
350
355
 
356
+ const outputMode = task.outputMode ?? "inline";
351
357
  const model = task.model ?? config.model;
352
- return { output, reads, progress, skills, model };
358
+ return { output, outputMode, reads, progress, skills, model };
353
359
  });
354
360
  }
355
361
 
@@ -17,6 +17,15 @@ export interface MaxOutputConfig {
17
17
  lines?: number;
18
18
  }
19
19
 
20
+ export type OutputMode = "inline" | "file-only";
21
+
22
+ export interface SavedOutputReference {
23
+ path: string;
24
+ bytes: number;
25
+ lines: number;
26
+ message: string;
27
+ }
28
+
20
29
  interface TruncationResult {
21
30
  text: string;
22
31
  truncated: boolean;
@@ -189,7 +198,9 @@ export interface SingleResult {
189
198
  artifactPaths?: ArtifactPaths;
190
199
  truncation?: TruncationResult;
191
200
  finalOutput?: string;
201
+ outputMode?: OutputMode;
192
202
  savedOutputPath?: string;
203
+ outputReference?: SavedOutputReference;
193
204
  outputSaveError?: string;
194
205
  }
195
206
 
@@ -356,6 +367,7 @@ export interface SubagentState {
356
367
  interrupt?: () => boolean;
357
368
  }>;
358
369
  lastForegroundControlId: string | null;
370
+ pendingForegroundControlNotices?: Map<string, ReturnType<typeof setTimeout>>;
359
371
  cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
360
372
  lastUiContext: ExtensionContext | null;
361
373
  poller: NodeJS.Timeout | null;
@@ -424,6 +436,7 @@ export interface RunSyncOptions {
424
436
  sessionFile?: string;
425
437
  share?: boolean;
426
438
  outputPath?: string;
439
+ outputMode?: OutputMode;
427
440
  maxSubagentDepth?: number;
428
441
  /** Override the agent's default model (format: "provider/id" or just "id") */
429
442
  modelOverride?: string;
@@ -30,6 +30,7 @@ import {
30
30
 
31
31
  interface InlineConfig {
32
32
  output?: string | false;
33
+ outputMode?: "inline" | "file-only";
33
34
  reads?: string[] | false;
34
35
  model?: string;
35
36
  skill?: string[] | false;
@@ -50,6 +51,7 @@ const parseInlineConfig = (raw: string): InlineConfig => {
50
51
  const val = trimmed.slice(eq + 1).trim();
51
52
  switch (key) {
52
53
  case "output": config.output = val === "false" ? false : val; break;
54
+ case "outputMode": if (val === "inline" || val === "file-only") config.outputMode = val; break;
53
55
  case "reads": config.reads = val === "false" ? false : val.split("+").filter(Boolean); break;
54
56
  case "model": config.model = val || undefined; break;
55
57
  case "skill": case "skills": config.skill = val === "false" ? false : val.split("+").filter(Boolean); break;
@@ -131,6 +133,7 @@ const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] =
131
133
  agent: step.agent,
132
134
  task: step.task || undefined,
133
135
  output: step.output,
136
+ outputMode: step.outputMode,
134
137
  reads: step.reads,
135
138
  progress: step.progress,
136
139
  skill: step.skill ?? step.skills,
@@ -479,6 +482,7 @@ export function registerSlashCommands(
479
482
  }
480
483
  const params: SubagentParamsLike = { agent: agentName, task: finalTask, clarify: false, agentScope: "both" };
481
484
  if (inline.output !== undefined) params.output = inline.output;
485
+ if (inline.outputMode !== undefined) params.outputMode = inline.outputMode;
482
486
  if (inline.skill !== undefined) params.skill = inline.skill;
483
487
  if (inline.model) params.model = inline.model;
484
488
  if (bg) params.async = true;
@@ -498,6 +502,7 @@ export function registerSlashCommands(
498
502
  agent: name,
499
503
  ...(stepTask ? { task: stepTask } : i === 0 && parsed.task ? { task: parsed.task } : {}),
500
504
  ...(config.output !== undefined ? { output: config.output } : {}),
505
+ ...(config.outputMode !== undefined ? { outputMode: config.outputMode } : {}),
501
506
  ...(config.reads !== undefined ? { reads: config.reads } : {}),
502
507
  ...(config.model ? { model: config.model } : {}),
503
508
  ...(config.skill !== undefined ? { skill: config.skill } : {}),
@@ -550,6 +555,7 @@ export function registerSlashCommands(
550
555
  agent: name,
551
556
  task: stepTask ?? parsed.task,
552
557
  ...(config.output !== undefined ? { output: config.output } : {}),
558
+ ...(config.outputMode !== undefined ? { outputMode: config.outputMode } : {}),
553
559
  ...(config.reads !== undefined ? { reads: config.reads } : {}),
554
560
  ...(config.model ? { model: config.model } : {}),
555
561
  ...(config.skill !== undefined ? { skill: config.skill } : {}),