oira666_pi-subagent 0.2.1 → 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 +16 -117
- package/package.json +1 -2
- package/runner.ts +19 -4
- package/shared.ts +2 -25
- 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,12 +13,10 @@ 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 { runAgentSameProcess, executeParallelSameProcess } from "./runner-sdk.js";
|
|
24
16
|
import {
|
|
25
17
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
26
18
|
SUBAGENT_MAX_PARALLEL_TASKS_ENV,
|
|
27
19
|
parseNonNegativeInt,
|
|
28
|
-
subagentContext,
|
|
29
20
|
} from "./shared.js";
|
|
30
21
|
|
|
31
22
|
import {
|
|
@@ -50,7 +41,6 @@ const SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
|
50
41
|
const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
51
42
|
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
52
43
|
const SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV = "PI_SUBAGENT_CONFIRM_PROJECT_AGENTS";
|
|
53
|
-
const SUBAGENTS_MODE_ENV = "PI_SUBAGENTS_MODE";
|
|
54
44
|
|
|
55
45
|
type ProjectAgentConfirmationSetting = "ask" | "never" | "session";
|
|
56
46
|
type ProjectAgentApproval = "once" | "session" | "no";
|
|
@@ -191,20 +181,6 @@ function getPreventCyclesFlagFromArgv(
|
|
|
191
181
|
}
|
|
192
182
|
|
|
193
183
|
function resolveDelegationDepthConfig(pi: ExtensionAPI): DelegationDepthConfig {
|
|
194
|
-
// When loaded inside an in-process child session (SDK mode), the correct
|
|
195
|
-
// depth/stack/limits are provided via AsyncLocalStorage rather than
|
|
196
|
-
// process.env (which still holds the root process values).
|
|
197
|
-
const inProcessCtx = subagentContext.getStore();
|
|
198
|
-
if (inProcessCtx) {
|
|
199
|
-
return {
|
|
200
|
-
currentDepth: inProcessCtx.depth,
|
|
201
|
-
maxDepth: inProcessCtx.maxDepth,
|
|
202
|
-
canDelegate: inProcessCtx.depth < inProcessCtx.maxDepth,
|
|
203
|
-
ancestorAgentStack: inProcessCtx.stack,
|
|
204
|
-
preventCycles: inProcessCtx.preventCycles,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
184
|
const depthRaw = process.env[SUBAGENT_DEPTH_ENV];
|
|
209
185
|
const parsedDepth = parseNonNegativeInt(depthRaw);
|
|
210
186
|
if (depthRaw !== undefined && parsedDepth === null) {
|
|
@@ -363,38 +339,6 @@ function getProjectAgentSessionKey(projectAgentsDir: string | null): string {
|
|
|
363
339
|
return projectAgentsDir ?? "(unknown-project-agents-dir)";
|
|
364
340
|
}
|
|
365
341
|
|
|
366
|
-
// ---------------------------------------------------------------------------
|
|
367
|
-
// Subagent mode helpers (exported for testing and external consumers)
|
|
368
|
-
// ---------------------------------------------------------------------------
|
|
369
|
-
|
|
370
|
-
/** The two supported subagent execution modes. */
|
|
371
|
-
export type SubagentMode = "subprocess" | "sdk";
|
|
372
|
-
|
|
373
|
-
/** Default subagent execution mode. */
|
|
374
|
-
export const DEFAULT_SUBAGENTS_MODE: SubagentMode = "subprocess";
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Parse a raw value into a SubagentMode.
|
|
378
|
-
* Returns null for invalid/unrecognized values, the default mode for undefined.
|
|
379
|
-
*/
|
|
380
|
-
export function parseSubagentMode(raw: unknown): SubagentMode | null {
|
|
381
|
-
if (raw === undefined) return DEFAULT_SUBAGENTS_MODE;
|
|
382
|
-
if (typeof raw !== "string") return null;
|
|
383
|
-
const normalized = raw.trim().toLowerCase();
|
|
384
|
-
if (normalized === "subprocess") return "subprocess";
|
|
385
|
-
if (normalized === "sdk") return "sdk";
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Read the subagent mode from PI_SUBAGENTS_MODE env var.
|
|
391
|
-
* Falls back to DEFAULT_SUBAGENTS_MODE if the env var is absent or invalid.
|
|
392
|
-
*/
|
|
393
|
-
export function getSubagentMode(): SubagentMode {
|
|
394
|
-
const parsed = parseSubagentMode(process.env["PI_SUBAGENTS_MODE"]);
|
|
395
|
-
return parsed ?? DEFAULT_SUBAGENTS_MODE;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
342
|
// ---------------------------------------------------------------------------
|
|
399
343
|
// Extension entry point
|
|
400
344
|
// ---------------------------------------------------------------------------
|
|
@@ -413,7 +357,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
357
|
const depthConfig = resolveDelegationDepthConfig(pi);
|
|
414
358
|
const { currentDepth, maxDepth, canDelegate, ancestorAgentStack, preventCycles } =
|
|
415
359
|
depthConfig;
|
|
416
|
-
const subagentMode = getSubagentMode();
|
|
417
360
|
const maxParallelTasks =
|
|
418
361
|
parseNonNegativeInt(process.env[SUBAGENT_MAX_PARALLEL_TASKS_ENV]) ??
|
|
419
362
|
DEFAULT_MAX_PARALLEL_TASKS;
|
|
@@ -496,9 +439,7 @@ calls one after another. Do NOT put dependent tasks in the same array.
|
|
|
496
439
|
name: "subagent",
|
|
497
440
|
label: "Subagent",
|
|
498
441
|
description: [
|
|
499
|
-
|
|
500
|
-
subagentMode === "sdk" ? "in-process SDK sessions" : "isolated pi processes"
|
|
501
|
-
}.`,
|
|
442
|
+
"Delegate work to specialized subagents running as isolated pi processes.",
|
|
502
443
|
"",
|
|
503
444
|
"Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
|
|
504
445
|
" - 1 task -> single delegation",
|
|
@@ -628,8 +569,6 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
628
569
|
signal,
|
|
629
570
|
onUpdate,
|
|
630
571
|
makeDetails,
|
|
631
|
-
ctx.modelRegistry,
|
|
632
|
-
ctx.model,
|
|
633
572
|
);
|
|
634
573
|
}
|
|
635
574
|
|
|
@@ -640,8 +579,6 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
640
579
|
signal,
|
|
641
580
|
onUpdate,
|
|
642
581
|
makeDetails,
|
|
643
|
-
ctx.modelRegistry,
|
|
644
|
-
ctx.model,
|
|
645
582
|
);
|
|
646
583
|
} catch (err) {
|
|
647
584
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -674,41 +611,21 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
674
611
|
signal: AbortSignal | undefined,
|
|
675
612
|
onUpdate: ((partial: any) => void) | undefined,
|
|
676
613
|
makeDetails: ReturnType<typeof makeDetailsFactory>,
|
|
677
|
-
modelRegistry: any,
|
|
678
|
-
parentModel: any,
|
|
679
614
|
) {
|
|
680
|
-
const result =
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
signal,
|
|
695
|
-
onUpdate,
|
|
696
|
-
makeDetails: makeDetails("single"),
|
|
697
|
-
})
|
|
698
|
-
: await runAgentSubprocess({
|
|
699
|
-
cwd: defaultCwd,
|
|
700
|
-
agents,
|
|
701
|
-
agentName,
|
|
702
|
-
task,
|
|
703
|
-
taskCwd: cwd,
|
|
704
|
-
parentDepth: currentDepth,
|
|
705
|
-
parentAgentStack: ancestorAgentStack,
|
|
706
|
-
maxDepth,
|
|
707
|
-
preventCycles,
|
|
708
|
-
signal,
|
|
709
|
-
onUpdate,
|
|
710
|
-
makeDetails: makeDetails("single"),
|
|
711
|
-
});
|
|
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
|
+
});
|
|
712
629
|
|
|
713
630
|
if (isResultError(result)) {
|
|
714
631
|
const errorMsg =
|
|
@@ -745,25 +662,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
745
662
|
signal: AbortSignal | undefined,
|
|
746
663
|
onUpdate: ((partial: any) => void) | undefined,
|
|
747
664
|
makeDetails: ReturnType<typeof makeDetailsFactory>,
|
|
748
|
-
modelRegistry: any,
|
|
749
|
-
parentModel: any,
|
|
750
665
|
) {
|
|
751
|
-
if (subagentMode === "sdk") {
|
|
752
|
-
return executeParallelSameProcess(
|
|
753
|
-
tasks,
|
|
754
|
-
agents,
|
|
755
|
-
defaultCwd,
|
|
756
|
-
currentDepth,
|
|
757
|
-
maxDepth,
|
|
758
|
-
ancestorAgentStack,
|
|
759
|
-
preventCycles,
|
|
760
|
-
modelRegistry,
|
|
761
|
-
parentModel,
|
|
762
|
-
signal,
|
|
763
|
-
onUpdate,
|
|
764
|
-
makeDetails("parallel"),
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
666
|
return executeParallelSubprocess(
|
|
768
667
|
tasks,
|
|
769
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,32 +1,9 @@
|
|
|
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
|
// ---------------------------------------------------------------------------
|
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
|
-
}
|