@ferris1225/pi-subagents 4.1.7 → 4.1.8
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 +16 -18
- package/package.json +1 -1
- package/src/announcements.ts +22 -67
- package/src/background.ts +5 -10
- package/src/config.ts +9 -170
- package/src/dispatch.ts +9 -10
- package/src/fixloop.ts +11 -7
- package/src/index.ts +1 -1
- package/src/models.ts +16 -0
- package/src/prompt.ts +3 -4
- package/src/runtime.ts +3 -6
- package/src/setup.ts +0 -41
- package/src/spawn.ts +1 -1
- package/src/thread-lifecycle.ts +1 -4
package/README.md
CHANGED
|
@@ -207,10 +207,10 @@ subagent({
|
|
|
207
207
|
});
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
Independent tasks run up to
|
|
211
|
-
contain at most that many tasks and is rejected if it
|
|
212
|
-
background work from separate calls waits in the
|
|
213
|
-
busy.
|
|
210
|
+
Independent tasks run up to a fixed limit of `4` concurrent sub-agent processes.
|
|
211
|
+
One parallel call may contain at most that many tasks and is rejected if it
|
|
212
|
+
exceeds the limit. Accepted background work from separate calls waits in the
|
|
213
|
+
shared queue when all slots are busy.
|
|
214
214
|
|
|
215
215
|
### Run an independent quality gate
|
|
216
216
|
|
|
@@ -245,8 +245,9 @@ cannot recursively start another chain. Gate reviewers keep documentation drift
|
|
|
245
245
|
out of the code verdict while `documenter` is enabled by recording it under
|
|
246
246
|
`## Documentation notes`; with documenter disabled, drift is a normal finding.
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
The loop is fixed at two worker fix rounds (each fix is re-reviewed); disabling
|
|
249
|
+
the `worker` agent is the way to turn fixes off. The post-writer review gate still
|
|
250
|
+
runs regardless, and only a terminal `REVIEW_PASS` can decide whether docs
|
|
250
251
|
sync is needed. Generic audits and read-only reviews are advisory: they omit
|
|
251
252
|
`VERDICT` and documentation machine markers, remain read-only, and never trigger
|
|
252
253
|
edits.
|
|
@@ -503,8 +504,8 @@ subagent({
|
|
|
503
504
|
|
|
504
505
|
## Configuration
|
|
505
506
|
|
|
506
|
-
The wizard covers enabled agents, per-agent models and thinking,
|
|
507
|
-
|
|
507
|
+
The wizard covers enabled agents, per-agent models and thinking, and the idle
|
|
508
|
+
timeout:
|
|
508
509
|
|
|
509
510
|
```text
|
|
510
511
|
/subagents-setup
|
|
@@ -534,8 +535,6 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json` and follows
|
|
|
534
535
|
"maxResultLines": 80,
|
|
535
536
|
"proactiveInjection": true,
|
|
536
537
|
"agentScope": "user",
|
|
537
|
-
"maxConcurrency": 4,
|
|
538
|
-
"maxFixRounds": 2,
|
|
539
538
|
"idleTimeoutSec": 90
|
|
540
539
|
}
|
|
541
540
|
```
|
|
@@ -549,16 +548,15 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json` and follows
|
|
|
549
548
|
| `maxResultLines` | Lines kept in a completion message before the full result moves to a temporary artifact. Default `80`. |
|
|
550
549
|
| `proactiveInjection` | Teach the main model when and how to delegate. Default `true`. |
|
|
551
550
|
| `agentScope` | Discover `user`, `project`, or `both` agent directories. Default `user`. |
|
|
552
|
-
| `maxConcurrency` | Running process limit and maximum tasks in one parallel call, from `1` to `16`. Default `4`. |
|
|
553
|
-
| `maxFixRounds` | Maximum worker fixes after `REVIEW_FAIL`; each fix is re-reviewed. After terminal `REVIEW_PASS`, docs sync runs only for `DOCUMENTATION: NEEDED` or a missing marker. `0` disables fixes but not the post-writer gate or conditional/reviewer-disabled docs behavior. Default `2`. |
|
|
554
551
|
| `idleTimeoutSec` | Seconds without RPC output before termination. `0` disables the watchdog. Default `90`. |
|
|
555
552
|
|
|
556
|
-
Invalid values fall back safely.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
553
|
+
Invalid values fall back safely. Keys from older versions (including the former
|
|
554
|
+
`maxConcurrency` and `maxFixRounds` tuning options, now fixed at `4` concurrent
|
|
555
|
+
processes and `2` fix rounds) are dropped automatically and the normalized
|
|
556
|
+
shape is saved back. At session start, per-agent model overrides that Pi no
|
|
557
|
+
longer reports as available are removed with a one-time notice; those agents
|
|
558
|
+
fall back to the current main model until you re-pick them in
|
|
559
|
+
`/subagents-setup`.
|
|
562
560
|
|
|
563
561
|
## Custom and overridden agents
|
|
564
562
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.8",
|
|
4
4
|
"description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/announcements.ts
CHANGED
|
@@ -1,82 +1,37 @@
|
|
|
1
|
-
/** Session-start recovery and
|
|
1
|
+
/** Session-start recovery, stale-config migration, and widget installation. */
|
|
2
2
|
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
CLEANER_INHERITED_FEATURE,
|
|
8
|
-
DOCUMENTER_AUTO_ENABLED_FEATURE,
|
|
9
|
-
DOCUMENTER_INHERITED_FEATURE,
|
|
10
|
-
loadConfig,
|
|
11
|
-
saveConfig,
|
|
12
|
-
} from "./config.ts";
|
|
4
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
5
|
+
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
13
6
|
import { announceRecoveryRecords } from "./recovery.ts";
|
|
14
7
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
15
8
|
import { pruneResultArtifacts } from "./spawn.ts";
|
|
16
9
|
import { installActiveRunsWidget } from "./widget.ts";
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// enabledAgents check keeps the notice silent when the user already
|
|
27
|
-
// disabled cleaner (e.g. via full setup) before it could fire.
|
|
28
|
-
key: "cleanerAutoEnabledNotice",
|
|
29
|
-
condition: (config) =>
|
|
30
|
-
config.announcedFeatures.includes(CLEANER_AUTO_ENABLED_FEATURE) &&
|
|
31
|
-
config.enabledAgents.includes("cleaner"),
|
|
32
|
-
// The inheritance clause matches reality: its stamp is only set when the
|
|
33
|
-
// upgrade actually copied reviewer model/thinking settings.
|
|
34
|
-
message: (config) =>
|
|
35
|
-
config.announcedFeatures.includes(CLEANER_INHERITED_FEATURE)
|
|
36
|
-
? "pi-subagents: the built-in cleaner agent was enabled by default and inherited your reviewer model/thinking settings. Run /subagents-setup to adjust or disable it."
|
|
37
|
-
: "pi-subagents: the built-in cleaner agent was enabled by default. Run /subagents-setup to adjust or disable it.",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
key: "documenterAutoEnabledNotice",
|
|
41
|
-
condition: (config) =>
|
|
42
|
-
config.announcedFeatures.includes(DOCUMENTER_AUTO_ENABLED_FEATURE) &&
|
|
43
|
-
config.enabledAgents.includes("documenter"),
|
|
44
|
-
message: (config) =>
|
|
45
|
-
config.announcedFeatures.includes(DOCUMENTER_INHERITED_FEATURE)
|
|
46
|
-
? "pi-subagents: the new documenter agent was enabled for your existing config and inherited your explorer model/thinking settings. It synchronizes comments and README/docs before commit; run /subagents-setup to adjust or disable it."
|
|
47
|
-
: "pi-subagents: the new documenter agent was enabled for your existing config. It synchronizes comments and README/docs before commit; run /subagents-setup to adjust or disable it.",
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
async function announceNewFeatures(
|
|
52
|
-
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } },
|
|
11
|
+
/**
|
|
12
|
+
* One-time-per-stale-override migration: keep agent model selections Pi still
|
|
13
|
+
* reports as available, drop the rest back to dynamic main-model routing, and
|
|
14
|
+
* tell the user what was removed. Saving the cleaned config is what makes it
|
|
15
|
+
* one-time — the dropped refs no longer exist to re-trigger the notice.
|
|
16
|
+
*/
|
|
17
|
+
async function migrateUnavailableAgentModels(
|
|
18
|
+
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } } & Parameters<typeof availableModelsInScope>[0],
|
|
53
19
|
runtime: SubagentRuntime,
|
|
54
20
|
): Promise<void> {
|
|
55
21
|
try {
|
|
56
|
-
let configExists = true;
|
|
57
|
-
try {
|
|
58
|
-
await stat(runtime.configPath);
|
|
59
|
-
} catch {
|
|
60
|
-
configExists = false;
|
|
61
|
-
}
|
|
62
|
-
if (!configExists) return;
|
|
63
|
-
|
|
64
22
|
const config = await loadConfig(runtime.configPath);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
runtime.configPath,
|
|
23
|
+
const overrides = Object.entries(config.agentModels);
|
|
24
|
+
if (overrides.length === 0) return;
|
|
25
|
+
const { kept, dropped } = filterUnavailableModelOverrides(config.agentModels, availableModelsInScope(ctx));
|
|
26
|
+
if (dropped.length === 0) return;
|
|
27
|
+
await saveConfig({ ...config, agentModels: kept }, runtime.configPath);
|
|
28
|
+
const list = dropped.map(({ agent, ref }) => `${agent}: ${ref}`).join(", ");
|
|
29
|
+
ctx.ui.notify(
|
|
30
|
+
`pi-subagents: removed stale agent model overrides that are no longer available (${list}). Those agents now follow the current main model; run /subagents-setup to re-pick.`,
|
|
31
|
+
"warning",
|
|
76
32
|
);
|
|
77
|
-
for (const announcement of pending) ctx.ui.notify(announcement.message(config), "info");
|
|
78
33
|
} catch {
|
|
79
|
-
/*
|
|
34
|
+
/* migration failures are non-fatal */
|
|
80
35
|
}
|
|
81
36
|
}
|
|
82
37
|
|
|
@@ -84,8 +39,8 @@ export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime
|
|
|
84
39
|
pi.on("session_start", async (_event, ctx) => {
|
|
85
40
|
pruneResultArtifacts();
|
|
86
41
|
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
42
|
+
await migrateUnavailableAgentModels(ctx, runtime);
|
|
87
43
|
if (ctx.mode !== "tui") return;
|
|
88
44
|
installActiveRunsWidget(ctx);
|
|
89
|
-
await announceNewFeatures(ctx, runtime);
|
|
90
45
|
});
|
|
91
46
|
}
|
package/src/background.ts
CHANGED
|
@@ -27,6 +27,11 @@ interface PendingTask {
|
|
|
27
27
|
onError?: (error: unknown) => void | Promise<void>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** How many sub-agent processes may run at once, and how many tasks one
|
|
31
|
+
* parallel `subagent` call may contain. Fixed by design: the queue sheds load
|
|
32
|
+
* by waiting, so the knob bought nothing worth its maintenance. */
|
|
33
|
+
export const MAX_CONCURRENT_SUBAGENTS = 4;
|
|
34
|
+
|
|
30
35
|
export class BackgroundTaskQueue {
|
|
31
36
|
private concurrency: number;
|
|
32
37
|
private readonly pending: PendingTask[] = [];
|
|
@@ -39,16 +44,6 @@ export class BackgroundTaskQueue {
|
|
|
39
44
|
this.concurrency = Math.max(1, concurrency);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/**
|
|
43
|
-
* Update the concurrency limit (e.g. after a config change). Raising it
|
|
44
|
-
* immediately starts more queued work; lowering it takes effect as running
|
|
45
|
-
* tasks finish — already-running tasks are never interrupted.
|
|
46
|
-
*/
|
|
47
|
-
setConcurrency(concurrency: number): void {
|
|
48
|
-
this.concurrency = Math.max(1, concurrency);
|
|
49
|
-
this.drain();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
enqueue(task: BackgroundTask, onCancelled?: () => void, onError?: (error: unknown) => void | Promise<void>): AbortController {
|
|
53
48
|
const controller = new AbortController();
|
|
54
49
|
let complete!: () => void;
|
package/src/config.ts
CHANGED
|
@@ -4,57 +4,23 @@
|
|
|
4
4
|
* Config lives at <agentDir>/pi-subagents.json (agentDir defaults to ~/.pi/agent
|
|
5
5
|
* and honors PI_CODING_AGENT_DIR). Parsing is defensive: invalid fields fall back
|
|
6
6
|
* to defaults instead of throwing, so a hand-edited or partially-written file can
|
|
7
|
-
* never break the extension at runtime.
|
|
8
|
-
*
|
|
9
|
-
* Schema upgrades happen transparently on load: a config written by an older
|
|
10
|
-
* version (missing newer keys or containing invalid
|
|
11
|
-
* values) is normalized and persisted back with the new fields filled in.
|
|
7
|
+
* never break the extension at runtime. Unknown keys from older versions are
|
|
8
|
+
* dropped and the normalized shape persisted back on load.
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
11
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
12
|
import { dirname, join } from "node:path";
|
|
17
13
|
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
18
14
|
|
|
19
15
|
/** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
|
|
20
16
|
export const BUILTIN_AGENT_NAMES = ["explorer", "worker", "cleaner", "documenter", "reviewer"] as const;
|
|
21
17
|
|
|
22
|
-
/** Agents enabled out of the box on a fresh install.
|
|
23
|
-
* explicit setup choice; existing non-empty configs receive it via migration. */
|
|
18
|
+
/** Agents enabled out of the box on a fresh install. */
|
|
24
19
|
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explorer", "worker", "cleaner", "reviewer"];
|
|
25
20
|
|
|
26
21
|
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
27
22
|
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
28
23
|
|
|
29
|
-
const LEGACY_EXPLORER_NAME = "explore";
|
|
30
|
-
const EXPLORER_NAME = "explorer";
|
|
31
|
-
const CLEANER_NAME = "cleaner";
|
|
32
|
-
const DOCUMENTER_NAME = "documenter";
|
|
33
|
-
const REVIEWER_NAME = "reviewer";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Stamps recorded in `announcedFeatures` by the one-time upgrade that defaults
|
|
37
|
-
* cleaner on for configs written before it shipped. The first marks a config as
|
|
38
|
-
* processed, so a later deliberate disable is not undone; the second records
|
|
39
|
-
* that cleaner was actually injected, so session start can tell the user once;
|
|
40
|
-
* the third records that reviewer model/thinking settings were actually copied,
|
|
41
|
-
* so that notice never claims an inheritance that did not happen.
|
|
42
|
-
*/
|
|
43
|
-
export const CLEANER_DEFAULTED_FEATURE = "cleanerDefaulted";
|
|
44
|
-
export const CLEANER_AUTO_ENABLED_FEATURE = "cleanerAutoEnabled";
|
|
45
|
-
export const CLEANER_INHERITED_FEATURE = "cleanerInheritedReviewer";
|
|
46
|
-
|
|
47
|
-
/** One-time upgrade stamps for the pre-commit documenter role. Existing
|
|
48
|
-
* non-empty configs gain it before reviewer and inherit explorer routing; fresh
|
|
49
|
-
* installs keep it off until setup explicitly enables it. */
|
|
50
|
-
export const DOCUMENTER_DEFAULTED_FEATURE = "documenterDefaulted";
|
|
51
|
-
export const DOCUMENTER_AUTO_ENABLED_FEATURE = "documenterAutoEnabled";
|
|
52
|
-
export const DOCUMENTER_INHERITED_FEATURE = "documenterInheritedExplorer";
|
|
53
|
-
|
|
54
|
-
function migrateAgentName(name: string): string {
|
|
55
|
-
return name === LEGACY_EXPLORER_NAME ? EXPLORER_NAME : name;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
24
|
/** Thinking levels accepted by pi's `--thinking` option. */
|
|
59
25
|
export const THINKING_LEVEL_VALUES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
60
26
|
export type ThinkingLevel = (typeof THINKING_LEVEL_VALUES)[number];
|
|
@@ -67,20 +33,6 @@ export const MAX_RESULT_LINES_LIMIT = 2000;
|
|
|
67
33
|
|
|
68
34
|
const CONFIG_FILE_NAME = "pi-subagents.json";
|
|
69
35
|
|
|
70
|
-
/** How many sub-agent processes may run at once, and how many tasks one parallel `subagent` call may contain. Default: 4. */
|
|
71
|
-
export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
72
|
-
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
73
|
-
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
74
|
-
/**
|
|
75
|
-
* Maximum worker fixes after REVIEW_FAIL. Each fix is followed by a reviewer
|
|
76
|
-
* re-review; this cap does not suppress the post-writer review gate or its
|
|
77
|
-
* conditional/reviewer-disabled documentation fallback. 0 disables fixes.
|
|
78
|
-
* Default: 2.
|
|
79
|
-
*/
|
|
80
|
-
export const DEFAULT_MAX_FIX_ROUNDS = 2;
|
|
81
|
-
/** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
|
|
82
|
-
export const MAX_FIX_ROUNDS_LIMIT = 5;
|
|
83
|
-
|
|
84
36
|
/**
|
|
85
37
|
* Default idle timeout in seconds: a sub-agent whose stdout (JSON event stream)
|
|
86
38
|
* goes silent for this long is terminated; a selected model then hands the
|
|
@@ -112,27 +64,12 @@ export interface SubagentsConfig {
|
|
|
112
64
|
proactiveInjection: boolean;
|
|
113
65
|
/** Which agent directories to discover from. Default: "user". */
|
|
114
66
|
agentScope: AgentScope;
|
|
115
|
-
/** Max sub-agent processes running at once (extra work queues) and the max tasks
|
|
116
|
-
* one parallel `subagent` call may contain. Default: 4. */
|
|
117
|
-
maxConcurrency: number;
|
|
118
|
-
/**
|
|
119
|
-
* Maximum worker fixes after REVIEW_FAIL. Every fix receives the full review,
|
|
120
|
-
* then a re-review runs; any documentation sync selected after the terminal
|
|
121
|
-
* healthy review does not consume this budget. 0 disables fixes. Default: 2.
|
|
122
|
-
*/
|
|
123
|
-
maxFixRounds: number;
|
|
124
67
|
/**
|
|
125
68
|
* Idle timeout in seconds: a sub-agent whose stdout (JSON event stream) goes
|
|
126
69
|
* silent for this long is terminated; a configured agent model then hands
|
|
127
70
|
* off to the current main model. 0 disables the idle watchdog. Default: 90.
|
|
128
71
|
*/
|
|
129
72
|
idleTimeoutSec: number;
|
|
130
|
-
/**
|
|
131
|
-
* One-time feature announcements already shown to the user, plus schema
|
|
132
|
-
* upgrade stamps (e.g. the cleaner default-enable upgrade). Persisted so
|
|
133
|
-
* notices and migrations never repeat.
|
|
134
|
-
*/
|
|
135
|
-
announcedFeatures: string[];
|
|
136
73
|
}
|
|
137
74
|
|
|
138
75
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
@@ -143,10 +80,7 @@ export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
|
143
80
|
maxResultLines: DEFAULT_MAX_RESULT_LINES,
|
|
144
81
|
proactiveInjection: true,
|
|
145
82
|
agentScope: "user",
|
|
146
|
-
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
|
|
147
|
-
maxFixRounds: DEFAULT_MAX_FIX_ROUNDS,
|
|
148
83
|
idleTimeoutSec: DEFAULT_IDLE_TIMEOUT_SEC,
|
|
149
|
-
announcedFeatures: [],
|
|
150
84
|
};
|
|
151
85
|
|
|
152
86
|
export function getConfigPath(agentDir: string = getAgentDir()): string {
|
|
@@ -186,48 +120,30 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
186
120
|
const names = raw.enabledAgents.filter(
|
|
187
121
|
(name): name is string => typeof name === "string" && name.trim().length > 0,
|
|
188
122
|
);
|
|
189
|
-
// An explicitly empty array is honored
|
|
190
|
-
|
|
191
|
-
config.enabledAgents = [...new Set(names.map((name) => migrateAgentName(name.trim())))];
|
|
123
|
+
// An explicitly empty array is honored; duplicates collapse.
|
|
124
|
+
config.enabledAgents = [...new Set(names.map((name) => name.trim()))];
|
|
192
125
|
}
|
|
193
126
|
|
|
194
127
|
if (isRecord(raw.agentModels)) {
|
|
195
|
-
const
|
|
196
|
-
// A valid explicit new key wins regardless of JSON property order.
|
|
197
|
-
for (const [rawKey, value] of entries) {
|
|
128
|
+
for (const [rawKey, value] of Object.entries(raw.agentModels)) {
|
|
198
129
|
const key = rawKey.trim();
|
|
199
|
-
if (key !==
|
|
130
|
+
if (key !== "" && isModelReference(value)) {
|
|
200
131
|
config.agentModels[key] = value.trim();
|
|
201
132
|
}
|
|
202
133
|
}
|
|
203
|
-
if (!Object.hasOwn(config.agentModels, EXPLORER_NAME)) {
|
|
204
|
-
const legacy = entries.find(([key, value]) =>
|
|
205
|
-
key.trim() === LEGACY_EXPLORER_NAME && isModelReference(value)
|
|
206
|
-
);
|
|
207
|
-
if (legacy && isModelReference(legacy[1])) config.agentModels[EXPLORER_NAME] = legacy[1].trim();
|
|
208
|
-
}
|
|
209
134
|
}
|
|
210
135
|
|
|
211
136
|
if (isRecord(raw.agentThinkingLevels)) {
|
|
212
|
-
const
|
|
213
|
-
for (const [rawKey, value] of entries) {
|
|
137
|
+
for (const [rawKey, value] of Object.entries(raw.agentThinkingLevels)) {
|
|
214
138
|
const key = rawKey.trim();
|
|
215
139
|
if (
|
|
216
|
-
key !==
|
|
140
|
+
key !== "" &&
|
|
217
141
|
typeof value === "string" &&
|
|
218
142
|
(THINKING_LEVEL_VALUES as readonly string[]).includes(value)
|
|
219
143
|
) {
|
|
220
144
|
config.agentThinkingLevels[key] = value as ThinkingLevel;
|
|
221
145
|
}
|
|
222
146
|
}
|
|
223
|
-
if (!Object.hasOwn(config.agentThinkingLevels, EXPLORER_NAME)) {
|
|
224
|
-
const legacy = entries.find(([key, value]) =>
|
|
225
|
-
key.trim() === LEGACY_EXPLORER_NAME &&
|
|
226
|
-
typeof value === "string" &&
|
|
227
|
-
(THINKING_LEVEL_VALUES as readonly string[]).includes(value)
|
|
228
|
-
);
|
|
229
|
-
if (legacy) config.agentThinkingLevels[EXPLORER_NAME] = legacy[1] as ThinkingLevel;
|
|
230
|
-
}
|
|
231
147
|
}
|
|
232
148
|
|
|
233
149
|
if (typeof raw.notifyOnReviewPass === "boolean") {
|
|
@@ -245,75 +161,11 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
245
161
|
config.agentScope = raw.agentScope;
|
|
246
162
|
}
|
|
247
163
|
|
|
248
|
-
const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
|
|
249
|
-
if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
|
|
250
|
-
|
|
251
|
-
// 0 disables worker fixes, not the independent post-writer review gate or
|
|
252
|
-
// conditional/reviewer-disabled documentation fallback.
|
|
253
|
-
if (typeof raw.maxFixRounds === "number" && Number.isFinite(raw.maxFixRounds)) {
|
|
254
|
-
config.maxFixRounds = Math.max(0, Math.min(MAX_FIX_ROUNDS_LIMIT, Math.round(raw.maxFixRounds)));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
164
|
// 0 disables the idle watchdog; otherwise clamp to [0, upper].
|
|
258
165
|
if (typeof raw.idleTimeoutSec === "number" && Number.isFinite(raw.idleTimeoutSec)) {
|
|
259
166
|
config.idleTimeoutSec = Math.max(0, Math.min(IDLE_TIMEOUT_SEC_LIMIT, Math.round(raw.idleTimeoutSec)));
|
|
260
167
|
}
|
|
261
168
|
|
|
262
|
-
if (Array.isArray(raw.announcedFeatures)) {
|
|
263
|
-
config.announcedFeatures = raw.announcedFeatures.filter(
|
|
264
|
-
(feature): feature is string => typeof feature === "string" && feature.trim().length > 0,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// One-time upgrade for configs written before cleaner shipped: a non-empty
|
|
269
|
-
// explicit enabledAgents list gets cleaner defaulted on (inserted before
|
|
270
|
-
// reviewer, matching the fresh-install order) and inherits the reviewer's
|
|
271
|
-
// configured model and thinking level — its closest peer. The stamps make
|
|
272
|
-
// the upgrade idempotent and keep a later deliberate disable from being undone.
|
|
273
|
-
if (!config.announcedFeatures.includes(CLEANER_DEFAULTED_FEATURE)) {
|
|
274
|
-
config.announcedFeatures.push(CLEANER_DEFAULTED_FEATURE);
|
|
275
|
-
if (config.enabledAgents.length > 0 && !config.enabledAgents.includes(CLEANER_NAME)) {
|
|
276
|
-
const reviewerIndex = config.enabledAgents.indexOf(REVIEWER_NAME);
|
|
277
|
-
config.enabledAgents.splice(reviewerIndex === -1 ? config.enabledAgents.length : reviewerIndex, 0, CLEANER_NAME);
|
|
278
|
-
config.announcedFeatures.push(CLEANER_AUTO_ENABLED_FEATURE);
|
|
279
|
-
let inherited = false;
|
|
280
|
-
if (!config.agentModels[CLEANER_NAME] && config.agentModels[REVIEWER_NAME]) {
|
|
281
|
-
config.agentModels[CLEANER_NAME] = config.agentModels[REVIEWER_NAME];
|
|
282
|
-
inherited = true;
|
|
283
|
-
}
|
|
284
|
-
if (!config.agentThinkingLevels[CLEANER_NAME] && config.agentThinkingLevels[REVIEWER_NAME]) {
|
|
285
|
-
config.agentThinkingLevels[CLEANER_NAME] = config.agentThinkingLevels[REVIEWER_NAME];
|
|
286
|
-
inherited = true;
|
|
287
|
-
}
|
|
288
|
-
// An old explicit list may have no reviewer overrides to copy; only the
|
|
289
|
-
// copied case is stamped so the one-time notice stays accurate.
|
|
290
|
-
if (inherited) config.announcedFeatures.push(CLEANER_INHERITED_FEATURE);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (!config.announcedFeatures.includes(DOCUMENTER_DEFAULTED_FEATURE)) {
|
|
295
|
-
config.announcedFeatures.push(DOCUMENTER_DEFAULTED_FEATURE);
|
|
296
|
-
if (config.enabledAgents.length > 0 && !config.enabledAgents.includes(DOCUMENTER_NAME)) {
|
|
297
|
-
const reviewerIndex = config.enabledAgents.indexOf(REVIEWER_NAME);
|
|
298
|
-
config.enabledAgents.splice(
|
|
299
|
-
reviewerIndex === -1 ? config.enabledAgents.length : reviewerIndex,
|
|
300
|
-
0,
|
|
301
|
-
DOCUMENTER_NAME,
|
|
302
|
-
);
|
|
303
|
-
config.announcedFeatures.push(DOCUMENTER_AUTO_ENABLED_FEATURE);
|
|
304
|
-
let inherited = false;
|
|
305
|
-
if (!config.agentModels[DOCUMENTER_NAME] && config.agentModels[EXPLORER_NAME]) {
|
|
306
|
-
config.agentModels[DOCUMENTER_NAME] = config.agentModels[EXPLORER_NAME];
|
|
307
|
-
inherited = true;
|
|
308
|
-
}
|
|
309
|
-
if (!config.agentThinkingLevels[DOCUMENTER_NAME] && config.agentThinkingLevels[EXPLORER_NAME]) {
|
|
310
|
-
config.agentThinkingLevels[DOCUMENTER_NAME] = config.agentThinkingLevels[EXPLORER_NAME];
|
|
311
|
-
inherited = true;
|
|
312
|
-
}
|
|
313
|
-
if (inherited) config.announcedFeatures.push(DOCUMENTER_INHERITED_FEATURE);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
169
|
return config;
|
|
318
170
|
}
|
|
319
171
|
|
|
@@ -323,7 +175,6 @@ function defaultConfig(): SubagentsConfig {
|
|
|
323
175
|
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
324
176
|
agentModels: {},
|
|
325
177
|
agentThinkingLevels: {},
|
|
326
|
-
announcedFeatures: [],
|
|
327
178
|
};
|
|
328
179
|
}
|
|
329
180
|
|
|
@@ -364,18 +215,6 @@ export async function loadConfig(configPath: string = getConfigPath()): Promise<
|
|
|
364
215
|
return config;
|
|
365
216
|
}
|
|
366
217
|
|
|
367
|
-
/**
|
|
368
|
-
* Synchronous load for the extension's init-time decisions (e.g. the recursion
|
|
369
|
-
* guard). Runs before any async context is available; never migrates or saves.
|
|
370
|
-
*/
|
|
371
|
-
export function loadConfigSync(configPath: string = getConfigPath()): SubagentsConfig {
|
|
372
|
-
try {
|
|
373
|
-
return normalizeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
374
|
-
} catch {
|
|
375
|
-
return defaultConfig();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
218
|
/**
|
|
380
219
|
* Save config atomically (temp file + rename) serialized through pi's per-file
|
|
381
220
|
* mutation queue so concurrent writers cannot interleave.
|
package/src/dispatch.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { realpath } from "node:fs/promises";
|
|
|
15
15
|
import { resolve } from "node:path";
|
|
16
16
|
import { Type } from "typebox";
|
|
17
17
|
import { discoverAgents, resolveAgentTools, type AgentConfig } from "./agents.ts";
|
|
18
|
+
import { MAX_CONCURRENT_SUBAGENTS } from "./background.ts";
|
|
18
19
|
import { loadConfig } from "./config.ts";
|
|
19
20
|
import { formatUsage, queuedResult } from "./format.ts";
|
|
20
21
|
import {
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
buildFixTaskBrief,
|
|
24
25
|
buildReReviewBrief,
|
|
25
26
|
documentationDisposition,
|
|
27
|
+
MAX_FIX_ROUNDS,
|
|
26
28
|
type ChainStep,
|
|
27
29
|
type ManagedWorkflowOutcome,
|
|
28
30
|
} from "./fixloop.ts";
|
|
@@ -193,8 +195,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
193
195
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
194
196
|
monitor.beginTurn();
|
|
195
197
|
const config = await loadConfig(runtime.configPath);
|
|
196
|
-
// Pick up concurrency changes from /subagents-setup without a restart.
|
|
197
|
-
runtime.backgroundQueue.setConcurrency(config.maxConcurrency);
|
|
198
198
|
|
|
199
199
|
// Finished runs leave the active monitor immediately. Their final findings
|
|
200
200
|
// are sent as a custom message that starts a follow-up turn.
|
|
@@ -493,19 +493,19 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
493
493
|
): Promise<{ lastReview?: SingleResult; lastWorker?: SingleResult }> => {
|
|
494
494
|
let lastReviewer = triggeringReviewer;
|
|
495
495
|
const outcome: { lastReview?: SingleResult; lastWorker?: SingleResult } = {};
|
|
496
|
-
for (let round = 1; round <=
|
|
496
|
+
for (let round = 1; round <= MAX_FIX_ROUNDS; round++) {
|
|
497
497
|
if (!canContinue()) break;
|
|
498
|
-
const fixRelation = `fix ${round}/${
|
|
498
|
+
const fixRelation = `fix ${round}/${MAX_FIX_ROUNDS}`;
|
|
499
499
|
const workerResult = await launchStep(
|
|
500
500
|
"worker",
|
|
501
|
-
buildFixTaskBrief(lastReviewer, round,
|
|
501
|
+
buildFixTaskBrief(lastReviewer, round, MAX_FIX_ROUNDS),
|
|
502
502
|
`fix round ${round}`,
|
|
503
503
|
{ timelineRelation: fixRelation, childRelation: fixRelation },
|
|
504
504
|
);
|
|
505
505
|
if (isFailedResult(workerResult) || !canContinue()) break;
|
|
506
506
|
outcome.lastWorker = workerResult;
|
|
507
507
|
|
|
508
|
-
const reReviewRelation = `re-review ${round}/${
|
|
508
|
+
const reReviewRelation = `re-review ${round}/${MAX_FIX_ROUNDS}`;
|
|
509
509
|
const reviewResult = await launchStep(
|
|
510
510
|
"reviewer",
|
|
511
511
|
buildReReviewBrief(lastReviewer, round, workerResult, {
|
|
@@ -586,8 +586,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
586
586
|
!isFailedResult(gateReview) &&
|
|
587
587
|
canContinue() &&
|
|
588
588
|
reviewVerdict(getResultOutput(gateReview)) === "fail" &&
|
|
589
|
-
enabled("worker")
|
|
590
|
-
request.config.maxFixRounds > 0
|
|
589
|
+
enabled("worker")
|
|
591
590
|
) {
|
|
592
591
|
fixOutcome = await runFixRounds(gateReview);
|
|
593
592
|
}
|
|
@@ -620,12 +619,12 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
620
619
|
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
621
620
|
// editor available immediately; completion messages later wake the main agent.
|
|
622
621
|
if (params.tasks && params.tasks.length > 0) {
|
|
623
|
-
if (params.tasks.length >
|
|
622
|
+
if (params.tasks.length > MAX_CONCURRENT_SUBAGENTS) {
|
|
624
623
|
return {
|
|
625
624
|
content: [
|
|
626
625
|
{
|
|
627
626
|
type: "text",
|
|
628
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${
|
|
627
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_CONCURRENT_SUBAGENTS}.`,
|
|
629
628
|
},
|
|
630
629
|
],
|
|
631
630
|
details: makeDetails("parallel", true)([]),
|
package/src/fixloop.ts
CHANGED
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
|
|
19
19
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
20
20
|
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Worker fixes allowed after REVIEW_FAIL. Each fix is followed by a reviewer
|
|
24
|
+
* re-review; this cap does not suppress the post-writer review gate or its
|
|
25
|
+
* conditional/reviewer-disabled documentation fallback.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_FIX_ROUNDS = 2;
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Whether a completed result should trigger the auto-fix loop instead of being
|
|
@@ -27,8 +33,7 @@ import type { SubagentsConfig } from "./config.ts";
|
|
|
27
33
|
* normally. Loop-internal re-review results never reach this path (they are
|
|
28
34
|
* awaited inside the loop, not delivered through the completion flow).
|
|
29
35
|
*/
|
|
30
|
-
export function shouldTriggerFixLoop(result: SingleResult
|
|
31
|
-
if (config.maxFixRounds <= 0) return false;
|
|
36
|
+
export function shouldTriggerFixLoop(result: SingleResult): boolean {
|
|
32
37
|
if (result.agent !== "reviewer") return false;
|
|
33
38
|
if (isFailedResult(result)) return false;
|
|
34
39
|
// A dispatch crash (spawn infra, delivery API, ...) is never a real review
|
|
@@ -97,8 +102,8 @@ export function canStartManagedWorkflow(
|
|
|
97
102
|
if (isWriteCapableAgent(agent)) return true;
|
|
98
103
|
if (agent.name === "reviewer") {
|
|
99
104
|
// Hold a stable diff snapshot against every discoverable writer even when
|
|
100
|
-
// this review is advisory
|
|
101
|
-
//
|
|
105
|
+
// this review is advisory. Classification happens only after the read-only
|
|
106
|
+
// child returns, too late to acquire the lane safely.
|
|
102
107
|
return availability.writer;
|
|
103
108
|
}
|
|
104
109
|
return false;
|
|
@@ -108,7 +113,6 @@ export function canStartManagedWorkflow(
|
|
|
108
113
|
* machine verdict is advisory and cannot start any write-capable child. */
|
|
109
114
|
export function getManagedWorkflowPlan(
|
|
110
115
|
result: SingleResult,
|
|
111
|
-
config: SubagentsConfig,
|
|
112
116
|
availability: WorkflowAgentAvailability,
|
|
113
117
|
): ManagedWorkflowPlan | undefined {
|
|
114
118
|
if (result.parked || result.dispatchFailed || isFailedResult(result)) return undefined;
|
|
@@ -135,7 +139,7 @@ export function getManagedWorkflowPlan(
|
|
|
135
139
|
) {
|
|
136
140
|
return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
|
|
137
141
|
}
|
|
138
|
-
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result
|
|
142
|
+
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result)) {
|
|
139
143
|
return { kind: "auto-fix", initialRelation: "initial review" };
|
|
140
144
|
}
|
|
141
145
|
return undefined;
|
package/src/index.ts
CHANGED
|
@@ -86,7 +86,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
86
86
|
enabledNames: config.enabledAgents,
|
|
87
87
|
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
88
88
|
});
|
|
89
|
-
const directive = buildDelegationDirective(agents
|
|
89
|
+
const directive = buildDelegationDirective(agents);
|
|
90
90
|
if (!directive) return undefined;
|
|
91
91
|
return { systemPrompt: `${event.systemPrompt}\n${directive}` };
|
|
92
92
|
});
|
package/src/models.ts
CHANGED
|
@@ -86,6 +86,22 @@ export function findModelByRef(
|
|
|
86
86
|
return normalized ? models.find((model) => modelRef(model) === normalized) : undefined;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** Split persisted agent model overrides into the ones Pi still reports as
|
|
90
|
+
* available and the stale ones. Stale refs are dropped at session start (with
|
|
91
|
+
* a user notice) so the config never carries models that can no longer run. */
|
|
92
|
+
export function filterUnavailableModelOverrides(
|
|
93
|
+
agentModels: Record<string, string>,
|
|
94
|
+
models: readonly Model<Api>[],
|
|
95
|
+
): { kept: Record<string, string>; dropped: Array<{ agent: string; ref: string }> } {
|
|
96
|
+
const kept: Record<string, string> = {};
|
|
97
|
+
const dropped: Array<{ agent: string; ref: string }> = [];
|
|
98
|
+
for (const [agent, ref] of Object.entries(agentModels)) {
|
|
99
|
+
if (findModelByRef(models, ref)) kept[agent] = ref;
|
|
100
|
+
else dropped.push({ agent, ref });
|
|
101
|
+
}
|
|
102
|
+
return { kept, dropped };
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
/**
|
|
90
106
|
* Resolve one agent's runtime route:
|
|
91
107
|
*
|
package/src/prompt.ts
CHANGED
|
@@ -13,7 +13,6 @@ function bullets(lines: readonly string[]): string {
|
|
|
13
13
|
|
|
14
14
|
export function buildDelegationDirective(
|
|
15
15
|
agents: AgentConfig[],
|
|
16
|
-
options: { maxFixRounds?: number } = {},
|
|
17
16
|
): string {
|
|
18
17
|
if (agents.length === 0) return "";
|
|
19
18
|
|
|
@@ -24,7 +23,7 @@ export function buildDelegationDirective(
|
|
|
24
23
|
const hasDocumenter = agents.some((agent) => agent.name === "documenter");
|
|
25
24
|
const hasReviewer = agents.some((agent) => agent.name === "reviewer");
|
|
26
25
|
const hasMultiple = agents.length > 1;
|
|
27
|
-
const autoFixEnabled = hasWorker
|
|
26
|
+
const autoFixEnabled = hasWorker;
|
|
28
27
|
const codeWriterNames = [
|
|
29
28
|
...(hasWorker ? ["worker"] : []),
|
|
30
29
|
...(hasCleaner ? ["cleaner"] : []),
|
|
@@ -101,10 +100,10 @@ export function buildDelegationDirective(
|
|
|
101
100
|
? [
|
|
102
101
|
...(hasDocumenter
|
|
103
102
|
? [
|
|
104
|
-
`A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps bounded worker/reviewer auto-fix, with docs considered only after its terminal REVIEW_PASS." : "cannot start fixes while worker
|
|
103
|
+
`A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps bounded worker/reviewer auto-fix, with docs considered only after its terminal REVIEW_PASS." : "cannot start fixes while worker is disabled."}`,
|
|
105
104
|
]
|
|
106
105
|
: []),
|
|
107
|
-
"Resolve every gate finding; do not bypass the
|
|
106
|
+
"Resolve every gate finding; do not bypass the auto-fix/re-review cap. A reviewer report without a standalone VERDICT is advisory and cannot trigger writes.",
|
|
108
107
|
"Use multi-model cross-review only when explicitly requested or for genuinely high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
|
|
109
108
|
]
|
|
110
109
|
: []),
|
package/src/runtime.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { rmSync } from "node:fs";
|
|
13
|
-
import { BackgroundTaskQueue } from "./background.ts";
|
|
13
|
+
import { BackgroundTaskQueue, MAX_CONCURRENT_SUBAGENTS } from "./background.ts";
|
|
14
14
|
import {
|
|
15
15
|
completionGroupTriggersTurn,
|
|
16
16
|
createCompletionBatcher,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type CompletionBatcher,
|
|
20
20
|
type CompletionMessageItem,
|
|
21
21
|
} from "./completion.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { type ThinkingLevel } from "./config.ts";
|
|
23
23
|
import { isRunActiveStatus, monitor } from "./monitor.ts";
|
|
24
24
|
import {
|
|
25
25
|
persistRecoveryRecords,
|
|
@@ -124,10 +124,7 @@ export interface SubagentRuntime {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRuntime {
|
|
127
|
-
|
|
128
|
-
// async load runs per tool call.
|
|
129
|
-
const initialConfig = loadConfigSync(configPath);
|
|
130
|
-
const backgroundQueue = new BackgroundTaskQueue(initialConfig.maxConcurrency);
|
|
127
|
+
const backgroundQueue = new BackgroundTaskQueue(MAX_CONCURRENT_SUBAGENTS);
|
|
131
128
|
|
|
132
129
|
const runtime: SubagentRuntime = {
|
|
133
130
|
configPath,
|
package/src/setup.ts
CHANGED
|
@@ -13,13 +13,9 @@ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import {
|
|
14
14
|
AGENT_SCOPE_VALUES,
|
|
15
15
|
BUILTIN_AGENT_NAMES,
|
|
16
|
-
CLEANER_DEFAULTED_FEATURE,
|
|
17
|
-
DOCUMENTER_DEFAULTED_FEATURE,
|
|
18
16
|
DEFAULT_CONFIG,
|
|
19
17
|
DEFAULT_ENABLED_AGENTS,
|
|
20
18
|
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
21
|
-
DEFAULT_MAX_CONCURRENCY,
|
|
22
|
-
DEFAULT_MAX_FIX_ROUNDS,
|
|
23
19
|
DEFAULT_THINKING_LEVEL,
|
|
24
20
|
type AgentScope,
|
|
25
21
|
type SubagentsConfig,
|
|
@@ -243,8 +239,6 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
|
|
|
243
239
|
return choice.startsWith("On");
|
|
244
240
|
}
|
|
245
241
|
|
|
246
|
-
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
247
|
-
const FIX_ROUNDS_STEPS = [0, 1, 2, 3, 5];
|
|
248
242
|
const IDLE_TIMEOUT_STEPS = [0, 30, 60, 90, 120, 180, 300, 600];
|
|
249
243
|
|
|
250
244
|
async function pickCount(
|
|
@@ -299,22 +293,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
299
293
|
if (injection === undefined) return false;
|
|
300
294
|
const scope = await pickScope(ctx, base.agentScope);
|
|
301
295
|
if (scope === undefined) return false;
|
|
302
|
-
const maxConcurrency = await pickCount(
|
|
303
|
-
ctx,
|
|
304
|
-
"Max sub-agents running at once?",
|
|
305
|
-
CONCURRENCY_STEPS,
|
|
306
|
-
base.maxConcurrency,
|
|
307
|
-
DEFAULT_MAX_CONCURRENCY,
|
|
308
|
-
);
|
|
309
|
-
if (maxConcurrency === undefined) return false;
|
|
310
|
-
const maxFixRounds = await pickCount(
|
|
311
|
-
ctx,
|
|
312
|
-
"Reviewer worker-fix rounds? (0 = no automatic fixes)",
|
|
313
|
-
FIX_ROUNDS_STEPS,
|
|
314
|
-
base.maxFixRounds,
|
|
315
|
-
DEFAULT_MAX_FIX_ROUNDS,
|
|
316
|
-
);
|
|
317
|
-
if (maxFixRounds === undefined) return false;
|
|
318
296
|
const idleTimeoutSec = await pickCount(
|
|
319
297
|
ctx,
|
|
320
298
|
"Idle timeout in seconds? (0 = disabled)",
|
|
@@ -333,16 +311,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
333
311
|
maxResultLines: base.maxResultLines,
|
|
334
312
|
proactiveInjection: injection,
|
|
335
313
|
agentScope: scope,
|
|
336
|
-
maxConcurrency,
|
|
337
|
-
maxFixRounds,
|
|
338
314
|
idleTimeoutSec,
|
|
339
|
-
// Full setup is an explicit decision point: mark role-enable migrations as
|
|
340
|
-
// processed so the user's saved selection is kept as-is.
|
|
341
|
-
announcedFeatures: [...new Set([
|
|
342
|
-
...base.announcedFeatures,
|
|
343
|
-
CLEANER_DEFAULTED_FEATURE,
|
|
344
|
-
DOCUMENTER_DEFAULTED_FEATURE,
|
|
345
|
-
])],
|
|
346
315
|
};
|
|
347
316
|
await saveConfig(next, configPath);
|
|
348
317
|
ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
|
|
@@ -357,8 +326,6 @@ async function updateRuntimeSetting(
|
|
|
357
326
|
const choice = await ctx.ui.select("Runtime setting", [
|
|
358
327
|
"Proactive injection",
|
|
359
328
|
"Agent scope",
|
|
360
|
-
"Max concurrency",
|
|
361
|
-
"Reviewer worker-fix rounds",
|
|
362
329
|
"Idle timeout",
|
|
363
330
|
]);
|
|
364
331
|
if (choice === undefined) return undefined;
|
|
@@ -371,14 +338,6 @@ async function updateRuntimeSetting(
|
|
|
371
338
|
const value = await pickScope(ctx, config.agentScope);
|
|
372
339
|
if (value === undefined) continue;
|
|
373
340
|
next.agentScope = value;
|
|
374
|
-
} else if (choice.startsWith("Max concurrency")) {
|
|
375
|
-
const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
|
|
376
|
-
if (value === undefined) continue;
|
|
377
|
-
next.maxConcurrency = value;
|
|
378
|
-
} else if (choice.startsWith("Reviewer")) {
|
|
379
|
-
const value = await pickCount(ctx, "Reviewer worker-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
|
|
380
|
-
if (value === undefined) continue;
|
|
381
|
-
next.maxFixRounds = value;
|
|
382
341
|
} else {
|
|
383
342
|
const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
|
|
384
343
|
if (value === undefined) continue;
|
package/src/spawn.ts
CHANGED
|
@@ -272,7 +272,7 @@ export function isRetryableStartupFailure(result: SingleResult, durationMs: numb
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
export function formatStartupRetryExhaustedError(model: string, attempts: number): string {
|
|
275
|
-
return `Subagent failed to start after ${attempts} attempt${attempts === 1 ? "" : "s"} on ${model}: the child failed before its initial RPC prompt was dispatched and produced no model, tool, output, or usage activity. This is typically a concurrent pi startup race (several sub-agents starting at once). Retry the dispatch, or
|
|
275
|
+
return `Subagent failed to start after ${attempts} attempt${attempts === 1 ? "" : "s"} on ${model}: the child failed before its initial RPC prompt was dispatched and produced no model, tool, output, or usage activity. This is typically a concurrent pi startup race (several sub-agents starting at once). Retry the dispatch, or dispatch fewer sub-agents at once.`;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
export async function waitForStartupRetry(delayMs: number, signal?: AbortSignal): Promise<boolean> {
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -720,7 +720,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
720
720
|
if (!ownsResumeReservation(thread, reservation)) {
|
|
721
721
|
throw new Error(`Run #${runId} changed while resume configuration was loading.`);
|
|
722
722
|
}
|
|
723
|
-
runtime.backgroundQueue.setConcurrency(currentConfig.maxConcurrency);
|
|
724
723
|
const currentAgents = discoverAgents(currentCtx.cwd, {
|
|
725
724
|
scope: currentConfig.agentScope,
|
|
726
725
|
enabledNames: currentConfig.enabledAgents,
|
|
@@ -888,7 +887,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
888
887
|
if (!ownsFork()) throw new Error(`Run #${runId} changed while its retained session was being forked.`);
|
|
889
888
|
const currentConfig = await loadConfig(runtime.configPath);
|
|
890
889
|
if (!ownsFork()) throw new Error(`Run #${runId} changed while fork configuration was loading.`);
|
|
891
|
-
runtime.backgroundQueue.setConcurrency(currentConfig.maxConcurrency);
|
|
892
890
|
const currentAgents = discoverAgents(currentCtx.cwd, {
|
|
893
891
|
scope: currentConfig.agentScope,
|
|
894
892
|
enabledNames: currentConfig.enabledAgents,
|
|
@@ -971,7 +969,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
971
969
|
let activeIdleTimeoutMs = runConfig.idleTimeoutSec * 1000;
|
|
972
970
|
try {
|
|
973
971
|
const startConfig = await loadConfig(runtime.configPath);
|
|
974
|
-
runtime.backgroundQueue.setConcurrency(startConfig.maxConcurrency);
|
|
975
972
|
const resolvedStart = resolveDispatchModelRoute(agent, startConfig, runCtx);
|
|
976
973
|
activeRoute = isolation === "worktree"
|
|
977
974
|
? { ...resolvedStart, agent: withWorktreeSystemPrompt(resolvedStart.agent) }
|
|
@@ -1064,7 +1061,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1064
1061
|
|
|
1065
1062
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
1066
1063
|
let workflowOutcome: ManagedWorkflowOutcome | undefined;
|
|
1067
|
-
const workflowPlan = getManagedWorkflowPlan(result,
|
|
1064
|
+
const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability);
|
|
1068
1065
|
if (workflowPlan && runtime.sessionActive) {
|
|
1069
1066
|
thread.state = "running";
|
|
1070
1067
|
// The stable parent row now represents workflow ownership, not whichever
|