oira666_pi-subagent 0.2.0 → 0.2.2
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/README.md +4 -46
- package/index.ts +42 -133
- package/package.json +1 -2
- package/runner.ts +19 -4
- package/shared.ts +3 -26
- package/runner-sdk.ts +0 -481
package/README.md
CHANGED
|
@@ -22,26 +22,13 @@ pi remove npm:oira666_pi-subagent
|
|
|
22
22
|
|
|
23
23
|
## How It Works
|
|
24
24
|
|
|
25
|
-
The extension supports two execution modes, selectable via `PI_SUBAGENTS_MODE`.
|
|
26
|
-
|
|
27
|
-
### Execution mode: `subprocess` (default)
|
|
28
|
-
|
|
29
25
|
Each subagent runs as a **separate `pi` process** — fully isolated memory, its own model/tool loop.
|
|
30
26
|
Processes are spawned via the operating system and communicate through JSON-line stdout.
|
|
31
27
|
|
|
32
28
|
- Full OS-level isolation — a crashed subagent cannot affect the parent
|
|
33
29
|
- True parallel execution across all CPU cores
|
|
34
|
-
- Each subprocess boots a fresh Node.js runtime
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
### Execution mode: `sdk`
|
|
38
|
-
|
|
39
|
-
Each subagent runs as an **in-process `AgentSession`** created via the pi SDK — no new process is spawned.
|
|
40
|
-
|
|
41
|
-
- No spawn overhead — sessions start in milliseconds
|
|
42
|
-
- No temp files for system prompts
|
|
43
|
-
- Concurrency through the Node.js event loop (fine for I/O-bound LLM work)
|
|
44
|
-
- All sessions share the same memory and event loop
|
|
30
|
+
- Each subprocess boots a fresh Node.js runtime
|
|
31
|
+
- Uses the same Pi CLI entrypoint as the parent process when available
|
|
45
32
|
|
|
46
33
|
Each subagent receives only the task string. The main agent in turn receives
|
|
47
34
|
only the **final text output** from subagents (no tool calls, no reasoning).
|
|
@@ -110,31 +97,6 @@ Available tools: `read`, `bash`, `edit`, `write`.
|
|
|
110
97
|
|
|
111
98
|
The Markdown body becomes the agent's system prompt (appended to Pi's default, not replacing it).
|
|
112
99
|
|
|
113
|
-
## Execution Mode
|
|
114
|
-
|
|
115
|
-
| Env Var | Default | Values | Description |
|
|
116
|
-
| --------------------- | ------------ | -------------------- | -------------------------------------- |
|
|
117
|
-
| `PI_SUBAGENTS_MODE` | `subprocess` | `subprocess` / `sdk` | How subagent sessions are created |
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
# Run subagents as in-process SDK sessions (faster startup, no spawn overhead)
|
|
121
|
-
PI_SUBAGENTS_MODE=sdk pi
|
|
122
|
-
|
|
123
|
-
# Run subagents as isolated subprocess pi instances (default, full isolation)
|
|
124
|
-
PI_SUBAGENTS_MODE=subprocess pi
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Comparison
|
|
128
|
-
|
|
129
|
-
| | `subprocess` | `sdk` |
|
|
130
|
-
|---|---|---|
|
|
131
|
-
| Session isolation | Full OS-level | Shared memory/event loop |
|
|
132
|
-
| Startup overhead | ~200–500 ms per agent | ~10–50 ms per agent |
|
|
133
|
-
| Parallelism | True OS parallelism | Event-loop concurrency |
|
|
134
|
-
| Depth tracking | Env vars in child process | Closure parameters |
|
|
135
|
-
| `pi` binary required | Yes | No |
|
|
136
|
-
| Crash isolation | Yes — subprocess crash is contained | No — exception bubbles up |
|
|
137
|
-
|
|
138
100
|
## Delegation Guards
|
|
139
101
|
|
|
140
102
|
Depth and cycle guards prevent runaway recursive delegation.
|
|
@@ -165,11 +127,7 @@ pi --no-subagent-prevent-cycles # allow cycles (not recommended)
|
|
|
165
127
|
|
|
166
128
|
## CLI Argument Proxying
|
|
167
129
|
|
|
168
|
-
|
|
169
|
-
> inherit provider and model configuration directly from the calling session's model registry
|
|
170
|
-
> and API keys from environment variables, so no forwarding is needed.
|
|
171
|
-
|
|
172
|
-
In `subprocess` mode, all flags passed to the parent `pi` process are forwarded to subagent child
|
|
130
|
+
Flags passed to the parent `pi` process are forwarded to subagent child
|
|
173
131
|
processes, so they inherit the same provider, API key, model, and other runtime settings. Flags the
|
|
174
132
|
extension manages itself are blocked from being forwarded.
|
|
175
133
|
|
|
@@ -466,7 +424,7 @@ for await (const line of jsonLines) {
|
|
|
466
424
|
|
|
467
425
|
Note: if you also track the main agent's own usage from `message_end` events, make sure **not** to
|
|
468
426
|
double-count the subagent costs there — the main agent's own token usage (from its own `message_end`
|
|
469
|
-
events) does not include subagent work
|
|
427
|
+
events) does not include subagent work.
|
|
470
428
|
|
|
471
429
|
---
|
|
472
430
|
|
package/index.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pi Subagent Extension
|
|
3
3
|
*
|
|
4
|
-
* Delegates tasks to specialized subagents running as
|
|
5
|
-
* (via the pi SDK) rather than spawning separate `pi` processes.
|
|
6
|
-
*
|
|
7
|
-
* Environment variables (PI_SUBAGENT_DEPTH, PI_SUBAGENT_MAX_DEPTH,
|
|
8
|
-
* PI_SUBAGENT_STACK, PI_SUBAGENT_PREVENT_CYCLES, PI_SUBAGENT_MAX_PARALLEL_TASKS,
|
|
9
|
-
* PI_SUBAGENT_MAX_CONCURRENCY, PI_SUBAGENT_CONFIRM_PROJECT_AGENTS) are read
|
|
10
|
-
* here at root startup exactly as before. Depth/stack propagate to nested
|
|
11
|
-
* sessions via closure in runner-sdk.ts instead of via subprocess env vars.
|
|
4
|
+
* Delegates tasks to specialized subagents running as isolated `pi` processes.
|
|
12
5
|
*
|
|
13
6
|
* The tool always accepts a `tasks` array:
|
|
14
7
|
* - One task: treated as a single-agent delegation.
|
|
@@ -20,8 +13,11 @@ import { Type } from "@sinclair/typebox";
|
|
|
20
13
|
import { type AgentConfig, discoverAgents } from "./agents.js";
|
|
21
14
|
import { renderCall, renderResult } from "./render.js";
|
|
22
15
|
import { runAgentSubprocess, executeParallelSubprocess } from "./runner.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_MAX_PARALLEL_TASKS,
|
|
18
|
+
SUBAGENT_MAX_PARALLEL_TASKS_ENV,
|
|
19
|
+
parseNonNegativeInt,
|
|
20
|
+
} from "./shared.js";
|
|
25
21
|
|
|
26
22
|
import {
|
|
27
23
|
type DelegationMode,
|
|
@@ -45,7 +41,6 @@ const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
|
45
41
|
const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
46
42
|
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
47
43
|
const SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV = "PI_SUBAGENT_CONFIRM_PROJECT_AGENTS";
|
|
48
|
-
const SUBAGENTS_MODE_ENV = "PI_SUBAGENTS_MODE";
|
|
49
44
|
|
|
50
45
|
type ProjectAgentConfirmationSetting = "ask" | "never" | "session";
|
|
51
46
|
type ProjectAgentApproval = "once" | "session" | "no";
|
|
@@ -186,20 +181,6 @@ function getPreventCyclesFlagFromArgv(
|
|
|
186
181
|
}
|
|
187
182
|
|
|
188
183
|
function resolveDelegationDepthConfig(pi: ExtensionAPI): DelegationDepthConfig {
|
|
189
|
-
// When loaded inside an in-process child session (SDK mode), the correct
|
|
190
|
-
// depth/stack/limits are provided via AsyncLocalStorage rather than
|
|
191
|
-
// process.env (which still holds the root process values).
|
|
192
|
-
const inProcessCtx = subagentContext.getStore();
|
|
193
|
-
if (inProcessCtx) {
|
|
194
|
-
return {
|
|
195
|
-
currentDepth: inProcessCtx.depth,
|
|
196
|
-
maxDepth: inProcessCtx.maxDepth,
|
|
197
|
-
canDelegate: inProcessCtx.depth < inProcessCtx.maxDepth,
|
|
198
|
-
ancestorAgentStack: inProcessCtx.stack,
|
|
199
|
-
preventCycles: inProcessCtx.preventCycles,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
184
|
const depthRaw = process.env[SUBAGENT_DEPTH_ENV];
|
|
204
185
|
const parsedDepth = parseNonNegativeInt(depthRaw);
|
|
205
186
|
if (depthRaw !== undefined && parsedDepth === null) {
|
|
@@ -358,38 +339,6 @@ function getProjectAgentSessionKey(projectAgentsDir: string | null): string {
|
|
|
358
339
|
return projectAgentsDir ?? "(unknown-project-agents-dir)";
|
|
359
340
|
}
|
|
360
341
|
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
|
-
// Subagent mode helpers (exported for testing and external consumers)
|
|
363
|
-
// ---------------------------------------------------------------------------
|
|
364
|
-
|
|
365
|
-
/** The two supported subagent execution modes. */
|
|
366
|
-
export type SubagentMode = "subprocess" | "sdk";
|
|
367
|
-
|
|
368
|
-
/** Default subagent execution mode. */
|
|
369
|
-
export const DEFAULT_SUBAGENTS_MODE: SubagentMode = "subprocess";
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Parse a raw value into a SubagentMode.
|
|
373
|
-
* Returns null for invalid/unrecognized values, the default mode for undefined.
|
|
374
|
-
*/
|
|
375
|
-
export function parseSubagentMode(raw: unknown): SubagentMode | null {
|
|
376
|
-
if (raw === undefined) return DEFAULT_SUBAGENTS_MODE;
|
|
377
|
-
if (typeof raw !== "string") return null;
|
|
378
|
-
const normalized = raw.trim().toLowerCase();
|
|
379
|
-
if (normalized === "subprocess") return "subprocess";
|
|
380
|
-
if (normalized === "sdk") return "sdk";
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Read the subagent mode from PI_SUBAGENTS_MODE env var.
|
|
386
|
-
* Falls back to DEFAULT_SUBAGENTS_MODE if the env var is absent or invalid.
|
|
387
|
-
*/
|
|
388
|
-
export function getSubagentMode(): SubagentMode {
|
|
389
|
-
const parsed = parseSubagentMode(process.env["PI_SUBAGENTS_MODE"]);
|
|
390
|
-
return parsed ?? DEFAULT_SUBAGENTS_MODE;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
342
|
// ---------------------------------------------------------------------------
|
|
394
343
|
// Extension entry point
|
|
395
344
|
// ---------------------------------------------------------------------------
|
|
@@ -408,7 +357,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
408
357
|
const depthConfig = resolveDelegationDepthConfig(pi);
|
|
409
358
|
const { currentDepth, maxDepth, canDelegate, ancestorAgentStack, preventCycles } =
|
|
410
359
|
depthConfig;
|
|
411
|
-
const
|
|
360
|
+
const maxParallelTasks =
|
|
361
|
+
parseNonNegativeInt(process.env[SUBAGENT_MAX_PARALLEL_TASKS_ENV]) ??
|
|
362
|
+
DEFAULT_MAX_PARALLEL_TASKS;
|
|
412
363
|
|
|
413
364
|
let discoveredAgents: AgentConfig[] = [];
|
|
414
365
|
const approvedProjectAgentDirsForSession = new Set<string>();
|
|
@@ -456,26 +407,25 @@ ${agentList}
|
|
|
456
407
|
|
|
457
408
|
Each subagent runs in an **isolated process**.
|
|
458
409
|
|
|
459
|
-
|
|
460
|
-
-
|
|
461
|
-
-
|
|
410
|
+
Pass a \`tasks\` array. **Every task in the same call runs in parallel.**
|
|
411
|
+
- 1 task -> single delegation
|
|
412
|
+
- N tasks -> all N run concurrently in one call
|
|
462
413
|
|
|
463
|
-
**
|
|
414
|
+
For **sequential** work (task B needs task A's output), make separate tool
|
|
415
|
+
calls one after another. Do NOT put dependent tasks in the same array.
|
|
416
|
+
|
|
417
|
+
**Single (1 agent)**:
|
|
464
418
|
\`\`\`json
|
|
465
419
|
{ "tasks": [{ "agent": "agent-name", "task": "Detailed task..." }] }
|
|
466
420
|
\`\`\`
|
|
467
421
|
|
|
468
|
-
**
|
|
422
|
+
**Parallel (N agents at once)**:
|
|
469
423
|
\`\`\`json
|
|
470
|
-
{ "tasks": [{ "agent": "agent-
|
|
424
|
+
{ "tasks": [{ "agent": "agent-a", "task": "..." }, { "agent": "agent-b", "task": "..." }] }
|
|
471
425
|
\`\`\`
|
|
472
426
|
|
|
473
|
-
### Runtime delegation guards
|
|
474
|
-
|
|
475
427
|
- Max depth: current depth ${currentDepth}, max depth ${maxDepth}
|
|
476
|
-
-
|
|
477
|
-
- Current delegation stack: ${ancestorAgentStack.length > 0 ? ancestorAgentStack.join(" -> ") : "(root)"}
|
|
478
|
-
- Execution mode: ${subagentMode === "sdk" ? "in-process SDK" : "subprocess"}
|
|
428
|
+
- Max subagents per tool call: ${maxParallelTasks}
|
|
479
429
|
`,
|
|
480
430
|
};
|
|
481
431
|
} catch (err) {
|
|
@@ -489,16 +439,17 @@ The tool always accepts a \`tasks\` array:
|
|
|
489
439
|
name: "subagent",
|
|
490
440
|
label: "Subagent",
|
|
491
441
|
description: [
|
|
492
|
-
|
|
493
|
-
subagentMode === "sdk" ? "in-process SDK sessions" : "isolated pi processes"
|
|
494
|
-
}.`,
|
|
442
|
+
"Delegate work to specialized subagents running as isolated pi processes.",
|
|
495
443
|
"",
|
|
496
|
-
"
|
|
497
|
-
" -
|
|
498
|
-
" -
|
|
444
|
+
"Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
|
|
445
|
+
" - 1 task -> single delegation",
|
|
446
|
+
" - N tasks -> all N run concurrently in one call",
|
|
499
447
|
"",
|
|
500
|
-
|
|
501
|
-
|
|
448
|
+
"For sequential work (task B depends on task A's output), make separate",
|
|
449
|
+
"tool calls one after another. Do NOT put dependent tasks in the same array.",
|
|
450
|
+
"",
|
|
451
|
+
'Single: { tasks: [{ agent: "writer", task: "Rewrite README.md" }] }',
|
|
452
|
+
'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
|
|
502
453
|
].join("\n"),
|
|
503
454
|
parameters: SubagentParams,
|
|
504
455
|
|
|
@@ -618,8 +569,6 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
618
569
|
signal,
|
|
619
570
|
onUpdate,
|
|
620
571
|
makeDetails,
|
|
621
|
-
ctx.modelRegistry,
|
|
622
|
-
ctx.model,
|
|
623
572
|
);
|
|
624
573
|
}
|
|
625
574
|
|
|
@@ -630,8 +579,6 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
630
579
|
signal,
|
|
631
580
|
onUpdate,
|
|
632
581
|
makeDetails,
|
|
633
|
-
ctx.modelRegistry,
|
|
634
|
-
ctx.model,
|
|
635
582
|
);
|
|
636
583
|
} catch (err) {
|
|
637
584
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -664,41 +611,21 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
664
611
|
signal: AbortSignal | undefined,
|
|
665
612
|
onUpdate: ((partial: any) => void) | undefined,
|
|
666
613
|
makeDetails: ReturnType<typeof makeDetailsFactory>,
|
|
667
|
-
modelRegistry: any,
|
|
668
|
-
parentModel: any,
|
|
669
614
|
) {
|
|
670
|
-
const result =
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
signal,
|
|
685
|
-
onUpdate,
|
|
686
|
-
makeDetails: makeDetails("single"),
|
|
687
|
-
})
|
|
688
|
-
: await runAgentSubprocess({
|
|
689
|
-
cwd: defaultCwd,
|
|
690
|
-
agents,
|
|
691
|
-
agentName,
|
|
692
|
-
task,
|
|
693
|
-
taskCwd: cwd,
|
|
694
|
-
parentDepth: currentDepth,
|
|
695
|
-
parentAgentStack: ancestorAgentStack,
|
|
696
|
-
maxDepth,
|
|
697
|
-
preventCycles,
|
|
698
|
-
signal,
|
|
699
|
-
onUpdate,
|
|
700
|
-
makeDetails: makeDetails("single"),
|
|
701
|
-
});
|
|
615
|
+
const result = await runAgentSubprocess({
|
|
616
|
+
cwd: defaultCwd,
|
|
617
|
+
agents,
|
|
618
|
+
agentName,
|
|
619
|
+
task,
|
|
620
|
+
taskCwd: cwd,
|
|
621
|
+
parentDepth: currentDepth,
|
|
622
|
+
parentAgentStack: ancestorAgentStack,
|
|
623
|
+
maxDepth,
|
|
624
|
+
preventCycles,
|
|
625
|
+
signal,
|
|
626
|
+
onUpdate,
|
|
627
|
+
makeDetails: makeDetails("single"),
|
|
628
|
+
});
|
|
702
629
|
|
|
703
630
|
if (isResultError(result)) {
|
|
704
631
|
const errorMsg =
|
|
@@ -735,25 +662,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
735
662
|
signal: AbortSignal | undefined,
|
|
736
663
|
onUpdate: ((partial: any) => void) | undefined,
|
|
737
664
|
makeDetails: ReturnType<typeof makeDetailsFactory>,
|
|
738
|
-
modelRegistry: any,
|
|
739
|
-
parentModel: any,
|
|
740
665
|
) {
|
|
741
|
-
if (subagentMode === "sdk") {
|
|
742
|
-
return executeParallelSameProcess(
|
|
743
|
-
tasks,
|
|
744
|
-
agents,
|
|
745
|
-
defaultCwd,
|
|
746
|
-
currentDepth,
|
|
747
|
-
maxDepth,
|
|
748
|
-
ancestorAgentStack,
|
|
749
|
-
preventCycles,
|
|
750
|
-
modelRegistry,
|
|
751
|
-
parentModel,
|
|
752
|
-
signal,
|
|
753
|
-
onUpdate,
|
|
754
|
-
makeDetails("parallel"),
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
666
|
return executeParallelSubprocess(
|
|
758
667
|
tasks,
|
|
759
668
|
agents,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"agents.ts",
|
|
10
10
|
"runner.ts",
|
|
11
11
|
"shared.ts",
|
|
12
|
-
"runner-sdk.ts",
|
|
13
12
|
"render.ts",
|
|
14
13
|
"types.ts",
|
|
15
14
|
"agents/*.md",
|
package/runner.ts
CHANGED
|
@@ -81,6 +81,21 @@ function cleanupTempDir(dir: string | null): void {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function getCurrentPiCliScript(): string | null {
|
|
85
|
+
const script = process.argv[1];
|
|
86
|
+
if (!script) return null;
|
|
87
|
+
|
|
88
|
+
// When this extension is loaded by pi, process.argv[1] is the pi CLI JS
|
|
89
|
+
// entrypoint. Reusing it with process.execPath avoids relying on PATH while
|
|
90
|
+
// still running the exact same pi installation as the parent process.
|
|
91
|
+
const normalized = script.replace(/\\/g, "/");
|
|
92
|
+
if (!normalized.includes("/pi-coding-agent/") || !normalized.endsWith("/dist/cli.js")) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return script;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
function resolveExtensionArg(value: string): string {
|
|
85
100
|
if (!value) return value;
|
|
86
101
|
if (value.startsWith("npm:") || value.startsWith("git:")) return value;
|
|
@@ -514,8 +529,9 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
514
529
|
// but shell:true splits arguments on whitespace — breaking task strings.
|
|
515
530
|
// Fix: reuse the running node binary + the pi CLI script path directly,
|
|
516
531
|
// so the child is spawned without a shell and args are passed safely.
|
|
517
|
-
const
|
|
518
|
-
const
|
|
532
|
+
const currentPiCli = getCurrentPiCliScript();
|
|
533
|
+
const spawnCmd = currentPiCli ? process.execPath : "pi";
|
|
534
|
+
const spawnArgs = currentPiCli ? [currentPiCli, ...piArgs] : piArgs;
|
|
519
535
|
const proc = spawn(spawnCmd, spawnArgs, {
|
|
520
536
|
cwd: taskCwd ?? cwd,
|
|
521
537
|
shell: false,
|
|
@@ -705,8 +721,7 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
|
|
|
705
721
|
|
|
706
722
|
|
|
707
723
|
// ---------------------------------------------------------------------------
|
|
708
|
-
// Parallel execution (subprocess
|
|
709
|
-
// Symmetric to executeParallelSameProcess in runner-sdk.ts but uses runAgentSubprocess().
|
|
724
|
+
// Parallel execution (subprocess runner).
|
|
710
725
|
// ---------------------------------------------------------------------------
|
|
711
726
|
|
|
712
727
|
|
package/shared.ts
CHANGED
|
@@ -1,37 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared constants and utilities used by
|
|
2
|
+
* Shared constants and utilities used by the subprocess runner.
|
|
3
3
|
*
|
|
4
|
-
* Single source of truth for parallel-execution defaults and
|
|
5
|
-
* that both runners need. Change a value here and it propagates everywhere.
|
|
4
|
+
* Single source of truth for parallel-execution defaults and helpers.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// In-process child session context
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Depth and delegation config for an in-process child AgentSession.
|
|
16
|
-
*
|
|
17
|
-
* Stored via AsyncLocalStorage so each nested session sees its own correct
|
|
18
|
-
* values without mutating process.env. index.ts reads this first; if absent
|
|
19
|
-
* it falls back to env vars (subprocess mode).
|
|
20
|
-
*/
|
|
21
|
-
export interface SubagentSessionContext {
|
|
22
|
-
depth: number;
|
|
23
|
-
maxDepth: number;
|
|
24
|
-
stack: string[];
|
|
25
|
-
preventCycles: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const subagentContext = new AsyncLocalStorage<SubagentSessionContext>();
|
|
29
|
-
|
|
30
7
|
// ---------------------------------------------------------------------------
|
|
31
8
|
// Parallel execution limits
|
|
32
9
|
// ---------------------------------------------------------------------------
|
|
33
10
|
|
|
34
|
-
export const DEFAULT_MAX_PARALLEL_TASKS =
|
|
11
|
+
export const DEFAULT_MAX_PARALLEL_TASKS = 30;
|
|
35
12
|
export const DEFAULT_MAX_CONCURRENCY = 8;
|
|
36
13
|
export const PARALLEL_HEARTBEAT_MS = 1000;
|
|
37
14
|
export const SUBAGENT_MAX_PARALLEL_TASKS_ENV = "PI_SUBAGENT_MAX_PARALLEL_TASKS";
|
package/runner-sdk.ts
DELETED
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-process subagent runner using the pi SDK.
|
|
3
|
-
*
|
|
4
|
-
* Creates an isolated AgentSession per subagent call instead of spawning a
|
|
5
|
-
* separate `pi` process. All environment variables (PI_SUBAGENT_DEPTH,
|
|
6
|
-
* PI_SUBAGENT_MAX_DEPTH, PI_SUBAGENT_STACK, PI_SUBAGENT_PREVENT_CYCLES,
|
|
7
|
-
* PI_SUBAGENT_MAX_PARALLEL_TASKS, PI_SUBAGENT_MAX_CONCURRENCY, …) are still
|
|
8
|
-
* read at the root level exactly as before — they're just propagated to nested
|
|
9
|
-
* sessions via closure parameters rather than via subprocess env inheritance.
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
createAgentSession,
|
|
15
|
-
DefaultResourceLoader,
|
|
16
|
-
SessionManager,
|
|
17
|
-
createCodingTools,
|
|
18
|
-
createReadTool,
|
|
19
|
-
createBashTool,
|
|
20
|
-
createEditTool,
|
|
21
|
-
createWriteTool,
|
|
22
|
-
createGrepTool,
|
|
23
|
-
createFindTool,
|
|
24
|
-
createLsTool,
|
|
25
|
-
type ToolDefinition,
|
|
26
|
-
} from "@mariozechner/pi-coding-agent";
|
|
27
|
-
import type { AgentConfig } from "./agents.js";
|
|
28
|
-
import {
|
|
29
|
-
type LiveLogEntry,
|
|
30
|
-
type SingleResult,
|
|
31
|
-
type SubagentDetails,
|
|
32
|
-
MAX_LIVE_LOG_ENTRIES,
|
|
33
|
-
emptyUsage,
|
|
34
|
-
extractToolCalls,
|
|
35
|
-
getFinalOutput,
|
|
36
|
-
getNestedSubagentErrorSummary,
|
|
37
|
-
isResultError,
|
|
38
|
-
} from "./types.js";
|
|
39
|
-
import { renderCall, renderResult } from "./render.js";
|
|
40
|
-
import {
|
|
41
|
-
DEFAULT_MAX_PARALLEL_TASKS,
|
|
42
|
-
DEFAULT_MAX_CONCURRENCY,
|
|
43
|
-
PARALLEL_HEARTBEAT_MS,
|
|
44
|
-
SUBAGENT_MAX_PARALLEL_TASKS_ENV,
|
|
45
|
-
SUBAGENT_MAX_CONCURRENCY_ENV,
|
|
46
|
-
parseNonNegativeInt,
|
|
47
|
-
mapConcurrent,
|
|
48
|
-
subagentContext,
|
|
49
|
-
type SubagentSessionContext,
|
|
50
|
-
} from "./shared.js";
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Helpers
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Build the tool list for a child agent.
|
|
59
|
-
* When the agent config restricts tools, only those are included.
|
|
60
|
-
* Otherwise the full default coding tools are used.
|
|
61
|
-
*/
|
|
62
|
-
function buildAgentTools(agent: AgentConfig, cwd: string) {
|
|
63
|
-
if (!agent.tools || agent.tools.length === 0) {
|
|
64
|
-
return createCodingTools(cwd);
|
|
65
|
-
}
|
|
66
|
-
const factories: Record<string, () => unknown> = {
|
|
67
|
-
read: () => createReadTool(cwd),
|
|
68
|
-
bash: () => createBashTool(cwd),
|
|
69
|
-
edit: () => createEditTool(cwd),
|
|
70
|
-
write: () => createWriteTool(cwd),
|
|
71
|
-
grep: () => createGrepTool(cwd),
|
|
72
|
-
find: () => createFindTool(cwd),
|
|
73
|
-
ls: () => createLsTool(cwd),
|
|
74
|
-
};
|
|
75
|
-
const tools = agent.tools
|
|
76
|
-
.map((name) => factories[name.toLowerCase()]?.())
|
|
77
|
-
.filter(Boolean) as ReturnType<typeof createCodingTools>;
|
|
78
|
-
return tools.length > 0 ? tools : createCodingTools(cwd);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Search available models by id.
|
|
83
|
-
* Falls back to undefined when the model name can't be resolved.
|
|
84
|
-
*/
|
|
85
|
-
function resolveModelByName(modelName: string, modelRegistry: any): any {
|
|
86
|
-
try {
|
|
87
|
-
const available: any[] = modelRegistry.getAvailable();
|
|
88
|
-
return available.find((m) => m.id === modelName) ?? undefined;
|
|
89
|
-
} catch {
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// Public API: RunAgentSameProcessOptions / runAgentSameProcess
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
export interface RunAgentSameProcessOptions {
|
|
99
|
-
cwd: string;
|
|
100
|
-
agents: AgentConfig[];
|
|
101
|
-
agentName: string;
|
|
102
|
-
task: string;
|
|
103
|
-
taskCwd?: string;
|
|
104
|
-
parentDepth: number;
|
|
105
|
-
parentAgentStack: string[];
|
|
106
|
-
maxDepth: number;
|
|
107
|
-
preventCycles: boolean;
|
|
108
|
-
/** Pass ctx.modelRegistry from the calling tool's ExtensionContext */
|
|
109
|
-
modelRegistry: any;
|
|
110
|
-
/** Pass ctx.model from the calling tool's ExtensionContext */
|
|
111
|
-
parentModel: any;
|
|
112
|
-
signal?: AbortSignal;
|
|
113
|
-
onUpdate?: (partial: any) => void;
|
|
114
|
-
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Run a single subagent in-process using createAgentSession().
|
|
119
|
-
*
|
|
120
|
-
* Replaces runner.ts::runAgentSubprocess — no subprocess is spawned.
|
|
121
|
-
*
|
|
122
|
-
* Environment variables (PI_SUBAGENT_DEPTH, PI_SUBAGENT_MAX_DEPTH,
|
|
123
|
-
* PI_SUBAGENT_STACK, PI_SUBAGENT_PREVENT_CYCLES, …) are read at the root
|
|
124
|
-
* level in index.ts exactly as before. Here depth/stack propagate via
|
|
125
|
-
* the opts parameters and are captured in closure by child extension
|
|
126
|
-
* factories, so nested levels always see the correct values.
|
|
127
|
-
*/
|
|
128
|
-
export async function runAgentSameProcess(
|
|
129
|
-
opts: RunAgentSameProcessOptions,
|
|
130
|
-
): Promise<SingleResult> {
|
|
131
|
-
const {
|
|
132
|
-
cwd,
|
|
133
|
-
agents,
|
|
134
|
-
agentName,
|
|
135
|
-
task,
|
|
136
|
-
taskCwd,
|
|
137
|
-
parentDepth,
|
|
138
|
-
parentAgentStack,
|
|
139
|
-
maxDepth,
|
|
140
|
-
preventCycles,
|
|
141
|
-
modelRegistry,
|
|
142
|
-
parentModel,
|
|
143
|
-
signal,
|
|
144
|
-
onUpdate,
|
|
145
|
-
makeDetails,
|
|
146
|
-
} = opts;
|
|
147
|
-
|
|
148
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
149
|
-
if (!agent) {
|
|
150
|
-
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
151
|
-
return {
|
|
152
|
-
agent: agentName,
|
|
153
|
-
agentSource: "unknown",
|
|
154
|
-
task,
|
|
155
|
-
exitCode: 1,
|
|
156
|
-
messages: [],
|
|
157
|
-
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
158
|
-
usage: emptyUsage(),
|
|
159
|
-
toolCalls: {},
|
|
160
|
-
completedTurns: 0,
|
|
161
|
-
turnInProgress: false,
|
|
162
|
-
liveLog: [],
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const childDepth = Math.max(0, Math.floor(parentDepth)) + 1;
|
|
167
|
-
const childStack = [...parentAgentStack, agentName];
|
|
168
|
-
const effectiveCwd = taskCwd ?? cwd;
|
|
169
|
-
|
|
170
|
-
const result: SingleResult = {
|
|
171
|
-
agent: agentName,
|
|
172
|
-
agentSource: agent.source,
|
|
173
|
-
task,
|
|
174
|
-
exitCode: -1,
|
|
175
|
-
messages: [],
|
|
176
|
-
stderr: "",
|
|
177
|
-
usage: emptyUsage(),
|
|
178
|
-
toolCalls: {},
|
|
179
|
-
model: agent.model,
|
|
180
|
-
completedTurns: 0,
|
|
181
|
-
turnInProgress: false,
|
|
182
|
-
liveLog: [],
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const emitUpdate = () =>
|
|
186
|
-
onUpdate?.({
|
|
187
|
-
content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
|
|
188
|
-
details: makeDetails([result]),
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
emitUpdate();
|
|
192
|
-
|
|
193
|
-
const childCtx: SubagentSessionContext = {
|
|
194
|
-
depth: childDepth,
|
|
195
|
-
maxDepth,
|
|
196
|
-
stack: childStack,
|
|
197
|
-
preventCycles,
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
// Run the child session inside an AsyncLocalStorage context so that all
|
|
202
|
-
// extensions loaded within it — including the subagent extension itself —
|
|
203
|
-
// see the correct depth/stack/limits for this nesting level rather than
|
|
204
|
-
// the stale root values that live in process.env.
|
|
205
|
-
const loader = await subagentContext.run(childCtx, async () => {
|
|
206
|
-
const l = new DefaultResourceLoader({
|
|
207
|
-
cwd: effectiveCwd,
|
|
208
|
-
// Append agent system prompt on top of the base pi prompt —
|
|
209
|
-
// matches the --append-system-prompt behaviour of the subprocess runner
|
|
210
|
-
...(agent.systemPrompt.trim() ? { appendSystemPrompt: agent.systemPrompt } : {}),
|
|
211
|
-
});
|
|
212
|
-
await l.reload();
|
|
213
|
-
return l;
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Resolve model: agent config overrides parent fallback
|
|
217
|
-
const model = agent.model
|
|
218
|
-
? (resolveModelByName(agent.model, modelRegistry) ?? parentModel)
|
|
219
|
-
: parentModel;
|
|
220
|
-
|
|
221
|
-
// Thinking level from agent config (same string values as --thinking flag)
|
|
222
|
-
const thinkingLevel = agent.thinking as any | undefined;
|
|
223
|
-
|
|
224
|
-
const sessionManager = SessionManager.inMemory();
|
|
225
|
-
|
|
226
|
-
const { session } = await subagentContext.run(childCtx, () =>
|
|
227
|
-
createAgentSession({
|
|
228
|
-
cwd: effectiveCwd,
|
|
229
|
-
sessionManager,
|
|
230
|
-
resourceLoader: loader,
|
|
231
|
-
tools: buildAgentTools(agent, effectiveCwd),
|
|
232
|
-
modelRegistry,
|
|
233
|
-
...(model ? { model } : {}),
|
|
234
|
-
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
235
|
-
})
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
// Forward abort signal to the child session
|
|
239
|
-
const onAbort = () => session.abort();
|
|
240
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
241
|
-
|
|
242
|
-
const pushLiveLog = (entry: LiveLogEntry) => {
|
|
243
|
-
result.liveLog.push(entry);
|
|
244
|
-
if (result.liveLog.length > MAX_LIVE_LOG_ENTRIES) result.liveLog.shift();
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const unsub = session.subscribe((event: any) => {
|
|
248
|
-
if (event.type === "message_end" && event.message) {
|
|
249
|
-
result.messages.push(event.message);
|
|
250
|
-
if (event.message.role === "assistant") {
|
|
251
|
-
const u = event.message.usage;
|
|
252
|
-
if (u) {
|
|
253
|
-
result.usage.input += u.input ?? 0;
|
|
254
|
-
result.usage.output += u.output ?? 0;
|
|
255
|
-
result.usage.cacheRead += u.cacheRead ?? 0;
|
|
256
|
-
result.usage.cacheWrite += u.cacheWrite ?? 0;
|
|
257
|
-
result.usage.cost += u.cost?.total ?? 0;
|
|
258
|
-
result.usage.contextTokens = u.totalTokens ?? 0;
|
|
259
|
-
result.usage.turns++;
|
|
260
|
-
}
|
|
261
|
-
if (event.message.stopReason) result.stopReason = event.message.stopReason;
|
|
262
|
-
if (event.message.errorMessage)
|
|
263
|
-
result.errorMessage = event.message.errorMessage;
|
|
264
|
-
if (!result.model && event.message.model) result.model = event.message.model;
|
|
265
|
-
}
|
|
266
|
-
emitUpdate();
|
|
267
|
-
|
|
268
|
-
} else if (event.type === "turn_start") {
|
|
269
|
-
result.turnInProgress = true;
|
|
270
|
-
pushLiveLog({ kind: "turn_start" });
|
|
271
|
-
emitUpdate();
|
|
272
|
-
|
|
273
|
-
} else if (event.type === "turn_end") {
|
|
274
|
-
result.completedTurns++;
|
|
275
|
-
result.turnInProgress = false;
|
|
276
|
-
const u = (event.message as any)?.usage;
|
|
277
|
-
pushLiveLog({
|
|
278
|
-
kind: "turn_end",
|
|
279
|
-
turn: result.completedTurns,
|
|
280
|
-
inputTokens: u?.input ?? 0,
|
|
281
|
-
outputTokens: u?.output ?? 0,
|
|
282
|
-
});
|
|
283
|
-
emitUpdate();
|
|
284
|
-
|
|
285
|
-
} else if (event.type === "tool_execution_start") {
|
|
286
|
-
result.liveToolExecutions ??= {};
|
|
287
|
-
result.liveToolExecutions[event.toolCallId] = {
|
|
288
|
-
toolName: event.toolName,
|
|
289
|
-
args: event.args,
|
|
290
|
-
};
|
|
291
|
-
pushLiveLog({ kind: "tool_start", toolName: event.toolName, args: event.args });
|
|
292
|
-
emitUpdate();
|
|
293
|
-
|
|
294
|
-
} else if (event.type === "tool_execution_end") {
|
|
295
|
-
if (result.liveToolExecutions) {
|
|
296
|
-
delete result.liveToolExecutions[event.toolCallId];
|
|
297
|
-
}
|
|
298
|
-
pushLiveLog({ kind: "tool_end", toolName: event.toolName });
|
|
299
|
-
emitUpdate();
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
await session.prompt(`Task: ${task}`);
|
|
305
|
-
result.exitCode = 0;
|
|
306
|
-
result.toolCalls = extractToolCalls(result.messages);
|
|
307
|
-
|
|
308
|
-
if (result.exitCode === 0) {
|
|
309
|
-
const nestedError = getNestedSubagentErrorSummary(result.messages);
|
|
310
|
-
if (nestedError) {
|
|
311
|
-
result.exitCode = 1;
|
|
312
|
-
result.stopReason = "error";
|
|
313
|
-
result.errorMessage = nestedError;
|
|
314
|
-
if (!result.stderr.trim()) result.stderr = nestedError;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} catch (err: any) {
|
|
318
|
-
if (signal?.aborted) {
|
|
319
|
-
result.exitCode = 130;
|
|
320
|
-
result.stopReason = "aborted";
|
|
321
|
-
result.errorMessage = "Subagent was aborted.";
|
|
322
|
-
if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
|
|
323
|
-
} else {
|
|
324
|
-
result.exitCode = 1;
|
|
325
|
-
result.stopReason = "error";
|
|
326
|
-
result.errorMessage = err?.message ?? String(err);
|
|
327
|
-
result.stderr = result.errorMessage ?? "";
|
|
328
|
-
}
|
|
329
|
-
} finally {
|
|
330
|
-
signal?.removeEventListener("abort", onAbort);
|
|
331
|
-
unsub();
|
|
332
|
-
session.dispose();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return result;
|
|
336
|
-
} catch (err: any) {
|
|
337
|
-
// Outer safety net: catches anything thrown by loader.reload(),
|
|
338
|
-
// createAgentSession(), or other setup code outside the inner try/catch.
|
|
339
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
340
|
-
result.exitCode = result.exitCode === -1 ? 1 : result.exitCode;
|
|
341
|
-
result.stopReason = result.stopReason ?? "error";
|
|
342
|
-
result.errorMessage = result.errorMessage ?? msg;
|
|
343
|
-
if (!result.stderr.trim()) result.stderr = msg;
|
|
344
|
-
return result;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
// Parallel execution (shared by root index.ts and child extension factories)
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
|
|
352
|
-
export async function executeParallelSameProcess(
|
|
353
|
-
tasks: Array<{ agent: string; task: string; cwd?: string }>,
|
|
354
|
-
agents: AgentConfig[],
|
|
355
|
-
defaultCwd: string,
|
|
356
|
-
depth: number,
|
|
357
|
-
maxDepth: number,
|
|
358
|
-
stack: string[],
|
|
359
|
-
preventCycles: boolean,
|
|
360
|
-
modelRegistry: any,
|
|
361
|
-
parentModel: any,
|
|
362
|
-
signal: AbortSignal | undefined,
|
|
363
|
-
onUpdate: ((partial: any) => void) | undefined,
|
|
364
|
-
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
365
|
-
) {
|
|
366
|
-
// Limits still read from env vars — same as before
|
|
367
|
-
const maxParallelTasksRaw = process.env[SUBAGENT_MAX_PARALLEL_TASKS_ENV];
|
|
368
|
-
const maxParallelTasksParsed = parseNonNegativeInt(maxParallelTasksRaw);
|
|
369
|
-
if (maxParallelTasksRaw !== undefined && maxParallelTasksParsed === null) {
|
|
370
|
-
console.warn(
|
|
371
|
-
`[pi-subagent] Ignoring invalid ${SUBAGENT_MAX_PARALLEL_TASKS_ENV}="${maxParallelTasksRaw}". Expected a non-negative integer.`,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
const maxParallelTasks = maxParallelTasksParsed ?? DEFAULT_MAX_PARALLEL_TASKS;
|
|
375
|
-
|
|
376
|
-
const maxConcurrencyRaw = process.env[SUBAGENT_MAX_CONCURRENCY_ENV];
|
|
377
|
-
const maxConcurrencyParsed = parseNonNegativeInt(maxConcurrencyRaw);
|
|
378
|
-
if (maxConcurrencyRaw !== undefined && maxConcurrencyParsed === null) {
|
|
379
|
-
console.warn(
|
|
380
|
-
`[pi-subagent] Ignoring invalid ${SUBAGENT_MAX_CONCURRENCY_ENV}="${maxConcurrencyRaw}". Expected a non-negative integer.`,
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
const maxConcurrency = maxConcurrencyParsed ?? DEFAULT_MAX_CONCURRENCY;
|
|
384
|
-
|
|
385
|
-
if (tasks.length > maxParallelTasks) {
|
|
386
|
-
return {
|
|
387
|
-
content: [
|
|
388
|
-
{
|
|
389
|
-
type: "text" as const,
|
|
390
|
-
text: `Too many parallel tasks (${tasks.length}). Max is ${maxParallelTasks}.`,
|
|
391
|
-
},
|
|
392
|
-
],
|
|
393
|
-
details: makeDetails([]),
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const allResults: SingleResult[] = tasks.map((t) => ({
|
|
398
|
-
agent: t.agent,
|
|
399
|
-
agentSource: "unknown" as const,
|
|
400
|
-
task: t.task,
|
|
401
|
-
exitCode: -1,
|
|
402
|
-
messages: [],
|
|
403
|
-
stderr: "",
|
|
404
|
-
usage: emptyUsage(),
|
|
405
|
-
toolCalls: {},
|
|
406
|
-
completedTurns: 0,
|
|
407
|
-
turnInProgress: false,
|
|
408
|
-
liveLog: [],
|
|
409
|
-
}));
|
|
410
|
-
|
|
411
|
-
const emitProgress = () => {
|
|
412
|
-
if (!onUpdate) return;
|
|
413
|
-
const running = allResults.filter((r) => r.exitCode === -1).length;
|
|
414
|
-
const done = allResults.filter((r) => r.exitCode !== -1).length;
|
|
415
|
-
onUpdate({
|
|
416
|
-
content: [
|
|
417
|
-
{
|
|
418
|
-
type: "text",
|
|
419
|
-
text: `Parallel: ${done}/${allResults.length} done, ${running} running...`,
|
|
420
|
-
},
|
|
421
|
-
],
|
|
422
|
-
details: makeDetails([...allResults]),
|
|
423
|
-
});
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
let heartbeat: NodeJS.Timeout | undefined;
|
|
427
|
-
if (onUpdate) {
|
|
428
|
-
emitProgress();
|
|
429
|
-
heartbeat = setInterval(() => {
|
|
430
|
-
if (allResults.some((r) => r.exitCode === -1)) emitProgress();
|
|
431
|
-
}, PARALLEL_HEARTBEAT_MS);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
let results: SingleResult[];
|
|
435
|
-
try {
|
|
436
|
-
results = await mapConcurrent(tasks, maxConcurrency, async (t, index) => {
|
|
437
|
-
const result = await runAgentSameProcess({
|
|
438
|
-
cwd: defaultCwd,
|
|
439
|
-
agents,
|
|
440
|
-
agentName: t.agent,
|
|
441
|
-
task: t.task,
|
|
442
|
-
taskCwd: t.cwd,
|
|
443
|
-
parentDepth: depth,
|
|
444
|
-
parentAgentStack: stack,
|
|
445
|
-
maxDepth,
|
|
446
|
-
preventCycles,
|
|
447
|
-
modelRegistry,
|
|
448
|
-
parentModel,
|
|
449
|
-
signal,
|
|
450
|
-
onUpdate: (partial: any) => {
|
|
451
|
-
if (partial.details?.results[0]) {
|
|
452
|
-
allResults[index] = partial.details.results[0];
|
|
453
|
-
emitProgress();
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
makeDetails,
|
|
457
|
-
});
|
|
458
|
-
allResults[index] = result;
|
|
459
|
-
emitProgress();
|
|
460
|
-
return result;
|
|
461
|
-
});
|
|
462
|
-
} finally {
|
|
463
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
467
|
-
const summaries = results.map((r) => {
|
|
468
|
-
const output = getFinalOutput(r.messages);
|
|
469
|
-
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${output || "(no output)"}`;
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
return {
|
|
473
|
-
content: [
|
|
474
|
-
{
|
|
475
|
-
type: "text" as const,
|
|
476
|
-
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
477
|
-
},
|
|
478
|
-
],
|
|
479
|
-
details: makeDetails(results),
|
|
480
|
-
};
|
|
481
|
-
}
|