pi-subagents 0.15.0 → 0.16.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 +13 -0
- package/README.md +28 -1
- package/agents.ts +32 -12
- package/fork-context.ts +7 -2
- package/formatters.ts +5 -3
- package/index.ts +1 -1
- package/package.json +1 -1
- package/render.ts +73 -49
- package/schemas.ts +1 -0
- package/slash-commands.ts +12 -17
- package/subagent-executor.ts +68 -19
- package/types.ts +31 -0
- package/utils.ts +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.16.0] - 2026-04-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Top-level parallel `tasks` mode now supports a per-call `concurrency` override, matching the existing chain parallel-step concurrency control. This ships part of issue `#91`. Thanks @Gabrielgvl.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Top-level parallel defaults and limits can now be configured through `~/.pi/agent/extensions/subagent/config.json` under `parallel.maxTasks` and `parallel.concurrency`, while keeping the existing defaults of 8 tasks and concurrency 4 when unset. This completes issue `#91`. Thanks @Gabrielgvl.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- `context: "fork"` sync runs now create child sessions from a throwaway session-manager instance opened on the persisted parent session file, instead of mutating the live parent session manager. This keeps the parent session writing to its own file so the matching `toolResult(subagent)` no longer lands in a descendant session by accident. This fixes issue `#87`. Thanks @asmisha.
|
|
15
|
+
- Project agent and chain discovery now reads both `.agents/` and `.pi/agents/`, while preferring `.pi/agents/` when both locations define the same parsed name and keeping manager writes on the `.pi/agents/` path. This fixes issue `#88`. Thanks @desek.
|
|
16
|
+
- Ctrl+O expanded subagent results now actually show expanded content. Previously the `expanded` flag was received but ignored, so task text and tool-call args were identically truncated in both views. Now expanded mode shows the full task and longer (but still bounded) tool-call previews. Additionally, tool calls are no longer lost after foreground compaction: compact display summaries are preserved and shown in expanded view even after `messages` are stripped. This addresses issue `#90`. Thanks @asagajda.
|
|
17
|
+
|
|
5
18
|
## [0.15.0] - 2026-04-16
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
|
|
|
42
42
|
| User | `~/.pi/agent/agents/{name}.md` | Medium |
|
|
43
43
|
| Project | `.pi/agents/{name}.md` (searches up directory tree) | Highest |
|
|
44
44
|
|
|
45
|
+
Project discovery also reads legacy `.agents/{name}.md` files. If both `.agents/` and `.pi/agents/` contain a project agent with the same parsed `name`, `.pi/agents/` wins and the agent manager still writes to `.pi/agents/`.
|
|
46
|
+
|
|
45
47
|
Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
|
|
46
48
|
|
|
47
49
|
**Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, and `delegate`. They load at lowest priority so any user or project agent with the same name overrides them.
|
|
@@ -255,7 +257,7 @@ Append `[key=value,...]` to any agent name to override its defaults:
|
|
|
255
257
|
```
|
|
256
258
|
/chain scout[output=context.md] "scan code" -> planner[reads=context.md] "analyze auth"
|
|
257
259
|
/run scout[model=anthropic/claude-sonnet-4] summarize this codebase
|
|
258
|
-
/parallel scanner[
|
|
260
|
+
/parallel scanner[model=anthropic/claude-sonnet-4] "find issues" -> reviewer[skills=code-review+security] "check style"
|
|
259
261
|
```
|
|
260
262
|
|
|
261
263
|
| Key | Example | Description |
|
|
@@ -359,6 +361,8 @@ Chains are `.chain.md` files stored alongside agent files. They define reusable
|
|
|
359
361
|
| User | `~/.pi/agent/agents/{name}.chain.md` |
|
|
360
362
|
| Project | `.pi/agents/{name}.chain.md` |
|
|
361
363
|
|
|
364
|
+
Project chain discovery also reads legacy `.agents/{name}.chain.md` files. If both locations define the same parsed chain `name`, `.pi/agents/` wins and manager writes stay in `.pi/agents/`.
|
|
365
|
+
|
|
362
366
|
**Format:**
|
|
363
367
|
|
|
364
368
|
```markdown
|
|
@@ -545,6 +549,9 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
545
549
|
// Parallel
|
|
546
550
|
{ tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
|
|
547
551
|
|
|
552
|
+
// Parallel with top-level concurrency override
|
|
553
|
+
{ tasks: [{ agent: "scout", task: "a" }, { agent: "reviewer", task: "b" }], concurrency: 2 }
|
|
554
|
+
|
|
548
555
|
// Parallel with count shorthand (run the same task 3 times)
|
|
549
556
|
{ tasks: [{ agent: "scout", task: "audit auth", count: 3 }] }
|
|
550
557
|
|
|
@@ -711,6 +718,7 @@ Notes:
|
|
|
711
718
|
| `model` | string | agent default | Override model for single agent |
|
|
712
719
|
| `fallbackModels` | `string \| string[]` | agent default | Management/config-only field for ordered backup models. Markdown frontmatter uses a comma-separated string. |
|
|
713
720
|
| `tasks` | `{agent, task, cwd?, count?, skill?}[]` | - | Parallel tasks. Foreground runs directly; background requests are converted to an equivalent chain. `count` repeats one task entry N times with the same settings. |
|
|
721
|
+
| `concurrency` | number | `config.parallel.concurrency` or `4` | Top-level `tasks` mode only: max concurrent tasks. |
|
|
714
722
|
| `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
|
|
715
723
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
716
724
|
| `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
|
|
@@ -845,6 +853,25 @@ This aggregated output becomes `{previous}` for the next step.
|
|
|
845
853
|
|
|
846
854
|
`pi-subagents` reads optional JSON config from `~/.pi/agent/extensions/subagent/config.json`.
|
|
847
855
|
|
|
856
|
+
### `parallel`
|
|
857
|
+
|
|
858
|
+
`parallel` controls top-level `tasks` mode defaults and limits.
|
|
859
|
+
|
|
860
|
+
```json
|
|
861
|
+
{
|
|
862
|
+
"parallel": {
|
|
863
|
+
"maxTasks": 12,
|
|
864
|
+
"concurrency": 6
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Fields:
|
|
870
|
+
- `maxTasks` defaults to `8` when unset or invalid
|
|
871
|
+
- `concurrency` defaults to `4` when unset or invalid
|
|
872
|
+
|
|
873
|
+
Per-call `concurrency` on the `subagent` tool takes precedence over `config.parallel.concurrency` for top-level `tasks` runs.
|
|
874
|
+
|
|
848
875
|
### `defaultSessionDir`
|
|
849
876
|
|
|
850
877
|
`defaultSessionDir` sets the fallback directory used for session logs. Eg:
|
package/agents.ts
CHANGED
|
@@ -606,13 +606,20 @@ function isDirectory(p: string): boolean {
|
|
|
606
606
|
}
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
-
function
|
|
609
|
+
function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
610
610
|
const projectRoot = findNearestProjectRoot(cwd);
|
|
611
|
-
if (!projectRoot) return null;
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
611
|
+
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
612
|
+
|
|
613
|
+
const legacyDir = path.join(projectRoot, ".agents");
|
|
614
|
+
const preferredDir = path.join(projectRoot, ".pi", "agents");
|
|
615
|
+
const readDirs: string[] = [];
|
|
616
|
+
if (isDirectory(legacyDir)) readDirs.push(legacyDir);
|
|
617
|
+
if (isDirectory(preferredDir)) readDirs.push(preferredDir);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
readDirs,
|
|
621
|
+
preferredDir,
|
|
622
|
+
};
|
|
616
623
|
}
|
|
617
624
|
|
|
618
625
|
const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
|
|
@@ -620,7 +627,7 @@ const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)
|
|
|
620
627
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
621
628
|
const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
622
629
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
623
|
-
const projectAgentsDir =
|
|
630
|
+
const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
|
|
624
631
|
const userSettingsPath = getUserAgentSettingsPath();
|
|
625
632
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
626
633
|
|
|
@@ -631,12 +638,12 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
631
638
|
userSettingsPath,
|
|
632
639
|
projectSettingsPath,
|
|
633
640
|
);
|
|
634
|
-
|
|
641
|
+
|
|
635
642
|
const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
|
|
636
643
|
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
637
644
|
const userAgents = [...userAgentsOld, ...userAgentsNew];
|
|
638
645
|
|
|
639
|
-
const projectAgents = scope === "user"
|
|
646
|
+
const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
|
|
640
647
|
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents);
|
|
641
648
|
|
|
642
649
|
return { agents, projectAgentsDir };
|
|
@@ -654,7 +661,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
654
661
|
} {
|
|
655
662
|
const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
656
663
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
657
|
-
const projectDir =
|
|
664
|
+
const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
|
|
658
665
|
const userSettingsPath = getUserAgentSettingsPath();
|
|
659
666
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
660
667
|
|
|
@@ -669,11 +676,24 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
669
676
|
...loadAgentsFromDir(userDirOld, "user"),
|
|
670
677
|
...loadAgentsFromDir(userDirNew, "user"),
|
|
671
678
|
];
|
|
672
|
-
const
|
|
679
|
+
const projectMap = new Map<string, AgentConfig>();
|
|
680
|
+
for (const dir of projectDirs) {
|
|
681
|
+
for (const agent of loadAgentsFromDir(dir, "project")) {
|
|
682
|
+
projectMap.set(agent.name, agent);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const project = Array.from(projectMap.values());
|
|
686
|
+
|
|
687
|
+
const chainMap = new Map<string, ChainConfig>();
|
|
688
|
+
for (const dir of projectDirs) {
|
|
689
|
+
for (const chain of loadChainsFromDir(dir, "project")) {
|
|
690
|
+
chainMap.set(chain.name, chain);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
673
693
|
const chains = [
|
|
674
694
|
...loadChainsFromDir(userDirOld, "user"),
|
|
675
695
|
...loadChainsFromDir(userDirNew, "user"),
|
|
676
|
-
...(
|
|
696
|
+
...Array.from(chainMap.values()),
|
|
677
697
|
];
|
|
678
698
|
|
|
679
699
|
const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
package/fork-context.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export type SubagentExecutionContext = "fresh" | "fork";
|
|
2
2
|
|
|
3
|
+
interface ForkableSessionManagerStatic {
|
|
4
|
+
open(path: string): { createBranchedSession(leafId: string): string | undefined };
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export interface ForkableSessionManager {
|
|
4
8
|
getSessionFile(): string | undefined;
|
|
5
9
|
getLeafId(): string | null;
|
|
6
|
-
|
|
10
|
+
constructor: ForkableSessionManagerStatic;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export interface ForkContextResolver {
|
|
@@ -41,7 +45,8 @@ export function createForkContextResolver(
|
|
|
41
45
|
const cached = cachedSessionFiles.get(index);
|
|
42
46
|
if (cached) return cached;
|
|
43
47
|
try {
|
|
44
|
-
const
|
|
48
|
+
const sourceManager = sessionManager.constructor.open(parentSessionFile);
|
|
49
|
+
const sessionFile = sourceManager.createBranchedSession(leafId);
|
|
45
50
|
if (!sessionFile) {
|
|
46
51
|
throw new Error("Session manager did not return a session file.");
|
|
47
52
|
}
|
package/formatters.ts
CHANGED
|
@@ -83,11 +83,12 @@ export function buildChainSummary(
|
|
|
83
83
|
/**
|
|
84
84
|
* Format a tool call for display
|
|
85
85
|
*/
|
|
86
|
-
export function formatToolCall(name: string, args: Record<string, unknown
|
|
86
|
+
export function formatToolCall(name: string, args: Record<string, unknown>, expanded = false): string {
|
|
87
87
|
switch (name) {
|
|
88
88
|
case "bash": {
|
|
89
89
|
const command = typeof args.command === "string" ? args.command : "";
|
|
90
|
-
|
|
90
|
+
const maxLength = expanded ? 240 : 60;
|
|
91
|
+
return `$ ${command.slice(0, maxLength)}${command.length > maxLength ? "..." : ""}`;
|
|
91
92
|
}
|
|
92
93
|
case "read":
|
|
93
94
|
case "write":
|
|
@@ -101,7 +102,8 @@ export function formatToolCall(name: string, args: Record<string, unknown>): str
|
|
|
101
102
|
}
|
|
102
103
|
default: {
|
|
103
104
|
const s = JSON.stringify(args);
|
|
104
|
-
|
|
105
|
+
const maxLength = expanded ? 160 : 40;
|
|
106
|
+
return `${name} ${s.slice(0, maxLength)}${s.length > maxLength ? "..." : ""}`;
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
}
|
package/index.ts
CHANGED
|
@@ -253,7 +253,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
253
253
|
EXECUTION (use exactly ONE mode):
|
|
254
254
|
• SINGLE: { agent, task } - one task
|
|
255
255
|
• CHAIN: { chain: [{agent:"scout"}, {parallel:[{agent:"worker",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
256
|
-
• PARALLEL: { tasks: [{agent,task,count?}, ...], worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
256
|
+
• PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
257
257
|
• Optional context: { context: "fresh" | "fork" } (default: "fresh")
|
|
258
258
|
|
|
259
259
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -101,6 +101,18 @@ function hasEmptyTextOutputWithoutOutputTarget(task: string, output: string): bo
|
|
|
101
101
|
return !extractOutputTarget(task);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function getToolCallLines(
|
|
105
|
+
result: Pick<Details["results"][number], "messages" | "toolCalls">,
|
|
106
|
+
expanded: boolean,
|
|
107
|
+
): string[] {
|
|
108
|
+
if (result.messages) {
|
|
109
|
+
return getDisplayItems(result.messages)
|
|
110
|
+
.filter((item): item is { type: "tool"; name: string; args: Record<string, unknown> } => item.type === "tool")
|
|
111
|
+
.map((item) => formatToolCall(item.name, item.args, expanded));
|
|
112
|
+
}
|
|
113
|
+
return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
|
|
114
|
+
}
|
|
115
|
+
|
|
104
116
|
/**
|
|
105
117
|
* Render the async jobs widget
|
|
106
118
|
*/
|
|
@@ -165,7 +177,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
165
177
|
*/
|
|
166
178
|
export function renderSubagentResult(
|
|
167
179
|
result: AgentToolResult<Details>,
|
|
168
|
-
|
|
180
|
+
options: { expanded: boolean },
|
|
169
181
|
theme: Theme,
|
|
170
182
|
): Component {
|
|
171
183
|
const d = result.details;
|
|
@@ -176,6 +188,7 @@ export function renderSubagentResult(
|
|
|
176
188
|
return new Text(truncLine(`${contextPrefix}${text}`, getTermWidth() - 4), 0, 0);
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
const expanded = options.expanded;
|
|
179
192
|
const mdTheme = getMarkdownTheme();
|
|
180
193
|
|
|
181
194
|
if (d.mode === "single" && d.results.length === 1) {
|
|
@@ -198,15 +211,17 @@ export function renderSubagentResult(
|
|
|
198
211
|
: "";
|
|
199
212
|
|
|
200
213
|
const w = getTermWidth() - 4;
|
|
214
|
+
const fit = (text: string) => expanded ? text : truncLine(text, w);
|
|
215
|
+
const toolCallLines = getToolCallLines(r, expanded);
|
|
201
216
|
const c = new Container();
|
|
202
|
-
c.addChild(new Text(
|
|
217
|
+
c.addChild(new Text(fit(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`), 0, 0));
|
|
203
218
|
c.addChild(new Spacer(1));
|
|
204
219
|
const taskMaxLen = Math.max(20, w - 8);
|
|
205
|
-
const taskPreview = r.task.length
|
|
206
|
-
?
|
|
207
|
-
: r.task
|
|
220
|
+
const taskPreview = expanded || r.task.length <= taskMaxLen
|
|
221
|
+
? r.task
|
|
222
|
+
: `${r.task.slice(0, taskMaxLen)}...`;
|
|
208
223
|
c.addChild(
|
|
209
|
-
new Text(
|
|
224
|
+
new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0),
|
|
210
225
|
);
|
|
211
226
|
c.addChild(new Spacer(1));
|
|
212
227
|
|
|
@@ -214,58 +229,58 @@ export function renderSubagentResult(
|
|
|
214
229
|
if (r.progress.currentTool) {
|
|
215
230
|
const maxToolArgsLen = Math.max(50, w - 20);
|
|
216
231
|
const toolArgsPreview = r.progress.currentToolArgs
|
|
217
|
-
? (r.progress.currentToolArgs.length
|
|
218
|
-
?
|
|
219
|
-
: r.progress.currentToolArgs)
|
|
232
|
+
? (expanded || r.progress.currentToolArgs.length <= maxToolArgsLen
|
|
233
|
+
? r.progress.currentToolArgs
|
|
234
|
+
: `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
220
235
|
: "";
|
|
221
236
|
const toolLine = toolArgsPreview
|
|
222
237
|
? `${r.progress.currentTool}: ${toolArgsPreview}`
|
|
223
238
|
: r.progress.currentTool;
|
|
224
|
-
c.addChild(new Text(
|
|
239
|
+
c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
|
|
225
240
|
}
|
|
226
241
|
if (r.progress.recentTools?.length) {
|
|
227
242
|
for (const t of r.progress.recentTools.slice(-3)) {
|
|
228
243
|
const maxArgsLen = Math.max(40, w - 24);
|
|
229
|
-
const argsPreview = t.args.length
|
|
230
|
-
?
|
|
231
|
-
: t.args
|
|
232
|
-
c.addChild(new Text(
|
|
244
|
+
const argsPreview = expanded || t.args.length <= maxArgsLen
|
|
245
|
+
? t.args
|
|
246
|
+
: `${t.args.slice(0, maxArgsLen)}...`;
|
|
247
|
+
c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
|
|
233
248
|
}
|
|
234
249
|
}
|
|
235
250
|
for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
|
|
236
|
-
c.addChild(new Text(
|
|
251
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
237
252
|
}
|
|
238
253
|
if (r.progress.currentTool || r.progress.recentTools?.length || r.progress.recentOutput?.length) {
|
|
239
254
|
c.addChild(new Spacer(1));
|
|
240
255
|
}
|
|
241
256
|
}
|
|
242
257
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
258
|
+
if (expanded) {
|
|
259
|
+
for (const line of toolCallLines) {
|
|
260
|
+
c.addChild(new Text(fit(theme.fg("muted", line)), 0, 0));
|
|
261
|
+
}
|
|
262
|
+
if (toolCallLines.length) c.addChild(new Spacer(1));
|
|
247
263
|
}
|
|
248
|
-
if (items.length) c.addChild(new Spacer(1));
|
|
249
264
|
|
|
250
265
|
if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
|
|
251
266
|
c.addChild(new Spacer(1));
|
|
252
267
|
if (r.skills?.length) {
|
|
253
|
-
c.addChild(new Text(
|
|
268
|
+
c.addChild(new Text(fit(theme.fg("dim", `Skills: ${r.skills.join(", ")}`)), 0, 0));
|
|
254
269
|
}
|
|
255
270
|
if (r.skillsWarning) {
|
|
256
|
-
c.addChild(new Text(
|
|
271
|
+
c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
|
|
257
272
|
}
|
|
258
273
|
if (r.attemptedModels && r.attemptedModels.length > 1) {
|
|
259
|
-
c.addChild(new Text(
|
|
274
|
+
c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
|
|
260
275
|
}
|
|
261
|
-
c.addChild(new Text(
|
|
276
|
+
c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
|
|
262
277
|
if (r.sessionFile) {
|
|
263
|
-
c.addChild(new Text(
|
|
278
|
+
c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
if (r.artifactPaths) {
|
|
267
282
|
c.addChild(new Spacer(1));
|
|
268
|
-
c.addChild(new Text(
|
|
283
|
+
c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
|
|
269
284
|
}
|
|
270
285
|
return c;
|
|
271
286
|
}
|
|
@@ -341,16 +356,17 @@ export function renderSubagentResult(
|
|
|
341
356
|
: null;
|
|
342
357
|
|
|
343
358
|
const w = getTermWidth() - 4;
|
|
359
|
+
const fit = (text: string) => expanded ? text : truncLine(text, w);
|
|
344
360
|
const c = new Container();
|
|
345
361
|
c.addChild(
|
|
346
362
|
new Text(
|
|
347
|
-
|
|
363
|
+
fit(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge}${stepInfo}${summaryStr}`),
|
|
348
364
|
0,
|
|
349
365
|
0,
|
|
350
366
|
),
|
|
351
367
|
);
|
|
352
368
|
if (chainVis) {
|
|
353
|
-
c.addChild(new Text(
|
|
369
|
+
c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
|
|
354
370
|
}
|
|
355
371
|
|
|
356
372
|
const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
|
|
@@ -365,7 +381,7 @@ export function renderSubagentResult(
|
|
|
365
381
|
: (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
|
|
366
382
|
|
|
367
383
|
if (!r) {
|
|
368
|
-
c.addChild(new Text(
|
|
384
|
+
c.addChild(new Text(fit(theme.fg("dim", ` Step ${i + 1}: ${agentName}`)), 0, 0));
|
|
369
385
|
c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
|
|
370
386
|
c.addChild(new Spacer(1));
|
|
371
387
|
continue;
|
|
@@ -389,58 +405,66 @@ export function renderSubagentResult(
|
|
|
389
405
|
const stepHeader = rRunning
|
|
390
406
|
? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
391
407
|
: `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
|
|
392
|
-
|
|
408
|
+
const toolCallLines = getToolCallLines(r, expanded);
|
|
409
|
+
c.addChild(new Text(fit(stepHeader), 0, 0));
|
|
393
410
|
|
|
394
411
|
const taskMaxLen = Math.max(20, w - 12);
|
|
395
|
-
const taskPreview = r.task.length
|
|
396
|
-
?
|
|
397
|
-
: r.task
|
|
398
|
-
c.addChild(new Text(
|
|
412
|
+
const taskPreview = expanded || r.task.length <= taskMaxLen
|
|
413
|
+
? r.task
|
|
414
|
+
: `${r.task.slice(0, taskMaxLen)}...`;
|
|
415
|
+
c.addChild(new Text(fit(theme.fg("dim", ` task: ${taskPreview}`)), 0, 0));
|
|
399
416
|
|
|
400
417
|
const outputTarget = extractOutputTarget(r.task);
|
|
401
418
|
if (outputTarget) {
|
|
402
|
-
c.addChild(new Text(
|
|
419
|
+
c.addChild(new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0));
|
|
403
420
|
}
|
|
404
421
|
|
|
405
422
|
if (r.skills?.length) {
|
|
406
|
-
c.addChild(new Text(
|
|
423
|
+
c.addChild(new Text(fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)), 0, 0));
|
|
407
424
|
}
|
|
408
425
|
if (r.skillsWarning) {
|
|
409
|
-
c.addChild(new Text(
|
|
426
|
+
c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
|
|
410
427
|
}
|
|
411
428
|
if (r.attemptedModels && r.attemptedModels.length > 1) {
|
|
412
|
-
c.addChild(new Text(
|
|
429
|
+
c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
|
|
413
430
|
}
|
|
414
431
|
|
|
415
432
|
if (rRunning && rProg) {
|
|
416
433
|
if (rProg.skills?.length) {
|
|
417
|
-
c.addChild(new Text(
|
|
434
|
+
c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
|
|
418
435
|
}
|
|
419
436
|
if (rProg.currentTool) {
|
|
420
437
|
const maxToolArgsLen = Math.max(50, w - 20);
|
|
421
438
|
const toolArgsPreview = rProg.currentToolArgs
|
|
422
|
-
? (rProg.currentToolArgs.length
|
|
423
|
-
?
|
|
424
|
-
: rProg.currentToolArgs)
|
|
439
|
+
? (expanded || rProg.currentToolArgs.length <= maxToolArgsLen
|
|
440
|
+
? rProg.currentToolArgs
|
|
441
|
+
: `${rProg.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
425
442
|
: "";
|
|
426
443
|
const toolLine = toolArgsPreview
|
|
427
444
|
? `${rProg.currentTool}: ${toolArgsPreview}`
|
|
428
445
|
: rProg.currentTool;
|
|
429
|
-
c.addChild(new Text(
|
|
446
|
+
c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
|
|
430
447
|
}
|
|
431
448
|
if (rProg.recentTools?.length) {
|
|
432
449
|
for (const t of rProg.recentTools.slice(-3)) {
|
|
433
450
|
const maxArgsLen = Math.max(40, w - 30);
|
|
434
|
-
const argsPreview = t.args.length
|
|
435
|
-
?
|
|
436
|
-
: t.args
|
|
437
|
-
c.addChild(new Text(
|
|
451
|
+
const argsPreview = expanded || t.args.length <= maxArgsLen
|
|
452
|
+
? t.args
|
|
453
|
+
: `${t.args.slice(0, maxArgsLen)}...`;
|
|
454
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
|
|
438
455
|
}
|
|
439
456
|
}
|
|
440
457
|
const recentLines = (rProg.recentOutput ?? []).slice(-5);
|
|
441
458
|
for (const line of recentLines) {
|
|
442
|
-
c.addChild(new Text(
|
|
459
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (expanded && !rRunning) {
|
|
464
|
+
for (const line of toolCallLines) {
|
|
465
|
+
c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
|
|
443
466
|
}
|
|
467
|
+
if (toolCallLines.length) c.addChild(new Spacer(1));
|
|
444
468
|
}
|
|
445
469
|
|
|
446
470
|
c.addChild(new Spacer(1));
|
|
@@ -448,7 +472,7 @@ export function renderSubagentResult(
|
|
|
448
472
|
|
|
449
473
|
if (d.artifacts) {
|
|
450
474
|
c.addChild(new Spacer(1));
|
|
451
|
-
c.addChild(new Text(
|
|
475
|
+
c.addChild(new Text(fit(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`)), 0, 0));
|
|
452
476
|
}
|
|
453
477
|
return c;
|
|
454
478
|
}
|
package/schemas.ts
CHANGED
|
@@ -73,6 +73,7 @@ export const SubagentParams = Type.Object({
|
|
|
73
73
|
description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
|
|
74
74
|
})),
|
|
75
75
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),
|
|
76
|
+
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
76
77
|
worktree: Type.Optional(Type.Boolean({
|
|
77
78
|
description: "Create isolated git worktrees for each parallel task. " +
|
|
78
79
|
"Prevents filesystem conflicts. Requires clean git state. " +
|
package/slash-commands.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
finalizeSlashResult,
|
|
15
15
|
} from "./slash-live-state.ts";
|
|
16
16
|
import {
|
|
17
|
-
MAX_PARALLEL,
|
|
18
17
|
SLASH_RESULT_TYPE,
|
|
19
18
|
SLASH_SUBAGENT_CANCEL_EVENT,
|
|
20
19
|
SLASH_SUBAGENT_REQUEST_EVENT,
|
|
@@ -453,22 +452,18 @@ export function registerSlashCommands(
|
|
|
453
452
|
pi.registerCommand("parallel", {
|
|
454
453
|
description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
|
|
455
454
|
getArgumentCompletions: makeAgentCompletions(state, true),
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
...(config.progress !== undefined ? { progress: config.progress } : {}),
|
|
469
|
-
}));
|
|
470
|
-
const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
|
|
471
|
-
if (bg) params.async = true;
|
|
455
|
+
handler: async (args, ctx) => {
|
|
456
|
+
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
457
|
+
const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
|
|
458
|
+
if (!parsed) return;
|
|
459
|
+
const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
|
|
460
|
+
agent: name,
|
|
461
|
+
task: stepTask ?? parsed.task,
|
|
462
|
+
...(config.model ? { model: config.model } : {}),
|
|
463
|
+
...(config.skill !== undefined ? { skill: config.skill } : {}),
|
|
464
|
+
}));
|
|
465
|
+
const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
|
|
466
|
+
if (bg) params.async = true;
|
|
472
467
|
if (fork) params.context = "fork";
|
|
473
468
|
await runSlashSubagent(pi, ctx, params);
|
|
474
469
|
},
|
package/subagent-executor.ts
CHANGED
|
@@ -45,9 +45,9 @@ import {
|
|
|
45
45
|
type SingleResult,
|
|
46
46
|
type SubagentState,
|
|
47
47
|
DEFAULT_ARTIFACT_CONFIG,
|
|
48
|
-
MAX_CONCURRENCY,
|
|
49
|
-
MAX_PARALLEL,
|
|
50
48
|
checkSubagentDepth,
|
|
49
|
+
resolveTopLevelParallelConcurrency,
|
|
50
|
+
resolveTopLevelParallelMaxTasks,
|
|
51
51
|
resolveChildMaxSubagentDepth,
|
|
52
52
|
resolveCurrentMaxSubagentDepth,
|
|
53
53
|
wrapForkTask,
|
|
@@ -60,9 +60,6 @@ interface TaskParam {
|
|
|
60
60
|
count?: number;
|
|
61
61
|
model?: string;
|
|
62
62
|
skill?: string | string[] | boolean;
|
|
63
|
-
output?: string | false;
|
|
64
|
-
reads?: string[] | false;
|
|
65
|
-
progress?: boolean;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
export interface SubagentParamsLike {
|
|
@@ -71,6 +68,7 @@ export interface SubagentParamsLike {
|
|
|
71
68
|
task?: string;
|
|
72
69
|
chain?: ChainStep[];
|
|
73
70
|
tasks?: TaskParam[];
|
|
71
|
+
concurrency?: number;
|
|
74
72
|
worktree?: boolean;
|
|
75
73
|
context?: "fresh" | "fork";
|
|
76
74
|
async?: boolean;
|
|
@@ -113,7 +111,7 @@ interface ExecutionContextData {
|
|
|
113
111
|
sessionFileForIndex: (idx?: number) => string | undefined;
|
|
114
112
|
artifactConfig: ArtifactConfig;
|
|
115
113
|
artifactsDir: string;
|
|
116
|
-
|
|
114
|
+
backgroundRequestedWhileClarifying: boolean;
|
|
117
115
|
effectiveAsync: boolean;
|
|
118
116
|
}
|
|
119
117
|
|
|
@@ -349,6 +347,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
349
347
|
effectiveAsync,
|
|
350
348
|
} = data;
|
|
351
349
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
350
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
352
351
|
const hasSingle = Boolean(params.agent && params.task);
|
|
353
352
|
if (!effectiveAsync) return null;
|
|
354
353
|
|
|
@@ -363,6 +362,17 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
363
362
|
}
|
|
364
363
|
}
|
|
365
364
|
|
|
365
|
+
if (hasTasks && params.tasks) {
|
|
366
|
+
const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
|
|
367
|
+
if (params.tasks.length > maxParallelTasks) {
|
|
368
|
+
return buildParallelModeError(`Max ${maxParallelTasks} tasks`);
|
|
369
|
+
}
|
|
370
|
+
if (params.worktree) {
|
|
371
|
+
const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(params.tasks, effectiveCwd);
|
|
372
|
+
if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
366
376
|
if (!isAsyncAvailable()) {
|
|
367
377
|
return {
|
|
368
378
|
content: [{ type: "text", text: "Async mode requires jiti for TypeScript execution but it could not be found. Install globally: npm install -g jiti" }],
|
|
@@ -383,6 +393,43 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
383
393
|
fullId: `${m.provider}/${m.id}`,
|
|
384
394
|
}));
|
|
385
395
|
const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
|
|
396
|
+
const currentProvider = ctx.model?.provider;
|
|
397
|
+
|
|
398
|
+
if (hasTasks && params.tasks) {
|
|
399
|
+
const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
|
|
400
|
+
const modelOverrides = params.tasks.map((task, index) =>
|
|
401
|
+
resolveModelCandidate(task.model ?? agentConfigs[index]?.model, availableModels, currentProvider),
|
|
402
|
+
);
|
|
403
|
+
const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
|
|
404
|
+
const parallelTasks = params.tasks.map((task, index) => ({
|
|
405
|
+
agent: task.agent,
|
|
406
|
+
task: params.context === "fork" ? wrapForkTask(task.task) : task.task,
|
|
407
|
+
cwd: task.cwd,
|
|
408
|
+
...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
|
|
409
|
+
...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
|
|
410
|
+
}));
|
|
411
|
+
return executeAsyncChain(id, {
|
|
412
|
+
chain: [{
|
|
413
|
+
parallel: parallelTasks,
|
|
414
|
+
concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency),
|
|
415
|
+
worktree: params.worktree,
|
|
416
|
+
}],
|
|
417
|
+
agents,
|
|
418
|
+
ctx: asyncCtx,
|
|
419
|
+
availableModels,
|
|
420
|
+
cwd: effectiveCwd,
|
|
421
|
+
maxOutput: params.maxOutput,
|
|
422
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
423
|
+
artifactConfig,
|
|
424
|
+
shareEnabled,
|
|
425
|
+
sessionRoot,
|
|
426
|
+
chainSkills: [],
|
|
427
|
+
sessionFilesByFlatIndex: params.tasks.map((_, index) => sessionFileForIndex(index)),
|
|
428
|
+
maxSubagentDepth: currentMaxSubagentDepth,
|
|
429
|
+
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
430
|
+
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
386
433
|
|
|
387
434
|
if (hasChain && params.chain) {
|
|
388
435
|
const normalized = normalizeSkillInput(params.skill);
|
|
@@ -421,7 +468,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
421
468
|
const normalizedSkills = normalizeSkillInput(params.skill);
|
|
422
469
|
const skills = normalizedSkills === false ? [] : normalizedSkills;
|
|
423
470
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
|
|
424
|
-
const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels,
|
|
471
|
+
const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
|
|
425
472
|
return executeAsyncSingle(id, {
|
|
426
473
|
agent: params.agent!,
|
|
427
474
|
task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
|
|
@@ -551,6 +598,7 @@ interface ForegroundParallelRunInput {
|
|
|
551
598
|
modelOverrides: (string | undefined)[];
|
|
552
599
|
skillOverrides: (string[] | false | undefined)[];
|
|
553
600
|
behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
|
|
601
|
+
concurrencyLimit: number;
|
|
554
602
|
liveResults: (SingleResult | undefined)[];
|
|
555
603
|
liveProgress: (AgentProgress | undefined)[];
|
|
556
604
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
@@ -634,7 +682,7 @@ function buildParallelWorktreeSuffix(
|
|
|
634
682
|
}
|
|
635
683
|
|
|
636
684
|
async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
|
|
637
|
-
return mapConcurrent(input.tasks,
|
|
685
|
+
return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
|
|
638
686
|
const overrideSkills = input.skillOverrides[index];
|
|
639
687
|
const effectiveSkills = overrideSkills === undefined ? input.behaviors[index]?.skills : overrideSkills;
|
|
640
688
|
const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
|
|
@@ -690,17 +738,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
690
738
|
shareEnabled,
|
|
691
739
|
artifactConfig,
|
|
692
740
|
artifactsDir,
|
|
693
|
-
|
|
741
|
+
backgroundRequestedWhileClarifying,
|
|
694
742
|
onUpdate,
|
|
695
743
|
sessionRoot,
|
|
696
744
|
} = data;
|
|
697
745
|
const allProgress: AgentProgress[] = [];
|
|
698
746
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
699
747
|
const tasks = params.tasks!;
|
|
748
|
+
const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
|
|
749
|
+
const parallelConcurrency = resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency);
|
|
700
750
|
|
|
701
|
-
if (tasks.length >
|
|
751
|
+
if (tasks.length > maxParallelTasks)
|
|
702
752
|
return {
|
|
703
|
-
content: [{ type: "text", text: `Max ${
|
|
753
|
+
content: [{ type: "text", text: `Max ${maxParallelTasks} tasks` }],
|
|
704
754
|
isError: true,
|
|
705
755
|
details: { mode: "parallel" as const, results: [] },
|
|
706
756
|
};
|
|
@@ -800,7 +850,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
800
850
|
...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
|
|
801
851
|
}));
|
|
802
852
|
return executeAsyncChain(id, {
|
|
803
|
-
chain: [{ parallel: parallelTasks, worktree: params.worktree }],
|
|
853
|
+
chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
|
|
804
854
|
agents,
|
|
805
855
|
ctx: asyncCtx,
|
|
806
856
|
availableModels,
|
|
@@ -857,6 +907,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
857
907
|
modelOverrides,
|
|
858
908
|
skillOverrides,
|
|
859
909
|
behaviors,
|
|
910
|
+
concurrencyLimit: parallelConcurrency,
|
|
860
911
|
maxSubagentDepths,
|
|
861
912
|
liveResults,
|
|
862
913
|
liveProgress,
|
|
@@ -875,7 +926,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
875
926
|
|
|
876
927
|
const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
|
|
877
928
|
const ok = results.filter((result) => result.exitCode === 0).length;
|
|
878
|
-
const downgradeNote =
|
|
929
|
+
const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
|
|
879
930
|
const aggregatedOutput = aggregateParallelOutputs(
|
|
880
931
|
results.map((result) => ({
|
|
881
932
|
agent: result.agent,
|
|
@@ -1200,11 +1251,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1200
1251
|
}
|
|
1201
1252
|
|
|
1202
1253
|
const requestedAsync = normalizedParams.async ?? deps.asyncByDefault;
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
effectiveAsync = hasChain ? normalizedParams.clarify === false : normalizedParams.clarify !== true;
|
|
1207
|
-
}
|
|
1254
|
+
const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && normalizedParams.clarify === true;
|
|
1255
|
+
const effectiveAsync = requestedAsync
|
|
1256
|
+
&& (hasChain ? normalizedParams.clarify === false : normalizedParams.clarify !== true);
|
|
1208
1257
|
|
|
1209
1258
|
const artifactConfig: ArtifactConfig = {
|
|
1210
1259
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -1251,7 +1300,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1251
1300
|
sessionFileForIndex,
|
|
1252
1301
|
artifactConfig,
|
|
1253
1302
|
artifactsDir,
|
|
1254
|
-
|
|
1303
|
+
backgroundRequestedWhileClarifying,
|
|
1255
1304
|
effectiveAsync,
|
|
1256
1305
|
};
|
|
1257
1306
|
|
package/types.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface AgentProgress {
|
|
|
61
61
|
failedTool?: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface ToolCallSummary {
|
|
65
|
+
text: string;
|
|
66
|
+
expandedText: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
export interface ProgressSummary {
|
|
65
70
|
toolCount: number;
|
|
66
71
|
tokens: number;
|
|
@@ -96,6 +101,7 @@ export interface SingleResult {
|
|
|
96
101
|
skillsWarning?: string;
|
|
97
102
|
progress?: AgentProgress;
|
|
98
103
|
progressSummary?: ProgressSummary;
|
|
104
|
+
toolCalls?: ToolCallSummary[];
|
|
99
105
|
artifactPaths?: ArtifactPaths;
|
|
100
106
|
truncation?: TruncationResult;
|
|
101
107
|
finalOutput?: string;
|
|
@@ -273,10 +279,16 @@ export interface IntercomBridgeConfig {
|
|
|
273
279
|
instructionFile?: string;
|
|
274
280
|
}
|
|
275
281
|
|
|
282
|
+
export interface TopLevelParallelConfig {
|
|
283
|
+
maxTasks?: number;
|
|
284
|
+
concurrency?: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
276
287
|
export interface ExtensionConfig {
|
|
277
288
|
asyncByDefault?: boolean;
|
|
278
289
|
defaultSessionDir?: string;
|
|
279
290
|
maxSubagentDepth?: number;
|
|
291
|
+
parallel?: TopLevelParallelConfig;
|
|
280
292
|
worktreeSetupHook?: string;
|
|
281
293
|
worktreeSetupHookTimeoutMs?: number;
|
|
282
294
|
intercomBridge?: IntercomBridgeConfig;
|
|
@@ -376,6 +388,25 @@ export const DEFAULT_FORK_PREAMBLE =
|
|
|
376
388
|
"Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
|
|
377
389
|
"— focus exclusively on completing this task using your tools.";
|
|
378
390
|
|
|
391
|
+
function normalizeTopLevelParallelValue(value: unknown): number | undefined {
|
|
392
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
393
|
+
if (!Number.isInteger(parsed) || parsed < 1) return undefined;
|
|
394
|
+
return parsed;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function resolveTopLevelParallelMaxTasks(value: unknown): number {
|
|
398
|
+
return normalizeTopLevelParallelValue(value) ?? MAX_PARALLEL;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function resolveTopLevelParallelConcurrency(
|
|
402
|
+
override: unknown,
|
|
403
|
+
configValue: unknown,
|
|
404
|
+
): number {
|
|
405
|
+
return normalizeTopLevelParallelValue(override)
|
|
406
|
+
?? normalizeTopLevelParallelValue(configValue)
|
|
407
|
+
?? MAX_CONCURRENCY;
|
|
408
|
+
}
|
|
409
|
+
|
|
379
410
|
export function getAsyncConfigPath(suffix: string): string {
|
|
380
411
|
return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
|
|
381
412
|
}
|
package/utils.ts
CHANGED
|
@@ -6,7 +6,8 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@mariozechner/pi-ai";
|
|
9
|
-
import
|
|
9
|
+
import { formatToolCall } from "./formatters.ts";
|
|
10
|
+
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult, ToolCallSummary } from "./types.ts";
|
|
10
11
|
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// File System Utilities
|
|
@@ -242,12 +243,33 @@ function compactCompletedProgress(progress: AgentProgress): AgentProgress {
|
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
export function extractToolCallSummaries(messages: Message[] | undefined): ToolCallSummary[] {
|
|
247
|
+
if (!messages?.length) return [];
|
|
248
|
+
const summaries: ToolCallSummary[] = [];
|
|
249
|
+
for (const msg of messages) {
|
|
250
|
+
if (msg.role !== "assistant") continue;
|
|
251
|
+
for (const part of msg.content) {
|
|
252
|
+
if (part.type !== "toolCall") continue;
|
|
253
|
+
const args = typeof part.arguments === "object" && part.arguments !== null && !Array.isArray(part.arguments)
|
|
254
|
+
? part.arguments
|
|
255
|
+
: {};
|
|
256
|
+
summaries.push({
|
|
257
|
+
text: formatToolCall(part.name, args),
|
|
258
|
+
expandedText: formatToolCall(part.name, args, true),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return summaries;
|
|
263
|
+
}
|
|
264
|
+
|
|
245
265
|
export function compactForegroundResult(result: SingleResult): SingleResult {
|
|
246
266
|
if (result.progress?.status === "running") return result;
|
|
267
|
+
const toolCalls = result.toolCalls?.length ? result.toolCalls : extractToolCallSummaries(result.messages);
|
|
247
268
|
return {
|
|
248
269
|
...result,
|
|
249
270
|
messages: undefined,
|
|
250
271
|
progress: undefined,
|
|
272
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
251
273
|
};
|
|
252
274
|
}
|
|
253
275
|
|