@ferris1225/pi-subagents 0.11.0 → 0.13.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 +209 -14
- package/agents/worker.md +3 -1
- package/package.json +1 -1
- package/src/config.ts +9 -27
- package/src/index.ts +43 -36
- package/src/setup.ts +2 -26
- package/src/spawn.ts +42 -3
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ agent, and keep the workflow moving without manual polling.
|
|
|
23
23
|
elapsed time; completion also produces a concise notification.
|
|
24
24
|
- **Per-agent configuration** — enable agents, pick model and thinking strength per agent,
|
|
25
25
|
tune concurrency limits, and choose discovery scope from `/subagents-setup`.
|
|
26
|
+
- **Automatic model fallback** — if an agent's model fails at the provider level before
|
|
27
|
+
producing any output, the run is retried once with the main window's current model.
|
|
28
|
+
Per-run only, never persisted: a transient provider hiccup does not silently downgrade
|
|
29
|
+
the configured model.
|
|
26
30
|
- **Leaf processes** — child agents cannot access the `subagent` tool, so delegation cannot
|
|
27
31
|
recurse.
|
|
28
32
|
|
|
@@ -44,15 +48,198 @@ The default configuration enables `explore`, `worker`, and `reviewer`.
|
|
|
44
48
|
|
|
45
49
|
## Included agents
|
|
46
50
|
|
|
47
|
-
| Agent | Default | Access | Purpose |
|
|
48
|
-
| --- | :---: | --- | --- |
|
|
49
|
-
| `explore` | Yes | Read-only | Fast codebase reconnaissance and structured findings. |
|
|
50
|
-
| `worker` | Yes | Full | Implements, fixes, refactors, and tests a self-contained task. |
|
|
51
|
-
| `reviewer` | Yes | Read-only |
|
|
51
|
+
| Agent | Default | Access | Default model | Thinking | Purpose |
|
|
52
|
+
| --- | :---: | --- | --- | --- | --- |
|
|
53
|
+
| `explore` | Yes | Read-only | `claude-haiku-4-5` | `low` | Fast codebase reconnaissance and structured findings. |
|
|
54
|
+
| `worker` | Yes | Full | `claude-sonnet-4-5` | `high` | Implements, fixes, refactors, and tests a self-contained task. |
|
|
55
|
+
| `reviewer` | Yes | Read-only | `claude-sonnet-4-5` | `high` | Adversarial quality gate: diff review (default), plus plan, proposed-solution, codebase-health, and PR/issue validation. |
|
|
52
56
|
|
|
53
57
|
Agents are Markdown files in `agents/`. Each file contains YAML frontmatter and a system
|
|
54
|
-
prompt. User and project scopes can override a built-in agent with the same name
|
|
58
|
+
prompt. User and project scopes can override a built-in agent with the same name; the
|
|
59
|
+
frontmatter defaults above are overridden by `agentModels` / `agentThinkingLevels` when set.
|
|
60
|
+
|
|
61
|
+
### Agent prompts
|
|
62
|
+
|
|
63
|
+
The prompts below mirror `agents/*.md` — the source of truth loaded at dispatch time. They are
|
|
64
|
+
the contract: each agent's role, hard constraints, and output format. Prompt drift shows up
|
|
65
|
+
here first.
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary><code>agents/explore.md</code> — reconnaissance</summary>
|
|
69
|
+
|
|
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
|
|
113
|
+
Terse and factual. Exact paths and line numbers. Compress — do not narrate your search process or pad with prose.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary><code>agents/worker.md</code> — implementation</summary>
|
|
120
|
+
|
|
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.
|
|
55
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
|
+
Root-cause fixes over patches. No unrelated churn. Honest verification — an unrun check is never a passed check.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
<details>
|
|
174
|
+
<summary><code>agents/reviewer.md</code> — quality gate</summary>
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
---
|
|
178
|
+
name: reviewer
|
|
179
|
+
description: Adversarial code reviewer and pre-commit quality gate. Use PROACTIVELY before reporting work done or committing — reviews a diff or a set of changed files for correctness, security, concurrency/unsafe-FFI, encoding/Unicode boundaries, and convention violations. Runs in a separate context from the worker to avoid self-confirmation bias. Read-only; never edits, builds, or runs tests. Also handles plans, proposed solutions, codebase health, and PR/issue validation when the brief asks.
|
|
180
|
+
tools: read, grep, find, ls, bash
|
|
181
|
+
model: claude-sonnet-4-5
|
|
182
|
+
thinking: high
|
|
183
|
+
# Model selection: ATTENTION TO DETAIL + SECURITY AWARENESS. This is the quality gate —
|
|
184
|
+
# use the strongest available reasoning model.
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
You are a senior, adversarial code reviewer. Your job is to FIND WHAT IS WRONG, not to validate. Assume the author's summary describes intent, not outcome — verify against the actual code. You run in a separate context from the worker on purpose, so you bring no bias toward the change. You have NOT got the caller's conversation history.
|
|
188
|
+
|
|
189
|
+
## Hard constraints
|
|
190
|
+
- You are READ-ONLY. Do NOT modify files, run builds, or run tests.
|
|
191
|
+
- Bash is for read-only commands only: `git diff`, `git status`, `git log`, `git show`, `grep`, `find`, `cat`.
|
|
192
|
+
- Assume tool permissions are not perfectly enforceable; keep every command strictly read-only by intent.
|
|
193
|
+
|
|
194
|
+
## Review types you handle
|
|
195
|
+
Match the type to the task brief; the hunt checklist below applies to every type.
|
|
196
|
+
|
|
197
|
+
### 1. Code diffs (default)
|
|
198
|
+
1. Run `git diff` and `git status` to see the recent changes. If a specific file set was given, read those files.
|
|
199
|
+
2. Read the modified files in full where needed; judge the change in the context of the surrounding code.
|
|
200
|
+
|
|
201
|
+
### 2. Plans
|
|
202
|
+
Validate a proposed plan for feasibility and completeness: missing steps, hidden risks, alignment with the existing architecture, and whether the scope is appropriately bounded.
|
|
203
|
+
|
|
204
|
+
### 3. Proposed solutions
|
|
205
|
+
Evaluate a suggested approach: correctness and tradeoffs, fit with existing codebase patterns, simpler alternatives, edge cases the proposal may miss.
|
|
206
|
+
|
|
207
|
+
### 4. Codebase health
|
|
208
|
+
Assess key files, tests, and structure: architecture drift or tech debt, inconsistent patterns, untested or undocumented areas, obvious bugs, fragile code.
|
|
209
|
+
|
|
210
|
+
### 5. Specific PR or issue
|
|
211
|
+
Understand the context first, then verify: the fix addresses the root cause, changes are minimal and focused, no regressions, tests and docs updated as needed.
|
|
212
|
+
|
|
213
|
+
## Hunt across these categories
|
|
214
|
+
- Logic bugs, off-by-one, wrong edge-case handling.
|
|
215
|
+
- Error handling gaps; swallowed failures; unreported unrun checks.
|
|
216
|
+
- Security: injection, path traversal, secrets in code/logs, trusting untrusted input.
|
|
217
|
+
- Concurrency: shared mutable state, locks held across await, races.
|
|
218
|
+
- Encoding/Unicode: assuming `char*`/files/CLI text is UTF-8; wrong `A` vs `W` Win32 APIs; boundary conversions.
|
|
219
|
+
- Resource leaks; violations of the project's stated conventions.
|
|
220
|
+
- Classify severity honestly. Distinguish blockers from nits; do not pad with style preferences.
|
|
221
|
+
|
|
222
|
+
## Collaboration
|
|
223
|
+
- Independent of `worker` by design — your verdict is the gate before commit. Fix nothing yourself; report so the caller can dispatch a worker.
|
|
224
|
+
|
|
225
|
+
## Output format
|
|
226
|
+
## Files Reviewed
|
|
227
|
+
- `path/to/file.ts`
|
|
228
|
+
## Critical (must fix)
|
|
229
|
+
- `file.ts:42` — concrete issue and why it breaks.
|
|
230
|
+
## Warnings (should fix)
|
|
231
|
+
- `file.ts:10` — issue and suggested direction.
|
|
232
|
+
## Suggestions (consider)
|
|
233
|
+
- Optional improvements.
|
|
234
|
+
## Verdict
|
|
235
|
+
One of: APPROVE / APPROVE_WITH_NITS / REQUEST_CHANGES, plus a 2-3 sentence rationale.
|
|
236
|
+
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE or APPROVE_WITH_NITS; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
|
|
237
|
+
|
|
238
|
+
## Quality standards
|
|
239
|
+
Specific file paths and line numbers. No vague feedback. A clean report means you looked hard, not that you found nothing to say.
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
</details>
|
|
56
243
|
## Workflow
|
|
57
244
|
|
|
58
245
|
A typical flow is:
|
|
@@ -70,9 +257,9 @@ main agent
|
|
|
70
257
|
1. The main agent calls `subagent` with a self-contained brief.
|
|
71
258
|
2. The tool returns immediately and ends that foreground tool turn, leaving the editor ready
|
|
72
259
|
for input.
|
|
73
|
-
3. The child process works independently. By default up to four
|
|
74
|
-
|
|
75
|
-
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`).
|
|
76
263
|
4. On completion or failure, the extension sends a durable result message to the main
|
|
77
264
|
session. That message automatically wakes the main agent, or waits until its current turn
|
|
78
265
|
finishes.
|
|
@@ -144,8 +331,6 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
|
|
|
144
331
|
"proactiveInjection": true,
|
|
145
332
|
"agentScope": "user",
|
|
146
333
|
"maxConcurrency": 4,
|
|
147
|
-
"maxParallelTasks": 8,
|
|
148
|
-
"maxSubagentDepth": 1,
|
|
149
334
|
"maxFixRounds": 2
|
|
150
335
|
}
|
|
151
336
|
```
|
|
@@ -160,9 +345,7 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
|
|
|
160
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. |
|
|
161
346
|
| `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
|
|
162
347
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
163
|
-
| `maxConcurrency` |
|
|
164
|
-
| `maxParallelTasks` | Maximum tasks accepted by one parallel `subagent` call (1–32, default 8). |
|
|
165
|
-
| `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. |
|
|
166
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. |
|
|
167
350
|
|
|
168
351
|
### Configuration migration
|
|
@@ -173,6 +356,11 @@ The config file migrates itself on load — no manual steps after an upgrade:
|
|
|
173
356
|
holding invalid values) is normalized and saved back with the new fields filled in.
|
|
174
357
|
- **Removed agents** — agents no longer shipped (e.g. the old `plan` agent) are stripped
|
|
175
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": []`.
|
|
176
364
|
|
|
177
365
|
Model selection uses this precedence:
|
|
178
366
|
|
|
@@ -183,6 +371,13 @@ configured agent model → current main-session model → agent frontmatter mode
|
|
|
183
371
|
Unavailable configured models are replaced with a usable current-session model when possible
|
|
184
372
|
and the repaired configuration is saved.
|
|
185
373
|
|
|
374
|
+
At runtime, if an agent's model fails at the provider level before producing any output (bad
|
|
375
|
+
model id, auth, thinking level, quota, ...), the run is retried **once** with the main window's
|
|
376
|
+
current model. This per-run degradation is never persisted — a transient provider hiccup must
|
|
377
|
+
not silently downgrade the configured model — and it does not apply to task-level failures
|
|
378
|
+
(the model worked, the task failed), aborts, or timeouts. Results carry a `model fell back
|
|
379
|
+
from …` note when it happened.
|
|
380
|
+
|
|
186
381
|
Thinking strength uses this precedence: `agentThinkingLevels` entry → agent frontmatter `thinking` → `thinkingLevel` default.
|
|
187
382
|
|
|
188
383
|
## Agent discovery and overrides
|
package/agents/worker.md
CHANGED
|
@@ -28,7 +28,9 @@ Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the t
|
|
|
28
28
|
Summarize concretely so the caller can verify and, if needed, hand to a `reviewer`.
|
|
29
29
|
|
|
30
30
|
## Collaboration
|
|
31
|
-
-
|
|
31
|
+
- You cannot dispatch sub-agents (children are leaf processes with no `subagent` tool). When the
|
|
32
|
+
brief lacks context that needs broad code discovery, state concretely what an `explore` should
|
|
33
|
+
retrieve for the caller — do not guess.
|
|
32
34
|
- Recommend a `reviewer` pass before the caller reports work done or commits, especially for non-trivial diffs.
|
|
33
35
|
|
|
34
36
|
## Output format
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
getResultOutput,
|
|
36
36
|
isFailedResult,
|
|
37
37
|
reviewVerdict,
|
|
38
|
-
|
|
38
|
+
runSingleAgentWithModelFallback,
|
|
39
39
|
truncateResultOutput,
|
|
40
40
|
writeResultArtifact,
|
|
41
41
|
type SingleResult,
|
|
@@ -131,7 +131,10 @@ function formatCompletionBlock(result: SingleResult, maxResultLines: number): st
|
|
|
131
131
|
const usage = formatUsage(result.usage);
|
|
132
132
|
const output = getResultOutput(result);
|
|
133
133
|
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
134
|
-
const
|
|
134
|
+
const fallbackNote = result.modelFallbackFrom
|
|
135
|
+
? ` (model fell back from ${result.modelFallbackFrom} to ${result.model ?? "main-window model"})`
|
|
136
|
+
: "";
|
|
137
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}`, "", `Task: ${formatTaskSummary(result.task)}`, "", text];
|
|
135
138
|
if (truncated) {
|
|
136
139
|
// The full text lives on disk so the main agent can read it on demand.
|
|
137
140
|
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent)})`);
|
|
@@ -164,17 +167,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
167
|
};
|
|
165
168
|
const completionBatcher = createCompletionBatcher<CompletionMessageItem>({ emit: sendCompletionGroup });
|
|
166
169
|
|
|
167
|
-
// Recursion guard: sub-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
? "disabled by maxSubagentDepth 0 in pi-subagents.json"
|
|
173
|
-
: "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) {
|
|
174
175
|
pi.registerCommand("subagents-setup", {
|
|
175
|
-
description:
|
|
176
|
+
description: "Configure pi-subagents (unavailable in nested sub-agent processes)",
|
|
176
177
|
handler: async (_args, ctx) => {
|
|
177
|
-
ctx.ui.notify(
|
|
178
|
+
ctx.ui.notify("pi-subagents setup is unavailable in nested sub-agent processes.", "warning");
|
|
178
179
|
},
|
|
179
180
|
});
|
|
180
181
|
return;
|
|
@@ -352,16 +353,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
352
353
|
const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel, meta);
|
|
353
354
|
const onLive = makeLiveHandler(runId);
|
|
354
355
|
try {
|
|
355
|
-
const result = await
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
356
|
+
const result = await runSingleAgentWithModelFallback(
|
|
357
|
+
{
|
|
358
|
+
defaultCwd: ctx.cwd,
|
|
359
|
+
agent,
|
|
360
|
+
agentName,
|
|
361
|
+
task,
|
|
362
|
+
thinkingLevel,
|
|
363
|
+
signal,
|
|
364
|
+
onLive,
|
|
365
|
+
makeDetails: makeDetails("single", true),
|
|
366
|
+
},
|
|
367
|
+
sessionRef,
|
|
368
|
+
);
|
|
365
369
|
finishRun(runId, isFailedResult(result) ? "failed" : "done");
|
|
366
370
|
return result;
|
|
367
371
|
} catch (error) {
|
|
@@ -438,17 +442,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
438
442
|
async (backgroundSignal) => {
|
|
439
443
|
let result: SingleResult;
|
|
440
444
|
try {
|
|
441
|
-
result = await
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
445
|
+
result = await runSingleAgentWithModelFallback(
|
|
446
|
+
{
|
|
447
|
+
defaultCwd: ctx.cwd,
|
|
448
|
+
agent,
|
|
449
|
+
agentName,
|
|
450
|
+
task,
|
|
451
|
+
cwd,
|
|
452
|
+
thinkingLevel,
|
|
453
|
+
signal: backgroundSignal,
|
|
454
|
+
onLive,
|
|
455
|
+
makeDetails: makeDetails("single", true),
|
|
456
|
+
},
|
|
457
|
+
sessionRef,
|
|
458
|
+
);
|
|
452
459
|
} catch (error) {
|
|
453
460
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
454
461
|
result = {
|
|
@@ -495,12 +502,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
495
502
|
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
496
503
|
// editor available immediately; completion messages later wake the main agent.
|
|
497
504
|
if (params.tasks && params.tasks.length > 0) {
|
|
498
|
-
if (params.tasks.length > config.
|
|
505
|
+
if (params.tasks.length > config.maxConcurrency) {
|
|
499
506
|
return {
|
|
500
507
|
content: [
|
|
501
508
|
{
|
|
502
509
|
type: "text",
|
|
503
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.
|
|
510
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxConcurrency} (configurable via /subagents-setup).`,
|
|
504
511
|
},
|
|
505
512
|
],
|
|
506
513
|
details: makeDetails("parallel", true)([]),
|
|
@@ -570,7 +577,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
570
577
|
const pending = r.exitCode === -1;
|
|
571
578
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
572
579
|
const usage = formatUsage(r.usage);
|
|
573
|
-
const model = r.model ?? "?"
|
|
580
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (fell back from ${r.modelFallbackFrom})` : ""}`;
|
|
574
581
|
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
575
582
|
return new Text(line, 0, 0);
|
|
576
583
|
}
|
|
@@ -583,7 +590,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
583
590
|
const pending = r.exitCode === -1;
|
|
584
591
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
585
592
|
const usage = formatUsage(r.usage);
|
|
586
|
-
const model = r.model ?? "?"
|
|
593
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (fell back from ${r.modelFallbackFrom})` : ""}`;
|
|
587
594
|
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
588
595
|
}
|
|
589
596
|
return new Text(lines.join("\n"), 0, 0);
|
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
|
@@ -21,9 +21,8 @@ import type { AgentConfig, AgentSource } from "./agents.ts";
|
|
|
21
21
|
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Limits are configurable: see maxConcurrency
|
|
25
|
-
*
|
|
26
|
-
* or pi-subagents.json).
|
|
24
|
+
* Limits are configurable: see maxConcurrency in config.ts (default 4, via
|
|
25
|
+
* /subagents-setup or pi-subagents.json).
|
|
27
26
|
*/
|
|
28
27
|
/** Default thinking level for sub-agents. pi clamps it to the resolved model's support. */
|
|
29
28
|
export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
@@ -55,6 +54,8 @@ export interface SingleResult {
|
|
|
55
54
|
thinking?: string;
|
|
56
55
|
stopReason?: string;
|
|
57
56
|
errorMessage?: string;
|
|
57
|
+
/** Model the run degraded from: set when a failed run was retried with the main-window model. */
|
|
58
|
+
modelFallbackFrom?: string;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export interface SubagentDetails {
|
|
@@ -142,6 +143,23 @@ export function isFailedResult(result: SingleResult): boolean {
|
|
|
142
143
|
return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* True when a failed run never got usable output from its model: the provider
|
|
148
|
+
* rejected the call before the model produced any text (bad model id, auth,
|
|
149
|
+
* thinking level, quota, ...). Task-level failures — the model worked and the
|
|
150
|
+
* task failed — and aborts/timeouts are NOT model-level and must not degrade.
|
|
151
|
+
*/
|
|
152
|
+
export function isModelLevelFailure(result: SingleResult): boolean {
|
|
153
|
+
if (!isFailedResult(result)) return false;
|
|
154
|
+
if (result.stopReason === "aborted") return false;
|
|
155
|
+
// The model produced text: the failure belongs to the task, not the model.
|
|
156
|
+
if (getFinalOutput(result.messages)) return false;
|
|
157
|
+
if (result.errorMessage?.includes("timed out")) return false;
|
|
158
|
+
// Require evidence the failure came from the model/provider (an error
|
|
159
|
+
// message or stderr), not from the child process failing to start.
|
|
160
|
+
return result.messages.length > 0 || result.stderr.trim().length > 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
145
163
|
export function getResultOutput(result: SingleResult): string {
|
|
146
164
|
if (isFailedResult(result)) {
|
|
147
165
|
return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
@@ -525,3 +543,24 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
525
543
|
}
|
|
526
544
|
}
|
|
527
545
|
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Run one agent; when the configured model fails at the provider level before
|
|
549
|
+
* producing any output (see isModelLevelFailure), retry once with the main
|
|
550
|
+
* window's current model. The retried result is returned with `modelFallbackFrom`
|
|
551
|
+
* set so callers can surface the degradation. The fallback is per-run only and
|
|
552
|
+
* never persisted: a transient provider hiccup must not silently downgrade the
|
|
553
|
+
* configured agent model.
|
|
554
|
+
*/
|
|
555
|
+
export async function runSingleAgentWithModelFallback(
|
|
556
|
+
options: RunSingleOptions,
|
|
557
|
+
fallbackModelRef?: string,
|
|
558
|
+
): Promise<SingleResult> {
|
|
559
|
+
const result = await runSingleAgent(options);
|
|
560
|
+
const agent = options.agent;
|
|
561
|
+
const launchedRef = agent?.model;
|
|
562
|
+
if (!agent || !launchedRef || !fallbackModelRef || launchedRef === fallbackModelRef) return result;
|
|
563
|
+
if (!isModelLevelFailure(result)) return result;
|
|
564
|
+
const retried = await runSingleAgent({ ...options, agent: { ...agent, model: fallbackModelRef } });
|
|
565
|
+
return { ...retried, modelFallbackFrom: launchedRef };
|
|
566
|
+
}
|