@ferris1225/pi-subagents 0.10.0 → 0.12.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 +203 -7
- package/agents/worker.md +3 -1
- package/package.json +1 -1
- package/src/config.ts +23 -0
- package/src/fixloop.ts +76 -0
- package/src/index.ts +129 -17
- package/src/monitor.ts +14 -1
- package/src/setup.ts +26 -2
- package/src/spawn.ts +40 -0
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.
|
|
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
|
+
```
|
|
55
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:
|
|
@@ -145,7 +332,8 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
|
|
|
145
332
|
"agentScope": "user",
|
|
146
333
|
"maxConcurrency": 4,
|
|
147
334
|
"maxParallelTasks": 8,
|
|
148
|
-
"maxSubagentDepth": 1
|
|
335
|
+
"maxSubagentDepth": 1,
|
|
336
|
+
"maxFixRounds": 2
|
|
149
337
|
}
|
|
150
338
|
```
|
|
151
339
|
|
|
@@ -162,6 +350,7 @@ agent's default — its frontmatter `thinking`, else the global default). The gl
|
|
|
162
350
|
| `maxConcurrency` | How many sub-agent processes run at once (1–16, default 4). Extra work waits in the queue. |
|
|
163
351
|
| `maxParallelTasks` | Maximum tasks accepted by one parallel `subagent` call (1–32, default 8). |
|
|
164
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. |
|
|
353
|
+
| `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. |
|
|
165
354
|
|
|
166
355
|
### Configuration migration
|
|
167
356
|
|
|
@@ -181,6 +370,13 @@ configured agent model → current main-session model → agent frontmatter mode
|
|
|
181
370
|
Unavailable configured models are replaced with a usable current-session model when possible
|
|
182
371
|
and the repaired configuration is saved.
|
|
183
372
|
|
|
373
|
+
At runtime, if an agent's model fails at the provider level before producing any output (bad
|
|
374
|
+
model id, auth, thinking level, quota, ...), the run is retried **once** with the main window's
|
|
375
|
+
current model. This per-run degradation is never persisted — a transient provider hiccup must
|
|
376
|
+
not silently downgrade the configured model — and it does not apply to task-level failures
|
|
377
|
+
(the model worked, the task failed), aborts, or timeouts. Results carry a `model fell back
|
|
378
|
+
from …` note when it happened.
|
|
379
|
+
|
|
184
380
|
Thinking strength uses this precedence: `agentThinkingLevels` entry → agent frontmatter `thinking` → `thinkingLevel` default.
|
|
185
381
|
|
|
186
382
|
## 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.12.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
|
@@ -60,6 +60,14 @@ export const MAX_PARALLEL_TASKS_LIMIT = 32;
|
|
|
60
60
|
export const DEFAULT_MAX_SUBAGENT_DEPTH = 1;
|
|
61
61
|
/** Upper bound accepted for maxSubagentDepth (defensive clamp). */
|
|
62
62
|
export const MAX_SUBAGENT_DEPTH_LIMIT = 4;
|
|
63
|
+
/**
|
|
64
|
+
* How many automatic worker→reviewer fix rounds run when a reviewer returns
|
|
65
|
+
* REVIEW_FAIL before waking the main agent. 0 disables the auto-fix loop
|
|
66
|
+
* (the main agent is woken to dispatch fixes itself). Default: 2.
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_MAX_FIX_ROUNDS = 2;
|
|
69
|
+
/** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
|
|
70
|
+
export const MAX_FIX_ROUNDS_LIMIT = 5;
|
|
63
71
|
|
|
64
72
|
export interface SubagentsConfig {
|
|
65
73
|
/** Agent names that are discoverable and injected. Default: explore, worker, reviewer. */
|
|
@@ -91,6 +99,14 @@ export interface SubagentsConfig {
|
|
|
91
99
|
maxParallelTasks: number;
|
|
92
100
|
/** Depth at which the subagent tool is no longer registered. Default: 1. */
|
|
93
101
|
maxSubagentDepth: number;
|
|
102
|
+
/**
|
|
103
|
+
* Auto-fix rounds when a reviewer returns REVIEW_FAIL: the extension dispatches
|
|
104
|
+
* a worker (briefed with the review's concrete findings) then a reviewer
|
|
105
|
+
* re-review, repeating up to this many times before waking the main agent with
|
|
106
|
+
* the full chain. 0 disables it (the main agent handles fixes itself).
|
|
107
|
+
* Default: 2.
|
|
108
|
+
*/
|
|
109
|
+
maxFixRounds: number;
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
@@ -105,6 +121,7 @@ export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
|
105
121
|
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
|
|
106
122
|
maxParallelTasks: DEFAULT_MAX_PARALLEL_TASKS,
|
|
107
123
|
maxSubagentDepth: DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
124
|
+
maxFixRounds: DEFAULT_MAX_FIX_ROUNDS,
|
|
108
125
|
};
|
|
109
126
|
|
|
110
127
|
export function getConfigPath(agentDir: string = getAgentDir()): string {
|
|
@@ -151,6 +168,7 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
151
168
|
maxConcurrency: DEFAULT_CONFIG.maxConcurrency,
|
|
152
169
|
maxParallelTasks: DEFAULT_CONFIG.maxParallelTasks,
|
|
153
170
|
maxSubagentDepth: DEFAULT_CONFIG.maxSubagentDepth,
|
|
171
|
+
maxFixRounds: DEFAULT_CONFIG.maxFixRounds,
|
|
154
172
|
};
|
|
155
173
|
|
|
156
174
|
if (Array.isArray(raw.enabledAgents)) {
|
|
@@ -213,6 +231,11 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
213
231
|
config.maxSubagentDepth = Math.max(0, Math.min(MAX_SUBAGENT_DEPTH_LIMIT, Math.round(raw.maxSubagentDepth)));
|
|
214
232
|
}
|
|
215
233
|
|
|
234
|
+
// 0 disables the auto-fix loop (main agent handles fixes itself).
|
|
235
|
+
if (typeof raw.maxFixRounds === "number" && Number.isFinite(raw.maxFixRounds)) {
|
|
236
|
+
config.maxFixRounds = Math.max(0, Math.min(MAX_FIX_ROUNDS_LIMIT, Math.round(raw.maxFixRounds)));
|
|
237
|
+
}
|
|
238
|
+
|
|
216
239
|
return config;
|
|
217
240
|
}
|
|
218
241
|
|
package/src/fixloop.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-fix loop: when a reviewer returns REVIEW_FAIL, the extension dispatches a
|
|
3
|
+
* worker (briefed with the review's concrete findings) and then a reviewer
|
|
4
|
+
* re-review, repeating up to maxFixRounds times before waking the main agent with
|
|
5
|
+
* the full chain. The reviewer stays read-only and in its own context; the loop
|
|
6
|
+
* is orchestrated by the extension layer, not by the reviewer itself, so the
|
|
7
|
+
* independence guarantee (no self-confirmation bias) is preserved.
|
|
8
|
+
*
|
|
9
|
+
* The main agent is never woken mid-loop: the reviewer's FAIL result is intercepted
|
|
10
|
+
* before delivery, the chain runs in the background, and only the final group
|
|
11
|
+
* (initial review → worker fixes → re-reviews) is delivered at the end.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
15
|
+
import type { SubagentsConfig } from "./config.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a completed result should trigger the auto-fix loop instead of being
|
|
19
|
+
* delivered to the main agent. Only a REVIEW_FAIL verdict from a healthy
|
|
20
|
+
* reviewer run counts; failed processes and passing reviews are delivered
|
|
21
|
+
* normally. Loop-internal re-review results never reach this path (they are
|
|
22
|
+
* awaited inside the loop, not delivered through the completion flow).
|
|
23
|
+
*/
|
|
24
|
+
export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConfig): boolean {
|
|
25
|
+
if (config.maxFixRounds <= 0) return false;
|
|
26
|
+
if (result.agent !== "reviewer") return false;
|
|
27
|
+
if (isFailedResult(result)) return false;
|
|
28
|
+
return reviewVerdict(getResultOutput(result)) === "fail";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the worker task brief for one fix round from a reviewer's findings.
|
|
33
|
+
* The worker gets the full review text so it can address concrete file:line
|
|
34
|
+
* issues, with instructions to fix only blockers and self-verify.
|
|
35
|
+
*/
|
|
36
|
+
export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
|
|
37
|
+
const review = getResultOutput(reviewerResult);
|
|
38
|
+
const remaining = maxRounds - round;
|
|
39
|
+
return [
|
|
40
|
+
`Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
|
|
41
|
+
``,
|
|
42
|
+
`A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
|
|
43
|
+
`---`,
|
|
44
|
+
review,
|
|
45
|
+
`---`,
|
|
46
|
+
``,
|
|
47
|
+
`Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
|
|
48
|
+
`Address every "Critical" item; address "Warnings" only if they are genuine.`,
|
|
49
|
+
`After editing, run the project's format/build/tests when they exist and report`,
|
|
50
|
+
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
51
|
+
remaining > 0
|
|
52
|
+
? `A reviewer will re-review your changes automatically after you finish.`
|
|
53
|
+
: `This is the last auto-fix round; the main agent will be woken with the full chain.`,
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
59
|
+
* the prior review so the reviewer can verify the fixes without re-discovering
|
|
60
|
+
* the original issues.
|
|
61
|
+
*/
|
|
62
|
+
export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
|
|
63
|
+
const review = getResultOutput(reviewerResult);
|
|
64
|
+
return [
|
|
65
|
+
`Re-review after auto-fix round ${round}.`,
|
|
66
|
+
``,
|
|
67
|
+
`The previous review (REQUEST_CHANGES) found these issues:`,
|
|
68
|
+
`---`,
|
|
69
|
+
review,
|
|
70
|
+
`---`,
|
|
71
|
+
``,
|
|
72
|
+
`Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
|
|
73
|
+
`Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
|
|
74
|
+
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
getFinalOutput,
|
|
35
35
|
getResultOutput,
|
|
36
36
|
isFailedResult,
|
|
37
|
-
|
|
37
|
+
reviewVerdict,
|
|
38
|
+
runSingleAgentWithModelFallback,
|
|
38
39
|
truncateResultOutput,
|
|
39
40
|
writeResultArtifact,
|
|
40
41
|
type SingleResult,
|
|
@@ -42,7 +43,8 @@ import {
|
|
|
42
43
|
type SubagentLiveEvent,
|
|
43
44
|
type UsageStats,
|
|
44
45
|
} from "./spawn.ts";
|
|
45
|
-
import {
|
|
46
|
+
import { buildFixTaskBrief, buildReReviewBrief, shouldTriggerFixLoop } from "./fixloop.ts";
|
|
47
|
+
import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel, type RunChainMeta } from "./monitor.ts";
|
|
46
48
|
|
|
47
49
|
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
48
50
|
|
|
@@ -129,7 +131,10 @@ function formatCompletionBlock(result: SingleResult, maxResultLines: number): st
|
|
|
129
131
|
const usage = formatUsage(result.usage);
|
|
130
132
|
const output = getResultOutput(result);
|
|
131
133
|
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
132
|
-
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];
|
|
133
138
|
if (truncated) {
|
|
134
139
|
// The full text lives on disk so the main agent can read it on demand.
|
|
135
140
|
lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent)})`);
|
|
@@ -333,6 +338,98 @@ export default function (pi: ExtensionAPI): void {
|
|
|
333
338
|
};
|
|
334
339
|
}
|
|
335
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Dispatch one agent inside an auto-fix chain: tracked in the widget with a
|
|
343
|
+
* groupId/relationLabel, but NOT delivered through the completion flow — the
|
|
344
|
+
* chain owner assembles and delivers the whole group at the end.
|
|
345
|
+
*/
|
|
346
|
+
const launchInLoop = async (
|
|
347
|
+
agentName: string,
|
|
348
|
+
task: string,
|
|
349
|
+
signal: AbortSignal,
|
|
350
|
+
meta: RunChainMeta,
|
|
351
|
+
): Promise<SingleResult> => {
|
|
352
|
+
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
353
|
+
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
354
|
+
const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
|
|
355
|
+
const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel, meta);
|
|
356
|
+
const onLive = makeLiveHandler(runId);
|
|
357
|
+
try {
|
|
358
|
+
const result = await runSingleAgentWithModelFallback(
|
|
359
|
+
{
|
|
360
|
+
defaultCwd: ctx.cwd,
|
|
361
|
+
agent,
|
|
362
|
+
agentName,
|
|
363
|
+
task,
|
|
364
|
+
thinkingLevel,
|
|
365
|
+
signal,
|
|
366
|
+
onLive,
|
|
367
|
+
makeDetails: makeDetails("single", true),
|
|
368
|
+
},
|
|
369
|
+
sessionRef,
|
|
370
|
+
);
|
|
371
|
+
finishRun(runId, isFailedResult(result) ? "failed" : "done");
|
|
372
|
+
return result;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
finishRun(runId, "failed");
|
|
375
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
376
|
+
return {
|
|
377
|
+
...queuedResult(agent, task, thinkingLevel),
|
|
378
|
+
exitCode: 1,
|
|
379
|
+
stderr: errorMessage,
|
|
380
|
+
stopReason: signal.aborted ? "aborted" : "error",
|
|
381
|
+
errorMessage,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Run the auto-fix chain in the background: worker (briefed with the review's
|
|
388
|
+
* findings) → reviewer re-review, up to maxFixRounds times. The main agent is
|
|
389
|
+
* not woken mid-loop; the full chain is delivered as one group at the end.
|
|
390
|
+
* Failures short-circuit: a crashed worker skips its re-review and delivers.
|
|
391
|
+
*/
|
|
392
|
+
const startFixLoop = (initialReviewerResult: SingleResult, parentGroupId: string): void => {
|
|
393
|
+
backgroundQueue.enqueue(
|
|
394
|
+
async (signal) => {
|
|
395
|
+
const chain: SingleResult[] = [initialReviewerResult];
|
|
396
|
+
let lastReviewer = initialReviewerResult;
|
|
397
|
+
for (let round = 1; round <= config.maxFixRounds; round++) {
|
|
398
|
+
if (!sessionActive) break;
|
|
399
|
+
const fixBrief = buildFixTaskBrief(lastReviewer, round, config.maxFixRounds);
|
|
400
|
+
const workerResult = await launchInLoop("worker", fixBrief, signal, {
|
|
401
|
+
groupId: parentGroupId,
|
|
402
|
+
relationLabel: `fix round ${round}`,
|
|
403
|
+
});
|
|
404
|
+
chain.push(workerResult);
|
|
405
|
+
if (!sessionActive || isFailedResult(workerResult)) break;
|
|
406
|
+
const reReviewBrief = buildReReviewBrief(lastReviewer, round);
|
|
407
|
+
const reviewResult = await launchInLoop("reviewer", reReviewBrief, signal, {
|
|
408
|
+
groupId: parentGroupId,
|
|
409
|
+
relationLabel: `re-review round ${round}`,
|
|
410
|
+
});
|
|
411
|
+
chain.push(reviewResult);
|
|
412
|
+
lastReviewer = reviewResult;
|
|
413
|
+
if (!sessionActive) break;
|
|
414
|
+
if (reviewVerdict(getResultOutput(reviewResult)) === "pass") break;
|
|
415
|
+
}
|
|
416
|
+
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
|
+
const items: CompletionMessageItem[] = chain.map((r) => ({
|
|
420
|
+
agent: r.agent,
|
|
421
|
+
block: formatCompletionBlock(r, config.maxResultLines),
|
|
422
|
+
triggerTurn: true,
|
|
423
|
+
}));
|
|
424
|
+
sendCompletionGroup(items);
|
|
425
|
+
completionBatcher.flush();
|
|
426
|
+
},
|
|
427
|
+
() => {
|
|
428
|
+
// Cancelled: each in-flight run was already finished by its launchInLoop path.
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
};
|
|
432
|
+
|
|
336
433
|
const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
|
|
337
434
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
338
435
|
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
@@ -347,17 +444,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
347
444
|
async (backgroundSignal) => {
|
|
348
445
|
let result: SingleResult;
|
|
349
446
|
try {
|
|
350
|
-
result = await
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
447
|
+
result = await runSingleAgentWithModelFallback(
|
|
448
|
+
{
|
|
449
|
+
defaultCwd: ctx.cwd,
|
|
450
|
+
agent,
|
|
451
|
+
agentName,
|
|
452
|
+
task,
|
|
453
|
+
cwd,
|
|
454
|
+
thinkingLevel,
|
|
455
|
+
signal: backgroundSignal,
|
|
456
|
+
onLive,
|
|
457
|
+
makeDetails: makeDetails("single", true),
|
|
458
|
+
},
|
|
459
|
+
sessionRef,
|
|
460
|
+
);
|
|
361
461
|
} catch (error) {
|
|
362
462
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
363
463
|
result = {
|
|
@@ -371,6 +471,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
371
471
|
}
|
|
372
472
|
|
|
373
473
|
if (!sessionActive) return;
|
|
474
|
+
// Auto-fix loop: a REVIEW_FAIL from a main-agent-dispatched reviewer
|
|
475
|
+
// triggers a worker→reviewer chain (up to maxFixRounds) without waking
|
|
476
|
+
// the main agent. Loop-internal re-reviews never reach here (they are
|
|
477
|
+
// awaited inside launchInLoop); the initial review is delivered with
|
|
478
|
+
// the chain at the end.
|
|
479
|
+
if (shouldTriggerFixLoop(result, config)) {
|
|
480
|
+
startFixLoop(result, `fix-${runId}`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
374
483
|
const failed = isFailedResult(result);
|
|
375
484
|
const completion: CompletionMessageItem = {
|
|
376
485
|
agent: result.agent,
|
|
@@ -470,7 +579,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
470
579
|
const pending = r.exitCode === -1;
|
|
471
580
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
472
581
|
const usage = formatUsage(r.usage);
|
|
473
|
-
const model = r.model ?? "?"
|
|
582
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (fell back from ${r.modelFallbackFrom})` : ""}`;
|
|
474
583
|
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}` : ""}`)}`;
|
|
475
584
|
return new Text(line, 0, 0);
|
|
476
585
|
}
|
|
@@ -483,7 +592,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
483
592
|
const pending = r.exitCode === -1;
|
|
484
593
|
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
485
594
|
const usage = formatUsage(r.usage);
|
|
486
|
-
const model = r.model ?? "?"
|
|
595
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (fell back from ${r.modelFallbackFrom})` : ""}`;
|
|
487
596
|
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
488
597
|
}
|
|
489
598
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -518,7 +627,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
518
627
|
for (const r of runs) {
|
|
519
628
|
const icon = statusIcon(r.status, theme);
|
|
520
629
|
const label = theme.fg(statusColor(r.status), statusLabel(r.status));
|
|
521
|
-
|
|
630
|
+
// Chain-internal runs (auto-fix worker/reviewer) indent under their
|
|
631
|
+
// parent reviewer; summarize() already carries the relationLabel.
|
|
632
|
+
const head = r.groupId ? theme.fg("dim", " ↳ ") : " ";
|
|
633
|
+
lines.push(truncateToWidth(`${head}${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
|
|
522
634
|
if (r.status === "queued" || r.status === "running") {
|
|
523
635
|
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
|
|
524
636
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -36,6 +36,16 @@ export interface RunView {
|
|
|
36
36
|
startedAt?: number;
|
|
37
37
|
/** Epoch ms when the run finished (set on "done"/"failed"). */
|
|
38
38
|
endedAt?: number;
|
|
39
|
+
/** When set, this run belongs to an auto-fix chain (e.g. worker fixing a reviewer's findings). */
|
|
40
|
+
groupId?: string;
|
|
41
|
+
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
42
|
+
relationLabel?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Optional chain metadata for runs spawned by an auto-fix loop. */
|
|
46
|
+
export interface RunChainMeta {
|
|
47
|
+
groupId?: string;
|
|
48
|
+
relationLabel?: string;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
// ---------------------------------------------------------------------------
|
|
@@ -168,7 +178,7 @@ export class MonitorStore {
|
|
|
168
178
|
this.notify();
|
|
169
179
|
}
|
|
170
180
|
|
|
171
|
-
addRun(agent: string, task: string, model?: string, thinking?: string): number {
|
|
181
|
+
addRun(agent: string, task: string, model?: string, thinking?: string, meta?: RunChainMeta): number {
|
|
172
182
|
const id = this.nextId++;
|
|
173
183
|
this.runs.push({
|
|
174
184
|
id,
|
|
@@ -178,6 +188,8 @@ export class MonitorStore {
|
|
|
178
188
|
thinking,
|
|
179
189
|
status: "queued",
|
|
180
190
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
191
|
+
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
192
|
+
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
181
193
|
});
|
|
182
194
|
this.notify();
|
|
183
195
|
return id;
|
|
@@ -233,6 +245,7 @@ export class MonitorStore {
|
|
|
233
245
|
summarize(run: RunView): string {
|
|
234
246
|
const usage = formatUsageCompact(run.usage);
|
|
235
247
|
const parts = [run.agent];
|
|
248
|
+
if (run.relationLabel) parts.push(run.relationLabel);
|
|
236
249
|
if (run.model) parts.push(run.model);
|
|
237
250
|
if (run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
238
251
|
if (usage) parts.push(usage);
|
package/src/setup.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DEFAULT_CONFIG,
|
|
16
16
|
DEFAULT_ENABLED_AGENTS,
|
|
17
17
|
DEFAULT_MAX_CONCURRENCY,
|
|
18
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
18
19
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
19
20
|
THINKING_LEVEL_VALUES,
|
|
20
21
|
type AgentScope,
|
|
@@ -196,6 +197,8 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
|
|
|
196
197
|
/** Preset steps offered for the two numeric limits (selection-only wizard). */
|
|
197
198
|
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
198
199
|
const PARALLEL_TASK_STEPS = [2, 4, 6, 8, 12, 16, 24, 32];
|
|
200
|
+
/** Preset rounds offered for the auto-fix loop (0 disables it). */
|
|
201
|
+
const FIX_ROUNDS_STEPS = [0, 1, 2, 3, 5];
|
|
199
202
|
|
|
200
203
|
async function pickCount(
|
|
201
204
|
ctx: ExtensionCommandContext,
|
|
@@ -295,9 +298,18 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
295
298
|
base.maxParallelTasks,
|
|
296
299
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
297
300
|
);
|
|
298
|
-
|
|
301
|
+
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
302
|
+
|
|
303
|
+
const maxFixRounds = await pickCount(
|
|
304
|
+
ctx,
|
|
305
|
+
"Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
|
|
306
|
+
FIX_ROUNDS_STEPS,
|
|
307
|
+
base.maxFixRounds,
|
|
308
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
309
|
+
);
|
|
310
|
+
if (maxFixRounds === undefined) return notifyCancelled(ctx);
|
|
299
311
|
|
|
300
|
-
|
|
312
|
+
const next: SubagentsConfig = {
|
|
301
313
|
enabledAgents: enabled,
|
|
302
314
|
agentModels: repairStaleModels(ctx, picked.models),
|
|
303
315
|
agentThinkingLevels: picked.strengths,
|
|
@@ -309,6 +321,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
309
321
|
maxConcurrency,
|
|
310
322
|
maxParallelTasks,
|
|
311
323
|
maxSubagentDepth: base.maxSubagentDepth,
|
|
324
|
+
maxFixRounds,
|
|
312
325
|
};
|
|
313
326
|
await saveConfig(next, configPath);
|
|
314
327
|
ctx.ui.notify(`pi-subagents configured. Saved to ${configPath}`, "info");
|
|
@@ -323,6 +336,7 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
323
336
|
"Change agent scope",
|
|
324
337
|
"Change max concurrent sub-agents",
|
|
325
338
|
"Change max parallel tasks",
|
|
339
|
+
"Change max fix rounds",
|
|
326
340
|
"Full re-setup",
|
|
327
341
|
]);
|
|
328
342
|
if (choice === undefined) return notifyCancelled(ctx);
|
|
@@ -384,6 +398,16 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
384
398
|
);
|
|
385
399
|
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
386
400
|
next.maxParallelTasks = maxParallelTasks;
|
|
401
|
+
} else if (choice.startsWith("Change max fix")) {
|
|
402
|
+
const maxFixRounds = await pickCount(
|
|
403
|
+
ctx,
|
|
404
|
+
"Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
|
|
405
|
+
FIX_ROUNDS_STEPS,
|
|
406
|
+
config.maxFixRounds,
|
|
407
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
408
|
+
);
|
|
409
|
+
if (maxFixRounds === undefined) return notifyCancelled(ctx);
|
|
410
|
+
next.maxFixRounds = maxFixRounds;
|
|
387
411
|
}
|
|
388
412
|
|
|
389
413
|
await saveConfig(next, configPath);
|
package/src/spawn.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface SingleResult {
|
|
|
55
55
|
thinking?: string;
|
|
56
56
|
stopReason?: string;
|
|
57
57
|
errorMessage?: string;
|
|
58
|
+
/** Model the run degraded from: set when a failed run was retried with the main-window model. */
|
|
59
|
+
modelFallbackFrom?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export interface SubagentDetails {
|
|
@@ -142,6 +144,23 @@ export function isFailedResult(result: SingleResult): boolean {
|
|
|
142
144
|
return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* True when a failed run never got usable output from its model: the provider
|
|
149
|
+
* rejected the call before the model produced any text (bad model id, auth,
|
|
150
|
+
* thinking level, quota, ...). Task-level failures — the model worked and the
|
|
151
|
+
* task failed — and aborts/timeouts are NOT model-level and must not degrade.
|
|
152
|
+
*/
|
|
153
|
+
export function isModelLevelFailure(result: SingleResult): boolean {
|
|
154
|
+
if (!isFailedResult(result)) return false;
|
|
155
|
+
if (result.stopReason === "aborted") return false;
|
|
156
|
+
// The model produced text: the failure belongs to the task, not the model.
|
|
157
|
+
if (getFinalOutput(result.messages)) return false;
|
|
158
|
+
if (result.errorMessage?.includes("timed out")) return false;
|
|
159
|
+
// Require evidence the failure came from the model/provider (an error
|
|
160
|
+
// message or stderr), not from the child process failing to start.
|
|
161
|
+
return result.messages.length > 0 || result.stderr.trim().length > 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
export function getResultOutput(result: SingleResult): string {
|
|
146
165
|
if (isFailedResult(result)) {
|
|
147
166
|
return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
@@ -525,3 +544,24 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
525
544
|
}
|
|
526
545
|
}
|
|
527
546
|
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Run one agent; when the configured model fails at the provider level before
|
|
550
|
+
* producing any output (see isModelLevelFailure), retry once with the main
|
|
551
|
+
* window's current model. The retried result is returned with `modelFallbackFrom`
|
|
552
|
+
* set so callers can surface the degradation. The fallback is per-run only and
|
|
553
|
+
* never persisted: a transient provider hiccup must not silently downgrade the
|
|
554
|
+
* configured agent model.
|
|
555
|
+
*/
|
|
556
|
+
export async function runSingleAgentWithModelFallback(
|
|
557
|
+
options: RunSingleOptions,
|
|
558
|
+
fallbackModelRef?: string,
|
|
559
|
+
): Promise<SingleResult> {
|
|
560
|
+
const result = await runSingleAgent(options);
|
|
561
|
+
const agent = options.agent;
|
|
562
|
+
const launchedRef = agent?.model;
|
|
563
|
+
if (!agent || !launchedRef || !fallbackModelRef || launchedRef === fallbackModelRef) return result;
|
|
564
|
+
if (!isModelLevelFailure(result)) return result;
|
|
565
|
+
const retried = await runSingleAgent({ ...options, agent: { ...agent, model: fallbackModelRef } });
|
|
566
|
+
return { ...retried, modelFallbackFrom: launchedRef };
|
|
567
|
+
}
|