@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/src/setup.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DEFAULT_CONFIG,
|
|
16
16
|
DEFAULT_ENABLED_AGENTS,
|
|
17
17
|
DEFAULT_MAX_CONCURRENCY,
|
|
18
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
18
19
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
19
20
|
THINKING_LEVEL_VALUES,
|
|
20
21
|
type AgentScope,
|
|
@@ -27,9 +28,19 @@ import {
|
|
|
27
28
|
} from "./config.ts";
|
|
28
29
|
import { availableModelRefs, repairUnavailableModelOverrides } from "./models.ts";
|
|
29
30
|
import { promptSelectMany, promptSelectOne } from "./ui.ts";
|
|
31
|
+
import { loadBuiltinAgents } from "./agents.ts";
|
|
30
32
|
|
|
31
33
|
const INHERIT = "__inherit__";
|
|
32
34
|
|
|
35
|
+
/** Effective per-agent default strength from builtin frontmatter (config overrides win at spawn). */
|
|
36
|
+
function builtinThinkingDefaults(): Map<string, ThinkingLevel> {
|
|
37
|
+
const map = new Map<string, ThinkingLevel>();
|
|
38
|
+
for (const agent of loadBuiltinAgents()) {
|
|
39
|
+
if (agent.thinking) map.set(agent.name, agent.thinking);
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
/** Short, selection-friendly descriptions for the built-in agents. */
|
|
34
45
|
const MODULE_HINTS: Record<string, string> = {
|
|
35
46
|
explore: "read-only codebase recon (fast model)",
|
|
@@ -65,20 +76,24 @@ async function pickEnabledAgents(
|
|
|
65
76
|
);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
async function
|
|
79
|
+
async function pickAgentModelsAndStrength(
|
|
69
80
|
ctx: ExtensionCommandContext,
|
|
70
81
|
enabledAgents: readonly string[],
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
currentModels: Record<string, string>,
|
|
83
|
+
currentStrengths: Record<string, ThinkingLevel>,
|
|
84
|
+
defaultLevel: ThinkingLevel,
|
|
85
|
+
defaults: ReadonlyMap<string, ThinkingLevel>,
|
|
86
|
+
): Promise<{ models: Record<string, string>; strengths: Record<string, ThinkingLevel> } | undefined> {
|
|
73
87
|
const refs = availableModelRefs(ctx);
|
|
74
88
|
if (refs.length === 0) {
|
|
75
89
|
ctx.ui.notify("No Pi models are currently available; model overrides left unchanged.", "warning");
|
|
76
|
-
return { ...
|
|
90
|
+
return { models: { ...currentModels }, strengths: { ...currentStrengths } };
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
const
|
|
93
|
+
const models: Record<string, string> = {};
|
|
94
|
+
const strengths: Record<string, ThinkingLevel> = {};
|
|
80
95
|
for (const name of enabledAgents) {
|
|
81
|
-
const currentRef =
|
|
96
|
+
const currentRef = currentModels[name];
|
|
82
97
|
const items = [
|
|
83
98
|
{
|
|
84
99
|
value: INHERIT,
|
|
@@ -95,9 +110,15 @@ async function pickAgentModels(
|
|
|
95
110
|
items,
|
|
96
111
|
);
|
|
97
112
|
if (choice === undefined) return undefined; // Esc aborts the whole wizard
|
|
98
|
-
if (choice !== INHERIT)
|
|
113
|
+
if (choice !== INHERIT) models[name] = choice;
|
|
114
|
+
|
|
115
|
+
// Convenience: the model pick is immediately followed by the strength pick,
|
|
116
|
+
// so per-agent model + strength are configured in one pass.
|
|
117
|
+
const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults);
|
|
118
|
+
if (strength === undefined) return undefined; // Esc aborts the whole wizard
|
|
119
|
+
if (strength !== INHERIT) strengths[name] = strength;
|
|
99
120
|
}
|
|
100
|
-
return
|
|
121
|
+
return { models, strengths };
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
@@ -107,9 +128,52 @@ const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
|
107
128
|
medium: "balanced reasoning",
|
|
108
129
|
high: "deep reasoning",
|
|
109
130
|
xhigh: "extra-deep reasoning",
|
|
110
|
-
max: "strongest reasoning
|
|
131
|
+
max: "strongest reasoning",
|
|
111
132
|
};
|
|
112
133
|
|
|
134
|
+
/** Single strength pick for one agent; the inherit option keeps the effective default. */
|
|
135
|
+
async function pickAgentStrength(
|
|
136
|
+
ctx: ExtensionCommandContext,
|
|
137
|
+
agentName: string,
|
|
138
|
+
current: ThinkingLevel | undefined,
|
|
139
|
+
defaultLevel: ThinkingLevel,
|
|
140
|
+
defaults: ReadonlyMap<string, ThinkingLevel>,
|
|
141
|
+
): Promise<ThinkingLevel | typeof INHERIT | undefined> {
|
|
142
|
+
const options = THINKING_LEVEL_VALUES.map((level) => ({
|
|
143
|
+
value: level,
|
|
144
|
+
label: current === level ? `${level} — ${THINKING_LEVEL_HINTS[level]} (current)` : `${level} — ${THINKING_LEVEL_HINTS[level]}`,
|
|
145
|
+
}));
|
|
146
|
+
const agentDefault = defaults.get(agentName);
|
|
147
|
+
const inheritLabel = agentDefault
|
|
148
|
+
? `(inherit agent default — ${agentDefault})`
|
|
149
|
+
: `(inherit global default — ${defaultLevel})`;
|
|
150
|
+
const choice = await promptSelectOne(
|
|
151
|
+
ctx,
|
|
152
|
+
`Thinking strength for "${agentName}"?`,
|
|
153
|
+
"Type to filter • ↑/↓ • Enter selects • Esc cancels setup",
|
|
154
|
+
[{ value: INHERIT, label: inheritLabel }, ...options],
|
|
155
|
+
);
|
|
156
|
+
if (choice === undefined) return undefined;
|
|
157
|
+
return choice === INHERIT ? INHERIT : (choice as ThinkingLevel);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Strength picks for every enabled agent (inherit keeps the effective default). */
|
|
161
|
+
async function pickAgentStrengths(
|
|
162
|
+
ctx: ExtensionCommandContext,
|
|
163
|
+
enabledAgents: readonly string[],
|
|
164
|
+
currentStrengths: Record<string, ThinkingLevel>,
|
|
165
|
+
defaultLevel: ThinkingLevel,
|
|
166
|
+
defaults: ReadonlyMap<string, ThinkingLevel>,
|
|
167
|
+
): Promise<Record<string, ThinkingLevel> | undefined> {
|
|
168
|
+
const strengths: Record<string, ThinkingLevel> = {};
|
|
169
|
+
for (const name of enabledAgents) {
|
|
170
|
+
const strength = await pickAgentStrength(ctx, name, currentStrengths[name], defaultLevel, defaults);
|
|
171
|
+
if (strength === undefined) return undefined; // Esc aborts
|
|
172
|
+
if (strength !== INHERIT) strengths[name] = strength;
|
|
173
|
+
}
|
|
174
|
+
return strengths;
|
|
175
|
+
}
|
|
176
|
+
|
|
113
177
|
async function pickThinkingLevel(
|
|
114
178
|
ctx: ExtensionCommandContext,
|
|
115
179
|
current: ThinkingLevel,
|
|
@@ -117,7 +181,7 @@ async function pickThinkingLevel(
|
|
|
117
181
|
const options = THINKING_LEVEL_VALUES.map((level) =>
|
|
118
182
|
level === current ? `${level} — ${THINKING_LEVEL_HINTS[level]} (current)` : `${level} — ${THINKING_LEVEL_HINTS[level]}`,
|
|
119
183
|
);
|
|
120
|
-
const choice = await ctx.ui.select("
|
|
184
|
+
const choice = await ctx.ui.select("Default thinking strength for sub-agents?", options);
|
|
121
185
|
if (choice === undefined) return undefined;
|
|
122
186
|
return THINKING_LEVEL_VALUES.find((level) => choice.startsWith(`${level} —`));
|
|
123
187
|
}
|
|
@@ -133,6 +197,8 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
|
|
|
133
197
|
/** Preset steps offered for the two numeric limits (selection-only wizard). */
|
|
134
198
|
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
135
199
|
const PARALLEL_TASK_STEPS = [2, 4, 6, 8, 12, 16, 24, 32];
|
|
200
|
+
/** Preset rounds offered for the auto-fix loop (0 disables it). */
|
|
201
|
+
const FIX_ROUNDS_STEPS = [0, 1, 2, 3, 5];
|
|
136
202
|
|
|
137
203
|
async function pickCount(
|
|
138
204
|
ctx: ExtensionCommandContext,
|
|
@@ -202,12 +268,14 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
202
268
|
const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
|
|
203
269
|
if (enabled === undefined) return notifyCancelled(ctx);
|
|
204
270
|
|
|
205
|
-
|
|
206
|
-
if (models === undefined) return notifyCancelled(ctx);
|
|
207
|
-
|
|
271
|
+
// Global default first, so per-agent strength picks can show "inherit" against it.
|
|
208
272
|
const thinkingLevel = await pickThinkingLevel(ctx, base.thinkingLevel);
|
|
209
273
|
if (thinkingLevel === undefined) return notifyCancelled(ctx);
|
|
210
274
|
|
|
275
|
+
const defaults = builtinThinkingDefaults();
|
|
276
|
+
const picked = await pickAgentModelsAndStrength(ctx, enabled, base.agentModels, base.agentThinkingLevels, thinkingLevel, defaults);
|
|
277
|
+
if (picked === undefined) return notifyCancelled(ctx);
|
|
278
|
+
|
|
211
279
|
const injection = await pickInjection(ctx, base.proactiveInjection);
|
|
212
280
|
if (injection === undefined) return notifyCancelled(ctx);
|
|
213
281
|
|
|
@@ -230,17 +298,30 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
230
298
|
base.maxParallelTasks,
|
|
231
299
|
DEFAULT_MAX_PARALLEL_TASKS,
|
|
232
300
|
);
|
|
233
|
-
|
|
301
|
+
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
234
302
|
|
|
235
|
-
|
|
303
|
+
const maxFixRounds = await pickCount(
|
|
304
|
+
ctx,
|
|
305
|
+
"Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
|
|
306
|
+
FIX_ROUNDS_STEPS,
|
|
307
|
+
base.maxFixRounds,
|
|
308
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
309
|
+
);
|
|
310
|
+
if (maxFixRounds === undefined) return notifyCancelled(ctx);
|
|
311
|
+
|
|
312
|
+
const next: SubagentsConfig = {
|
|
236
313
|
enabledAgents: enabled,
|
|
237
|
-
agentModels: repairStaleModels(ctx, models),
|
|
314
|
+
agentModels: repairStaleModels(ctx, picked.models),
|
|
315
|
+
agentThinkingLevels: picked.strengths,
|
|
238
316
|
thinkingLevel,
|
|
317
|
+
notifyOnReviewPass: base.notifyOnReviewPass,
|
|
318
|
+
maxResultLines: base.maxResultLines,
|
|
239
319
|
proactiveInjection: injection,
|
|
240
320
|
agentScope: scope,
|
|
241
321
|
maxConcurrency,
|
|
242
322
|
maxParallelTasks,
|
|
243
323
|
maxSubagentDepth: base.maxSubagentDepth,
|
|
324
|
+
maxFixRounds,
|
|
244
325
|
};
|
|
245
326
|
await saveConfig(next, configPath);
|
|
246
327
|
ctx.ui.notify(`pi-subagents configured. Saved to ${configPath}`, "info");
|
|
@@ -255,6 +336,7 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
255
336
|
"Change agent scope",
|
|
256
337
|
"Change max concurrent sub-agents",
|
|
257
338
|
"Change max parallel tasks",
|
|
339
|
+
"Change max fix rounds",
|
|
258
340
|
"Full re-setup",
|
|
259
341
|
]);
|
|
260
342
|
if (choice === undefined) return notifyCancelled(ctx);
|
|
@@ -268,13 +350,26 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
268
350
|
if (enabled === undefined) return notifyCancelled(ctx);
|
|
269
351
|
next.enabledAgents = enabled;
|
|
270
352
|
} else if (choice.startsWith("Change agent models")) {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
353
|
+
const defaults = builtinThinkingDefaults();
|
|
354
|
+
const picked = await pickAgentModelsAndStrength(
|
|
355
|
+
ctx,
|
|
356
|
+
config.enabledAgents,
|
|
357
|
+
config.agentModels,
|
|
358
|
+
config.agentThinkingLevels,
|
|
359
|
+
config.thinkingLevel,
|
|
360
|
+
defaults,
|
|
361
|
+
);
|
|
362
|
+
if (picked === undefined) return notifyCancelled(ctx);
|
|
363
|
+
next.agentModels = repairStaleModels(ctx, picked.models);
|
|
364
|
+
next.agentThinkingLevels = picked.strengths;
|
|
274
365
|
} else if (choice.startsWith("Change thinking")) {
|
|
366
|
+
// Global first so per-agent "inherit" labels reflect the value that will be stored.
|
|
275
367
|
const thinkingLevel = await pickThinkingLevel(ctx, config.thinkingLevel);
|
|
276
368
|
if (thinkingLevel === undefined) return notifyCancelled(ctx);
|
|
277
369
|
next.thinkingLevel = thinkingLevel;
|
|
370
|
+
const strengths = await pickAgentStrengths(ctx, config.enabledAgents, config.agentThinkingLevels, thinkingLevel, builtinThinkingDefaults());
|
|
371
|
+
if (strengths === undefined) return notifyCancelled(ctx);
|
|
372
|
+
next.agentThinkingLevels = strengths;
|
|
278
373
|
} else if (choice.startsWith("Toggle")) {
|
|
279
374
|
const injection = await pickInjection(ctx, config.proactiveInjection);
|
|
280
375
|
if (injection === undefined) return notifyCancelled(ctx);
|
|
@@ -303,6 +398,16 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
303
398
|
);
|
|
304
399
|
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
305
400
|
next.maxParallelTasks = maxParallelTasks;
|
|
401
|
+
} else if (choice.startsWith("Change max fix")) {
|
|
402
|
+
const maxFixRounds = await pickCount(
|
|
403
|
+
ctx,
|
|
404
|
+
"Auto-fix rounds when a reviewer returns REQUEST_CHANGES? (0 = main agent handles fixes)",
|
|
405
|
+
FIX_ROUNDS_STEPS,
|
|
406
|
+
config.maxFixRounds,
|
|
407
|
+
DEFAULT_MAX_FIX_ROUNDS,
|
|
408
|
+
);
|
|
409
|
+
if (maxFixRounds === undefined) return notifyCancelled(ctx);
|
|
410
|
+
next.maxFixRounds = maxFixRounds;
|
|
306
411
|
}
|
|
307
412
|
|
|
308
413
|
await saveConfig(next, configPath);
|
package/src/spawn.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
14
|
+
import { existsSync, mkdirSync, unlinkSync, rmdirSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
15
|
-
import { existsSync, unlinkSync, rmdirSync } from "node:fs";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
18
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
@@ -51,6 +51,8 @@ export interface SingleResult {
|
|
|
51
51
|
stderr: string;
|
|
52
52
|
usage: UsageStats;
|
|
53
53
|
model?: string;
|
|
54
|
+
/** Effective thinking strength this run was launched with. */
|
|
55
|
+
thinking?: string;
|
|
54
56
|
stopReason?: string;
|
|
55
57
|
errorMessage?: string;
|
|
56
58
|
}
|
|
@@ -88,6 +90,54 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
88
90
|
return "";
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Parse the machine-readable verdict a reviewer emits (see agents/reviewer.md).
|
|
95
|
+
* Only the LAST standalone `VERDICT: REVIEW_PASS/FAIL` line counts, so a report
|
|
96
|
+
* that merely discusses the tokens cannot be misclassified. Returns undefined
|
|
97
|
+
* when no verdict marker is present, so non-review agents are never mistaken
|
|
98
|
+
* for reviews.
|
|
99
|
+
*/
|
|
100
|
+
export function reviewVerdict(output: string): "pass" | "fail" | undefined {
|
|
101
|
+
const lines = output.split("\n");
|
|
102
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
103
|
+
const match = /^\s*VERDICT:\s*REVIEW_(PASS|FAIL)\s*$/i.exec(lines[index]);
|
|
104
|
+
if (match) return match[1].toUpperCase() === "PASS" ? "pass" : "fail";
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Hard cap for a single line inside a truncated result (minified blobs must not blow up). */
|
|
110
|
+
export const RESULT_LINE_MAX = 200;
|
|
111
|
+
|
|
112
|
+
export interface TruncatedOutput {
|
|
113
|
+
/** The result text that fits in the completion message. */
|
|
114
|
+
text: string;
|
|
115
|
+
/** True when lines were dropped or shortened, so the full text is written to disk. */
|
|
116
|
+
truncated: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Cap result text for the main conversation: keep the first `maxLines` lines, at most RESULT_LINE_MAX chars each. */
|
|
120
|
+
export function truncateResultOutput(output: string, maxLines: number): TruncatedOutput {
|
|
121
|
+
const lines = output.split("\n");
|
|
122
|
+
if (lines.length <= maxLines && lines.every((line) => line.length <= RESULT_LINE_MAX)) {
|
|
123
|
+
return { text: output, truncated: false };
|
|
124
|
+
}
|
|
125
|
+
const kept = lines.slice(0, maxLines).map((line) =>
|
|
126
|
+
line.length > RESULT_LINE_MAX ? `${line.slice(0, RESULT_LINE_MAX)}…` : line,
|
|
127
|
+
);
|
|
128
|
+
return { text: kept.join("\n"), truncated: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Persist the full result where the main agent can read it on demand. Returns the file path. */
|
|
132
|
+
export function writeResultArtifact(output: string, agentName: string): string {
|
|
133
|
+
const dir = join(tmpdir(), "pi-subagents-results");
|
|
134
|
+
mkdirSync(dir, { recursive: true });
|
|
135
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
136
|
+
const filePath = join(dir, `${Date.now()}-${safeName}.md`);
|
|
137
|
+
writeFileSync(filePath, output, "utf8");
|
|
138
|
+
return filePath;
|
|
139
|
+
}
|
|
140
|
+
|
|
91
141
|
export function isFailedResult(result: SingleResult): boolean {
|
|
92
142
|
return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
93
143
|
}
|
|
@@ -238,6 +288,7 @@ export async function runSingleAgent(options: RunSingleOptions): Promise<SingleR
|
|
|
238
288
|
stderr: "",
|
|
239
289
|
usage: emptyUsage(),
|
|
240
290
|
model: agent.model,
|
|
291
|
+
thinking: thinkingLevel,
|
|
241
292
|
};
|
|
242
293
|
|
|
243
294
|
const emitUpdate = (): void => {
|