pi-subagents 0.22.0 → 0.23.1
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 +29 -0
- package/README.md +13 -10
- package/agents/reviewer.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +24 -6
- package/src/agents/agent-management.ts +3 -1
- package/src/agents/agents.ts +22 -4
- package/src/extension/doctor.ts +1 -0
- package/src/extension/index.ts +12 -6
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +140 -11
- package/src/intercom/result-intercom.ts +8 -3
- package/src/manager-ui/agent-manager.ts +6 -5
- package/src/runs/background/async-execution.ts +22 -7
- package/src/runs/background/async-job-tracker.ts +2 -2
- package/src/runs/background/async-resume.ts +57 -31
- package/src/runs/background/async-status.ts +7 -1
- package/src/runs/background/result-watcher.ts +3 -1
- package/src/runs/background/run-status.ts +22 -19
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +52 -7
- package/src/runs/foreground/chain-clarify.ts +2 -3
- package/src/runs/foreground/chain-execution.ts +55 -21
- package/src/runs/foreground/execution.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +157 -23
- package/src/runs/shared/single-output.ts +21 -6
- package/src/shared/settings.ts +19 -0
- package/src/shared/types.ts +26 -1
- package/src/slash/slash-commands.ts +1 -0
- package/src/tui/render.ts +202 -16
- package/src/tui/subagents-status.ts +18 -3
|
@@ -9,9 +9,8 @@ import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
|
9
9
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
10
10
|
import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
-
import * as os from "node:os";
|
|
13
12
|
import * as path from "node:path";
|
|
14
|
-
import type
|
|
13
|
+
import { getUserChainDir, type AgentConfig, type ChainConfig, type ChainStepConfig } from "../../agents/agents.ts";
|
|
15
14
|
import type { ResolvedStepBehavior } from "../../shared/settings.ts";
|
|
16
15
|
import type { TextEditorState } from "../../tui/text-editor.ts";
|
|
17
16
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../../tui/text-editor.ts";
|
|
@@ -317,7 +316,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
317
316
|
return;
|
|
318
317
|
}
|
|
319
318
|
try {
|
|
320
|
-
const dir =
|
|
319
|
+
const dir = getUserChainDir();
|
|
321
320
|
fs.mkdirSync(dir, { recursive: true });
|
|
322
321
|
const filePath = path.join(dir, `${name}.chain.md`);
|
|
323
322
|
const config = this.buildChainConfig(name);
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
buildChainInstructions,
|
|
19
19
|
writeInitialProgressFile,
|
|
20
20
|
createParallelDirs,
|
|
21
|
+
suppressProgressForReadOnlyTask,
|
|
21
22
|
aggregateParallelOutputs,
|
|
22
23
|
isParallelStep,
|
|
23
24
|
type StepOverrides,
|
|
@@ -178,8 +179,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
178
179
|
} as SingleResult;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
const behavior = input.parallelBehaviors[taskIndex]!;
|
|
182
182
|
const taskTemplate = input.parallelTemplates[taskIndex] ?? "{previous}";
|
|
183
|
+
const behavior = suppressProgressForReadOnlyTask(input.parallelBehaviors[taskIndex]!, taskTemplate, input.originalTask);
|
|
183
184
|
const templateHasPrevious = taskTemplate.includes("{previous}");
|
|
184
185
|
const { prefix, suffix } = buildChainInstructions(
|
|
185
186
|
behavior,
|
|
@@ -537,7 +538,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
537
538
|
|
|
538
539
|
try {
|
|
539
540
|
const agentNames = step.parallel.map((task) => task.agent);
|
|
540
|
-
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
541
|
+
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
542
|
+
.map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
|
|
541
543
|
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
542
544
|
const behavior = parallelBehaviors[taskIndex]!;
|
|
543
545
|
const outputPath = typeof behavior.output === "string"
|
|
@@ -616,6 +618,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
616
618
|
}),
|
|
617
619
|
};
|
|
618
620
|
}
|
|
621
|
+
const detachedIndexInStep = parallelResults.findIndex((result) => result.detached);
|
|
622
|
+
const detached = detachedIndexInStep >= 0 ? parallelResults[detachedIndexInStep] : undefined;
|
|
623
|
+
if (detached) {
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
626
|
+
details: buildChainExecutionDetails({
|
|
627
|
+
results,
|
|
628
|
+
includeProgress,
|
|
629
|
+
allProgress,
|
|
630
|
+
allArtifactPaths,
|
|
631
|
+
artifactsDir,
|
|
632
|
+
chainAgents,
|
|
633
|
+
totalSteps,
|
|
634
|
+
currentStepIndex: stepIndex,
|
|
635
|
+
}),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
619
638
|
|
|
620
639
|
const failures = parallelResults
|
|
621
640
|
.map((result, originalIndex) => ({ ...result, originalIndex }))
|
|
@@ -695,7 +714,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
695
714
|
? tuiOverride.skills
|
|
696
715
|
: normalizeSkillInput(seqStep.skill),
|
|
697
716
|
};
|
|
698
|
-
const behavior = resolveStepBehavior(agentConfig, stepOverride, chainSkills);
|
|
717
|
+
const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfig, stepOverride, chainSkills), stepTemplate, originalTask);
|
|
699
718
|
|
|
700
719
|
const isFirstProgress = behavior.progress && !progressCreated;
|
|
701
720
|
if (isFirstProgress) {
|
|
@@ -822,24 +841,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
822
841
|
if (r.progress) allProgress.push(r.progress);
|
|
823
842
|
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
824
843
|
|
|
825
|
-
if (behavior.output && r.exitCode === 0) {
|
|
826
|
-
try {
|
|
827
|
-
const expectedPath = path.isAbsolute(behavior.output)
|
|
828
|
-
? behavior.output
|
|
829
|
-
: path.join(chainDir, behavior.output);
|
|
830
|
-
if (!fs.existsSync(expectedPath)) {
|
|
831
|
-
const dirFiles = fs.readdirSync(chainDir);
|
|
832
|
-
const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
|
|
833
|
-
const warning = mdFiles.length > 0
|
|
834
|
-
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
835
|
-
: `Agent did not create expected output file: ${behavior.output}`;
|
|
836
|
-
r.error = r.error ? `${r.error}\n${warning}` : warning;
|
|
837
|
-
}
|
|
838
|
-
} catch {
|
|
839
|
-
// Ignore validation errors - this is just a diagnostic
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
844
|
if (r.interrupted) {
|
|
844
845
|
return {
|
|
845
846
|
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
|
|
@@ -855,6 +856,21 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
855
856
|
}),
|
|
856
857
|
};
|
|
857
858
|
}
|
|
859
|
+
if (r.detached) {
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${r.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
862
|
+
details: buildChainExecutionDetails({
|
|
863
|
+
results,
|
|
864
|
+
includeProgress,
|
|
865
|
+
allProgress,
|
|
866
|
+
allArtifactPaths,
|
|
867
|
+
artifactsDir,
|
|
868
|
+
chainAgents,
|
|
869
|
+
totalSteps,
|
|
870
|
+
currentStepIndex: stepIndex,
|
|
871
|
+
}),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
858
874
|
|
|
859
875
|
if (r.exitCode !== 0) {
|
|
860
876
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
@@ -877,6 +893,24 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
877
893
|
};
|
|
878
894
|
}
|
|
879
895
|
|
|
896
|
+
if (behavior.output) {
|
|
897
|
+
try {
|
|
898
|
+
const expectedPath = path.isAbsolute(behavior.output)
|
|
899
|
+
? behavior.output
|
|
900
|
+
: path.join(chainDir, behavior.output);
|
|
901
|
+
if (!fs.existsSync(expectedPath)) {
|
|
902
|
+
const dirFiles = fs.readdirSync(chainDir);
|
|
903
|
+
const mdFiles = dirFiles.filter((file) => file.endsWith(".md") && file !== "progress.md");
|
|
904
|
+
const warning = mdFiles.length > 0
|
|
905
|
+
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
906
|
+
: `Agent did not create expected output file: ${behavior.output}`;
|
|
907
|
+
r.error = r.error ? `${r.error}\n${warning}` : warning;
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
// Ignore validation errors; this diagnostic should not mask successful chain output.
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
880
914
|
prev = getSingleResultOutput(r);
|
|
881
915
|
}
|
|
882
916
|
}
|
|
@@ -421,7 +421,7 @@ async function runSingleAttempt(
|
|
|
421
421
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
422
422
|
? evt.args as Record<string, unknown>
|
|
423
423
|
: {};
|
|
424
|
-
if (options.allowIntercomDetach && evt.toolName === "intercom") {
|
|
424
|
+
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
425
425
|
intercomStarted = true;
|
|
426
426
|
}
|
|
427
427
|
progress.toolCount++;
|
|
@@ -633,7 +633,10 @@ async function runSingleAttempt(
|
|
|
633
633
|
return result;
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
if (exitCode === 0
|
|
636
|
+
if (result.error && result.exitCode === 0) {
|
|
637
|
+
result.exitCode = 1;
|
|
638
|
+
}
|
|
639
|
+
if (result.exitCode === 0 && !result.error) {
|
|
637
640
|
const errInfo = detectSubagentError(result.messages);
|
|
638
641
|
if (errInfo.hasError) {
|
|
639
642
|
result.exitCode = errInfo.exitCode ?? 1;
|
|
@@ -746,7 +749,6 @@ export async function runSync(
|
|
|
746
749
|
|
|
747
750
|
const shareEnabled = options.share === true;
|
|
748
751
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
749
|
-
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
750
752
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
751
753
|
const skillCwd = options.cwd ?? runtimeCwd;
|
|
752
754
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
|
|
@@ -797,6 +799,7 @@ export async function runSync(
|
|
|
797
799
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
798
800
|
const candidate = modelsToTry[i];
|
|
799
801
|
if (candidate) attemptedModels.push(candidate);
|
|
802
|
+
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
800
803
|
const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
|
|
801
804
|
sessionEnabled,
|
|
802
805
|
systemPrompt,
|
|
@@ -811,15 +814,16 @@ export async function runSync(
|
|
|
811
814
|
sumUsage(aggregateUsage, result.usage);
|
|
812
815
|
totalToolCount += result.progressSummary?.toolCount ?? 0;
|
|
813
816
|
totalDurationMs += result.progressSummary?.durationMs ?? 0;
|
|
817
|
+
const attemptSucceeded = result.exitCode === 0 && !result.error;
|
|
814
818
|
const attempt: ModelAttempt = {
|
|
815
819
|
model: candidate ?? result.model ?? agent.model ?? "default",
|
|
816
|
-
success:
|
|
820
|
+
success: attemptSucceeded,
|
|
817
821
|
exitCode: result.exitCode,
|
|
818
822
|
error: result.error,
|
|
819
823
|
usage: { ...result.usage },
|
|
820
824
|
};
|
|
821
825
|
modelAttempts.push(attempt);
|
|
822
|
-
if (
|
|
826
|
+
if (attemptSucceeded) {
|
|
823
827
|
break;
|
|
824
828
|
}
|
|
825
829
|
if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
|
|
@@ -22,13 +22,15 @@ import {
|
|
|
22
22
|
getStepAgents,
|
|
23
23
|
isParallelStep,
|
|
24
24
|
resolveStepBehavior,
|
|
25
|
+
suppressProgressForReadOnlyTask,
|
|
26
|
+
taskDisallowsFileUpdates,
|
|
25
27
|
type ChainStep,
|
|
26
28
|
type ResolvedStepBehavior,
|
|
27
29
|
type SequentialStep,
|
|
28
30
|
type StepOverrides,
|
|
29
31
|
} from "../../shared/settings.ts";
|
|
30
32
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
31
|
-
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "../background/async-execution.ts";
|
|
33
|
+
import { executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
|
|
32
34
|
import { createForkContextResolver } from "../../shared/fork-context.ts";
|
|
33
35
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
34
36
|
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
|
|
@@ -206,6 +208,115 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
|
|
|
206
208
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
|
|
212
|
+
state.foregroundRuns ??= new Map();
|
|
213
|
+
state.foregroundRuns.set(input.runId, {
|
|
214
|
+
runId: input.runId,
|
|
215
|
+
mode: input.mode,
|
|
216
|
+
cwd: input.cwd,
|
|
217
|
+
updatedAt: Date.now(),
|
|
218
|
+
children: input.results.map((result, index) => ({
|
|
219
|
+
agent: result.agent,
|
|
220
|
+
index,
|
|
221
|
+
status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
|
|
222
|
+
...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
|
|
223
|
+
})),
|
|
224
|
+
});
|
|
225
|
+
while (state.foregroundRuns.size > 50) {
|
|
226
|
+
const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
|
|
227
|
+
if (!oldest) break;
|
|
228
|
+
state.foregroundRuns.delete(oldest.runId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined {
|
|
233
|
+
const requested = (params.id ?? params.runId)?.trim();
|
|
234
|
+
if (!requested || !state.foregroundRuns?.size) return undefined;
|
|
235
|
+
const direct = state.foregroundRuns.get(requested);
|
|
236
|
+
const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested));
|
|
237
|
+
if (matches.length === 0) return undefined;
|
|
238
|
+
if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`);
|
|
239
|
+
const run = matches[0]!;
|
|
240
|
+
if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`);
|
|
241
|
+
const index = params.index ?? 0;
|
|
242
|
+
if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`);
|
|
243
|
+
if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`);
|
|
244
|
+
const child = run.children[index]!;
|
|
245
|
+
if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`);
|
|
246
|
+
if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`);
|
|
247
|
+
if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
|
|
248
|
+
const sessionFile = path.resolve(child.sessionFile);
|
|
249
|
+
if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
|
|
250
|
+
return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
|
|
254
|
+
type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
|
|
255
|
+
type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget;
|
|
256
|
+
|
|
257
|
+
function isAsyncRunNotFound(error: unknown): boolean {
|
|
258
|
+
return error instanceof Error && error.message.startsWith("Async run not found.");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isResumeAmbiguity(error: unknown): boolean {
|
|
262
|
+
return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function resumeTargetExact(target: { runId: string } | undefined, requested: string): boolean {
|
|
266
|
+
return target?.runId === requested;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function escapeRegExp(value: string): string {
|
|
270
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isExactResumeError(error: unknown, source: "async" | "foreground", requested: string): boolean {
|
|
274
|
+
if (!(error instanceof Error) || !requested) return false;
|
|
275
|
+
return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
|
|
279
|
+
const requested = (params.id ?? params.runId)?.trim() ?? "";
|
|
280
|
+
let foregroundTarget: ForegroundResumeSourceTarget | undefined;
|
|
281
|
+
let foregroundError: unknown;
|
|
282
|
+
let asyncTarget: AsyncResumeSourceTarget | undefined;
|
|
283
|
+
let asyncError: unknown;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const target = resolveForegroundResumeTarget(params, state);
|
|
287
|
+
if (target) foregroundTarget = { kind: "revive", source: "foreground", ...target };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
foregroundError = error;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
|
|
293
|
+
} catch (error) {
|
|
294
|
+
asyncError = error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (foregroundTarget && asyncTarget) {
|
|
298
|
+
const foregroundExact = resumeTargetExact(foregroundTarget, requested);
|
|
299
|
+
const asyncExact = resumeTargetExact(asyncTarget, requested);
|
|
300
|
+
if (foregroundExact && !asyncExact) return foregroundTarget;
|
|
301
|
+
if (asyncExact && !foregroundExact) return asyncTarget;
|
|
302
|
+
throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`);
|
|
303
|
+
}
|
|
304
|
+
if (foregroundTarget) {
|
|
305
|
+
if (isExactResumeError(asyncError, "async", requested)) throw asyncError;
|
|
306
|
+
if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError;
|
|
307
|
+
return foregroundTarget;
|
|
308
|
+
}
|
|
309
|
+
if (asyncTarget) {
|
|
310
|
+
if (isExactResumeError(foregroundError, "foreground", requested)) throw foregroundError;
|
|
311
|
+
if (isResumeAmbiguity(foregroundError) && !resumeTargetExact(asyncTarget, requested)) throw foregroundError;
|
|
312
|
+
return asyncTarget;
|
|
313
|
+
}
|
|
314
|
+
if (foregroundError && !isAsyncRunNotFound(asyncError)) throw foregroundError;
|
|
315
|
+
if (foregroundError) throw foregroundError;
|
|
316
|
+
if (asyncError) throw asyncError;
|
|
317
|
+
throw new Error("Run not found. Provide id or runId.");
|
|
318
|
+
}
|
|
319
|
+
|
|
209
320
|
function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
|
|
210
321
|
if (runId) {
|
|
211
322
|
const direct = state.asyncJobs.get(runId);
|
|
@@ -296,9 +407,9 @@ async function resumeAsyncRun(input: {
|
|
|
296
407
|
};
|
|
297
408
|
}
|
|
298
409
|
|
|
299
|
-
let target:
|
|
410
|
+
let target: ResumeSourceTarget;
|
|
300
411
|
try {
|
|
301
|
-
target =
|
|
412
|
+
target = resolveResumeTarget(input.params, input.deps.state);
|
|
302
413
|
} catch (error) {
|
|
303
414
|
const message = error instanceof Error ? error.message : String(error);
|
|
304
415
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
@@ -344,6 +455,7 @@ async function resumeAsyncRun(input: {
|
|
|
344
455
|
config: input.deps.config.intercomBridge,
|
|
345
456
|
context: input.params.context,
|
|
346
457
|
orchestratorTarget: sessionName,
|
|
458
|
+
cwd: effectiveCwd,
|
|
347
459
|
});
|
|
348
460
|
const agents = intercomBridge.active
|
|
349
461
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
@@ -351,7 +463,7 @@ async function resumeAsyncRun(input: {
|
|
|
351
463
|
const agentConfig = agents.find((agent) => agent.name === target.agent);
|
|
352
464
|
if (!agentConfig) {
|
|
353
465
|
return {
|
|
354
|
-
content: [{ type: "text", text: `Unknown agent for
|
|
466
|
+
content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
|
|
355
467
|
isError: true,
|
|
356
468
|
details: { mode: "management", results: [] },
|
|
357
469
|
};
|
|
@@ -389,16 +501,17 @@ async function resumeAsyncRun(input: {
|
|
|
389
501
|
|
|
390
502
|
const revivedId = result.details.asyncId ?? runId;
|
|
391
503
|
const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
|
|
504
|
+
const sourceLabel = target.source === "foreground" ? "foreground" : "async";
|
|
392
505
|
const lines = [
|
|
393
|
-
`Revived
|
|
506
|
+
`Revived ${sourceLabel} subagent from ${target.runId}.`,
|
|
394
507
|
`Revived run: ${revivedId}`,
|
|
395
508
|
`Agent: ${target.agent}`,
|
|
396
509
|
`Session: ${target.sessionFile}`,
|
|
397
510
|
result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
|
|
398
511
|
revivedTarget ? `Intercom target: ${revivedTarget} (if registered)` : undefined,
|
|
399
|
-
`
|
|
512
|
+
`Status if needed: subagent({ action: "status", id: "${revivedId}" })`,
|
|
400
513
|
].filter((line): line is string => Boolean(line));
|
|
401
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: result.details };
|
|
514
|
+
return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
|
|
402
515
|
}
|
|
403
516
|
|
|
404
517
|
function resultSummaryForIntercom(result: SingleResult): string {
|
|
@@ -835,6 +948,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
835
948
|
const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
|
|
836
949
|
return executeAsyncChain(id, {
|
|
837
950
|
chain,
|
|
951
|
+
task: params.task,
|
|
838
952
|
agents,
|
|
839
953
|
ctx: asyncCtx,
|
|
840
954
|
availableModels,
|
|
@@ -971,6 +1085,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
971
1085
|
const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
|
|
972
1086
|
return executeAsyncChain(id, {
|
|
973
1087
|
chain: asyncChain,
|
|
1088
|
+
task: params.task,
|
|
974
1089
|
agents,
|
|
975
1090
|
ctx: asyncCtx,
|
|
976
1091
|
availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
|
|
@@ -991,8 +1106,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
991
1106
|
});
|
|
992
1107
|
}
|
|
993
1108
|
|
|
994
|
-
const chainDetails = chainResult.details ? compactForegroundDetails(chainResult.details) : undefined;
|
|
995
|
-
|
|
1109
|
+
const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
|
|
1110
|
+
if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
|
|
1111
|
+
const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
|
|
996
1112
|
? await maybeBuildForegroundIntercomReceipt({
|
|
997
1113
|
pi: deps.pi,
|
|
998
1114
|
intercomBridge: data.intercomBridge,
|
|
@@ -1009,7 +1125,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1009
1125
|
};
|
|
1010
1126
|
}
|
|
1011
1127
|
|
|
1012
|
-
return chainResult;
|
|
1128
|
+
return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
|
|
1013
1129
|
}
|
|
1014
1130
|
|
|
1015
1131
|
interface ForegroundParallelRunInput {
|
|
@@ -1375,17 +1491,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1375
1491
|
currentSessionId: deps.state.currentSessionId!,
|
|
1376
1492
|
currentModelProvider: ctx.model?.provider,
|
|
1377
1493
|
};
|
|
1378
|
-
const parallelTasks = tasks.map((t, i) =>
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1494
|
+
const parallelTasks = tasks.map((t, i) => {
|
|
1495
|
+
const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
|
|
1496
|
+
const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
|
|
1497
|
+
return {
|
|
1498
|
+
agent: t.agent,
|
|
1499
|
+
task: taskText,
|
|
1500
|
+
cwd: t.cwd,
|
|
1501
|
+
...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
|
|
1502
|
+
...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
|
|
1503
|
+
...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
|
|
1504
|
+
...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
|
|
1505
|
+
...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
|
|
1506
|
+
...(progress !== undefined ? { progress } : {}),
|
|
1507
|
+
};
|
|
1508
|
+
});
|
|
1389
1509
|
return executeAsyncChain(id, {
|
|
1390
1510
|
chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
|
|
1391
1511
|
resultMode: "parallel",
|
|
@@ -1410,7 +1530,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1410
1530
|
}
|
|
1411
1531
|
}
|
|
1412
1532
|
|
|
1413
|
-
const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
|
|
1533
|
+
const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
|
|
1414
1534
|
const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
|
|
1415
1535
|
const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
1416
1536
|
const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
@@ -1494,16 +1614,26 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1494
1614
|
const interrupted = results.find((result) => result.interrupted);
|
|
1495
1615
|
const details = compactForegroundDetails({
|
|
1496
1616
|
mode: "parallel",
|
|
1617
|
+
runId,
|
|
1497
1618
|
results,
|
|
1498
1619
|
progress: params.includeProgress ? allProgress : undefined,
|
|
1499
1620
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1500
1621
|
});
|
|
1622
|
+
rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
|
|
1501
1623
|
if (interrupted) {
|
|
1502
1624
|
return {
|
|
1503
1625
|
content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
1504
1626
|
details,
|
|
1505
1627
|
};
|
|
1506
1628
|
}
|
|
1629
|
+
const detachedIndex = results.findIndex((result) => result.detached);
|
|
1630
|
+
const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
|
|
1631
|
+
if (detached) {
|
|
1632
|
+
return {
|
|
1633
|
+
content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
1634
|
+
details,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1507
1637
|
|
|
1508
1638
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
1509
1639
|
pi: deps.pi,
|
|
@@ -1775,11 +1905,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1775
1905
|
});
|
|
1776
1906
|
const details = compactForegroundDetails({
|
|
1777
1907
|
mode: "single",
|
|
1908
|
+
runId,
|
|
1778
1909
|
results: [r],
|
|
1779
1910
|
progress: params.includeProgress ? allProgress : undefined,
|
|
1780
1911
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1781
1912
|
truncation: r.truncation,
|
|
1782
1913
|
});
|
|
1914
|
+
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
|
|
1783
1915
|
|
|
1784
1916
|
if (!r.detached && !r.interrupted) {
|
|
1785
1917
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
@@ -1800,7 +1932,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1800
1932
|
|
|
1801
1933
|
if (r.detached) {
|
|
1802
1934
|
return {
|
|
1803
|
-
content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}
|
|
1935
|
+
content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
1804
1936
|
details,
|
|
1805
1937
|
};
|
|
1806
1938
|
}
|
|
@@ -1841,6 +1973,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1841
1973
|
ctx: ExtensionContext,
|
|
1842
1974
|
): Promise<AgentToolResult<Details>> => {
|
|
1843
1975
|
deps.state.baseCwd = ctx.cwd;
|
|
1976
|
+
deps.state.foregroundRuns ??= new Map();
|
|
1844
1977
|
deps.state.foregroundControls ??= new Map();
|
|
1845
1978
|
deps.state.lastForegroundControlId ??= null;
|
|
1846
1979
|
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
@@ -1962,6 +2095,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1962
2095
|
config: deps.config.intercomBridge,
|
|
1963
2096
|
context: effectiveParams.context,
|
|
1964
2097
|
orchestratorTarget: sessionName,
|
|
2098
|
+
cwd: effectiveCwd,
|
|
1965
2099
|
});
|
|
1966
2100
|
const agents = intercomBridge.active
|
|
1967
2101
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
@@ -69,6 +69,7 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
|
|
|
69
69
|
const stat = fs.statSync(outputPath);
|
|
70
70
|
return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
71
71
|
} catch {
|
|
72
|
+
// The snapshot is advisory; resolveSingleOutput reports concrete read/write failures.
|
|
72
73
|
return { exists: false };
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -94,18 +95,32 @@ export function resolveSingleOutput(
|
|
|
94
95
|
): { fullOutput: string; savedPath?: string; saveError?: string } {
|
|
95
96
|
if (!outputPath) return { fullOutput: fallbackOutput };
|
|
96
97
|
|
|
98
|
+
let changedSinceStart = false;
|
|
97
99
|
try {
|
|
98
100
|
const stat = fs.statSync(outputPath);
|
|
99
|
-
|
|
101
|
+
changedSinceStart = !beforeRun?.exists
|
|
100
102
|
|| stat.mtimeMs !== beforeRun.mtimeMs
|
|
101
103
|
|| stat.size !== beforeRun.size;
|
|
102
|
-
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
106
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
103
107
|
return {
|
|
104
|
-
fullOutput:
|
|
105
|
-
|
|
108
|
+
fullOutput: fallbackOutput,
|
|
109
|
+
saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (changedSinceStart) {
|
|
115
|
+
try {
|
|
116
|
+
return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
fullOutput: fallbackOutput,
|
|
120
|
+
saveError: `Failed to read changed output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
|
|
110
125
|
const save = persistSingleOutput(outputPath, fallbackOutput);
|
|
111
126
|
if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
|
|
@@ -132,7 +147,7 @@ export function finalizeSingleOutput(params: {
|
|
|
132
147
|
return { displayOutput, savedPath: params.savedPath, outputReference };
|
|
133
148
|
}
|
|
134
149
|
if (params.exitCode === 0 && params.saveError && params.outputPath) {
|
|
135
|
-
displayOutput += `\n\
|
|
150
|
+
displayOutput += `\n\nOutput file error: ${params.outputPath}\n${params.saveError}`;
|
|
136
151
|
return { displayOutput, saveError: params.saveError };
|
|
137
152
|
}
|
|
138
153
|
return { displayOutput };
|
package/src/shared/settings.ts
CHANGED
|
@@ -220,6 +220,25 @@ export function resolveStepBehavior(
|
|
|
220
220
|
return { output, outputMode, reads, progress, skills, model };
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
export function resolveTaskTextForFileUpdatePolicy(task: string | undefined, originalTask?: string): string | undefined {
|
|
224
|
+
if (!task) return originalTask;
|
|
225
|
+
return originalTask ? task.replaceAll("{task}", originalTask) : task;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function taskDisallowsFileUpdates(task: string | undefined): boolean {
|
|
229
|
+
if (!task) return false;
|
|
230
|
+
return /\breview[- ]only\b/i.test(task)
|
|
231
|
+
|| /\bread[- ]only\s+(?:review|audit|inspection|pass)\b/i.test(task)
|
|
232
|
+
|| /\b(?:no|without)\s+(?:file\s+)?edits?\b/i.test(task)
|
|
233
|
+
|| /\b(?:do not|don't|must not)\s+(?:edit|modify|write|touch)\b/i.test(task)
|
|
234
|
+
|| /\bleave\s+files?\s+unchanged\b/i.test(task);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function suppressProgressForReadOnlyTask(behavior: ResolvedStepBehavior, task: string | undefined, originalTask?: string): ResolvedStepBehavior {
|
|
238
|
+
const policyTask = resolveTaskTextForFileUpdatePolicy(task, originalTask);
|
|
239
|
+
return behavior.progress && taskDisallowsFileUpdates(policyTask) ? { ...behavior, progress: false } : behavior;
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
// =============================================================================
|
|
224
243
|
// Chain Instruction Injection
|
|
225
244
|
// =============================================================================
|