@ferris1225/pi-subagents 0.12.0 → 0.14.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/README.md +97 -96
- package/package.json +1 -1
- package/src/config.ts +9 -27
- package/src/index.ts +60 -27
- package/src/monitor.ts +20 -2
- package/src/setup.ts +2 -26
- package/src/spawn.ts +15 -5
package/README.md
CHANGED
|
@@ -68,48 +68,48 @@ here first.
|
|
|
68
68
|
<summary><code>agents/explore.md</code> — reconnaissance</summary>
|
|
69
69
|
|
|
70
70
|
```markdown
|
|
71
|
-
---
|
|
72
|
-
name: explore
|
|
73
|
-
description: Fast read-only codebase reconnaissance. Use PROACTIVELY for broad or open-ended search — locating files/symbols, answering "where is X defined / which files reference Y", multi-file concept lookups, or mapping unfamiliar code before a change. Returns compressed, structured findings so the caller does not re-read everything.
|
|
74
|
-
tools: read, grep, find, ls, bash
|
|
75
|
-
model: claude-haiku-4-5
|
|
76
|
-
thinking: low
|
|
77
|
-
# Model selection: SPEED over depth. Pick the fastest available model.
|
|
78
|
-
# What matters: fast grep/find/read, structured output. What doesn't: deep reasoning.
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
You are an explore agent: a fast, read-only reconnaissance specialist. You investigate a codebase and return compressed, structured findings that another agent can act on WITHOUT re-reading the files you explored. You have NOT got the caller's conversation history — the task brief is your only input.
|
|
82
|
-
|
|
83
|
-
## Hard constraints
|
|
84
|
-
- You are READ-ONLY. Never create, edit, or delete files; never run mutating commands.
|
|
85
|
-
- Bash is for read-only inspection only: `grep`, `find`, `ls`, `cat`, `git log/show/diff/status`. No installs, builds, or state changes.
|
|
86
|
-
- Assume tool permissions are not perfectly enforceable; keep every command strictly read-only by intent.
|
|
87
|
-
|
|
88
|
-
## When invoked
|
|
89
|
-
1. Orient with `grep`/`find` to locate the relevant code fast. Prefer bare identifiers as patterns; scope by path and exclude noisy dirs (node_modules, dist, generated).
|
|
90
|
-
2. Read KEY SECTIONS, not whole files. After 1-2 greps, read the top match instead of running more greps.
|
|
91
|
-
3. Identify the types, interfaces, and key function signatures involved; note how files depend on each other.
|
|
92
|
-
4. Record exact paths and line ranges so the caller can jump straight in.
|
|
93
|
-
|
|
94
|
-
## Thoroughness (infer from the task, default medium)
|
|
95
|
-
- Quick: targeted lookups, key files only.
|
|
96
|
-
- Medium: follow imports and callers, read critical sections.
|
|
97
|
-
- Thorough: trace dependencies across modules; check tests and types.
|
|
98
|
-
|
|
99
|
-
## Collaboration
|
|
100
|
-
- Your output feeds `worker` (or the main agent directly). Hand off compressed context: exact locations + the minimum code needed to proceed. Flag anything ambiguous so the caller can decide.
|
|
101
|
-
|
|
102
|
-
## Output format
|
|
103
|
-
## Files Retrieved
|
|
104
|
-
1. `path/to/file.ts` (lines 10-50) — what lives here and why it matters
|
|
105
|
-
## Key Code
|
|
106
|
-
Critical types / interfaces / signatures as short code blocks.
|
|
107
|
-
## Architecture
|
|
108
|
-
A brief explanation of how the pieces connect.
|
|
109
|
-
## Start Here
|
|
110
|
-
Which file to look at first, and why.
|
|
111
|
-
|
|
112
|
-
## Quality standards
|
|
71
|
+
---
|
|
72
|
+
name: explore
|
|
73
|
+
description: Fast read-only codebase reconnaissance. Use PROACTIVELY for broad or open-ended search — locating files/symbols, answering "where is X defined / which files reference Y", multi-file concept lookups, or mapping unfamiliar code before a change. Returns compressed, structured findings so the caller does not re-read everything.
|
|
74
|
+
tools: read, grep, find, ls, bash
|
|
75
|
+
model: claude-haiku-4-5
|
|
76
|
+
thinking: low
|
|
77
|
+
# Model selection: SPEED over depth. Pick the fastest available model.
|
|
78
|
+
# What matters: fast grep/find/read, structured output. What doesn't: deep reasoning.
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
You are an explore agent: a fast, read-only reconnaissance specialist. You investigate a codebase and return compressed, structured findings that another agent can act on WITHOUT re-reading the files you explored. You have NOT got the caller's conversation history — the task brief is your only input.
|
|
82
|
+
|
|
83
|
+
## Hard constraints
|
|
84
|
+
- You are READ-ONLY. Never create, edit, or delete files; never run mutating commands.
|
|
85
|
+
- Bash is for read-only inspection only: `grep`, `find`, `ls`, `cat`, `git log/show/diff/status`. No installs, builds, or state changes.
|
|
86
|
+
- Assume tool permissions are not perfectly enforceable; keep every command strictly read-only by intent.
|
|
87
|
+
|
|
88
|
+
## When invoked
|
|
89
|
+
1. Orient with `grep`/`find` to locate the relevant code fast. Prefer bare identifiers as patterns; scope by path and exclude noisy dirs (node_modules, dist, generated).
|
|
90
|
+
2. Read KEY SECTIONS, not whole files. After 1-2 greps, read the top match instead of running more greps.
|
|
91
|
+
3. Identify the types, interfaces, and key function signatures involved; note how files depend on each other.
|
|
92
|
+
4. Record exact paths and line ranges so the caller can jump straight in.
|
|
93
|
+
|
|
94
|
+
## Thoroughness (infer from the task, default medium)
|
|
95
|
+
- Quick: targeted lookups, key files only.
|
|
96
|
+
- Medium: follow imports and callers, read critical sections.
|
|
97
|
+
- Thorough: trace dependencies across modules; check tests and types.
|
|
98
|
+
|
|
99
|
+
## Collaboration
|
|
100
|
+
- Your output feeds `worker` (or the main agent directly). Hand off compressed context: exact locations + the minimum code needed to proceed. Flag anything ambiguous so the caller can decide.
|
|
101
|
+
|
|
102
|
+
## Output format
|
|
103
|
+
## Files Retrieved
|
|
104
|
+
1. `path/to/file.ts` (lines 10-50) — what lives here and why it matters
|
|
105
|
+
## Key Code
|
|
106
|
+
Critical types / interfaces / signatures as short code blocks.
|
|
107
|
+
## Architecture
|
|
108
|
+
A brief explanation of how the pieces connect.
|
|
109
|
+
## Start Here
|
|
110
|
+
Which file to look at first, and why.
|
|
111
|
+
|
|
112
|
+
## Quality standards
|
|
113
113
|
Terse and factual. Exact paths and line numbers. Compress — do not narrate your search process or pad with prose.
|
|
114
114
|
```
|
|
115
115
|
|
|
@@ -119,52 +119,52 @@ Terse and factual. Exact paths and line numbers. Compress — do not narrate you
|
|
|
119
119
|
<summary><code>agents/worker.md</code> — implementation</summary>
|
|
120
120
|
|
|
121
121
|
```markdown
|
|
122
|
-
---
|
|
123
|
-
name: worker
|
|
124
|
-
description: General-purpose implementation agent with full tools in an isolated context. Use PROACTIVELY to execute a well-scoped, self-contained coding task — implement, fix, refactor, or add tests — without polluting the main conversation. Plans internally, then implements and verifies. Give it a complete, self-contained brief.
|
|
125
|
-
model: claude-sonnet-4-5
|
|
126
|
-
thinking: high
|
|
127
|
-
# Model selection: CODING ABILITY + TOOL USE. The primary implementation model —
|
|
128
|
-
# balance quality against cost. No `tools` field => inherits all tools (full capability).
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
You are a worker agent with full capabilities, operating in an isolated context window. You own a delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
|
|
132
|
-
|
|
133
|
-
## Standard operating procedure
|
|
134
|
-
Work in phases. Do not skip planning or verification.
|
|
135
|
-
|
|
136
|
-
### Phase 1 — Context
|
|
137
|
-
Read the brief fully. If it references files, read them before editing. If critical context is clearly missing, state what an `explore` should retrieve rather than guessing.
|
|
138
|
-
|
|
139
|
-
### Phase 2 — Plan
|
|
140
|
-
Inspect existing code and conventions first. Form the smallest coherent root-cause change that satisfies the brief. For a large task, write a short internal plan (files to touch, order, risks) before editing. Do not refactor unrelated code or create docs unless the brief asks.
|
|
141
|
-
|
|
142
|
-
### Phase 3 — Implement
|
|
143
|
-
Make the change. Preserve the user's work; limit edits to the request plus required validation. Follow the project's existing error handling, naming, and style.
|
|
144
|
-
|
|
145
|
-
### Phase 4 — Verify
|
|
146
|
-
Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the test runner). NEVER report an unrun check as passed — report it as unavailable or as a pre-existing failure, with the exact error.
|
|
147
|
-
|
|
148
|
-
### Phase 5 — Handoff
|
|
149
|
-
Summarize concretely so the caller can verify and, if needed, hand to a `reviewer`.
|
|
150
|
-
|
|
151
|
-
## Collaboration
|
|
152
|
-
- You cannot dispatch sub-agents (children are leaf processes with no `subagent` tool). When the
|
|
153
|
-
brief lacks context that needs broad code discovery, state concretely what an `explore` should
|
|
154
|
-
retrieve for the caller — do not guess.
|
|
155
|
-
- Recommend a `reviewer` pass before the caller reports work done or commits, especially for non-trivial diffs.
|
|
156
|
-
|
|
157
|
-
## Output format
|
|
158
|
-
## Completed
|
|
159
|
-
What was done, in a few lines.
|
|
160
|
-
## Files Changed
|
|
161
|
-
- `path/to/file.ts` — what changed.
|
|
162
|
-
## Verification
|
|
163
|
-
Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean; `vitest` 12 passed). State explicitly anything you could not run and why.
|
|
164
|
-
## Notes (if any)
|
|
165
|
-
Follow-ups, decisions made, blockers. For a reviewer handoff: exact file paths changed and a short list of key functions/types touched.
|
|
166
|
-
|
|
167
|
-
## Quality standards
|
|
122
|
+
---
|
|
123
|
+
name: worker
|
|
124
|
+
description: General-purpose implementation agent with full tools in an isolated context. Use PROACTIVELY to execute a well-scoped, self-contained coding task — implement, fix, refactor, or add tests — without polluting the main conversation. Plans internally, then implements and verifies. Give it a complete, self-contained brief.
|
|
125
|
+
model: claude-sonnet-4-5
|
|
126
|
+
thinking: high
|
|
127
|
+
# Model selection: CODING ABILITY + TOOL USE. The primary implementation model —
|
|
128
|
+
# balance quality against cost. No `tools` field => inherits all tools (full capability).
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
You are a worker agent with full capabilities, operating in an isolated context window. You own a delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
|
|
132
|
+
|
|
133
|
+
## Standard operating procedure
|
|
134
|
+
Work in phases. Do not skip planning or verification.
|
|
135
|
+
|
|
136
|
+
### Phase 1 — Context
|
|
137
|
+
Read the brief fully. If it references files, read them before editing. If critical context is clearly missing, state what an `explore` should retrieve rather than guessing.
|
|
138
|
+
|
|
139
|
+
### Phase 2 — Plan
|
|
140
|
+
Inspect existing code and conventions first. Form the smallest coherent root-cause change that satisfies the brief. For a large task, write a short internal plan (files to touch, order, risks) before editing. Do not refactor unrelated code or create docs unless the brief asks.
|
|
141
|
+
|
|
142
|
+
### Phase 3 — Implement
|
|
143
|
+
Make the change. Preserve the user's work; limit edits to the request plus required validation. Follow the project's existing error handling, naming, and style.
|
|
144
|
+
|
|
145
|
+
### Phase 4 — Verify
|
|
146
|
+
Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the test runner). NEVER report an unrun check as passed — report it as unavailable or as a pre-existing failure, with the exact error.
|
|
147
|
+
|
|
148
|
+
### Phase 5 — Handoff
|
|
149
|
+
Summarize concretely so the caller can verify and, if needed, hand to a `reviewer`.
|
|
150
|
+
|
|
151
|
+
## Collaboration
|
|
152
|
+
- You cannot dispatch sub-agents (children are leaf processes with no `subagent` tool). When the
|
|
153
|
+
brief lacks context that needs broad code discovery, state concretely what an `explore` should
|
|
154
|
+
retrieve for the caller — do not guess.
|
|
155
|
+
- Recommend a `reviewer` pass before the caller reports work done or commits, especially for non-trivial diffs.
|
|
156
|
+
|
|
157
|
+
## Output format
|
|
158
|
+
## Completed
|
|
159
|
+
What was done, in a few lines.
|
|
160
|
+
## Files Changed
|
|
161
|
+
- `path/to/file.ts` — what changed.
|
|
162
|
+
## Verification
|
|
163
|
+
Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean; `vitest` 12 passed). State explicitly anything you could not run and why.
|
|
164
|
+
## Notes (if any)
|
|
165
|
+
Follow-ups, decisions made, blockers. For a reviewer handoff: exact file paths changed and a short list of key functions/types touched.
|
|
166
|
+
|
|
167
|
+
## Quality standards
|
|
168
168
|
Root-cause fixes over patches. No unrelated churn. Honest verification — an unrun check is never a passed check.
|
|
169
169
|
```
|
|
170
170
|
|
|
@@ -257,9 +257,9 @@ main agent
|
|
|
257
257
|
1. The main agent calls `subagent` with a self-contained brief.
|
|
258
258
|
2. The tool returns immediately and ends that foreground tool turn, leaving the editor ready
|
|
259
259
|
for input.
|
|
260
|
-
3. The child process works independently. By default up to four
|
|
261
|
-
|
|
262
|
-
configurable
|
|
260
|
+
3. The child process works independently. By default up to four sub-agents run at once —
|
|
261
|
+
and one parallel call accepts at most four tasks; extra runs queue up to `maxConcurrency`
|
|
262
|
+
(configurable via `/subagents-setup` or `pi-subagents.json`).
|
|
263
263
|
4. On completion or failure, the extension sends a durable result message to the main
|
|
264
264
|
session. That message automatically wakes the main agent, or waits until its current turn
|
|
265
265
|
finishes.
|
|
@@ -331,8 +331,6 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
|
|
|
331
331
|
"proactiveInjection": true,
|
|
332
332
|
"agentScope": "user",
|
|
333
333
|
"maxConcurrency": 4,
|
|
334
|
-
"maxParallelTasks": 8,
|
|
335
|
-
"maxSubagentDepth": 1,
|
|
336
334
|
"maxFixRounds": 2
|
|
337
335
|
}
|
|
338
336
|
```
|
|
@@ -347,9 +345,7 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
|
|
|
347
345
|
| `maxResultLines` | Max lines of a sub-agent result carried in the completion message (default `80`). Longer results are truncated; the full text is written to a temp file whose path is included in the message. |
|
|
348
346
|
| `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
|
|
349
347
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
350
|
-
| `maxConcurrency` |
|
|
351
|
-
| `maxParallelTasks` | Maximum tasks accepted by one parallel `subagent` call (1–32, default 8). |
|
|
352
|
-
| `maxSubagentDepth` | Depth at which the `subagent` tool is no longer registered (default 1: the main session delegates, children are leaf processes). `0` disables the tool entirely. Read once at extension load. |
|
|
348
|
+
| `maxConcurrency` | Max sub-agent processes running at once (1–16, default 4), and the max tasks one parallel `subagent` call accepts. Extra work waits in the queue. |
|
|
353
349
|
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL`: the extension dispatches a `worker` (briefed with the review's concrete findings) then a `reviewer` re-review, repeating up to this many times before waking the main agent with the full chain. `0` disables it (the main agent handles fixes itself). Default 2. The reviewer stays read-only and in its own context; the loop is orchestrated by the extension, not by the reviewer. |
|
|
354
350
|
|
|
355
351
|
### Configuration migration
|
|
@@ -360,6 +356,11 @@ The config file migrates itself on load — no manual steps after an upgrade:
|
|
|
360
356
|
holding invalid values) is normalized and saved back with the new fields filled in.
|
|
361
357
|
- **Removed agents** — agents no longer shipped (e.g. the old `plan` agent) are stripped
|
|
362
358
|
from `enabledAgents`, `agentModels`, and `agentThinkingLevels` automatically.
|
|
359
|
+
- **Merged limits** — the pre-0.13 `maxParallelTasks` key is folded into `maxConcurrency`
|
|
360
|
+
(the larger of the two wins) and dropped on the next save.
|
|
361
|
+
- **Removed keys** — `maxSubagentDepth` (0.14) is dropped on load: sub-agent children are
|
|
362
|
+
always leaf processes (the `subagent` tool is excluded from their toolset, with a depth
|
|
363
|
+
marker as defense in depth). To disable delegation entirely, use `"enabledAgents": []`.
|
|
363
364
|
|
|
364
365
|
Model selection uses this precedence:
|
|
365
366
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/config.ts
CHANGED
|
@@ -45,21 +45,10 @@ export const MAX_RESULT_LINES_LIMIT = 2000;
|
|
|
45
45
|
|
|
46
46
|
export const CONFIG_FILE_NAME = "pi-subagents.json";
|
|
47
47
|
|
|
48
|
-
/** How many sub-agent processes may run at once. Default: 4. */
|
|
48
|
+
/** How many sub-agent processes may run at once, and how many tasks one parallel `subagent` call may contain. Default: 4. */
|
|
49
49
|
export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
50
50
|
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
51
51
|
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
52
|
-
/** How many tasks a single parallel `subagent` call may contain. Default: 8. */
|
|
53
|
-
export const DEFAULT_MAX_PARALLEL_TASKS = 8;
|
|
54
|
-
/** Upper bound accepted for maxParallelTasks (defensive clamp). */
|
|
55
|
-
export const MAX_PARALLEL_TASKS_LIMIT = 32;
|
|
56
|
-
/**
|
|
57
|
-
* Depth at which the subagent tool stops being available. 1 = the main session
|
|
58
|
-
* delegates and child processes are leaves; 0 disables the tool entirely.
|
|
59
|
-
*/
|
|
60
|
-
export const DEFAULT_MAX_SUBAGENT_DEPTH = 1;
|
|
61
|
-
/** Upper bound accepted for maxSubagentDepth (defensive clamp). */
|
|
62
|
-
export const MAX_SUBAGENT_DEPTH_LIMIT = 4;
|
|
63
52
|
/**
|
|
64
53
|
* How many automatic worker→reviewer fix rounds run when a reviewer returns
|
|
65
54
|
* REVIEW_FAIL before waking the main agent. 0 disables the auto-fix loop
|
|
@@ -93,12 +82,9 @@ export interface SubagentsConfig {
|
|
|
93
82
|
proactiveInjection: boolean;
|
|
94
83
|
/** Which agent directories to discover from. Default: "user". */
|
|
95
84
|
agentScope: AgentScope;
|
|
96
|
-
/** Max sub-agent processes running at once (extra work queues)
|
|
85
|
+
/** Max sub-agent processes running at once (extra work queues) and the max tasks
|
|
86
|
+
* one parallel `subagent` call may contain. Default: 4. */
|
|
97
87
|
maxConcurrency: number;
|
|
98
|
-
/** Max tasks accepted by one parallel `subagent` call. Default: 8. */
|
|
99
|
-
maxParallelTasks: number;
|
|
100
|
-
/** Depth at which the subagent tool is no longer registered. Default: 1. */
|
|
101
|
-
maxSubagentDepth: number;
|
|
102
88
|
/**
|
|
103
89
|
* Auto-fix rounds when a reviewer returns REVIEW_FAIL: the extension dispatches
|
|
104
90
|
* a worker (briefed with the review's concrete findings) then a reviewer
|
|
@@ -119,8 +105,6 @@ export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
|
119
105
|
proactiveInjection: true,
|
|
120
106
|
agentScope: "user",
|
|
121
107
|
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
|
|
122
|
-
maxParallelTasks: DEFAULT_MAX_PARALLEL_TASKS,
|
|
123
|
-
maxSubagentDepth: DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
124
108
|
maxFixRounds: DEFAULT_MAX_FIX_ROUNDS,
|
|
125
109
|
};
|
|
126
110
|
|
|
@@ -166,8 +150,6 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
166
150
|
proactiveInjection: DEFAULT_CONFIG.proactiveInjection,
|
|
167
151
|
agentScope: DEFAULT_CONFIG.agentScope,
|
|
168
152
|
maxConcurrency: DEFAULT_CONFIG.maxConcurrency,
|
|
169
|
-
maxParallelTasks: DEFAULT_CONFIG.maxParallelTasks,
|
|
170
|
-
maxSubagentDepth: DEFAULT_CONFIG.maxSubagentDepth,
|
|
171
153
|
maxFixRounds: DEFAULT_CONFIG.maxFixRounds,
|
|
172
154
|
};
|
|
173
155
|
|
|
@@ -223,12 +205,12 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
223
205
|
const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
|
|
224
206
|
if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
|
|
225
207
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
config.
|
|
208
|
+
// Schema migration: maxParallelTasks (pre-0.13) merged into maxConcurrency.
|
|
209
|
+
// Take the larger of the two so an upgraded config never loses capacity it
|
|
210
|
+
// was explicitly given; the old key is dropped on the persisted save.
|
|
211
|
+
const legacyParallelTasks = clampCount(raw.maxParallelTasks, MAX_CONCURRENCY_LIMIT);
|
|
212
|
+
if (legacyParallelTasks !== undefined && legacyParallelTasks > config.maxConcurrency) {
|
|
213
|
+
config.maxConcurrency = legacyParallelTasks;
|
|
232
214
|
}
|
|
233
215
|
|
|
234
216
|
// 0 disables the auto-fix loop (main agent handles fixes itself).
|
package/src/index.ts
CHANGED
|
@@ -167,17 +167,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
167
167
|
};
|
|
168
168
|
const completionBatcher = createCompletionBatcher<CompletionMessageItem>({ emit: sendCompletionGroup });
|
|
169
169
|
|
|
170
|
-
// Recursion guard: sub-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
? "disabled by maxSubagentDepth 0 in pi-subagents.json"
|
|
176
|
-
: "disabled in nested sub-agent processes";
|
|
170
|
+
// Recursion guard: sub-agent children are leaf processes. The `subagent` tool is
|
|
171
|
+
// excluded from their toolset at spawn (--exclude-tools); this check is defense
|
|
172
|
+
// in depth so a child can never expose the tool back to its model, even if
|
|
173
|
+
// another extension ignores the depth marker.
|
|
174
|
+
if (currentSubagentDepth() >= 1) {
|
|
177
175
|
pi.registerCommand("subagents-setup", {
|
|
178
|
-
description:
|
|
176
|
+
description: "Configure pi-subagents (unavailable in nested sub-agent processes)",
|
|
179
177
|
handler: async (_args, ctx) => {
|
|
180
|
-
ctx.ui.notify(
|
|
178
|
+
ctx.ui.notify("pi-subagents setup is unavailable in nested sub-agent processes.", "warning");
|
|
181
179
|
},
|
|
182
180
|
});
|
|
183
181
|
return;
|
|
@@ -245,22 +243,34 @@ export default function (pi: ExtensionAPI): void {
|
|
|
245
243
|
|
|
246
244
|
// Finished runs leave the widget immediately. Their final findings are sent
|
|
247
245
|
// back as a custom message that automatically starts a follow-up turn.
|
|
248
|
-
const finishRun = (
|
|
246
|
+
const finishRun = (
|
|
247
|
+
runId: number,
|
|
248
|
+
status: "done" | "failed",
|
|
249
|
+
opts?: { silent?: boolean; retain?: boolean },
|
|
250
|
+
): void => {
|
|
249
251
|
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
250
|
-
const run = monitor.removeRun(runId);
|
|
252
|
+
const run = opts?.retain ? monitor.findRun(runId) : monitor.removeRun(runId);
|
|
251
253
|
if (!run) return; // already finished — stay idempotent
|
|
252
|
-
if (!sessionActive) return;
|
|
254
|
+
if (opts?.silent || !sessionActive) return;
|
|
253
255
|
const icon = status === "done" ? "✓" : "✗";
|
|
254
256
|
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
255
257
|
};
|
|
256
258
|
|
|
257
259
|
// Live sub-agent activity → concise one-line status ("thinking",
|
|
258
|
-
// "read src/index.ts", ...), never a raw args blob.
|
|
259
|
-
|
|
260
|
+
// "read src/index.ts", ...), never a raw args blob. Reviewer runs started
|
|
261
|
+
// by the main agent defer finishing so the queue task can decide between
|
|
262
|
+
// delivering the review and starting an auto-fix chain: a triggered chain
|
|
263
|
+
// keeps the parent row in the widget (annotated) until it completes and
|
|
264
|
+
// suppresses the premature "done" notification.
|
|
265
|
+
const makeLiveHandler = (runId: number, deferFinish = false) => (e: SubagentLiveEvent): void => {
|
|
260
266
|
switch (e.kind) {
|
|
261
267
|
case "status":
|
|
262
|
-
if (e.status === "done" || e.status === "failed")
|
|
263
|
-
|
|
268
|
+
if (e.status === "done" || e.status === "failed") {
|
|
269
|
+
// Deferred runs only update the widget; the queue task finishes
|
|
270
|
+
// them once it knows whether an auto-fix chain will follow.
|
|
271
|
+
if (deferFinish) monitor.setStatus(runId, e.status);
|
|
272
|
+
else finishRun(runId, e.status);
|
|
273
|
+
} else monitor.setStatus(runId, e.status);
|
|
264
274
|
break;
|
|
265
275
|
case "usage":
|
|
266
276
|
monitor.setUsage(runId, e.usage, e.model);
|
|
@@ -388,8 +398,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
388
398
|
* findings) → reviewer re-review, up to maxFixRounds times. The main agent is
|
|
389
399
|
* not woken mid-loop; the full chain is delivered as one group at the end.
|
|
390
400
|
* Failures short-circuit: a crashed worker skips its re-review and delivers.
|
|
401
|
+
* The triggering reviewer's run stays visible in the widget (annotated) until
|
|
402
|
+
* the chain resolves, so the ↳ rows have an obvious parent.
|
|
391
403
|
*/
|
|
392
|
-
const startFixLoop = (initialReviewerResult: SingleResult, parentGroupId: string): void => {
|
|
404
|
+
const startFixLoop = (initialReviewerResult: SingleResult, parentGroupId: string, parentRunId: number): void => {
|
|
393
405
|
backgroundQueue.enqueue(
|
|
394
406
|
async (signal) => {
|
|
395
407
|
const chain: SingleResult[] = [initialReviewerResult];
|
|
@@ -410,12 +422,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
410
422
|
});
|
|
411
423
|
chain.push(reviewResult);
|
|
412
424
|
lastReviewer = reviewResult;
|
|
413
|
-
|
|
425
|
+
// A crashed re-review must stop the chain like a crashed worker: its
|
|
426
|
+
// output (if any) is not a verdict, and feeding it to the next fix
|
|
427
|
+
// round would brief the worker from garbage.
|
|
428
|
+
if (!sessionActive || isFailedResult(reviewResult)) break;
|
|
414
429
|
if (reviewVerdict(getResultOutput(reviewResult)) === "pass") break;
|
|
415
430
|
}
|
|
431
|
+
// The chain is done (success, exhaustion, or abort): drop the retained
|
|
432
|
+
// parent row, then deliver the whole chain as one group. The loop's
|
|
433
|
+
// outcome always wakes the main agent (a passing chain reports
|
|
434
|
+
// success, a stuck one needs a human).
|
|
435
|
+
monitor.removeRun(parentRunId);
|
|
416
436
|
if (!sessionActive) return;
|
|
417
|
-
// Deliver the whole chain as one group; the loop's outcome always wakes
|
|
418
|
-
// the main agent (a passing chain reports success, a stuck one needs a human).
|
|
419
437
|
const items: CompletionMessageItem[] = chain.map((r) => ({
|
|
420
438
|
agent: r.agent,
|
|
421
439
|
block: formatCompletionBlock(r, config.maxResultLines),
|
|
@@ -425,7 +443,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
425
443
|
completionBatcher.flush();
|
|
426
444
|
},
|
|
427
445
|
() => {
|
|
428
|
-
// Cancelled
|
|
446
|
+
// Cancelled before delivery: clean up the retained parent row (each
|
|
447
|
+
// in-flight chain run was already finished by its launchInLoop path).
|
|
448
|
+
monitor.removeRun(parentRunId);
|
|
429
449
|
},
|
|
430
450
|
);
|
|
431
451
|
};
|
|
@@ -438,7 +458,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
438
458
|
const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
|
|
439
459
|
const pending = queuedResult(agent, task, thinkingLevel);
|
|
440
460
|
const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel);
|
|
441
|
-
|
|
461
|
+
// Only a main-agent-dispatched reviewer can trigger an auto-fix chain, so
|
|
462
|
+
// only its finish is deferred to the queue task (see startFixLoop).
|
|
463
|
+
const onLive = makeLiveHandler(runId, agent.name === "reviewer");
|
|
442
464
|
|
|
443
465
|
backgroundQueue.enqueue(
|
|
444
466
|
async (backgroundSignal) => {
|
|
@@ -475,12 +497,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
475
497
|
// triggers a worker→reviewer chain (up to maxFixRounds) without waking
|
|
476
498
|
// the main agent. Loop-internal re-reviews never reach here (they are
|
|
477
499
|
// awaited inside launchInLoop); the initial review is delivered with
|
|
478
|
-
// the chain at the end.
|
|
500
|
+
// the chain at the end. While the chain runs, the triggering review
|
|
501
|
+
// stays in the widget (annotated) so the chain rows have an obvious
|
|
502
|
+
// parent; no premature "done" notification is shown.
|
|
479
503
|
if (shouldTriggerFixLoop(result, config)) {
|
|
480
|
-
|
|
504
|
+
// The session is known active here (checked above), so the chain
|
|
505
|
+
// always starts: keep the triggering review in the widget
|
|
506
|
+
// (annotated) without a premature "done" notification, and let
|
|
507
|
+
// startFixLoop deliver the whole chain and drop the parent row.
|
|
508
|
+
finishRun(runId, "done", { silent: true, retain: true });
|
|
509
|
+
monitor.setAnnotation(runId, "auto-fix chain running");
|
|
510
|
+
startFixLoop(result, `fix-${runId}`, runId);
|
|
481
511
|
return;
|
|
482
512
|
}
|
|
483
513
|
const failed = isFailedResult(result);
|
|
514
|
+
finishRun(runId, failed ? "failed" : "done");
|
|
515
|
+
if (!sessionActive) return;
|
|
484
516
|
const completion: CompletionMessageItem = {
|
|
485
517
|
agent: result.agent,
|
|
486
518
|
block: formatCompletionBlock(result, config.maxResultLines),
|
|
@@ -504,12 +536,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
504
536
|
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
505
537
|
// editor available immediately; completion messages later wake the main agent.
|
|
506
538
|
if (params.tasks && params.tasks.length > 0) {
|
|
507
|
-
if (params.tasks.length > config.
|
|
539
|
+
if (params.tasks.length > config.maxConcurrency) {
|
|
508
540
|
return {
|
|
509
541
|
content: [
|
|
510
542
|
{
|
|
511
543
|
type: "text",
|
|
512
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.
|
|
544
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxConcurrency} (configurable via /subagents-setup).`,
|
|
513
545
|
},
|
|
514
546
|
],
|
|
515
547
|
details: makeDetails("parallel", true)([]),
|
|
@@ -630,7 +662,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
630
662
|
// Chain-internal runs (auto-fix worker/reviewer) indent under their
|
|
631
663
|
// parent reviewer; summarize() already carries the relationLabel.
|
|
632
664
|
const head = r.groupId ? theme.fg("dim", " ↳ ") : " ";
|
|
633
|
-
|
|
665
|
+
const note = r.annotation ? theme.fg("dim", ` · ${r.annotation}`) : "";
|
|
666
|
+
lines.push(truncateToWidth(`${head}${icon} ${monitor.summarize(r)} · ${label}${note}`, width, ""));
|
|
634
667
|
if (r.status === "queued" || r.status === "running") {
|
|
635
668
|
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
|
|
636
669
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface RunView {
|
|
|
40
40
|
groupId?: string;
|
|
41
41
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
42
42
|
relationLabel?: string;
|
|
43
|
+
/** Free-form note shown in the widget next to the status label (e.g. "auto-fix chain running"). */
|
|
44
|
+
annotation?: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/** Optional chain metadata for runs spawned by an auto-fix loop. */
|
|
@@ -199,8 +201,11 @@ export class MonitorStore {
|
|
|
199
201
|
const run = this.find(id);
|
|
200
202
|
if (!run) return;
|
|
201
203
|
run.status = status;
|
|
202
|
-
if (status === "running"
|
|
203
|
-
run.startedAt = Date.now();
|
|
204
|
+
if (status === "running") {
|
|
205
|
+
if (run.startedAt === undefined) run.startedAt = Date.now();
|
|
206
|
+
// A model-fallback retry after a failed attempt restarts the clock; a
|
|
207
|
+
// stale endedAt would freeze the elapsed display at the first attempt.
|
|
208
|
+
if (run.endedAt !== undefined) run.endedAt = undefined;
|
|
204
209
|
} else if ((status === "done" || status === "failed") && run.endedAt === undefined) {
|
|
205
210
|
run.endedAt = Date.now();
|
|
206
211
|
}
|
|
@@ -222,6 +227,19 @@ export class MonitorStore {
|
|
|
222
227
|
this.notify();
|
|
223
228
|
}
|
|
224
229
|
|
|
230
|
+
/** Set a widget note on the run (e.g. that its auto-fix chain is still running). */
|
|
231
|
+
setAnnotation(id: number, text: string): void {
|
|
232
|
+
const run = this.find(id);
|
|
233
|
+
if (!run) return;
|
|
234
|
+
run.annotation = text;
|
|
235
|
+
this.notify();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Look up a run by id without removing it. */
|
|
239
|
+
findRun(id: number): RunView | undefined {
|
|
240
|
+
return this.find(id);
|
|
241
|
+
}
|
|
242
|
+
|
|
225
243
|
/** Remove a run (finished runs leave the widget). Returns the removed run. */
|
|
226
244
|
removeRun(id: number): RunView | undefined {
|
|
227
245
|
const index = this.runs.findIndex((r) => r.id === id);
|
package/src/setup.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
DEFAULT_ENABLED_AGENTS,
|
|
17
17
|
DEFAULT_MAX_CONCURRENCY,
|
|
18
18
|
DEFAULT_MAX_FIX_ROUNDS,
|
|
19
|
-
DEFAULT_MAX_PARALLEL_TASKS,
|
|
20
19
|
THINKING_LEVEL_VALUES,
|
|
21
20
|
type AgentScope,
|
|
22
21
|
type SubagentsConfig,
|
|
@@ -196,7 +195,6 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
|
|
|
196
195
|
|
|
197
196
|
/** Preset steps offered for the two numeric limits (selection-only wizard). */
|
|
198
197
|
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
199
|
-
const PARALLEL_TASK_STEPS = [2, 4, 6, 8, 12, 16, 24, 32];
|
|
200
198
|
/** Preset rounds offered for the auto-fix loop (0 disables it). */
|
|
201
199
|
const FIX_ROUNDS_STEPS = [0, 1, 2, 3, 5];
|
|
202
200
|
|
|
@@ -284,22 +282,13 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
284
282
|
|
|
285
283
|
const maxConcurrency = await pickCount(
|
|
286
284
|
ctx,
|
|
287
|
-
"Max sub-agents running at once? (extra work queues)",
|
|
285
|
+
"Max sub-agents running at once (and per parallel call)? (extra work queues)",
|
|
288
286
|
CONCURRENCY_STEPS,
|
|
289
287
|
base.maxConcurrency,
|
|
290
288
|
DEFAULT_MAX_CONCURRENCY,
|
|
291
289
|
);
|
|
292
290
|
if (maxConcurrency === undefined) return notifyCancelled(ctx);
|
|
293
291
|
|
|
294
|
-
const maxParallelTasks = await pickCount(
|
|
295
|
-
ctx,
|
|
296
|
-
"Max tasks in one parallel subagent call?",
|
|
297
|
-
PARALLEL_TASK_STEPS,
|
|
298
|
-
base.maxParallelTasks,
|
|
299
|
-
DEFAULT_MAX_PARALLEL_TASKS,
|
|
300
|
-
);
|
|
301
|
-
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
302
|
-
|
|
303
292
|
const maxFixRounds = await pickCount(
|
|
304
293
|
ctx,
|
|
305
294
|
"Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
|
|
@@ -319,8 +308,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
319
308
|
proactiveInjection: injection,
|
|
320
309
|
agentScope: scope,
|
|
321
310
|
maxConcurrency,
|
|
322
|
-
maxParallelTasks,
|
|
323
|
-
maxSubagentDepth: base.maxSubagentDepth,
|
|
324
311
|
maxFixRounds,
|
|
325
312
|
};
|
|
326
313
|
await saveConfig(next, configPath);
|
|
@@ -335,7 +322,6 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
335
322
|
"Toggle proactive injection",
|
|
336
323
|
"Change agent scope",
|
|
337
324
|
"Change max concurrent sub-agents",
|
|
338
|
-
"Change max parallel tasks",
|
|
339
325
|
"Change max fix rounds",
|
|
340
326
|
"Full re-setup",
|
|
341
327
|
]);
|
|
@@ -381,23 +367,13 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
381
367
|
} else if (choice.startsWith("Change max concurrent")) {
|
|
382
368
|
const maxConcurrency = await pickCount(
|
|
383
369
|
ctx,
|
|
384
|
-
"Max sub-agents running at once? (extra work queues)",
|
|
370
|
+
"Max sub-agents running at once (and per parallel call)? (extra work queues)",
|
|
385
371
|
CONCURRENCY_STEPS,
|
|
386
372
|
config.maxConcurrency,
|
|
387
373
|
DEFAULT_MAX_CONCURRENCY,
|
|
388
374
|
);
|
|
389
375
|
if (maxConcurrency === undefined) return notifyCancelled(ctx);
|
|
390
376
|
next.maxConcurrency = maxConcurrency;
|
|
391
|
-
} else if (choice.startsWith("Change max parallel")) {
|
|
392
|
-
const maxParallelTasks = await pickCount(
|
|
393
|
-
ctx,
|
|
394
|
-
"Max tasks in one parallel subagent call?",
|
|
395
|
-
PARALLEL_TASK_STEPS,
|
|
396
|
-
config.maxParallelTasks,
|
|
397
|
-
DEFAULT_MAX_PARALLEL_TASKS,
|
|
398
|
-
);
|
|
399
|
-
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
400
|
-
next.maxParallelTasks = maxParallelTasks;
|
|
401
377
|
} else if (choice.startsWith("Change max fix")) {
|
|
402
378
|
const maxFixRounds = await pickCount(
|
|
403
379
|
ctx,
|
package/src/spawn.ts
CHANGED
|
@@ -15,15 +15,15 @@ import { existsSync, mkdirSync, unlinkSync, rmdirSync, writeFileSync } from "nod
|
|
|
15
15
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
|
+
import { StringDecoder } from "node:string_decoder";
|
|
18
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
19
20
|
import type { Message } from "@earendil-works/pi-ai";
|
|
20
21
|
import type { AgentConfig, AgentSource } from "./agents.ts";
|
|
21
22
|
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* Limits are configurable: see maxConcurrency
|
|
25
|
-
*
|
|
26
|
-
* or pi-subagents.json).
|
|
25
|
+
* Limits are configurable: see maxConcurrency in config.ts (default 4, via
|
|
26
|
+
* /subagents-setup or pi-subagents.json).
|
|
27
27
|
*/
|
|
28
28
|
/** Default thinking level for sub-agents. pi clamps it to the resolved model's support. */
|
|
29
29
|
export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
@@ -135,7 +135,9 @@ export function writeResultArtifact(output: string, agentName: string): string {
|
|
|
135
135
|
const dir = join(tmpdir(), "pi-subagents-results");
|
|
136
136
|
mkdirSync(dir, { recursive: true });
|
|
137
137
|
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
138
|
-
|
|
138
|
+
// A random suffix keeps same-millisecond writes from clobbering each other.
|
|
139
|
+
const unique = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
140
|
+
const filePath = join(dir, `${unique}-${safeName}.md`);
|
|
139
141
|
writeFileSync(filePath, output, "utf8");
|
|
140
142
|
return filePath;
|
|
141
143
|
}
|
|
@@ -458,8 +460,13 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
458
460
|
proc.stdin?.on("error", () => undefined);
|
|
459
461
|
proc.stdin?.end(`Task: ${task}`);
|
|
460
462
|
|
|
463
|
+
// Decode stdout through a StringDecoder so multi-byte UTF-8 characters
|
|
464
|
+
// (CJK, emoji) split across chunk boundaries never produce U+FFFD
|
|
465
|
+
// replacement characters — a corrupted JSON line would drop the whole
|
|
466
|
+
// message (including a reviewer's verdict line) from parsing.
|
|
467
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
461
468
|
proc.stdout.on("data", (data) => {
|
|
462
|
-
buffer +=
|
|
469
|
+
buffer += stdoutDecoder.write(data);
|
|
463
470
|
const lines = buffer.split("\n");
|
|
464
471
|
buffer = lines.pop() || "";
|
|
465
472
|
for (const line of lines) processLine(line);
|
|
@@ -470,6 +477,9 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
470
477
|
});
|
|
471
478
|
|
|
472
479
|
proc.on("close", (code) => {
|
|
480
|
+
// Flush any bytes still held by the decoder (a trailing incomplete
|
|
481
|
+
// multi-byte sequence) before processing the final buffer.
|
|
482
|
+
buffer += stdoutDecoder.end();
|
|
473
483
|
if (buffer.trim()) processLine(buffer);
|
|
474
484
|
// A null exit code means the process was terminated by a signal and
|
|
475
485
|
// must be reported as failure, never as a false clean completion.
|