@ferris1225/pi-subagents 0.9.0 → 0.11.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 +29 -6
- package/agents/explore.md +1 -0
- package/agents/reviewer.md +63 -45
- package/agents/worker.md +1 -0
- package/package.json +1 -1
- package/src/agents.ts +23 -6
- package/src/completion.ts +153 -0
- package/src/config.ts +74 -3
- package/src/fixloop.ts +76 -0
- package/src/index.ts +172 -24
- package/src/monitor.ts +18 -1
- package/src/prompt.ts +5 -4
- package/src/setup.ts +124 -19
- package/src/spawn.ts +52 -1
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ agent, and keep the workflow moving without manual polling.
|
|
|
21
21
|
- **Parallel fan-out** — run independent tasks together, with a bounded background queue.
|
|
22
22
|
- **Live progress** — a TUI widget shows each agent's status, activity, model, usage, and
|
|
23
23
|
elapsed time; completion also produces a concise notification.
|
|
24
|
-
- **Per-agent configuration** — enable agents,
|
|
25
|
-
concurrency limits, and choose discovery scope from `/subagents-setup`.
|
|
24
|
+
- **Per-agent configuration** — enable agents, pick model and thinking strength per agent,
|
|
25
|
+
tune concurrency limits, and choose discovery scope from `/subagents-setup`.
|
|
26
26
|
- **Leaf processes** — child agents cannot access the `subagent` tool, so delegation cannot
|
|
27
27
|
recurse.
|
|
28
28
|
|
|
@@ -122,18 +122,31 @@ Start dependent work after the relevant result has been delivered to the main ag
|
|
|
122
122
|
Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
|
|
123
123
|
`PI_CODING_AGENT_DIR` when set.
|
|
124
124
|
|
|
125
|
+
The `/subagents-setup` wizard drives the main fields interactively: for each agent, picking a
|
|
126
|
+
model is immediately followed by picking that agent's thinking strength (or inheriting the
|
|
127
|
+
agent's default — its frontmatter `thinking`, else the global default). The global
|
|
128
|
+
`thinkingLevel` is set first and applies as the final fallback. `notifyOnReviewPass` and
|
|
129
|
+
`maxResultLines` are edited directly in `pi-subagents.json`.
|
|
130
|
+
|
|
125
131
|
```json
|
|
126
132
|
{
|
|
127
133
|
"enabledAgents": ["explore", "worker", "reviewer"],
|
|
128
134
|
"agentModels": {
|
|
129
135
|
"explore": "anthropic/claude-haiku-4-5"
|
|
130
136
|
},
|
|
131
|
-
"
|
|
137
|
+
"agentThinkingLevels": {
|
|
138
|
+
"explore": "low",
|
|
139
|
+
"worker": "high"
|
|
140
|
+
},
|
|
141
|
+
"thinkingLevel": "high",
|
|
142
|
+
"notifyOnReviewPass": false,
|
|
143
|
+
"maxResultLines": 80,
|
|
132
144
|
"proactiveInjection": true,
|
|
133
145
|
"agentScope": "user",
|
|
134
146
|
"maxConcurrency": 4,
|
|
135
147
|
"maxParallelTasks": 8,
|
|
136
|
-
"maxSubagentDepth": 1
|
|
148
|
+
"maxSubagentDepth": 1,
|
|
149
|
+
"maxFixRounds": 2
|
|
137
150
|
}
|
|
138
151
|
```
|
|
139
152
|
|
|
@@ -141,12 +154,16 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
|
|
|
141
154
|
| --- | --- |
|
|
142
155
|
| `enabledAgents` | Agent names exposed to discovery and prompt injection. An empty array disables all agents. |
|
|
143
156
|
| `agentModels` | Optional `provider/model-id` override per agent. |
|
|
144
|
-
| `
|
|
157
|
+
| `agentThinkingLevels` | Optional thinking level per agent; agents without an entry use the agent's frontmatter `thinking`, then `thinkingLevel`. |
|
|
158
|
+
| `thinkingLevel` | Default thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (default `high`). |
|
|
159
|
+
| `notifyOnReviewPass` | When `true`, a passing reviewer result is delivered without waking the main agent (default `false`). |
|
|
160
|
+
| `maxResultLines` | Max lines of a sub-agent result carried in the completion message (default `80`). Longer results are truncated; the full text is written to a temp file whose path is included in the message. |
|
|
145
161
|
| `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
|
|
146
162
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
147
163
|
| `maxConcurrency` | How many sub-agent processes run at once (1–16, default 4). Extra work waits in the queue. |
|
|
148
164
|
| `maxParallelTasks` | Maximum tasks accepted by one parallel `subagent` call (1–32, default 8). |
|
|
149
165
|
| `maxSubagentDepth` | Depth at which the `subagent` tool is no longer registered (default 1: the main session delegates, children are leaf processes). `0` disables the tool entirely. Read once at extension load. |
|
|
166
|
+
| `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. |
|
|
150
167
|
|
|
151
168
|
### Configuration migration
|
|
152
169
|
|
|
@@ -155,7 +172,7 @@ The config file migrates itself on load — no manual steps after an upgrade:
|
|
|
155
172
|
- **Schema upgrades** — a config written by an older version (missing newer keys or
|
|
156
173
|
holding invalid values) is normalized and saved back with the new fields filled in.
|
|
157
174
|
- **Removed agents** — agents no longer shipped (e.g. the old `plan` agent) are stripped
|
|
158
|
-
from `enabledAgents` and `
|
|
175
|
+
from `enabledAgents`, `agentModels`, and `agentThinkingLevels` automatically.
|
|
159
176
|
|
|
160
177
|
Model selection uses this precedence:
|
|
161
178
|
|
|
@@ -166,6 +183,8 @@ configured agent model → current main-session model → agent frontmatter mode
|
|
|
166
183
|
Unavailable configured models are replaced with a usable current-session model when possible
|
|
167
184
|
and the repaired configuration is saved.
|
|
168
185
|
|
|
186
|
+
Thinking strength uses this precedence: `agentThinkingLevels` entry → agent frontmatter `thinking` → `thinkingLevel` default.
|
|
187
|
+
|
|
169
188
|
## Agent discovery and overrides
|
|
170
189
|
|
|
171
190
|
- Built-in agents are shipped with the package.
|
|
@@ -176,6 +195,10 @@ and the repaired configuration is saved.
|
|
|
176
195
|
Use a matching Markdown filename and `name` field to replace a built-in agent. Keep the task
|
|
177
196
|
brief explicit: include the goal, relevant paths, constraints, and expected handoff.
|
|
178
197
|
|
|
198
|
+
Optional frontmatter fields: `model` (default model reference) and `thinking` (default
|
|
199
|
+
thinking strength). Both are overridden by `agentModels` / `agentThinkingLevels` in
|
|
200
|
+
`pi-subagents.json` when set.
|
|
201
|
+
|
|
179
202
|
## Development
|
|
180
203
|
|
|
181
204
|
```bash
|
package/agents/explore.md
CHANGED
|
@@ -3,6 +3,7 @@ name: explore
|
|
|
3
3
|
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.
|
|
4
4
|
tools: read, grep, find, ls, bash
|
|
5
5
|
model: claude-haiku-4-5
|
|
6
|
+
thinking: low
|
|
6
7
|
# Model selection: SPEED over depth. Pick the fastest available model.
|
|
7
8
|
# What matters: fast grep/find/read, structured output. What doesn't: deep reasoning.
|
|
8
9
|
---
|
package/agents/reviewer.md
CHANGED
|
@@ -1,45 +1,63 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: reviewer
|
|
3
|
-
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.
|
|
4
|
-
tools: read, grep, find, ls, bash
|
|
5
|
-
model: claude-sonnet-4-5
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
##
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
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.
|
|
4
|
+
tools: read, grep, find, ls, bash
|
|
5
|
+
model: claude-sonnet-4-5
|
|
6
|
+
thinking: high
|
|
7
|
+
# Model selection: ATTENTION TO DETAIL + SECURITY AWARENESS. This is the quality gate —
|
|
8
|
+
# use the strongest available reasoning model.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
## Hard constraints
|
|
14
|
+
- You are READ-ONLY. Do NOT modify files, run builds, or run tests.
|
|
15
|
+
- Bash is for read-only commands only: `git diff`, `git status`, `git log`, `git show`, `grep`, `find`, `cat`.
|
|
16
|
+
- Assume tool permissions are not perfectly enforceable; keep every command strictly read-only by intent.
|
|
17
|
+
|
|
18
|
+
## Review types you handle
|
|
19
|
+
Match the type to the task brief; the hunt checklist below applies to every type.
|
|
20
|
+
|
|
21
|
+
### 1. Code diffs (default)
|
|
22
|
+
1. Run `git diff` and `git status` to see the recent changes. If a specific file set was given, read those files.
|
|
23
|
+
2. Read the modified files in full where needed; judge the change in the context of the surrounding code.
|
|
24
|
+
|
|
25
|
+
### 2. Plans
|
|
26
|
+
Validate a proposed plan for feasibility and completeness: missing steps, hidden risks, alignment with the existing architecture, and whether the scope is appropriately bounded.
|
|
27
|
+
|
|
28
|
+
### 3. Proposed solutions
|
|
29
|
+
Evaluate a suggested approach: correctness and tradeoffs, fit with existing codebase patterns, simpler alternatives, edge cases the proposal may miss.
|
|
30
|
+
|
|
31
|
+
### 4. Codebase health
|
|
32
|
+
Assess key files, tests, and structure: architecture drift or tech debt, inconsistent patterns, untested or undocumented areas, obvious bugs, fragile code.
|
|
33
|
+
|
|
34
|
+
### 5. Specific PR or issue
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
## Hunt across these categories
|
|
38
|
+
- Logic bugs, off-by-one, wrong edge-case handling.
|
|
39
|
+
- Error handling gaps; swallowed failures; unreported unrun checks.
|
|
40
|
+
- Security: injection, path traversal, secrets in code/logs, trusting untrusted input.
|
|
41
|
+
- Concurrency: shared mutable state, locks held across await, races.
|
|
42
|
+
- Encoding/Unicode: assuming `char*`/files/CLI text is UTF-8; wrong `A` vs `W` Win32 APIs; boundary conversions.
|
|
43
|
+
- Resource leaks; violations of the project's stated conventions.
|
|
44
|
+
- Classify severity honestly. Distinguish blockers from nits; do not pad with style preferences.
|
|
45
|
+
|
|
46
|
+
## Collaboration
|
|
47
|
+
- Independent of `worker` by design — your verdict is the gate before commit. Fix nothing yourself; report so the caller can dispatch a worker.
|
|
48
|
+
|
|
49
|
+
## Output format
|
|
50
|
+
## Files Reviewed
|
|
51
|
+
- `path/to/file.ts`
|
|
52
|
+
## Critical (must fix)
|
|
53
|
+
- `file.ts:42` — concrete issue and why it breaks.
|
|
54
|
+
## Warnings (should fix)
|
|
55
|
+
- `file.ts:10` — issue and suggested direction.
|
|
56
|
+
## Suggestions (consider)
|
|
57
|
+
- Optional improvements.
|
|
58
|
+
## Verdict
|
|
59
|
+
One of: APPROVE / APPROVE_WITH_NITS / REQUEST_CHANGES, plus a 2-3 sentence rationale.
|
|
60
|
+
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE or APPROVE_WITH_NITS; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
|
|
61
|
+
|
|
62
|
+
## Quality standards
|
|
63
|
+
Specific file paths and line numbers. No vague feedback. A clean report means you looked hard, not that you found nothing to say.
|
package/agents/worker.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: worker
|
|
3
3
|
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.
|
|
4
4
|
model: claude-sonnet-4-5
|
|
5
|
+
thinking: high
|
|
5
6
|
# Model selection: CODING ABILITY + TOOL USE. The primary implementation model —
|
|
6
7
|
# balance quality against cost. No `tools` field => inherits all tools (full capability).
|
|
7
8
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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/agents.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { type Dirent, existsSync, readdirSync, readFileSync, statSync } from "no
|
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import type
|
|
18
|
+
import { THINKING_LEVEL_VALUES, type AgentScope, type ThinkingLevel } from "./config.ts";
|
|
19
19
|
|
|
20
20
|
export type AgentSource = "builtin" | "user" | "project";
|
|
21
21
|
|
|
@@ -24,6 +24,8 @@ export interface AgentConfig {
|
|
|
24
24
|
description: string;
|
|
25
25
|
tools?: string[];
|
|
26
26
|
model?: string;
|
|
27
|
+
/** Per-agent default thinking strength (frontmatter `thinking`); config override wins. */
|
|
28
|
+
thinking?: ThinkingLevel;
|
|
27
29
|
systemPrompt: string;
|
|
28
30
|
source: AgentSource;
|
|
29
31
|
filePath: string;
|
|
@@ -38,6 +40,11 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
38
40
|
/** <package>/agents — the agents shipped with this extension. */
|
|
39
41
|
export const BUILTIN_AGENTS_DIR = join(here, "..", "agents");
|
|
40
42
|
|
|
43
|
+
/** Agents shipped with the package (used by the setup wizard for per-agent defaults). */
|
|
44
|
+
export function loadBuiltinAgents(): AgentConfig[] {
|
|
45
|
+
return loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
42
49
|
const agents: AgentConfig[] = [];
|
|
43
50
|
if (!existsSync(dir)) return agents;
|
|
@@ -62,19 +69,29 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
72
|
+
// YAML values are not guaranteed strings; anything non-string is invalid for these fields.
|
|
73
|
+
const str = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
|
|
74
|
+
const name = str(frontmatter.name);
|
|
75
|
+
const description = str(frontmatter.description);
|
|
65
76
|
// name + description are required; skip malformed files silently.
|
|
66
|
-
if (!
|
|
77
|
+
if (!name || !description) continue;
|
|
67
78
|
|
|
68
|
-
const
|
|
79
|
+
const rawTools = str(frontmatter.tools);
|
|
80
|
+
const tools = rawTools
|
|
69
81
|
?.split(",")
|
|
70
82
|
.map((t) => t.trim())
|
|
71
83
|
.filter(Boolean);
|
|
84
|
+
const rawThinking = str(frontmatter.thinking)?.trim();
|
|
85
|
+
const thinking = (THINKING_LEVEL_VALUES as readonly string[]).includes(rawThinking ?? "")
|
|
86
|
+
? (rawThinking as ThinkingLevel)
|
|
87
|
+
: undefined;
|
|
72
88
|
|
|
73
89
|
agents.push({
|
|
74
|
-
name
|
|
75
|
-
description
|
|
90
|
+
name,
|
|
91
|
+
description,
|
|
76
92
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
77
|
-
model: frontmatter.model,
|
|
93
|
+
model: str(frontmatter.model),
|
|
94
|
+
...(thinking ? { thinking } : {}),
|
|
78
95
|
systemPrompt: body,
|
|
79
96
|
source,
|
|
80
97
|
filePath,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart batching for successful background completions.
|
|
3
|
+
*
|
|
4
|
+
* A short debounce coalesces sibling runs while a max-wait timer, measured from
|
|
5
|
+
* the first item in the open group, bounds delivery latency. Runs that finish
|
|
6
|
+
* shortly after an emitted group use a smaller straggler window. Failures are
|
|
7
|
+
* intentionally handled by the caller: flush held successes, then emit the
|
|
8
|
+
* failure directly so it is never delayed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
12
|
+
|
|
13
|
+
export interface CompletionBatchTimings {
|
|
14
|
+
debounceMs: number;
|
|
15
|
+
maxWaitMs: number;
|
|
16
|
+
stragglerDebounceMs: number;
|
|
17
|
+
stragglerMaxWaitMs: number;
|
|
18
|
+
stragglerWindowMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
22
|
+
debounceMs: 150,
|
|
23
|
+
maxWaitMs: 1_000,
|
|
24
|
+
stragglerDebounceMs: 75,
|
|
25
|
+
stragglerMaxWaitMs: 400,
|
|
26
|
+
stragglerWindowMs: 2_000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type TimerHandle = unknown;
|
|
30
|
+
|
|
31
|
+
export interface TimerApi {
|
|
32
|
+
setTimeout(handler: () => void, delayMs: number): TimerHandle;
|
|
33
|
+
clearTimeout(handle: TimerHandle): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultTimers: TimerApi = {
|
|
37
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
38
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function unrefHandle(handle: TimerHandle): void {
|
|
42
|
+
if (
|
|
43
|
+
handle &&
|
|
44
|
+
typeof handle === "object" &&
|
|
45
|
+
"unref" in handle &&
|
|
46
|
+
typeof (handle as { unref: unknown }).unref === "function"
|
|
47
|
+
) {
|
|
48
|
+
(handle as { unref: () => void }).unref();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CompletionBatcherOptions<T> {
|
|
53
|
+
emit: (items: T[]) => void;
|
|
54
|
+
timings?: Partial<CompletionBatchTimings>;
|
|
55
|
+
timers?: TimerApi;
|
|
56
|
+
now?: () => number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CompletionBatcher<T> {
|
|
60
|
+
/** Add an item to the current debounced group. */
|
|
61
|
+
push(item: T): void;
|
|
62
|
+
/** Emit any held items immediately as one group. */
|
|
63
|
+
flush(): void;
|
|
64
|
+
/** Clear timers and return held items without emitting them. */
|
|
65
|
+
dispose(): T[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
69
|
+
const timers = options.timers ?? defaultTimers;
|
|
70
|
+
const now = options.now ?? Date.now;
|
|
71
|
+
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
72
|
+
let pending: T[] = [];
|
|
73
|
+
let debounceTimer: TimerHandle | null = null;
|
|
74
|
+
let maxWaitTimer: TimerHandle | null = null;
|
|
75
|
+
let straggler = false;
|
|
76
|
+
let lastEmitAt: number | null = null;
|
|
77
|
+
|
|
78
|
+
const clearTimers = (): void => {
|
|
79
|
+
if (debounceTimer !== null) {
|
|
80
|
+
timers.clearTimeout(debounceTimer);
|
|
81
|
+
debounceTimer = null;
|
|
82
|
+
}
|
|
83
|
+
if (maxWaitTimer !== null) {
|
|
84
|
+
timers.clearTimeout(maxWaitTimer);
|
|
85
|
+
maxWaitTimer = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const emitGroup = (): void => {
|
|
90
|
+
clearTimers();
|
|
91
|
+
if (pending.length === 0) return;
|
|
92
|
+
const items = pending;
|
|
93
|
+
pending = [];
|
|
94
|
+
lastEmitAt = now();
|
|
95
|
+
options.emit(items);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
push(item: T): void {
|
|
100
|
+
if (pending.length === 0) {
|
|
101
|
+
straggler = lastEmitAt !== null && now() - lastEmitAt < timings.stragglerWindowMs;
|
|
102
|
+
}
|
|
103
|
+
pending.push(item);
|
|
104
|
+
|
|
105
|
+
if (debounceTimer !== null) timers.clearTimeout(debounceTimer);
|
|
106
|
+
const debounceDelay = straggler ? timings.stragglerDebounceMs : timings.debounceMs;
|
|
107
|
+
debounceTimer = timers.setTimeout(emitGroup, debounceDelay);
|
|
108
|
+
unrefHandle(debounceTimer);
|
|
109
|
+
|
|
110
|
+
if (maxWaitTimer === null) {
|
|
111
|
+
const maxWaitDelay = straggler ? timings.stragglerMaxWaitMs : timings.maxWaitMs;
|
|
112
|
+
maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);
|
|
113
|
+
unrefHandle(maxWaitTimer);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
flush: emitGroup,
|
|
117
|
+
dispose(): T[] {
|
|
118
|
+
clearTimers();
|
|
119
|
+
const abandoned = pending;
|
|
120
|
+
pending = [];
|
|
121
|
+
return abandoned;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CompletionMessageItem {
|
|
127
|
+
agent: string;
|
|
128
|
+
block: string;
|
|
129
|
+
triggerTurn: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Keep the established single-result shape; add a summary only for real groups. */
|
|
133
|
+
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
134
|
+
if (items.length === 0) return "";
|
|
135
|
+
if (items.length === 1) return items[0].block;
|
|
136
|
+
const agents = items.map((item) => item.agent).join(", ");
|
|
137
|
+
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
141
|
+
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
142
|
+
return items.some((item) => item.triggerTurn);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Passing reviewer notifications may opt out of waking; every other result wakes. */
|
|
146
|
+
export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
|
|
147
|
+
if (isFailedResult(result)) return true;
|
|
148
|
+
return !(
|
|
149
|
+
notifyOnReviewPass &&
|
|
150
|
+
result.agent === "reviewer" &&
|
|
151
|
+
reviewVerdict(getResultOutput(result)) === "pass"
|
|
152
|
+
);
|
|
153
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -36,7 +36,12 @@ export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
|
36
36
|
/** Thinking levels accepted by pi's `--thinking` option. */
|
|
37
37
|
export const THINKING_LEVEL_VALUES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
38
38
|
export type ThinkingLevel = (typeof THINKING_LEVEL_VALUES)[number];
|
|
39
|
-
export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "
|
|
39
|
+
export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "high";
|
|
40
|
+
|
|
41
|
+
/** How many lines of a sub-agent result the completion message may carry. Default: 80. */
|
|
42
|
+
export const DEFAULT_MAX_RESULT_LINES = 80;
|
|
43
|
+
/** Upper bound accepted for maxResultLines (defensive clamp). */
|
|
44
|
+
export const MAX_RESULT_LINES_LIMIT = 2000;
|
|
40
45
|
|
|
41
46
|
export const CONFIG_FILE_NAME = "pi-subagents.json";
|
|
42
47
|
|
|
@@ -55,14 +60,35 @@ export const MAX_PARALLEL_TASKS_LIMIT = 32;
|
|
|
55
60
|
export const DEFAULT_MAX_SUBAGENT_DEPTH = 1;
|
|
56
61
|
/** Upper bound accepted for maxSubagentDepth (defensive clamp). */
|
|
57
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;
|
|
58
71
|
|
|
59
72
|
export interface SubagentsConfig {
|
|
60
73
|
/** Agent names that are discoverable and injected. Default: explore, worker, reviewer. */
|
|
61
74
|
enabledAgents: string[];
|
|
62
75
|
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
63
76
|
agentModels: Record<string, string>;
|
|
64
|
-
/**
|
|
77
|
+
/** Per-agent thinking-level override, keyed by agent name. */
|
|
78
|
+
agentThinkingLevels: Record<string, ThinkingLevel>;
|
|
79
|
+
/** Thinking level for sub-agents without a per-agent override or frontmatter default. Default: "high". */
|
|
65
80
|
thinkingLevel: ThinkingLevel;
|
|
81
|
+
/**
|
|
82
|
+
* When a review passes (REVIEW_PASS verdict), deliver it without waking the
|
|
83
|
+
* main agent. Disabled by default so passing reviews still resume orchestration.
|
|
84
|
+
*/
|
|
85
|
+
notifyOnReviewPass: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Max lines of a sub-agent result carried in the completion message. Longer
|
|
88
|
+
* results are truncated; the full text is written to a temp file whose path
|
|
89
|
+
* is included in the message. Default: 80.
|
|
90
|
+
*/
|
|
91
|
+
maxResultLines: number;
|
|
66
92
|
/** Whether to inject the delegation directive into the parent system prompt. Default: true. */
|
|
67
93
|
proactiveInjection: boolean;
|
|
68
94
|
/** Which agent directories to discover from. Default: "user". */
|
|
@@ -73,17 +99,29 @@ export interface SubagentsConfig {
|
|
|
73
99
|
maxParallelTasks: number;
|
|
74
100
|
/** Depth at which the subagent tool is no longer registered. Default: 1. */
|
|
75
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;
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
79
113
|
enabledAgents: [...DEFAULT_ENABLED_AGENTS],
|
|
80
114
|
agentModels: {},
|
|
115
|
+
agentThinkingLevels: {},
|
|
81
116
|
thinkingLevel: DEFAULT_THINKING_LEVEL,
|
|
117
|
+
notifyOnReviewPass: false,
|
|
118
|
+
maxResultLines: DEFAULT_MAX_RESULT_LINES,
|
|
82
119
|
proactiveInjection: true,
|
|
83
120
|
agentScope: "user",
|
|
84
121
|
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
|
|
85
122
|
maxParallelTasks: DEFAULT_MAX_PARALLEL_TASKS,
|
|
86
123
|
maxSubagentDepth: DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
124
|
+
maxFixRounds: DEFAULT_MAX_FIX_ROUNDS,
|
|
87
125
|
};
|
|
88
126
|
|
|
89
127
|
export function getConfigPath(agentDir: string = getAgentDir()): string {
|
|
@@ -121,12 +159,16 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
121
159
|
const config: SubagentsConfig = {
|
|
122
160
|
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
123
161
|
agentModels: {},
|
|
162
|
+
agentThinkingLevels: {},
|
|
124
163
|
thinkingLevel: DEFAULT_CONFIG.thinkingLevel,
|
|
164
|
+
notifyOnReviewPass: DEFAULT_CONFIG.notifyOnReviewPass,
|
|
165
|
+
maxResultLines: DEFAULT_CONFIG.maxResultLines,
|
|
125
166
|
proactiveInjection: DEFAULT_CONFIG.proactiveInjection,
|
|
126
167
|
agentScope: DEFAULT_CONFIG.agentScope,
|
|
127
168
|
maxConcurrency: DEFAULT_CONFIG.maxConcurrency,
|
|
128
169
|
maxParallelTasks: DEFAULT_CONFIG.maxParallelTasks,
|
|
129
170
|
maxSubagentDepth: DEFAULT_CONFIG.maxSubagentDepth,
|
|
171
|
+
maxFixRounds: DEFAULT_CONFIG.maxFixRounds,
|
|
130
172
|
};
|
|
131
173
|
|
|
132
174
|
if (Array.isArray(raw.enabledAgents)) {
|
|
@@ -147,10 +189,29 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
147
189
|
}
|
|
148
190
|
}
|
|
149
191
|
|
|
192
|
+
if (isRecord(raw.agentThinkingLevels)) {
|
|
193
|
+
for (const [key, value] of Object.entries(raw.agentThinkingLevels)) {
|
|
194
|
+
if (REMOVED_AGENT_NAMES.includes(key.trim())) continue;
|
|
195
|
+
if (
|
|
196
|
+
typeof value === "string" &&
|
|
197
|
+
(THINKING_LEVEL_VALUES as readonly string[]).includes(value)
|
|
198
|
+
) {
|
|
199
|
+
config.agentThinkingLevels[key.trim()] = value as ThinkingLevel;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
150
204
|
if (typeof raw.thinkingLevel === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(raw.thinkingLevel)) {
|
|
151
205
|
config.thinkingLevel = raw.thinkingLevel as ThinkingLevel;
|
|
152
206
|
}
|
|
153
207
|
|
|
208
|
+
if (typeof raw.notifyOnReviewPass === "boolean") {
|
|
209
|
+
config.notifyOnReviewPass = raw.notifyOnReviewPass;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const maxResultLines = clampCount(raw.maxResultLines, MAX_RESULT_LINES_LIMIT);
|
|
213
|
+
if (maxResultLines !== undefined) config.maxResultLines = maxResultLines;
|
|
214
|
+
|
|
154
215
|
if (typeof raw.proactiveInjection === "boolean") {
|
|
155
216
|
config.proactiveInjection = raw.proactiveInjection;
|
|
156
217
|
}
|
|
@@ -170,11 +231,21 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
170
231
|
config.maxSubagentDepth = Math.max(0, Math.min(MAX_SUBAGENT_DEPTH_LIMIT, Math.round(raw.maxSubagentDepth)));
|
|
171
232
|
}
|
|
172
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
|
+
|
|
173
239
|
return config;
|
|
174
240
|
}
|
|
175
241
|
|
|
176
242
|
function defaultConfig(): SubagentsConfig {
|
|
177
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
...DEFAULT_CONFIG,
|
|
245
|
+
enabledAgents: [...DEFAULT_CONFIG.enabledAgents],
|
|
246
|
+
agentModels: {},
|
|
247
|
+
agentThinkingLevels: {},
|
|
248
|
+
};
|
|
178
249
|
}
|
|
179
250
|
|
|
180
251
|
/**
|