@ferris1225/pi-subagents 0.31.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -71
- package/package.json +1 -1
- package/src/agents.ts +8 -13
- package/src/background.ts +59 -6
- package/src/config.ts +14 -2
- package/src/dispatch.ts +1429 -396
- package/src/format.ts +29 -4
- package/src/index.ts +7 -4
- package/src/inspector-panel.ts +363 -0
- package/src/inspector.ts +369 -0
- package/src/models.ts +192 -50
- package/src/monitor.ts +114 -7
- package/src/prompt.ts +3 -2
- package/src/recovery.ts +145 -0
- package/src/rpc-run.ts +1016 -0
- package/src/runtime.ts +167 -27
- package/src/session-fork.ts +84 -0
- package/src/setup.ts +271 -184
- package/src/spawn.ts +562 -977
- package/src/tools.ts +404 -65
- package/src/trajectory.ts +503 -0
- package/src/ui.ts +32 -16
- package/src/widget.ts +17 -4
- package/src/worktree.ts +687 -0
package/README.md
CHANGED
|
@@ -17,12 +17,11 @@ on its own — no prompt engineering, no babysitting.
|
|
|
17
17
|
into the main system prompt, so the main model automatically sends broad searches
|
|
18
18
|
to `explore`, self-contained implementations to `worker`, and pre-commit reviews
|
|
19
19
|
to `reviewer`. You just use pi; delegation happens by itself.
|
|
20
|
-
- **Vision-capable image tasks** —
|
|
21
|
-
|
|
22
|
-
the
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
image files when the brief asks.
|
|
20
|
+
- **Vision-capable image tasks** — flag screenshot/mockup/design work with
|
|
21
|
+
`vision: true`. The configured vision primary is followed by that agent's
|
|
22
|
+
backup and the current main-window model. The setup picker offers only
|
|
23
|
+
image-capable models (while clearly marking an incompatible stale saved ref),
|
|
24
|
+
and runtime failures never silently rewrite your configuration.
|
|
26
25
|
- **Results come back on their own** — completions are delivered as messages that
|
|
27
26
|
wake the main agent automatically, even mid-turn. No polling, no `sleep`, no
|
|
28
27
|
"go check" step. `subagent_wait` is a **non-blocking** in-turn lookup by default
|
|
@@ -35,20 +34,27 @@ on its own — no prompt engineering, no babysitting.
|
|
|
35
34
|
- **A quality gate that closes the loop** — when a reviewer returns `REVIEW_FAIL`,
|
|
36
35
|
the extension dispatches a worker briefed with the concrete findings, then a
|
|
37
36
|
re-review, up to `maxFixRounds` times — and only then wakes the main agent.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
Every round stays in the triggering reviewer's cwd, and chains that target the
|
|
38
|
+
same repository are serialized so shared-checkout edits cannot race.
|
|
39
|
+
- **Ordered model pools without config churn** — each agent can have a primary
|
|
40
|
+
and backup; the current main-window model is the final candidate. Transient
|
|
41
|
+
provider failures retry the same candidate with backoff, while permanent stale
|
|
42
|
+
model/config errors and quota/auth errors advance immediately. Saved refs are
|
|
43
|
+
never rewritten behind your back.
|
|
44
|
+
- **Resumes, retargets, and forks preserve context** — every run is session-backed.
|
|
45
|
+
`subagent_control` can steer active work, retarget it after a stable abort,
|
|
46
|
+
park/resume it under the same run id, or fork a parked/settled checkpoint into
|
|
47
|
+
a new independent run. Concurrent resume calls are serialized.
|
|
47
48
|
- **Honest completions** — a run that ended with failed tool calls (e.g. a broken
|
|
48
49
|
build) is reported as `completed with N failed tool call(s)` with the errors
|
|
49
50
|
attached — a cheerful final text can never hide a failure.
|
|
50
|
-
- **Parallel fan-out** — independent tasks run
|
|
51
|
-
limit (default 4).
|
|
51
|
+
- **Parallel fan-out with filesystem isolation** — independent tasks run up to a
|
|
52
|
+
configurable limit (default 4). Parallel workers default to detached Git
|
|
53
|
+
worktrees; tracked, deleted, untracked, and binary changes are applied back
|
|
54
|
+
without touching the parent index. Failed integration keeps recovery artifacts.
|
|
55
|
+
- **Live inspector** — `/subagents-inspect` opens a master/detail
|
|
56
|
+
overlay with thread state, model chain, usage, transcript, recent tools,
|
|
57
|
+
worktree/fork relations, and the append-only control trajectory.
|
|
52
58
|
- **Live progress widget** — each run's status, current activity, model, token
|
|
53
59
|
usage, and elapsed time; auto-fix chain rounds hang under their triggering
|
|
54
60
|
review as a tree, each finished round keeping a one-line outcome.
|
|
@@ -121,32 +127,68 @@ subagent({
|
|
|
121
127
|
});
|
|
122
128
|
```
|
|
123
129
|
|
|
124
|
-
The sub-agent reads
|
|
125
|
-
vision-flagged
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
The sub-agent reads images with its `read` tool. Runtime order for a
|
|
131
|
+
vision-flagged run is configured `visionModel` → that agent's configured backup
|
|
132
|
+
→ current main-window model (deduplicated). A stale configured ref is attempted
|
|
133
|
+
once, then skipped as a permanent candidate error; it is not rewritten. A
|
|
134
|
+
vision-flagged auto-fix chain keeps the flag for worker/re-review rounds because
|
|
135
|
+
they may need to inspect the same images.
|
|
130
136
|
|
|
131
|
-
###
|
|
137
|
+
### Controlling, inspecting, and stopping
|
|
132
138
|
|
|
139
|
+
- `subagent_control` — `steer`, `retarget`, `park`, `resume`, or `fork` a logical
|
|
140
|
+
thread by stable run id. Resume accepts an optional replacement objective;
|
|
141
|
+
fork creates a new id and leaves the source unchanged. Park active work before
|
|
142
|
+
forking it.
|
|
133
143
|
- `subagent_wait` — in-turn result lookup. **Non-blocking by default**: a settled
|
|
134
|
-
run returns
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
- `
|
|
138
|
-
|
|
139
|
-
- `subagent_stop` —
|
|
140
|
-
|
|
144
|
+
run returns immediately; an active run tells the model to end its turn. Pass
|
|
145
|
+
`timeoutMs` only when you must stay in the turn.
|
|
146
|
+
- `subagent_status` — active/parked/finished runs and full result by run id.
|
|
147
|
+
- `/subagents-inspect` — interactive live thread/transcript/tool/trajectory
|
|
148
|
+
overlay; press `p` to park or resume the selected thread.
|
|
149
|
+
- `subagent_stop` — destructive cancellation. It retires that thread's retained
|
|
150
|
+
session (independent forks survive) and delivers exactly one aborted partial
|
|
151
|
+
result after the run and any worktree integration have quiesced.
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
subagent_control({ action: "steer", id: 7, instruction: "Check the Windows path too." });
|
|
157
|
+
subagent_control({ action: "park", id: 7 });
|
|
158
|
+
subagent_control({ action: "resume", id: 7, objective: "Finish the tests." });
|
|
159
|
+
subagent_control({ action: "fork", id: 7, objective: "Try the smaller alternative." });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Worktree isolation
|
|
163
|
+
|
|
164
|
+
Single tasks default to `isolation: "shared"`. Parallel `worker` tasks default
|
|
165
|
+
to `isolation: "worktree"`; opt into shared mode only when a worker must see the
|
|
166
|
+
caller's live uncommitted tree. Worktree mode requires a Git repository with a
|
|
167
|
+
committed `HEAD` and is rejected for read-only agents.
|
|
168
|
+
|
|
169
|
+
A parked isolated thread keeps its current worktree. Resume it there; fork is
|
|
170
|
+
available after that isolated checkpoint settles and its seed is integrated.
|
|
171
|
+
Resuming or forking a settled isolated thread creates a fresh worktree, merges a
|
|
172
|
+
recorded checkpoint onto the current `HEAD` (including when the seed was already
|
|
173
|
+
committed), and clones the Pi session with the new cwd. Forks then integrate only
|
|
174
|
+
their unique follow-on edits, so a shared seed is applied once. A run remains
|
|
175
|
+
active while final Git integration is in progress and becomes `done` only after
|
|
176
|
+
that boundary finishes.
|
|
177
|
+
|
|
178
|
+
Every Git operation has a 120-second deadline and process-tree cleanup; captured
|
|
179
|
+
Git output and binary patches are capped at 64 MiB. Setup/bound failures surface
|
|
180
|
+
instead of hanging. Finalization failures retain the patch/worktree when
|
|
181
|
+
available and are recorded in `~/.pi/agent/pi-subagents-recovery.json`; later
|
|
182
|
+
sessions show the recovery paths again until the artifacts are removed.
|
|
141
183
|
|
|
142
184
|
## Configuration
|
|
143
185
|
|
|
144
186
|
Stored at `~/.pi/agent/pi-subagents.json` (follows `PI_CODING_AGENT_DIR` when
|
|
145
|
-
set). The `/subagents-setup` wizard drives every field interactively — models,
|
|
146
|
-
thinking
|
|
147
|
-
and injection — with a per-agent "configure
|
|
148
|
-
exists. `notifyOnReviewPass` and
|
|
149
|
-
file.
|
|
187
|
+
set). The `/subagents-setup` wizard drives every field interactively — models, the
|
|
188
|
+
default plus each enabled agent's thinking level, the vision model, concurrency,
|
|
189
|
+
fix rounds, idle timeout, scope, and injection — with a per-agent "configure
|
|
190
|
+
one" menu when the config already exists. `notifyOnReviewPass` and
|
|
191
|
+
`maxResultLines` are edited directly in the file.
|
|
150
192
|
|
|
151
193
|
```json
|
|
152
194
|
{
|
|
@@ -154,6 +196,9 @@ file.
|
|
|
154
196
|
"agentModels": {
|
|
155
197
|
"explore": "anthropic/claude-haiku-4-5"
|
|
156
198
|
},
|
|
199
|
+
"agentBackupModels": {
|
|
200
|
+
"explore": "openai/gpt-5-mini"
|
|
201
|
+
},
|
|
157
202
|
"agentThinkingLevels": {
|
|
158
203
|
"explore": "low"
|
|
159
204
|
},
|
|
@@ -172,7 +217,8 @@ file.
|
|
|
172
217
|
| Field | Description |
|
|
173
218
|
| --- | --- |
|
|
174
219
|
| `enabledAgents` | Agent names exposed to discovery and prompt injection. An empty array disables all agents. |
|
|
175
|
-
| `agentModels` | Optional `provider/model-id` override per agent. |
|
|
220
|
+
| `agentModels` | Optional primary `provider/model-id` override per agent. |
|
|
221
|
+
| `agentBackupModels` | Optional backup per agent, tried after its primary and before the current main-window model. |
|
|
176
222
|
| `agentThinkingLevels` | Optional thinking level per agent; agents without an entry use the agent's frontmatter `thinking`, then `thinkingLevel`. |
|
|
177
223
|
| `thinkingLevel` | Default thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (default `high`). |
|
|
178
224
|
| `visionModel` | Optional vision-capable model for `vision: true` tasks (screenshots/mockups/designs). Unset = falls back to the main session's current model. |
|
|
@@ -184,40 +230,41 @@ file.
|
|
|
184
230
|
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). |
|
|
185
231
|
| `idleTimeoutSec` | Idle watchdog: a sub-agent whose stdout goes silent for this long is terminated and retried. `0` disables it. Default 90. |
|
|
186
232
|
|
|
187
|
-
### Model precedence
|
|
188
|
-
|
|
189
|
-
```text
|
|
190
|
-
per-agent override → current main-session model → agent frontmatter model
|
|
191
|
-
```
|
|
233
|
+
### Model precedence and fallback
|
|
192
234
|
|
|
193
|
-
|
|
235
|
+
Normal run pool:
|
|
194
236
|
|
|
195
237
|
```text
|
|
196
|
-
|
|
238
|
+
configured agent primary → configured agent backup → current main-window model
|
|
197
239
|
```
|
|
198
240
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
241
|
+
If no primary is configured, the current main-window model is primary; the
|
|
242
|
+
agent frontmatter model is used only when no current main model exists. For
|
|
243
|
+
vision runs, `visionModel` replaces the first slot while the selected agent's
|
|
244
|
+
backup and current main model remain the fallbacks. Duplicate refs are removed.
|
|
245
|
+
|
|
246
|
+
Each candidate gets startup-race retries. Provider failures retry the same
|
|
247
|
+
candidate up to five times only when transient (timeouts, network, 429, 5xx).
|
|
248
|
+
Permanent model/config failures (`model not found`, unknown provider, 404) and
|
|
249
|
+
quota/auth/billing failures skip that delay and advance immediately. No runtime
|
|
250
|
+
outcome rewrites `pi-subagents.json`. If the whole pool fails, the result is
|
|
251
|
+
handed to the main window with its retained run id and session.
|
|
205
252
|
|
|
206
|
-
### Resuming
|
|
253
|
+
### Resuming retained context
|
|
207
254
|
|
|
208
|
-
Every
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
re-scans. If every available model is exhausted (e.g. the account is out of
|
|
213
|
-
quota), the run is handed back with its session preserved; once you have a
|
|
214
|
-
working model again, resume it in-context:
|
|
255
|
+
Every run stores its Pi session in a private temp directory. Same-model retries
|
|
256
|
+
and pool fallbacks resume that session, so searches, reads, reasoning, and edits
|
|
257
|
+
remain in context. A parked, completed, or failed thread can later be resumed
|
|
258
|
+
under its stable id:
|
|
215
259
|
|
|
216
260
|
```ts
|
|
217
|
-
|
|
261
|
+
subagent_control({ action: "resume", id: 7 });
|
|
262
|
+
subagent_control({ action: "resume", id: 7, objective: "Continue with the repaired credentials." });
|
|
218
263
|
```
|
|
219
264
|
|
|
220
|
-
|
|
265
|
+
Use `fork` when both paths should remain available. `subagent_stop` is the
|
|
266
|
+
explicit destructive operation that retires a retained session; otherwise
|
|
267
|
+
sessions live until the parent Pi session shuts down.
|
|
221
268
|
|
|
222
269
|
### Configuration migration
|
|
223
270
|
|
|
@@ -230,7 +277,8 @@ after an update via a toast (marker persisted in `announcedFeatures`).
|
|
|
230
277
|
## Agent discovery and overrides
|
|
231
278
|
|
|
232
279
|
- Built-in agents ship with the package; user agents live in `~/.pi/agent/agents/`;
|
|
233
|
-
project agents in the nearest `.pi/agents/` directory
|
|
280
|
+
project agents in the nearest `.pi/agents/` directory are loaded only when Pi
|
|
281
|
+
trusts that project.
|
|
234
282
|
- For duplicate names: project overrides user overrides built-in. Keep the
|
|
235
283
|
matching filename and `name` field to replace a built-in agent.
|
|
236
284
|
- Optional frontmatter: `model` (default model reference), `thinking` (default
|
|
@@ -239,16 +287,17 @@ after an update via a toast (marker persisted in `announcedFeatures`).
|
|
|
239
287
|
|
|
240
288
|
## How it stays reliable
|
|
241
289
|
|
|
242
|
-
- **
|
|
243
|
-
|
|
244
|
-
|
|
290
|
+
- **Ordered model resilience** — transient failures retry the same candidate,
|
|
291
|
+
then advance through configured backup and current-main candidates. Permanent
|
|
292
|
+
stale-model/config and quota/auth/billing errors skip same-model backoff.
|
|
245
293
|
- **Startup-race retries** — a silent zero-activity child exit (concurrent pi
|
|
246
294
|
startup lock contention) is relaunched with backoff; only clean silent exits
|
|
247
295
|
qualify, so real work is never duplicated.
|
|
248
296
|
- **Idle watchdog** — a stalled provider stream (no output for `idleTimeoutSec`)
|
|
249
297
|
terminates the child and retries via the normal fallback path.
|
|
250
|
-
- **Dispatch
|
|
251
|
-
|
|
298
|
+
- **Dispatch failures surface** — partial parallel startup reports every failed
|
|
299
|
+
item and reason; if none start, the tool throws so Pi records a real tool error.
|
|
300
|
+
Dispatch crashes likewise produce a failed result instead of a silent hang.
|
|
252
301
|
- **Leaf children** — no nested delegation, no runaway trees.
|
|
253
302
|
|
|
254
303
|
## Development
|
|
@@ -259,11 +308,13 @@ npm run check
|
|
|
259
308
|
npm test
|
|
260
309
|
```
|
|
261
310
|
|
|
262
|
-
The source is modular: `dispatch.ts` (
|
|
263
|
-
|
|
264
|
-
`
|
|
265
|
-
`
|
|
266
|
-
|
|
311
|
+
The source is modular: `dispatch.ts` (dispatch, controls, isolation, auto-fix),
|
|
312
|
+
`rpc-run.ts` / `spawn.ts` (persistent child transport + model pools),
|
|
313
|
+
`worktree.ts` / `session-fork.ts` (filesystem/session branching),
|
|
314
|
+
`trajectory.ts` / `inspector*.ts` (safe live inspection), `tools.ts`
|
|
315
|
+
(wait/status/control/stop), `widget.ts` (widget + recovery announcements), and
|
|
316
|
+
`runtime.ts` (session-scoped ownership). No runtime dependencies beyond pi peer
|
|
317
|
+
dependencies.
|
|
267
318
|
|
|
268
319
|
## License
|
|
269
320
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.2",
|
|
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/agents.ts
CHANGED
|
@@ -123,16 +123,16 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
123
123
|
export interface DiscoverOptions {
|
|
124
124
|
/** Which directories to read from. Default: "user". */
|
|
125
125
|
scope?: AgentScope;
|
|
126
|
-
/** If provided
|
|
126
|
+
/** If provided, only agents whose name is listed are returned. */
|
|
127
127
|
enabledNames?: readonly string[];
|
|
128
|
-
/**
|
|
129
|
-
|
|
128
|
+
/** Project-controlled prompts are loaded only after Pi trusts the project. */
|
|
129
|
+
projectTrusted?: boolean;
|
|
130
130
|
/** Override the built-in agents directory (used by tests). */
|
|
131
131
|
builtinDir?: string;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
* Discover agents across scopes
|
|
135
|
+
* Discover agents across scopes and apply the enabled-name filter.
|
|
136
136
|
* Override priority for the same name: project > user > builtin.
|
|
137
137
|
*/
|
|
138
138
|
export function discoverAgents(cwd: string, options: DiscoverOptions = {}): AgentDiscoveryResult {
|
|
@@ -143,7 +143,9 @@ export function discoverAgents(cwd: string, options: DiscoverOptions = {}): Agen
|
|
|
143
143
|
const builtin = loadAgentsFromDir(builtinDir, "builtin");
|
|
144
144
|
const user = scope === "project" ? [] : loadAgentsFromDir(join(getAgentDir(), "agents"), "user");
|
|
145
145
|
const project =
|
|
146
|
-
scope === "user" || !projectAgentsDir
|
|
146
|
+
scope === "user" || !projectAgentsDir || options.projectTrusted !== true
|
|
147
|
+
? []
|
|
148
|
+
: loadAgentsFromDir(projectAgentsDir, "project");
|
|
147
149
|
|
|
148
150
|
// Merge with override priority builtin < user < project.
|
|
149
151
|
const byName = new Map<string, AgentConfig>();
|
|
@@ -153,18 +155,11 @@ export function discoverAgents(cwd: string, options: DiscoverOptions = {}): Agen
|
|
|
153
155
|
|
|
154
156
|
let agents = Array.from(byName.values());
|
|
155
157
|
|
|
156
|
-
if (options.enabledNames
|
|
158
|
+
if (options.enabledNames !== undefined) {
|
|
157
159
|
const enabled = new Set(options.enabledNames);
|
|
158
160
|
agents = agents.filter((agent) => enabled.has(agent.name));
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
if (options.modelOverrides) {
|
|
162
|
-
agents = agents.map((agent) => {
|
|
163
|
-
const override = options.modelOverrides?.[agent.name];
|
|
164
|
-
return override ? { ...agent, model: override } : agent;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
163
|
return { agents, projectAgentsDir };
|
|
169
164
|
}
|
|
170
165
|
|
package/src/background.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
|
|
|
14
14
|
interface PendingTask {
|
|
15
15
|
task: BackgroundTask;
|
|
16
16
|
controller: AbortController;
|
|
17
|
+
complete: () => void;
|
|
17
18
|
/** Called when a queued task is aborted before its body ever runs (drain skips
|
|
18
19
|
* an already-aborted entry; cancelAll aborts every pending entry), so the
|
|
19
20
|
* task body never produces a result. Callers that resolve waiters on a run id
|
|
@@ -23,13 +24,15 @@ interface PendingTask {
|
|
|
23
24
|
onCancelled?: () => void;
|
|
24
25
|
/** Invoked when the task throws and was not cancelled (cancellation is not a
|
|
25
26
|
* failure — e.g. session shutdown races must never be reported as errors). */
|
|
26
|
-
onError?: (error: unknown) => void
|
|
27
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export class BackgroundTaskQueue {
|
|
30
31
|
private concurrency: number;
|
|
31
32
|
private readonly pending: PendingTask[] = [];
|
|
32
33
|
private readonly active = new Set<AbortController>();
|
|
34
|
+
private readonly completions = new WeakMap<AbortController, Promise<void>>();
|
|
35
|
+
private readonly idleWaiters = new Set<() => void>();
|
|
33
36
|
private stopped = false;
|
|
34
37
|
|
|
35
38
|
constructor(concurrency: number) {
|
|
@@ -46,19 +49,55 @@ export class BackgroundTaskQueue {
|
|
|
46
49
|
this.drain();
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
enqueue(task: BackgroundTask, onCancelled?: () => void, onError?: (error: unknown) => void): AbortController {
|
|
52
|
+
enqueue(task: BackgroundTask, onCancelled?: () => void, onError?: (error: unknown) => void | Promise<void>): AbortController {
|
|
50
53
|
const controller = new AbortController();
|
|
54
|
+
let complete!: () => void;
|
|
55
|
+
const completion = new Promise<void>((resolve) => {
|
|
56
|
+
complete = resolve;
|
|
57
|
+
});
|
|
58
|
+
this.completions.set(controller, completion);
|
|
51
59
|
if (this.stopped) {
|
|
52
60
|
controller.abort();
|
|
53
61
|
this.runCancelled(onCancelled);
|
|
62
|
+
complete();
|
|
54
63
|
return controller;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
this.pending.push({ task, controller, onCancelled, onError });
|
|
66
|
+
this.pending.push({ task, controller, complete, onCancelled, onError });
|
|
58
67
|
this.drain();
|
|
59
68
|
return controller;
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
/** Resolve after this exact task has left the pending/active sets. This is
|
|
72
|
+
* stronger than waiting for its task body: callers may safely reuse a
|
|
73
|
+
* concurrency slot or the task's persisted checkpoint after it resolves. */
|
|
74
|
+
waitForTask(controller: AbortController | undefined): Promise<void> {
|
|
75
|
+
if (!controller) return Promise.resolve();
|
|
76
|
+
return this.completions.get(controller) ?? Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Cancel one queued/running task. Queued entries are removed immediately;
|
|
80
|
+
* active entries resolve waitForTask only after their body and error handler
|
|
81
|
+
* have quiesced and the concurrency slot has been released. */
|
|
82
|
+
cancel(controller: AbortController | undefined): void {
|
|
83
|
+
if (!controller) return;
|
|
84
|
+
controller.abort();
|
|
85
|
+
const index = this.pending.findIndex((entry) => entry.controller === controller);
|
|
86
|
+
if (index !== -1) {
|
|
87
|
+
const [entry] = this.pending.splice(index, 1);
|
|
88
|
+
this.runCancelled(entry.onCancelled);
|
|
89
|
+
entry.complete();
|
|
90
|
+
this.drain();
|
|
91
|
+
this.resolveIdleWaiters();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve once no queued or running task remains. */
|
|
96
|
+
waitForIdle(): Promise<void> {
|
|
97
|
+
if (this.pending.length === 0 && this.active.size === 0) return Promise.resolve();
|
|
98
|
+
return new Promise<void>((resolve) => this.idleWaiters.add(resolve));
|
|
99
|
+
}
|
|
100
|
+
|
|
62
101
|
/** Stop queued work and request cancellation for running work. */
|
|
63
102
|
cancelAll(): void {
|
|
64
103
|
if (this.stopped) return;
|
|
@@ -67,8 +106,10 @@ export class BackgroundTaskQueue {
|
|
|
67
106
|
for (const entry of this.pending.splice(0)) {
|
|
68
107
|
entry.controller.abort();
|
|
69
108
|
this.runCancelled(entry.onCancelled);
|
|
109
|
+
entry.complete();
|
|
70
110
|
}
|
|
71
111
|
for (const controller of this.active) controller.abort();
|
|
112
|
+
this.resolveIdleWaiters();
|
|
72
113
|
}
|
|
73
114
|
|
|
74
115
|
/** Cancellation callbacks are user-supplied: a throw must never break the queue
|
|
@@ -85,28 +126,40 @@ export class BackgroundTaskQueue {
|
|
|
85
126
|
private drain(): void {
|
|
86
127
|
while (!this.stopped && this.active.size < this.concurrency) {
|
|
87
128
|
const entry = this.pending.shift();
|
|
88
|
-
if (!entry)
|
|
129
|
+
if (!entry) {
|
|
130
|
+
this.resolveIdleWaiters();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
89
133
|
if (entry.controller.signal.aborted) {
|
|
90
134
|
this.runCancelled(entry.onCancelled);
|
|
135
|
+
entry.complete();
|
|
91
136
|
continue;
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
this.active.add(entry.controller);
|
|
95
140
|
void entry.task(entry.controller.signal)
|
|
96
|
-
.catch((error: unknown) => {
|
|
141
|
+
.catch(async (error: unknown) => {
|
|
97
142
|
// Cancellation is not a failure: aborted work (e.g. session
|
|
98
143
|
// shutdown) must never be reported as an exception.
|
|
99
144
|
if (entry.controller.signal.aborted) return;
|
|
100
145
|
try {
|
|
101
|
-
entry.onError?.(error);
|
|
146
|
+
await entry.onError?.(error);
|
|
102
147
|
} catch {
|
|
103
148
|
/* error reporting must never break the queue */
|
|
104
149
|
}
|
|
105
150
|
})
|
|
106
151
|
.finally(() => {
|
|
107
152
|
this.active.delete(entry.controller);
|
|
153
|
+
entry.complete();
|
|
108
154
|
this.drain();
|
|
155
|
+
this.resolveIdleWaiters();
|
|
109
156
|
});
|
|
110
157
|
}
|
|
111
158
|
}
|
|
159
|
+
|
|
160
|
+
private resolveIdleWaiters(): void {
|
|
161
|
+
if (this.pending.length > 0 || this.active.size > 0) return;
|
|
162
|
+
for (const resolve of this.idleWaiters) resolve();
|
|
163
|
+
this.idleWaiters.clear();
|
|
164
|
+
}
|
|
112
165
|
}
|
package/src/config.ts
CHANGED
|
@@ -25,7 +25,7 @@ export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Agents that used to ship but were removed. normalizeConfig strips them from
|
|
28
|
-
* enabledAgents/
|
|
28
|
+
* enabledAgents/model mappings so upgraded installs clean their config automatically
|
|
29
29
|
* (the schema-upgrade save in loadConfig then persists the cleanup).
|
|
30
30
|
*/
|
|
31
31
|
export const REMOVED_AGENT_NAMES: readonly string[] = ["plan"];
|
|
@@ -70,8 +70,10 @@ export const IDLE_TIMEOUT_SEC_LIMIT = 600;
|
|
|
70
70
|
export interface SubagentsConfig {
|
|
71
71
|
/** Agent names that are discoverable and injected. Default: explore, worker, reviewer. */
|
|
72
72
|
enabledAgents: string[];
|
|
73
|
-
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
73
|
+
/** Per-agent primary model override, keyed by agent name, as "provider/model-id". */
|
|
74
74
|
agentModels: Record<string, string>;
|
|
75
|
+
/** Optional per-agent backup model, tried after the primary and before the current main-window model. */
|
|
76
|
+
agentBackupModels: Record<string, string>;
|
|
75
77
|
/** Per-agent thinking-level override, keyed by agent name. */
|
|
76
78
|
agentThinkingLevels: Record<string, ThinkingLevel>;
|
|
77
79
|
/** Thinking level for sub-agents without a per-agent override or frontmatter default. Default: "high". */
|
|
@@ -125,6 +127,7 @@ export interface SubagentsConfig {
|
|
|
125
127
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
126
128
|
enabledAgents: [...DEFAULT_ENABLED_AGENTS],
|
|
127
129
|
agentModels: {},
|
|
130
|
+
agentBackupModels: {},
|
|
128
131
|
agentThinkingLevels: {},
|
|
129
132
|
thinkingLevel: DEFAULT_THINKING_LEVEL,
|
|
130
133
|
notifyOnReviewPass: false,
|
|
@@ -172,6 +175,7 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
172
175
|
const config: SubagentsConfig = {
|
|
173
176
|
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
174
177
|
agentModels: {},
|
|
178
|
+
agentBackupModels: {},
|
|
175
179
|
agentThinkingLevels: {},
|
|
176
180
|
thinkingLevel: DEFAULT_CONFIG.thinkingLevel,
|
|
177
181
|
notifyOnReviewPass: DEFAULT_CONFIG.notifyOnReviewPass,
|
|
@@ -202,6 +206,13 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
if (isRecord(raw.agentBackupModels)) {
|
|
210
|
+
for (const [key, value] of Object.entries(raw.agentBackupModels)) {
|
|
211
|
+
if (REMOVED_AGENT_NAMES.includes(key.trim())) continue;
|
|
212
|
+
if (isModelReference(value)) config.agentBackupModels[key.trim()] = value.trim();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
if (isRecord(raw.agentThinkingLevels)) {
|
|
206
217
|
for (const [key, value] of Object.entries(raw.agentThinkingLevels)) {
|
|
207
218
|
if (REMOVED_AGENT_NAMES.includes(key.trim())) continue;
|
|
@@ -272,6 +283,7 @@ function defaultConfig(): SubagentsConfig {
|
|
|
272
283
|
...DEFAULT_CONFIG,
|
|
273
284
|
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
274
285
|
agentModels: {},
|
|
286
|
+
agentBackupModels: {},
|
|
275
287
|
agentThinkingLevels: {},
|
|
276
288
|
announcedFeatures: [],
|
|
277
289
|
};
|