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.
- package/CHANGELOG.md +15 -0
- package/README.md +23 -13
- package/package.json +15 -1
- package/skills/pi-subagents/SKILL.md +32 -12
- package/src/agents/agent-management.ts +58 -16
- package/src/agents/agent-serializer.ts +4 -1
- package/src/agents/agents.ts +39 -30
- package/src/agents/chain-serializer.ts +16 -3
- package/src/agents/identity.ts +30 -0
- package/src/extension/control-notices.ts +92 -0
- package/src/extension/index.ts +22 -41
- package/src/extension/schemas.ts +11 -2
- package/src/manager-ui/agent-manager-chain-detail.ts +4 -0
- package/src/manager-ui/agent-manager-detail.ts +4 -0
- package/src/manager-ui/agent-manager-edit.ts +25 -6
- package/src/manager-ui/agent-manager.ts +28 -8
- package/src/runs/background/async-execution.ts +12 -2
- package/src/runs/background/subagent-runner.ts +12 -10
- package/src/runs/foreground/chain-clarify.ts +2 -0
- package/src/runs/foreground/chain-execution.ts +36 -0
- package/src/runs/foreground/execution.ts +26 -4
- package/src/runs/foreground/subagent-executor.ts +26 -1
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/single-output.ts +47 -3
- package/src/shared/settings.ts +9 -3
- package/src/shared/types.ts +13 -0
- package/src/slash/slash-commands.ts +6 -0
|
@@ -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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
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;
|
|
@@ -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
|
-
|
|
88
|
-
|
|
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}`;
|
package/src/shared/settings.ts
CHANGED
|
@@ -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
|
|
package/src/shared/types.ts
CHANGED
|
@@ -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 } : {}),
|