pi-subagents 0.23.0 → 0.24.0
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 +30 -0
- package/README.md +17 -79
- package/agents/reviewer.md +2 -2
- package/package.json +1 -1
- package/prompts/parallel-cleanup.md +11 -1
- package/prompts/parallel-review.md +11 -1
- package/skills/pi-subagents/SKILL.md +29 -13
- package/src/agents/agent-serializer.ts +0 -42
- package/src/agents/agents.ts +1 -1
- package/src/extension/index.ts +14 -8
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +4 -1
- package/src/intercom/result-intercom.ts +8 -3
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-resume.ts +57 -31
- package/src/runs/background/async-status.ts +16 -50
- package/src/runs/background/result-watcher.ts +3 -1
- package/src/runs/background/run-status.ts +28 -26
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +21 -7
- package/src/runs/foreground/chain-clarify.ts +183 -218
- package/src/runs/foreground/chain-execution.ts +55 -21
- package/src/runs/foreground/execution.ts +6 -3
- package/src/runs/foreground/subagent-executor.ts +152 -20
- package/src/runs/shared/single-output.ts +21 -6
- package/src/shared/settings.ts +19 -0
- package/src/shared/status-format.ts +49 -0
- package/src/shared/types.ts +18 -5
- package/src/slash/slash-commands.ts +1 -74
- package/src/tui/render.ts +37 -61
- package/src/agents/agent-templates.ts +0 -60
- package/src/manager-ui/agent-manager-chain-detail.ts +0 -164
- package/src/manager-ui/agent-manager-detail.ts +0 -235
- package/src/manager-ui/agent-manager-edit.ts +0 -456
- package/src/manager-ui/agent-manager-list.ts +0 -283
- package/src/manager-ui/agent-manager-parallel.ts +0 -302
- package/src/manager-ui/agent-manager.ts +0 -732
- package/src/tui/subagents-status.ts +0 -621
- package/src/tui/text-editor.ts +0 -286
|
@@ -22,6 +22,8 @@ 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,
|
|
@@ -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: [] } };
|
|
@@ -352,7 +463,7 @@ async function resumeAsyncRun(input: {
|
|
|
352
463
|
const agentConfig = agents.find((agent) => agent.name === target.agent);
|
|
353
464
|
if (!agentConfig) {
|
|
354
465
|
return {
|
|
355
|
-
content: [{ type: "text", text: `Unknown agent for
|
|
466
|
+
content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
|
|
356
467
|
isError: true,
|
|
357
468
|
details: { mode: "management", results: [] },
|
|
358
469
|
};
|
|
@@ -390,8 +501,9 @@ async function resumeAsyncRun(input: {
|
|
|
390
501
|
|
|
391
502
|
const revivedId = result.details.asyncId ?? runId;
|
|
392
503
|
const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
|
|
504
|
+
const sourceLabel = target.source === "foreground" ? "foreground" : "async";
|
|
393
505
|
const lines = [
|
|
394
|
-
`Revived
|
|
506
|
+
`Revived ${sourceLabel} subagent from ${target.runId}.`,
|
|
395
507
|
`Revived run: ${revivedId}`,
|
|
396
508
|
`Agent: ${target.agent}`,
|
|
397
509
|
`Session: ${target.sessionFile}`,
|
|
@@ -836,6 +948,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
836
948
|
const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
|
|
837
949
|
return executeAsyncChain(id, {
|
|
838
950
|
chain,
|
|
951
|
+
task: params.task,
|
|
839
952
|
agents,
|
|
840
953
|
ctx: asyncCtx,
|
|
841
954
|
availableModels,
|
|
@@ -972,6 +1085,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
972
1085
|
const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
|
|
973
1086
|
return executeAsyncChain(id, {
|
|
974
1087
|
chain: asyncChain,
|
|
1088
|
+
task: params.task,
|
|
975
1089
|
agents,
|
|
976
1090
|
ctx: asyncCtx,
|
|
977
1091
|
availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
|
|
@@ -992,8 +1106,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
992
1106
|
});
|
|
993
1107
|
}
|
|
994
1108
|
|
|
995
|
-
const chainDetails = chainResult.details ? compactForegroundDetails(chainResult.details) : undefined;
|
|
996
|
-
|
|
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)
|
|
997
1112
|
? await maybeBuildForegroundIntercomReceipt({
|
|
998
1113
|
pi: deps.pi,
|
|
999
1114
|
intercomBridge: data.intercomBridge,
|
|
@@ -1010,7 +1125,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1010
1125
|
};
|
|
1011
1126
|
}
|
|
1012
1127
|
|
|
1013
|
-
return chainResult;
|
|
1128
|
+
return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
|
|
1014
1129
|
}
|
|
1015
1130
|
|
|
1016
1131
|
interface ForegroundParallelRunInput {
|
|
@@ -1376,17 +1491,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1376
1491
|
currentSessionId: deps.state.currentSessionId!,
|
|
1377
1492
|
currentModelProvider: ctx.model?.provider,
|
|
1378
1493
|
};
|
|
1379
|
-
const parallelTasks = tasks.map((t, i) =>
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
+
});
|
|
1390
1509
|
return executeAsyncChain(id, {
|
|
1391
1510
|
chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
|
|
1392
1511
|
resultMode: "parallel",
|
|
@@ -1411,7 +1530,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1411
1530
|
}
|
|
1412
1531
|
}
|
|
1413
1532
|
|
|
1414
|
-
const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
|
|
1533
|
+
const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index]));
|
|
1415
1534
|
const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
|
|
1416
1535
|
const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
1417
1536
|
const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
@@ -1495,16 +1614,26 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1495
1614
|
const interrupted = results.find((result) => result.interrupted);
|
|
1496
1615
|
const details = compactForegroundDetails({
|
|
1497
1616
|
mode: "parallel",
|
|
1617
|
+
runId,
|
|
1498
1618
|
results,
|
|
1499
1619
|
progress: params.includeProgress ? allProgress : undefined,
|
|
1500
1620
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1501
1621
|
});
|
|
1622
|
+
rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
|
|
1502
1623
|
if (interrupted) {
|
|
1503
1624
|
return {
|
|
1504
1625
|
content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
1505
1626
|
details,
|
|
1506
1627
|
};
|
|
1507
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
|
+
}
|
|
1508
1637
|
|
|
1509
1638
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
1510
1639
|
pi: deps.pi,
|
|
@@ -1776,11 +1905,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1776
1905
|
});
|
|
1777
1906
|
const details = compactForegroundDetails({
|
|
1778
1907
|
mode: "single",
|
|
1908
|
+
runId,
|
|
1779
1909
|
results: [r],
|
|
1780
1910
|
progress: params.includeProgress ? allProgress : undefined,
|
|
1781
1911
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1782
1912
|
truncation: r.truncation,
|
|
1783
1913
|
});
|
|
1914
|
+
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
|
|
1784
1915
|
|
|
1785
1916
|
if (!r.detached && !r.interrupted) {
|
|
1786
1917
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
@@ -1801,7 +1932,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1801
1932
|
|
|
1802
1933
|
if (r.detached) {
|
|
1803
1934
|
return {
|
|
1804
|
-
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.` }],
|
|
1805
1936
|
details,
|
|
1806
1937
|
};
|
|
1807
1938
|
}
|
|
@@ -1842,6 +1973,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1842
1973
|
ctx: ExtensionContext,
|
|
1843
1974
|
): Promise<AgentToolResult<Details>> => {
|
|
1844
1975
|
deps.state.baseCwd = ctx.cwd;
|
|
1976
|
+
deps.state.foregroundRuns ??= new Map();
|
|
1845
1977
|
deps.state.foregroundControls ??= new Map();
|
|
1846
1978
|
deps.state.lastForegroundControlId ??= null;
|
|
1847
1979
|
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
@@ -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
|
// =============================================================================
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ActivityState, AsyncJobStep } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
type StepStatusLike = Pick<AsyncJobStep, "status">;
|
|
4
|
+
|
|
5
|
+
function formatActivityAge(ms: number): string {
|
|
6
|
+
if (ms < 1000) return "now";
|
|
7
|
+
if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
|
|
8
|
+
return `${Math.floor(ms / 60000)}m`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatActivityLabel(lastActivityAt: number | undefined, activityState?: ActivityState, now = Date.now()): string | undefined {
|
|
12
|
+
if (lastActivityAt === undefined) {
|
|
13
|
+
if (activityState === "needs_attention") return "needs attention";
|
|
14
|
+
if (activityState === "active_long_running") return "active but long-running";
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const age = formatActivityAge(Math.max(0, now - lastActivityAt));
|
|
18
|
+
if (activityState === "needs_attention") return `no activity for ${age}`;
|
|
19
|
+
if (activityState === "active_long_running") return `active but long-running · last activity ${age} ago`;
|
|
20
|
+
return age === "now" ? "active now" : `active ${age} ago`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isCompletedStepStatus(status: AsyncJobStep["status"]): boolean {
|
|
24
|
+
return status === "complete" || status === "completed";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function aggregateStepStatus(steps: StepStatusLike[]): AsyncJobStep["status"] {
|
|
28
|
+
if (steps.some((step) => step.status === "running")) return "running";
|
|
29
|
+
if (steps.some((step) => step.status === "failed")) return "failed";
|
|
30
|
+
if (steps.some((step) => step.status === "paused")) return "paused";
|
|
31
|
+
if (steps.length > 0 && steps.every((step) => isCompletedStepStatus(step.status))) return "complete";
|
|
32
|
+
return "pending";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatAgentRunningLabel(count: number): string {
|
|
36
|
+
return count === 1 ? "1 agent running" : `${count} agents running`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatParallelOutcome(steps: StepStatusLike[], total: number, options: { showRunning?: boolean } = {}): string {
|
|
40
|
+
const running = steps.filter((step) => step.status === "running").length;
|
|
41
|
+
const done = steps.filter((step) => isCompletedStepStatus(step.status)).length;
|
|
42
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
43
|
+
const paused = steps.filter((step) => step.status === "paused").length;
|
|
44
|
+
const parts = [`${done}/${total} done`];
|
|
45
|
+
if (options.showRunning !== false && running > 0) parts.unshift(formatAgentRunningLabel(running));
|
|
46
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
47
|
+
if (paused > 0) parts.push(`${paused} paused`);
|
|
48
|
+
return parts.join(" · ");
|
|
49
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -207,6 +207,7 @@ export interface SingleResult {
|
|
|
207
207
|
|
|
208
208
|
export interface Details {
|
|
209
209
|
mode: SubagentRunMode | "management";
|
|
210
|
+
runId?: string;
|
|
210
211
|
context?: "fresh" | "fork";
|
|
211
212
|
results: SingleResult[];
|
|
212
213
|
controlEvents?: ControlEvent[];
|
|
@@ -296,6 +297,7 @@ export interface AsyncStatus {
|
|
|
296
297
|
steps?: Array<{
|
|
297
298
|
agent: string;
|
|
298
299
|
status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
|
|
300
|
+
sessionFile?: string;
|
|
299
301
|
activityState?: ActivityState;
|
|
300
302
|
lastActivityAt?: number;
|
|
301
303
|
currentTool?: string;
|
|
@@ -360,10 +362,26 @@ export interface AsyncJobState {
|
|
|
360
362
|
controlEventCursor?: number;
|
|
361
363
|
}
|
|
362
364
|
|
|
365
|
+
export interface ForegroundResumeChild {
|
|
366
|
+
agent: string;
|
|
367
|
+
index: number;
|
|
368
|
+
sessionFile?: string;
|
|
369
|
+
status: SubagentResultStatus;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface ForegroundResumeRun {
|
|
373
|
+
runId: string;
|
|
374
|
+
mode: SubagentRunMode;
|
|
375
|
+
cwd: string;
|
|
376
|
+
updatedAt: number;
|
|
377
|
+
children: ForegroundResumeChild[];
|
|
378
|
+
}
|
|
379
|
+
|
|
363
380
|
export interface SubagentState {
|
|
364
381
|
baseCwd: string;
|
|
365
382
|
currentSessionId: string | null;
|
|
366
383
|
asyncJobs: Map<string, AsyncJobState>;
|
|
384
|
+
foregroundRuns?: Map<string, ForegroundResumeRun>;
|
|
367
385
|
foregroundControls: Map<string, {
|
|
368
386
|
runId: string;
|
|
369
387
|
mode: SubagentRunMode;
|
|
@@ -476,10 +494,6 @@ interface TopLevelParallelConfig {
|
|
|
476
494
|
concurrency?: number;
|
|
477
495
|
}
|
|
478
496
|
|
|
479
|
-
interface AgentManagerConfig {
|
|
480
|
-
newShortcut?: string;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
497
|
export interface ExtensionConfig {
|
|
484
498
|
asyncByDefault?: boolean;
|
|
485
499
|
forceTopLevelAsync?: boolean;
|
|
@@ -487,7 +501,6 @@ export interface ExtensionConfig {
|
|
|
487
501
|
maxSubagentDepth?: number;
|
|
488
502
|
control?: ControlConfig;
|
|
489
503
|
parallel?: TopLevelParallelConfig;
|
|
490
|
-
agentManager?: AgentManagerConfig;
|
|
491
504
|
worktreeSetupHook?: string;
|
|
492
505
|
worktreeSetupHookTimeoutMs?: number;
|
|
493
506
|
intercomBridge?: IntercomBridgeConfig;
|
|
@@ -4,14 +4,9 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
6
6
|
import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
|
|
7
|
-
import { AgentManagerComponent, type ManagerResult } from "../manager-ui/agent-manager.ts";
|
|
8
|
-
import { SubagentsStatusComponent } from "../tui/subagents-status.ts";
|
|
9
|
-
import { discoverAvailableSkills } from "../agents/skills.ts";
|
|
10
7
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
11
|
-
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
12
8
|
import { isParallelStep, type ChainStep } from "../shared/settings.ts";
|
|
13
9
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
14
|
-
import { toModelInfo } from "../shared/model-info.ts";
|
|
15
10
|
import {
|
|
16
11
|
applySlashUpdate,
|
|
17
12
|
buildSlashInitialResult,
|
|
@@ -25,7 +20,6 @@ import {
|
|
|
25
20
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
26
21
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
27
22
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
28
|
-
type ExtensionConfig,
|
|
29
23
|
type SingleResult,
|
|
30
24
|
type SubagentState,
|
|
31
25
|
} from "../shared/types.ts";
|
|
@@ -279,6 +273,7 @@ async function runSlashSubagent(
|
|
|
279
273
|
ctx: ExtensionContext,
|
|
280
274
|
params: SubagentParamsLike,
|
|
281
275
|
): Promise<void> {
|
|
276
|
+
if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
|
|
282
277
|
const requestId = randomUUID();
|
|
283
278
|
const initialDetails = buildSlashInitialResult(requestId, params);
|
|
284
279
|
const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
|
|
@@ -327,49 +322,6 @@ async function runSlashSubagent(
|
|
|
327
322
|
}
|
|
328
323
|
}
|
|
329
324
|
|
|
330
|
-
async function openAgentManager(
|
|
331
|
-
pi: ExtensionAPI,
|
|
332
|
-
ctx: ExtensionContext,
|
|
333
|
-
config: ExtensionConfig = {},
|
|
334
|
-
): Promise<void> {
|
|
335
|
-
const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
|
|
336
|
-
const models = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
337
|
-
const skills = discoverAvailableSkills(ctx.cwd);
|
|
338
|
-
|
|
339
|
-
const result = await ctx.ui.custom<ManagerResult>(
|
|
340
|
-
(tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut, preferredModelProvider: ctx.model?.provider }),
|
|
341
|
-
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
342
|
-
);
|
|
343
|
-
if (!result) return;
|
|
344
|
-
|
|
345
|
-
const launchOptions: SubagentParamsLike = {
|
|
346
|
-
clarify: !result.skipClarify && !result.background,
|
|
347
|
-
agentScope: "both",
|
|
348
|
-
...(result.fork ? { context: "fork" as const } : {}),
|
|
349
|
-
...(result.background ? { async: true } : {}),
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
if (result.action === "chain") {
|
|
353
|
-
const chain = result.agents.map((name, i) => ({
|
|
354
|
-
agent: name,
|
|
355
|
-
...(i === 0 ? { task: result.task } : {}),
|
|
356
|
-
}));
|
|
357
|
-
await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (result.action === "launch") {
|
|
362
|
-
await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
|
|
363
|
-
} else if (result.action === "launch-chain") {
|
|
364
|
-
await runSlashSubagent(pi, ctx, { chain: mapSavedChainSteps(result.chain, result.worktree), task: result.task, ...launchOptions });
|
|
365
|
-
} else if (result.action === "parallel") {
|
|
366
|
-
await runSlashSubagent(pi, ctx, {
|
|
367
|
-
tasks: result.tasks,
|
|
368
|
-
...launchOptions,
|
|
369
|
-
...(result.worktree ? { worktree: true } : {}),
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
325
|
|
|
374
326
|
interface ParsedStep { name: string; config: InlineConfig; task?: string }
|
|
375
327
|
|
|
@@ -451,15 +403,7 @@ const parseAgentArgs = (
|
|
|
451
403
|
export function registerSlashCommands(
|
|
452
404
|
pi: ExtensionAPI,
|
|
453
405
|
state: SubagentState,
|
|
454
|
-
config: ExtensionConfig = {},
|
|
455
406
|
): void {
|
|
456
|
-
pi.registerCommand("agents", {
|
|
457
|
-
description: "Open the Agents Manager",
|
|
458
|
-
handler: async (_args, ctx) => {
|
|
459
|
-
await openAgentManager(pi, ctx, config);
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
|
|
463
407
|
pi.registerCommand("run", {
|
|
464
408
|
description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
|
|
465
409
|
getArgumentCompletions: makeAgentCompletions(state, false),
|
|
@@ -566,18 +510,6 @@ export function registerSlashCommands(
|
|
|
566
510
|
},
|
|
567
511
|
});
|
|
568
512
|
|
|
569
|
-
pi.registerCommand("subagents-status", {
|
|
570
|
-
description: "Show active and recent async subagent runs",
|
|
571
|
-
handler: async (_args, ctx) => {
|
|
572
|
-
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
573
|
-
state.baseCwd = ctx.cwd;
|
|
574
|
-
state.currentSessionId = sessionId;
|
|
575
|
-
await ctx.ui.custom<void>(
|
|
576
|
-
(tui, theme, _kb, done) => new SubagentsStatusComponent(tui, theme, () => done(undefined), { sessionId }),
|
|
577
|
-
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
578
|
-
);
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
513
|
|
|
582
514
|
pi.registerCommand("subagents-doctor", {
|
|
583
515
|
description: "Show subagent diagnostics",
|
|
@@ -586,9 +518,4 @@ export function registerSlashCommands(
|
|
|
586
518
|
},
|
|
587
519
|
});
|
|
588
520
|
|
|
589
|
-
pi.registerShortcut("ctrl+shift+a", {
|
|
590
|
-
handler: async (ctx) => {
|
|
591
|
-
await openAgentManager(pi, ctx, config);
|
|
592
|
-
},
|
|
593
|
-
});
|
|
594
521
|
}
|