pi-subagents 0.18.0 → 0.19.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 +24 -0
- package/README.md +4 -4
- package/agent-management.ts +11 -2
- package/agent-manager-chain-detail.ts +50 -6
- package/agent-manager-detail.ts +15 -2
- package/agent-manager.ts +76 -23
- package/async-execution.ts +47 -20
- package/chain-execution.ts +12 -2
- package/execution.ts +8 -10
- package/index.ts +104 -11
- package/intercom-bridge.ts +3 -3
- package/notify.ts +20 -10
- package/package.json +5 -1
- package/pi-args.ts +1 -0
- package/prompts/parallel-review.md +8 -0
- package/render.ts +371 -57
- package/schemas.ts +52 -14
- package/settings.ts +5 -0
- package/skills.ts +61 -0
- package/slash-commands.ts +82 -39
- package/slash-live-state.ts +3 -5
- package/subagent-executor.ts +110 -24
- package/subagent-runner.ts +8 -0
- package/subagents-status.ts +216 -2
- package/worktree.ts +19 -10
package/skills.ts
CHANGED
|
@@ -210,9 +210,70 @@ function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
210
210
|
return results;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function isSafePackagePath(value: string): boolean {
|
|
214
|
+
return value.length > 0
|
|
215
|
+
&& !path.isAbsolute(value)
|
|
216
|
+
&& value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseNpmPackageName(source: string): string | undefined {
|
|
220
|
+
const spec = source.slice(4).trim();
|
|
221
|
+
if (!spec) return undefined;
|
|
222
|
+
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
|
|
223
|
+
const packageName = match?.[1] ?? spec;
|
|
224
|
+
return isSafePackagePath(packageName) ? packageName : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function stripGitRef(repoPath: string): string {
|
|
228
|
+
const atIndex = repoPath.indexOf("@");
|
|
229
|
+
const hashIndex = repoPath.indexOf("#");
|
|
230
|
+
const refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
231
|
+
return refIndex === undefined ? repoPath : repoPath.slice(0, refIndex);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined {
|
|
235
|
+
const spec = source.slice(4).trim();
|
|
236
|
+
if (!spec) return undefined;
|
|
237
|
+
|
|
238
|
+
let host = "";
|
|
239
|
+
let repoPath = "";
|
|
240
|
+
const scpLike = spec.match(/^git@([^:]+):(.+)$/);
|
|
241
|
+
if (scpLike) {
|
|
242
|
+
host = scpLike[1] ?? "";
|
|
243
|
+
repoPath = scpLike[2] ?? "";
|
|
244
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(spec)) {
|
|
245
|
+
try {
|
|
246
|
+
const url = new URL(spec);
|
|
247
|
+
host = url.hostname;
|
|
248
|
+
repoPath = url.pathname.replace(/^\/+/, "");
|
|
249
|
+
} catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
const slashIndex = spec.indexOf("/");
|
|
254
|
+
if (slashIndex < 0) return undefined;
|
|
255
|
+
host = spec.slice(0, slashIndex);
|
|
256
|
+
repoPath = spec.slice(slashIndex + 1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const normalizedPath = stripGitRef(repoPath).replace(/\.git$/, "").replace(/^\/+/, "");
|
|
260
|
+
if (!host || !isSafePackagePath(host) || !isSafePackagePath(normalizedPath) || normalizedPath.split(/[\\/]/).length < 2) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return { host, repoPath: normalizedPath };
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {
|
|
214
267
|
const trimmed = source.trim();
|
|
215
268
|
if (!trimmed) return undefined;
|
|
269
|
+
if (trimmed.startsWith("git:")) {
|
|
270
|
+
const parsed = parseGitPackagePath(trimmed);
|
|
271
|
+
return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
|
|
272
|
+
}
|
|
273
|
+
if (trimmed.startsWith("npm:")) {
|
|
274
|
+
const packageName = parseNpmPackageName(trimmed);
|
|
275
|
+
return packageName ? path.join(baseDir, "npm", "node_modules", packageName) : undefined;
|
|
276
|
+
}
|
|
216
277
|
const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
|
|
217
278
|
if (normalized === "~") return os.homedir();
|
|
218
279
|
if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2));
|
package/slash-commands.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
5
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
4
6
|
import { discoverAgents, discoverAgentsAll } from "./agents.ts";
|
|
@@ -6,6 +8,7 @@ import { AgentManagerComponent, type ManagerResult } from "./agent-manager.ts";
|
|
|
6
8
|
import { SubagentsStatusComponent } from "./subagents-status.ts";
|
|
7
9
|
import { discoverAvailableSkills } from "./skills.ts";
|
|
8
10
|
import type { SubagentParamsLike } from "./subagent-executor.ts";
|
|
11
|
+
import { isParallelStep, type ChainStep } from "./settings.ts";
|
|
9
12
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
10
13
|
import {
|
|
11
14
|
applySlashUpdate,
|
|
@@ -20,6 +23,7 @@ import {
|
|
|
20
23
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
21
24
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
22
25
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
26
|
+
type SingleResult,
|
|
23
27
|
type SubagentState,
|
|
24
28
|
} from "./types.ts";
|
|
25
29
|
|
|
@@ -194,6 +198,46 @@ function extractSlashMessageText(content: string | Array<{ type?: string; text?:
|
|
|
194
198
|
.join("\n");
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
function formatExportPathList(paths: string[]): string {
|
|
202
|
+
return paths.map((file) => `- \`${file}\``).join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function collectResultPaths(results: SingleResult[], getPath: (result: SingleResult) => string | undefined): string[] {
|
|
206
|
+
return results
|
|
207
|
+
.map(getPath)
|
|
208
|
+
.filter((file): file is string => typeof file === "string" && file.length > 0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildSlashExportText(response: SlashSubagentResponse): string {
|
|
212
|
+
const output = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
|
|
213
|
+
const results = response.result.details?.results ?? [];
|
|
214
|
+
const sessionFiles = collectResultPaths(results, (result) => result.sessionFile);
|
|
215
|
+
const savedOutputs = collectResultPaths(results, (result) => result.savedOutputPath);
|
|
216
|
+
const artifactOutputs = collectResultPaths(results, (result) => result.artifactPaths?.outputPath);
|
|
217
|
+
const sections = ["## Subagent result", output];
|
|
218
|
+
if (sessionFiles.length > 0) sections.push("## Child session exports", formatExportPathList(sessionFiles));
|
|
219
|
+
if (savedOutputs.length > 0) sections.push("## Saved outputs", formatExportPathList(savedOutputs));
|
|
220
|
+
if (artifactOutputs.length > 0) sections.push("## Artifact outputs", formatExportPathList(artifactOutputs));
|
|
221
|
+
return sections.join("\n\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function persistSlashSessionSnapshot(ctx: ExtensionContext): void {
|
|
225
|
+
try {
|
|
226
|
+
if (!ctx.sessionManager) return;
|
|
227
|
+
const sessionManager = ctx.sessionManager as typeof ctx.sessionManager & {
|
|
228
|
+
_rewriteFile?: () => void;
|
|
229
|
+
flushed?: boolean;
|
|
230
|
+
};
|
|
231
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
232
|
+
if (!sessionFile || typeof sessionManager._rewriteFile !== "function") return;
|
|
233
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
234
|
+
sessionManager._rewriteFile();
|
|
235
|
+
sessionManager.flushed = true;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error("Failed to persist slash session snapshot for export:", error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
197
241
|
async function runSlashSubagent(
|
|
198
242
|
pi: ExtensionAPI,
|
|
199
243
|
ctx: ExtensionContext,
|
|
@@ -208,17 +252,18 @@ async function runSlashSubagent(
|
|
|
208
252
|
display: true,
|
|
209
253
|
details: initialDetails,
|
|
210
254
|
});
|
|
255
|
+
persistSlashSessionSnapshot(ctx);
|
|
211
256
|
|
|
212
257
|
try {
|
|
213
258
|
const response = await requestSlashRun(pi, ctx, requestId, params);
|
|
214
259
|
const finalDetails = finalizeSlashResult(response);
|
|
215
|
-
const text = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
|
|
216
260
|
pi.sendMessage({
|
|
217
261
|
customType: SLASH_RESULT_TYPE,
|
|
218
|
-
content:
|
|
219
|
-
display:
|
|
262
|
+
content: buildSlashExportText(response),
|
|
263
|
+
display: true,
|
|
220
264
|
details: finalDetails,
|
|
221
265
|
});
|
|
266
|
+
persistSlashSessionSnapshot(ctx);
|
|
222
267
|
if (ctx.hasUI) {
|
|
223
268
|
ctx.ui.setStatus("subagent-slash", undefined);
|
|
224
269
|
}
|
|
@@ -227,13 +272,14 @@ async function runSlashSubagent(
|
|
|
227
272
|
}
|
|
228
273
|
} catch (error) {
|
|
229
274
|
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
-
const failedDetails = failSlashResult(requestId, params, message
|
|
275
|
+
const failedDetails = failSlashResult(requestId, params, message);
|
|
231
276
|
pi.sendMessage({
|
|
232
277
|
customType: SLASH_RESULT_TYPE,
|
|
233
|
-
content: message
|
|
234
|
-
display:
|
|
278
|
+
content: `## Subagent result\n\n${message}`,
|
|
279
|
+
display: true,
|
|
235
280
|
details: failedDetails,
|
|
236
281
|
});
|
|
282
|
+
persistSlashSessionSnapshot(ctx);
|
|
237
283
|
if (ctx.hasUI) {
|
|
238
284
|
ctx.ui.setStatus("subagent-slash", undefined);
|
|
239
285
|
}
|
|
@@ -263,48 +309,43 @@ async function openAgentManager(
|
|
|
263
309
|
);
|
|
264
310
|
if (!result) return;
|
|
265
311
|
|
|
312
|
+
const launchOptions: SubagentParamsLike = {
|
|
313
|
+
clarify: !result.skipClarify && !result.background,
|
|
314
|
+
agentScope: "both",
|
|
315
|
+
...(result.fork ? { context: "fork" as const } : {}),
|
|
316
|
+
...(result.background ? { async: true } : {}),
|
|
317
|
+
};
|
|
318
|
+
|
|
266
319
|
if (result.action === "chain") {
|
|
267
320
|
const chain = result.agents.map((name, i) => ({
|
|
268
321
|
agent: name,
|
|
269
322
|
...(i === 0 ? { task: result.task } : {}),
|
|
270
323
|
}));
|
|
271
|
-
await runSlashSubagent(pi, ctx, {
|
|
272
|
-
chain,
|
|
273
|
-
task: result.task,
|
|
274
|
-
clarify: true,
|
|
275
|
-
agentScope: "both",
|
|
276
|
-
});
|
|
324
|
+
await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
|
|
277
325
|
return;
|
|
278
326
|
}
|
|
279
327
|
|
|
280
328
|
if (result.action === "launch") {
|
|
281
|
-
await runSlashSubagent(pi, ctx, {
|
|
282
|
-
agent: result.agent,
|
|
283
|
-
task: result.task,
|
|
284
|
-
clarify: !result.skipClarify,
|
|
285
|
-
agentScope: "both",
|
|
286
|
-
});
|
|
329
|
+
await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
|
|
287
330
|
} else if (result.action === "launch-chain") {
|
|
288
|
-
const chainParam = result.chain.steps.map((step) =>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
task: result.task,
|
|
300
|
-
clarify: !result.skipClarify,
|
|
301
|
-
agentScope: "both",
|
|
331
|
+
const chainParam = (result.chain.steps as unknown as ChainStep[]).map((step) => {
|
|
332
|
+
if (isParallelStep(step)) return result.worktree ? { ...step, worktree: true } : { ...step };
|
|
333
|
+
return {
|
|
334
|
+
agent: step.agent,
|
|
335
|
+
task: step.task || undefined,
|
|
336
|
+
output: step.output,
|
|
337
|
+
reads: step.reads,
|
|
338
|
+
progress: step.progress,
|
|
339
|
+
skill: step.skill ?? (step as typeof step & { skills?: string[] | false }).skills,
|
|
340
|
+
model: step.model,
|
|
341
|
+
};
|
|
302
342
|
});
|
|
343
|
+
await runSlashSubagent(pi, ctx, { chain: chainParam, task: result.task, ...launchOptions });
|
|
303
344
|
} else if (result.action === "parallel") {
|
|
304
345
|
await runSlashSubagent(pi, ctx, {
|
|
305
346
|
tasks: result.tasks,
|
|
306
|
-
|
|
307
|
-
|
|
347
|
+
...launchOptions,
|
|
348
|
+
...(result.worktree ? { worktree: true } : {}),
|
|
308
349
|
});
|
|
309
350
|
}
|
|
310
351
|
}
|
|
@@ -398,16 +439,15 @@ export function registerSlashCommands(
|
|
|
398
439
|
});
|
|
399
440
|
|
|
400
441
|
pi.registerCommand("run", {
|
|
401
|
-
description: "Run a subagent directly: /run agent[output=file] task [--bg] [--fork]",
|
|
442
|
+
description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
|
|
402
443
|
getArgumentCompletions: makeAgentCompletions(state, false),
|
|
403
444
|
handler: async (args, ctx) => {
|
|
404
445
|
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
405
446
|
const input = cleanedArgs.trim();
|
|
406
447
|
const firstSpace = input.indexOf(" ");
|
|
407
|
-
if (
|
|
408
|
-
const { name: agentName, config: inline } = parseAgentToken(input.slice(0, firstSpace));
|
|
409
|
-
const task = input.slice(firstSpace + 1).trim();
|
|
410
|
-
if (!task) { ctx.ui.notify("Usage: /run <agent> <task> [--bg] [--fork]", "error"); return; }
|
|
448
|
+
if (!input) { ctx.ui.notify("Usage: /run <agent> [task] [--bg] [--fork]", "error"); return; }
|
|
449
|
+
const { name: agentName, config: inline } = parseAgentToken(firstSpace === -1 ? input : input.slice(0, firstSpace));
|
|
450
|
+
const task = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
|
|
411
451
|
|
|
412
452
|
const agents = discoverAgents(state.baseCwd, "both").agents;
|
|
413
453
|
if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
|
|
@@ -459,8 +499,11 @@ export function registerSlashCommands(
|
|
|
459
499
|
const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
|
|
460
500
|
agent: name,
|
|
461
501
|
task: stepTask ?? parsed.task,
|
|
502
|
+
...(config.output !== undefined ? { output: config.output } : {}),
|
|
503
|
+
...(config.reads !== undefined ? { reads: config.reads } : {}),
|
|
462
504
|
...(config.model ? { model: config.model } : {}),
|
|
463
505
|
...(config.skill !== undefined ? { skill: config.skill } : {}),
|
|
506
|
+
...(config.progress !== undefined ? { progress: config.progress } : {}),
|
|
464
507
|
}));
|
|
465
508
|
const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
|
|
466
509
|
if (bg) params.async = true;
|
package/slash-live-state.ts
CHANGED
|
@@ -278,11 +278,9 @@ export function restoreSlashFinalSnapshots(entries: unknown[]): void {
|
|
|
278
278
|
liveSnapshots.clear();
|
|
279
279
|
finalSnapshots.clear();
|
|
280
280
|
for (const entry of entries) {
|
|
281
|
-
const e = entry as { type?: string;
|
|
282
|
-
if (e?.type !== "
|
|
283
|
-
const
|
|
284
|
-
if (!m || m.role !== "custom" || m.customType !== SLASH_RESULT_TYPE || m.display !== false) continue;
|
|
285
|
-
const details = resolveSlashMessageDetails(m.details);
|
|
281
|
+
const e = entry as { type?: string; customType?: string; details?: unknown };
|
|
282
|
+
if (e?.type !== "custom_message" || e.customType !== SLASH_RESULT_TYPE) continue;
|
|
283
|
+
const details = resolveSlashMessageDetails(e.details);
|
|
286
284
|
if (!details) continue;
|
|
287
285
|
finalSnapshots.set(details.requestId, { result: details.result, version: nextVersion() });
|
|
288
286
|
}
|
package/subagent-executor.ts
CHANGED
|
@@ -14,16 +14,20 @@ import { resolveModelCandidate } from "./model-fallback.ts";
|
|
|
14
14
|
import { aggregateParallelOutputs } from "./parallel-utils.ts";
|
|
15
15
|
import { recordRun } from "./run-history.ts";
|
|
16
16
|
import {
|
|
17
|
+
buildChainInstructions,
|
|
18
|
+
writeInitialProgressFile,
|
|
17
19
|
getStepAgents,
|
|
18
20
|
isParallelStep,
|
|
19
21
|
resolveStepBehavior,
|
|
20
22
|
type ChainStep,
|
|
23
|
+
type ResolvedStepBehavior,
|
|
21
24
|
type SequentialStep,
|
|
25
|
+
type StepOverrides,
|
|
22
26
|
} from "./settings.ts";
|
|
23
27
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
24
28
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
|
|
25
29
|
import { createForkContextResolver } from "./fork-context.ts";
|
|
26
|
-
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
|
|
30
|
+
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
|
|
27
31
|
import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "./subagent-control.ts";
|
|
28
32
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
29
33
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "./utils.ts";
|
|
@@ -46,6 +50,7 @@ import {
|
|
|
46
50
|
type ControlEvent,
|
|
47
51
|
type Details,
|
|
48
52
|
type ExtensionConfig,
|
|
53
|
+
type IntercomEventBus,
|
|
49
54
|
type MaxOutputConfig,
|
|
50
55
|
type ResolvedControlConfig,
|
|
51
56
|
type SingleResult,
|
|
@@ -68,6 +73,9 @@ interface TaskParam {
|
|
|
68
73
|
task: string;
|
|
69
74
|
cwd?: string;
|
|
70
75
|
count?: number;
|
|
76
|
+
output?: string | boolean;
|
|
77
|
+
reads?: string[] | boolean;
|
|
78
|
+
progress?: boolean;
|
|
71
79
|
model?: string;
|
|
72
80
|
skill?: string | string[] | boolean;
|
|
73
81
|
}
|
|
@@ -326,7 +334,7 @@ function validateExecutionInput(
|
|
|
326
334
|
function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
|
|
327
335
|
if ((params.chain?.length ?? 0) > 0) return "chain";
|
|
328
336
|
if ((params.tasks?.length ?? 0) > 0) return "parallel";
|
|
329
|
-
if (params.agent
|
|
337
|
+
if (params.agent) return "single";
|
|
330
338
|
return "single";
|
|
331
339
|
}
|
|
332
340
|
|
|
@@ -483,7 +491,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
483
491
|
} = data;
|
|
484
492
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
485
493
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
486
|
-
const hasSingle = Boolean(params.agent
|
|
494
|
+
const hasSingle = !hasChain && !hasTasks && Boolean(params.agent);
|
|
487
495
|
if (!effectiveAsync) return null;
|
|
488
496
|
|
|
489
497
|
if (hasChain && params.chain) {
|
|
@@ -544,6 +552,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
544
552
|
cwd: task.cwd,
|
|
545
553
|
...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
|
|
546
554
|
...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
|
|
555
|
+
...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}),
|
|
556
|
+
...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
|
|
557
|
+
...(task.progress !== undefined ? { progress: task.progress } : {}),
|
|
547
558
|
}));
|
|
548
559
|
return executeAsyncChain(id, {
|
|
549
560
|
chain: [{
|
|
@@ -614,7 +625,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
614
625
|
const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
|
|
615
626
|
return executeAsyncSingle(id, {
|
|
616
627
|
agent: params.agent!,
|
|
617
|
-
task: params.context === "fork" ? wrapForkTask(params.task
|
|
628
|
+
task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
|
|
618
629
|
agentConfig: a,
|
|
619
630
|
ctx: asyncCtx,
|
|
620
631
|
availableModels,
|
|
@@ -669,6 +680,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
669
680
|
task: params.task,
|
|
670
681
|
agents,
|
|
671
682
|
ctx,
|
|
683
|
+
intercomEvents: deps.pi.events,
|
|
672
684
|
signal,
|
|
673
685
|
runId,
|
|
674
686
|
cwd: effectiveCwd,
|
|
@@ -741,6 +753,7 @@ interface ForegroundParallelRunInput {
|
|
|
741
753
|
taskTexts: string[];
|
|
742
754
|
agents: AgentConfig[];
|
|
743
755
|
ctx: ExtensionContext;
|
|
756
|
+
intercomEvents: IntercomEventBus;
|
|
744
757
|
signal: AbortSignal;
|
|
745
758
|
runId: string;
|
|
746
759
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
@@ -749,12 +762,12 @@ interface ForegroundParallelRunInput {
|
|
|
749
762
|
artifactConfig: ArtifactConfig;
|
|
750
763
|
artifactsDir: string;
|
|
751
764
|
maxOutput?: MaxOutputConfig;
|
|
752
|
-
paramsCwd
|
|
765
|
+
paramsCwd: string;
|
|
753
766
|
maxSubagentDepths: number[];
|
|
754
767
|
availableModels: ModelInfo[];
|
|
755
768
|
modelOverrides: (string | undefined)[];
|
|
756
|
-
skillOverrides: (string[] | false | undefined)[];
|
|
757
769
|
behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
|
|
770
|
+
firstProgressIndex: number;
|
|
758
771
|
controlConfig: ResolvedControlConfig;
|
|
759
772
|
onControlEvent?: (event: ControlEvent) => void;
|
|
760
773
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
@@ -822,12 +835,11 @@ function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string):
|
|
|
822
835
|
|
|
823
836
|
function resolveParallelTaskCwd(
|
|
824
837
|
task: TaskParam,
|
|
825
|
-
paramsCwd: string
|
|
838
|
+
paramsCwd: string,
|
|
826
839
|
worktreeSetup: WorktreeSetup | undefined,
|
|
827
840
|
index: number,
|
|
828
|
-
): string
|
|
841
|
+
): string {
|
|
829
842
|
if (worktreeSetup) return worktreeSetup.worktrees[index]!.agentCwd;
|
|
830
|
-
if (!paramsCwd) return task.cwd;
|
|
831
843
|
return resolveChildCwd(paramsCwd, task.cwd);
|
|
832
844
|
}
|
|
833
845
|
|
|
@@ -842,11 +854,46 @@ function buildParallelWorktreeSuffix(
|
|
|
842
854
|
return formatWorktreeDiffSummary(diffs);
|
|
843
855
|
}
|
|
844
856
|
|
|
857
|
+
function findDuplicateParallelOutputPath(input: {
|
|
858
|
+
tasks: TaskParam[];
|
|
859
|
+
behaviors: ResolvedStepBehavior[];
|
|
860
|
+
paramsCwd: string;
|
|
861
|
+
ctxCwd: string;
|
|
862
|
+
worktreeSetup?: WorktreeSetup;
|
|
863
|
+
}): string | undefined {
|
|
864
|
+
const seen = new Map<string, { index: number; agent: string }>();
|
|
865
|
+
for (let index = 0; index < input.tasks.length; index++) {
|
|
866
|
+
const behavior = input.behaviors[index];
|
|
867
|
+
if (!behavior?.output) continue;
|
|
868
|
+
const task = input.tasks[index]!;
|
|
869
|
+
const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
|
|
870
|
+
const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd);
|
|
871
|
+
if (!outputPath) continue;
|
|
872
|
+
const previous = seen.get(outputPath);
|
|
873
|
+
if (previous) {
|
|
874
|
+
return `Parallel tasks ${previous.index + 1} (${previous.agent}) and ${index + 1} (${task.agent}) resolve output to the same path: ${outputPath}. Use distinct output paths.`;
|
|
875
|
+
}
|
|
876
|
+
seen.set(outputPath, { index, agent: task.agent });
|
|
877
|
+
}
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
|
|
845
881
|
async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
|
|
846
882
|
return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
|
|
847
|
-
const
|
|
848
|
-
const effectiveSkills =
|
|
883
|
+
const behavior = input.behaviors[index];
|
|
884
|
+
const effectiveSkills = behavior?.skills;
|
|
849
885
|
const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
|
|
886
|
+
const readInstructions = behavior
|
|
887
|
+
? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
|
|
888
|
+
: { prefix: "", suffix: "" };
|
|
889
|
+
const progressInstructions = behavior
|
|
890
|
+
? buildChainInstructions({ ...behavior, output: false, reads: false }, input.paramsCwd, index === input.firstProgressIndex)
|
|
891
|
+
: { prefix: "", suffix: "" };
|
|
892
|
+
const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
|
|
893
|
+
const taskText = injectSingleOutputInstruction(
|
|
894
|
+
`${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
|
|
895
|
+
outputPath,
|
|
896
|
+
);
|
|
850
897
|
const interruptController = new AbortController();
|
|
851
898
|
if (input.foregroundControl) {
|
|
852
899
|
input.foregroundControl.currentAgent = task.agent;
|
|
@@ -861,10 +908,13 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
861
908
|
return true;
|
|
862
909
|
};
|
|
863
910
|
}
|
|
864
|
-
|
|
911
|
+
const agentConfig = input.agents.find((agent) => agent.name === task.agent);
|
|
912
|
+
return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
|
|
865
913
|
cwd: taskCwd,
|
|
866
914
|
signal: input.signal,
|
|
867
915
|
interruptSignal: interruptController.signal,
|
|
916
|
+
allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
|
|
917
|
+
intercomEvents: input.intercomEvents,
|
|
868
918
|
runId: input.runId,
|
|
869
919
|
index,
|
|
870
920
|
sessionDir: input.sessionDirForIndex(index),
|
|
@@ -873,6 +923,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
873
923
|
artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
|
|
874
924
|
artifactConfig: input.artifactConfig,
|
|
875
925
|
maxOutput: input.maxOutput,
|
|
926
|
+
outputPath,
|
|
876
927
|
maxSubagentDepth: input.maxSubagentDepths[index],
|
|
877
928
|
controlConfig: input.controlConfig,
|
|
878
929
|
onControlEvent: input.onControlEvent,
|
|
@@ -983,16 +1034,23 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
983
1034
|
fullId: `${m.provider}/${m.id}`,
|
|
984
1035
|
}));
|
|
985
1036
|
let taskTexts = tasks.map((t) => t.task);
|
|
986
|
-
const modelOverrides: (string | undefined)[] = tasks.map((t, i) =>
|
|
987
|
-
resolveModelCandidate(t.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
|
|
988
|
-
);
|
|
989
1037
|
const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
|
|
990
1038
|
normalizeSkillInput(t.skill),
|
|
991
1039
|
);
|
|
1040
|
+
const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
|
|
1041
|
+
...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}),
|
|
1042
|
+
...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
|
|
1043
|
+
...(task.progress !== undefined ? { progress: task.progress } : {}),
|
|
1044
|
+
...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
|
|
1045
|
+
...(task.model ? { model: task.model } : {}),
|
|
1046
|
+
}));
|
|
1047
|
+
const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
|
|
1048
|
+
resolveModelCandidate(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
|
|
1049
|
+
);
|
|
992
1050
|
|
|
993
1051
|
if (params.clarify === true && ctx.hasUI) {
|
|
994
1052
|
const behaviors = agentConfigs.map((c, i) =>
|
|
995
|
-
resolveStepBehavior(c,
|
|
1053
|
+
resolveStepBehavior(c, behaviorOverrides[i]!),
|
|
996
1054
|
);
|
|
997
1055
|
const availableSkills = discoverAvailableSkills(effectiveCwd);
|
|
998
1056
|
|
|
@@ -1021,8 +1079,17 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1021
1079
|
taskTexts = result.templates;
|
|
1022
1080
|
for (let i = 0; i < result.behaviorOverrides.length; i++) {
|
|
1023
1081
|
const override = result.behaviorOverrides[i];
|
|
1024
|
-
if (override?.model)
|
|
1025
|
-
|
|
1082
|
+
if (override?.model) {
|
|
1083
|
+
modelOverrides[i] = override.model;
|
|
1084
|
+
behaviorOverrides[i]!.model = override.model;
|
|
1085
|
+
}
|
|
1086
|
+
if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
|
|
1087
|
+
if (override?.reads !== undefined) behaviorOverrides[i]!.reads = override.reads;
|
|
1088
|
+
if (override?.progress !== undefined) behaviorOverrides[i]!.progress = override.progress;
|
|
1089
|
+
if (override?.skills !== undefined) {
|
|
1090
|
+
skillOverrides[i] = override.skills;
|
|
1091
|
+
behaviorOverrides[i]!.skills = override.skills;
|
|
1092
|
+
}
|
|
1026
1093
|
}
|
|
1027
1094
|
|
|
1028
1095
|
if (result.runInBackground) {
|
|
@@ -1046,6 +1113,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1046
1113
|
cwd: t.cwd,
|
|
1047
1114
|
...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
|
|
1048
1115
|
...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
|
|
1116
|
+
...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
|
|
1117
|
+
...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
|
|
1118
|
+
...(behaviorOverrides[i]?.progress !== undefined ? { progress: behaviorOverrides[i]!.progress } : {}),
|
|
1049
1119
|
}));
|
|
1050
1120
|
return executeAsyncChain(id, {
|
|
1051
1121
|
chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
|
|
@@ -1070,7 +1140,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1070
1140
|
}
|
|
1071
1141
|
}
|
|
1072
1142
|
|
|
1073
|
-
const behaviors = agentConfigs.map((config) => resolveStepBehavior(config,
|
|
1143
|
+
const behaviors = agentConfigs.map((config, index) => resolveStepBehavior(config, behaviorOverrides[index]!));
|
|
1144
|
+
const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
|
|
1074
1145
|
const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
1075
1146
|
const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
1076
1147
|
const foregroundControl = deps.state.foregroundControls.get(runId);
|
|
@@ -1085,6 +1156,18 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1085
1156
|
if (errorResult) return errorResult;
|
|
1086
1157
|
|
|
1087
1158
|
try {
|
|
1159
|
+
const duplicateOutputError = findDuplicateParallelOutputPath({
|
|
1160
|
+
tasks,
|
|
1161
|
+
behaviors,
|
|
1162
|
+
paramsCwd: effectiveCwd,
|
|
1163
|
+
ctxCwd: ctx.cwd,
|
|
1164
|
+
worktreeSetup,
|
|
1165
|
+
});
|
|
1166
|
+
if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
|
|
1167
|
+
|
|
1168
|
+
const parallelProgressPrecreated = firstProgressIndex !== -1;
|
|
1169
|
+
if (parallelProgressPrecreated) writeInitialProgressFile(effectiveCwd);
|
|
1170
|
+
|
|
1088
1171
|
if (params.context === "fork") {
|
|
1089
1172
|
for (let i = 0; i < taskTexts.length; i++) {
|
|
1090
1173
|
taskTexts[i] = wrapForkTask(taskTexts[i]!);
|
|
@@ -1096,6 +1179,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1096
1179
|
taskTexts,
|
|
1097
1180
|
agents,
|
|
1098
1181
|
ctx,
|
|
1182
|
+
intercomEvents: deps.pi.events,
|
|
1099
1183
|
signal,
|
|
1100
1184
|
runId,
|
|
1101
1185
|
sessionDirForIndex,
|
|
@@ -1107,8 +1191,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1107
1191
|
paramsCwd: effectiveCwd,
|
|
1108
1192
|
availableModels,
|
|
1109
1193
|
modelOverrides,
|
|
1110
|
-
skillOverrides,
|
|
1111
1194
|
behaviors,
|
|
1195
|
+
firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
|
|
1112
1196
|
controlConfig,
|
|
1113
1197
|
onControlEvent,
|
|
1114
1198
|
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
|
|
@@ -1211,7 +1295,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1211
1295
|
id: m.id,
|
|
1212
1296
|
fullId: `${m.provider}/${m.id}`,
|
|
1213
1297
|
}));
|
|
1214
|
-
let task = params.task
|
|
1298
|
+
let task = params.task ?? "";
|
|
1215
1299
|
let modelOverride: string | undefined = resolveModelCandidate(
|
|
1216
1300
|
(params.model as string | undefined) ?? agentConfig.model,
|
|
1217
1301
|
availableModels,
|
|
@@ -1345,7 +1429,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1345
1429
|
cwd: effectiveCwd,
|
|
1346
1430
|
signal,
|
|
1347
1431
|
interruptSignal: interruptController.signal,
|
|
1348
|
-
allowIntercomDetach: agentConfig.systemPrompt?.includes(
|
|
1432
|
+
allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
|
|
1349
1433
|
intercomEvents: deps.pi.events,
|
|
1350
1434
|
runId,
|
|
1351
1435
|
sessionDir: sessionDirForIndex(0),
|
|
@@ -1548,7 +1632,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1548
1632
|
const shareEnabled = effectiveParams.share === true;
|
|
1549
1633
|
const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
|
|
1550
1634
|
const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
|
|
1551
|
-
const hasSingle = Boolean(effectiveParams.agent
|
|
1635
|
+
const hasSingle = !hasChain && !hasTasks && Boolean(effectiveParams.agent);
|
|
1552
1636
|
const allowClarifyTaskPrompt = hasChain
|
|
1553
1637
|
&& effectiveParams.clarify === true
|
|
1554
1638
|
&& ctx.hasUI
|
|
@@ -1602,6 +1686,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1602
1686
|
}
|
|
1603
1687
|
const sessionDirForIndex = (idx?: number) =>
|
|
1604
1688
|
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
1689
|
+
const childSessionFileForIndex = (idx?: number) =>
|
|
1690
|
+
sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
|
|
1605
1691
|
|
|
1606
1692
|
const onUpdateWithContext = onUpdate
|
|
1607
1693
|
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
|
|
@@ -1618,7 +1704,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1618
1704
|
shareEnabled,
|
|
1619
1705
|
sessionRoot,
|
|
1620
1706
|
sessionDirForIndex,
|
|
1621
|
-
sessionFileForIndex,
|
|
1707
|
+
sessionFileForIndex: childSessionFileForIndex,
|
|
1622
1708
|
artifactConfig,
|
|
1623
1709
|
artifactsDir,
|
|
1624
1710
|
backgroundRequestedWhileClarifying,
|
package/subagent-runner.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
formatWorktreeTaskCwdConflict,
|
|
52
52
|
type WorktreeSetup,
|
|
53
53
|
} from "./worktree.ts";
|
|
54
|
+
import { writeInitialProgressFile } from "./settings.ts";
|
|
54
55
|
|
|
55
56
|
interface SubagentRunConfig {
|
|
56
57
|
id: string;
|
|
@@ -817,6 +818,12 @@ function appendParallelWorktreeSummary(
|
|
|
817
818
|
return `${previousOutput}\n\n${diffSummary}`;
|
|
818
819
|
}
|
|
819
820
|
|
|
821
|
+
function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
|
|
822
|
+
const progressPath = path.join(cwd, "progress.md");
|
|
823
|
+
if (!group.parallel.some((task) => task.task.includes(`Update progress at: ${progressPath}`))) return;
|
|
824
|
+
writeInitialProgressFile(cwd);
|
|
825
|
+
}
|
|
826
|
+
|
|
820
827
|
async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
821
828
|
const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
|
|
822
829
|
config;
|
|
@@ -1039,6 +1046,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1039
1046
|
}
|
|
1040
1047
|
|
|
1041
1048
|
try {
|
|
1049
|
+
if (group.worktree) ensureParallelProgressFile(cwd, group);
|
|
1042
1050
|
const groupStartTime = Date.now();
|
|
1043
1051
|
markParallelGroupRunning({
|
|
1044
1052
|
statusPayload,
|