@ferris1225/pi-subagents 4.2.13 → 4.3.1
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/CHANGELOG.md +34 -0
- package/README.md +113 -149
- package/agents/artisan.md +19 -0
- package/agents/scout.md +18 -0
- package/agents/steward.md +19 -0
- package/package.json +4 -3
- package/src/agents.ts +24 -28
- package/src/announcements.ts +11 -18
- package/src/background.ts +56 -9
- package/src/completion.ts +0 -6
- package/src/config.ts +55 -90
- package/src/dispatch.ts +52 -67
- package/src/durable.ts +6 -53
- package/src/index.ts +7 -10
- package/src/monitor.ts +2 -2
- package/src/prompt.ts +104 -43
- package/src/recovery.ts +35 -10
- package/src/rpc-run.ts +59 -1
- package/src/runtime.ts +74 -17
- package/src/setup.ts +108 -102
- package/src/spawn.ts +6 -4
- package/src/thread-lifecycle.ts +127 -95
- package/src/tools.ts +4 -20
- package/agents/executor.md +0 -54
- package/agents/explorer.md +0 -37
package/src/setup.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive configuration wizard for /subagents-setup.
|
|
3
3
|
*
|
|
4
|
-
* The wizard stays one level deep
|
|
5
|
-
*
|
|
6
|
-
* Everything else (agent scope, idle timeout, result
|
|
7
|
-
* config-file-only
|
|
8
|
-
* and thinking defaults to capability-aware Auto.
|
|
4
|
+
* The wizard stays one level deep: which agents run, then a model and an
|
|
5
|
+
* optional thinking override per agent. Role thinking defaults apply until
|
|
6
|
+
* the user picks a level. Everything else (agent scope, idle timeout, result
|
|
7
|
+
* lines) is config-file-only.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { stat } from "node:fs/promises";
|
|
12
11
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
13
12
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { discoverAgents } from "./agents.ts";
|
|
15
13
|
import {
|
|
14
|
+
AGENT_PROFILES,
|
|
16
15
|
BUILTIN_AGENT_NAMES,
|
|
17
16
|
DEFAULT_CONFIG,
|
|
18
17
|
DEFAULT_ENABLED_AGENTS,
|
|
19
|
-
|
|
20
|
-
type SubagentsConfig,
|
|
21
|
-
type ThinkingLevel,
|
|
18
|
+
agentProfile,
|
|
22
19
|
errorMessage,
|
|
23
20
|
getConfigPath,
|
|
24
21
|
loadConfig,
|
|
22
|
+
roleThinkingLevel,
|
|
25
23
|
saveConfig,
|
|
24
|
+
type SubagentsConfig,
|
|
25
|
+
type ThinkingLevel,
|
|
26
26
|
} from "./config.ts";
|
|
27
27
|
import {
|
|
28
28
|
CURRENT_MAIN_MODEL,
|
|
@@ -37,15 +37,30 @@ import {
|
|
|
37
37
|
} from "./models.ts";
|
|
38
38
|
import { promptSelectMany, promptSelectOne } from "./ui.ts";
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
41
|
+
off: "no reasoning tokens",
|
|
42
|
+
minimal: "minimal reasoning",
|
|
43
|
+
low: "light reasoning",
|
|
44
|
+
medium: "balanced reasoning",
|
|
45
|
+
high: "deep reasoning",
|
|
46
|
+
xhigh: "extra-deep reasoning",
|
|
47
|
+
max: "strongest reasoning",
|
|
44
48
|
};
|
|
45
49
|
|
|
50
|
+
function agentPickerItems(): Array<{ value: string; label: string; description: string }> {
|
|
51
|
+
return BUILTIN_AGENT_NAMES.map((name) => {
|
|
52
|
+
const profile = AGENT_PROFILES[name];
|
|
53
|
+
return {
|
|
54
|
+
value: name,
|
|
55
|
+
label: name,
|
|
56
|
+
description: `${profile.summary} — ${profile.remark}`,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
function moduleLabel(name: string): string {
|
|
47
|
-
const
|
|
48
|
-
return
|
|
62
|
+
const profile = agentProfile(name);
|
|
63
|
+
return profile ? `${name} — ${profile.summary}` : name;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
async function configExists(configPath: string): Promise<boolean> {
|
|
@@ -61,14 +76,14 @@ async function pickEnabledAgents(
|
|
|
61
76
|
ctx: ExtensionCommandContext,
|
|
62
77
|
current: readonly string[],
|
|
63
78
|
): Promise<string[] | undefined> {
|
|
64
|
-
const
|
|
65
|
-
return promptSelectMany(
|
|
79
|
+
const picked = await promptSelectMany(
|
|
66
80
|
ctx,
|
|
67
|
-
"
|
|
68
|
-
"Space toggles • Enter confirms • Esc
|
|
69
|
-
|
|
81
|
+
"Which agents should run?",
|
|
82
|
+
"Each line is a role and its job. Space toggles • Enter confirms • Esc back",
|
|
83
|
+
agentPickerItems(),
|
|
70
84
|
current,
|
|
71
85
|
);
|
|
86
|
+
return picked;
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
async function pickConfiguredModel(
|
|
@@ -86,7 +101,7 @@ async function pickConfiguredModel(
|
|
|
86
101
|
return promptSelectOne(
|
|
87
102
|
ctx,
|
|
88
103
|
title,
|
|
89
|
-
`Type to filter by provider, model,
|
|
104
|
+
`Type to filter by provider, model, or capability • ↑/↓ • Enter selects • Esc ${escNote}`,
|
|
90
105
|
items,
|
|
91
106
|
configuredRef ?? CURRENT_MAIN_MODEL,
|
|
92
107
|
);
|
|
@@ -98,34 +113,11 @@ async function pickAgentModel(
|
|
|
98
113
|
currentRef: string | undefined,
|
|
99
114
|
escNote = "cancels this setup pass",
|
|
100
115
|
): Promise<string | undefined> {
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const AUTO_THINKING = "__auto_thinking__";
|
|
105
|
-
|
|
106
|
-
function actualAgentThinkingDefault(
|
|
107
|
-
ctx: ExtensionCommandContext,
|
|
108
|
-
config: SubagentsConfig,
|
|
109
|
-
agentName: string,
|
|
110
|
-
): ThinkingLevel {
|
|
111
|
-
const { agents } = discoverAgents(ctx.cwd, {
|
|
112
|
-
scope: config.agentScope,
|
|
113
|
-
enabledNames: config.enabledAgents,
|
|
114
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
115
|
-
});
|
|
116
|
-
return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
|
|
116
|
+
const profile = agentProfile(agentName);
|
|
117
|
+
const duty = profile ? ` — ${profile.summary}` : "";
|
|
118
|
+
return pickConfiguredModel(ctx, `Model for ${agentName}${duty}?`, currentRef, escNote);
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
|
|
120
|
-
off: "no reasoning tokens",
|
|
121
|
-
minimal: "minimal reasoning",
|
|
122
|
-
low: "light reasoning",
|
|
123
|
-
medium: "balanced reasoning",
|
|
124
|
-
high: "deep reasoning",
|
|
125
|
-
xhigh: "extra-deep reasoning",
|
|
126
|
-
max: "strongest reasoning",
|
|
127
|
-
};
|
|
128
|
-
|
|
129
121
|
function effectiveModelForChoice(
|
|
130
122
|
ctx: ExtensionCommandContext,
|
|
131
123
|
choice: string,
|
|
@@ -134,41 +126,37 @@ function effectiveModelForChoice(
|
|
|
134
126
|
return findModelByRef(availableModelsInScope(ctx), choice);
|
|
135
127
|
}
|
|
136
128
|
|
|
137
|
-
/**
|
|
138
|
-
* selected model; unsupported xhigh/max entries never appear. */
|
|
129
|
+
/** Role default is the first row. Picking it clears a stored override. */
|
|
139
130
|
async function pickAgentStrength(
|
|
140
131
|
ctx: ExtensionCommandContext,
|
|
141
132
|
agentName: string,
|
|
142
133
|
model: Model<Api> | undefined,
|
|
143
134
|
current: ThinkingLevel | undefined,
|
|
144
|
-
agentDefault: ThinkingLevel,
|
|
145
135
|
escNote = "cancels this setup pass",
|
|
146
|
-
): Promise<ThinkingLevel |
|
|
136
|
+
): Promise<ThinkingLevel | undefined> {
|
|
147
137
|
const supported = supportedThinkingLevels(model);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
// Auto is already the complete and least surprising choice.
|
|
151
|
-
if (supported.length <= 1) return AUTO_THINKING;
|
|
138
|
+
const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName));
|
|
139
|
+
if (supported.length <= 1) return roleDefault;
|
|
152
140
|
|
|
153
|
-
const currentEffective = current ? resolveThinkingLevel(model, current) :
|
|
141
|
+
const currentEffective = current ? resolveThinkingLevel(model, current) : roleDefault;
|
|
154
142
|
const modelName = model ? modelRef(model) : "current main model";
|
|
155
|
-
const options =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
const options = supported.map((level) => {
|
|
144
|
+
const tags = [
|
|
145
|
+
level === roleDefault ? "role default" : "",
|
|
146
|
+
current !== undefined && currentEffective === level ? "current" : "",
|
|
147
|
+
].filter(Boolean);
|
|
148
|
+
return {
|
|
161
149
|
value: level,
|
|
162
|
-
label: `${level} — ${THINKING_LEVEL_HINTS[level]}${
|
|
163
|
-
}
|
|
164
|
-
|
|
150
|
+
label: `${level} — ${THINKING_LEVEL_HINTS[level]}${tags.length ? ` (${tags.join(", ")})` : ""}`,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
165
153
|
return promptSelectOne(
|
|
166
154
|
ctx,
|
|
167
|
-
`Thinking for
|
|
168
|
-
|
|
155
|
+
`Thinking for ${agentName}?`,
|
|
156
|
+
`${agentName} defaults to ${roleDefault} on ${modelName} • Enter selects • Esc ${escNote}`,
|
|
169
157
|
options,
|
|
170
|
-
|
|
171
|
-
) as Promise<ThinkingLevel |
|
|
158
|
+
currentEffective,
|
|
159
|
+
) as Promise<ThinkingLevel | undefined>;
|
|
172
160
|
}
|
|
173
161
|
|
|
174
162
|
async function pickAgentToConfigure(
|
|
@@ -182,19 +170,24 @@ async function pickAgentToConfigure(
|
|
|
182
170
|
return promptSelectOne(
|
|
183
171
|
ctx,
|
|
184
172
|
"Configure which agent?",
|
|
185
|
-
"
|
|
186
|
-
enabledAgents.map((name) =>
|
|
173
|
+
"Name, then what it owns • ↑/↓ • Enter selects • Esc returns to settings",
|
|
174
|
+
enabledAgents.map((name) => {
|
|
175
|
+
const profile = agentProfile(name);
|
|
176
|
+
return {
|
|
177
|
+
value: name,
|
|
178
|
+
label: moduleLabel(name),
|
|
179
|
+
description: profile?.remark,
|
|
180
|
+
};
|
|
181
|
+
}),
|
|
187
182
|
);
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
interface ConfiguredAgentChoice {
|
|
191
186
|
name: string;
|
|
192
187
|
model: string;
|
|
193
|
-
strength: ThinkingLevel
|
|
188
|
+
strength: ThinkingLevel;
|
|
194
189
|
}
|
|
195
190
|
|
|
196
|
-
/** Configure one agent while preserving the UI back stack: thinking → model →
|
|
197
|
-
* agent selection. Esc from agent selection ends this configuration pass. */
|
|
198
191
|
async function configureOneAgent(
|
|
199
192
|
ctx: ExtensionCommandContext,
|
|
200
193
|
config: SubagentsConfig,
|
|
@@ -202,6 +195,8 @@ async function configureOneAgent(
|
|
|
202
195
|
while (true) {
|
|
203
196
|
const name = await pickAgentToConfigure(ctx, config.enabledAgents);
|
|
204
197
|
if (name === undefined) return undefined;
|
|
198
|
+
const profile = agentProfile(name);
|
|
199
|
+
if (profile) ctx.ui.notify(`${name}: ${profile.remark}`, "info");
|
|
205
200
|
|
|
206
201
|
while (true) {
|
|
207
202
|
const modelChoice = await pickAgentModel(
|
|
@@ -217,7 +212,6 @@ async function configureOneAgent(
|
|
|
217
212
|
name,
|
|
218
213
|
model,
|
|
219
214
|
config.agentThinkingLevels[name],
|
|
220
|
-
actualAgentThinkingDefault(ctx, config, name),
|
|
221
215
|
"returns to model selection",
|
|
222
216
|
);
|
|
223
217
|
if (strength === undefined) continue;
|
|
@@ -231,12 +225,42 @@ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string
|
|
|
231
225
|
return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
|
|
232
226
|
}
|
|
233
227
|
|
|
228
|
+
function applyThinkingChoice(
|
|
229
|
+
levels: Record<string, ThinkingLevel>,
|
|
230
|
+
agentName: string,
|
|
231
|
+
strength: ThinkingLevel,
|
|
232
|
+
): Record<string, ThinkingLevel> {
|
|
233
|
+
const next = { ...levels };
|
|
234
|
+
if (strength === roleThinkingLevel(agentName)) delete next[agentName];
|
|
235
|
+
else next[agentName] = strength;
|
|
236
|
+
return next;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function introduceSetup(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
240
|
+
const lines = BUILTIN_AGENT_NAMES.map((name) => {
|
|
241
|
+
const profile = AGENT_PROFILES[name];
|
|
242
|
+
return `${name} — ${profile.summary}. ${profile.remark}`;
|
|
243
|
+
});
|
|
244
|
+
ctx.ui.notify(
|
|
245
|
+
`pi-subagents: ${lines.join(" ")} Pick a model for each role next. Thinking defaults per role (scout low, artisan high, steward medium); change it on a role when you want.`,
|
|
246
|
+
"info",
|
|
247
|
+
);
|
|
248
|
+
const choice = await ctx.ui.select("How to configure pi-subagents", [
|
|
249
|
+
"Continue — pick a model for each role (thinking has a role default you can change later)",
|
|
250
|
+
]);
|
|
251
|
+
return choice !== undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
|
|
255
|
+
if (!(await introduceSetup(ctx))) return false;
|
|
256
|
+
|
|
235
257
|
const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
|
|
236
258
|
if (enabled === undefined) return false;
|
|
237
259
|
|
|
238
260
|
let agentModels = keepAgentEntries(base.agentModels, enabled);
|
|
239
261
|
for (const agentName of enabled) {
|
|
262
|
+
const profile = agentProfile(agentName);
|
|
263
|
+
if (profile) ctx.ui.notify(`${agentName}: ${profile.remark}`, "info");
|
|
240
264
|
const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
|
|
241
265
|
if (choice === undefined) return false;
|
|
242
266
|
agentModels = applyAgentModelChoice(agentModels, agentName, choice);
|
|
@@ -244,27 +268,26 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
244
268
|
|
|
245
269
|
const next: SubagentsConfig = {
|
|
246
270
|
enabledAgents: enabled,
|
|
247
|
-
// The wizard surfaces every built-in, so an untoggled one was seen and
|
|
248
|
-
// deliberately left off — record them all as known.
|
|
249
|
-
knownAgents: [...BUILTIN_AGENT_NAMES],
|
|
250
271
|
agentModels,
|
|
251
|
-
|
|
252
|
-
agentThinkingLevels: {},
|
|
272
|
+
agentThinkingLevels: keepAgentEntries(base.agentThinkingLevels, enabled),
|
|
253
273
|
maxResultLines: base.maxResultLines,
|
|
254
274
|
agentScope: base.agentScope,
|
|
255
275
|
idleTimeoutSec: base.idleTimeoutSec,
|
|
256
276
|
};
|
|
257
277
|
await saveConfig(next, configPath);
|
|
258
|
-
ctx.ui.notify(
|
|
278
|
+
ctx.ui.notify(
|
|
279
|
+
`pi-subagents saved to ${configPath}. Role thinking defaults apply; open Configure an agent to change one.`,
|
|
280
|
+
"info",
|
|
281
|
+
);
|
|
259
282
|
return true;
|
|
260
283
|
}
|
|
261
284
|
|
|
262
285
|
async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
|
|
263
286
|
while (true) {
|
|
264
287
|
const choice = await ctx.ui.select("pi-subagents settings", [
|
|
265
|
-
"Enable/disable agents",
|
|
266
|
-
"Configure an agent
|
|
267
|
-
"Full re-setup",
|
|
288
|
+
"Enable/disable agents — choose which roles are available",
|
|
289
|
+
"Configure an agent — model and thinking, with its job on the row",
|
|
290
|
+
"Full re-setup — walk through the team and pick models again",
|
|
268
291
|
]);
|
|
269
292
|
if (choice === undefined) return;
|
|
270
293
|
if (choice.startsWith("Full")) {
|
|
@@ -281,33 +304,16 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
281
304
|
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
282
305
|
if (enabled === undefined) continue;
|
|
283
306
|
next.enabledAgents = enabled;
|
|
284
|
-
// A newly enabled role inherits explorer's configured model and
|
|
285
|
-
// thinking level, so the file reflects what it will actually run
|
|
286
|
-
// instead of silently falling back to the current main model: these
|
|
287
|
-
// roles do light migration-grade work on the fast explorer lane.
|
|
288
|
-
for (const agent of enabled) {
|
|
289
|
-
if (agent === "explorer" || config.enabledAgents.includes(agent)) continue;
|
|
290
|
-
if (!next.agentModels[agent] && config.agentModels.explorer) {
|
|
291
|
-
next.agentModels[agent] = config.agentModels.explorer;
|
|
292
|
-
}
|
|
293
|
-
if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels.explorer) {
|
|
294
|
-
next.agentThinkingLevels[agent] = config.agentThinkingLevels.explorer;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
307
|
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
298
308
|
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
299
309
|
} else {
|
|
300
|
-
// Per-agent loop: thinking Esc returns to that agent's model picker;
|
|
301
|
-
// model Esc returns to the agent picker; agent-picker Esc saves completed
|
|
302
|
-
// choices and returns to this settings menu.
|
|
303
310
|
let configuredAny = false;
|
|
304
311
|
while (true) {
|
|
305
312
|
const picked = await configureOneAgent(ctx, next);
|
|
306
313
|
if (!picked) break;
|
|
307
314
|
configuredAny = true;
|
|
308
315
|
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
309
|
-
|
|
310
|
-
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
316
|
+
next.agentThinkingLevels = applyThinkingChoice(next.agentThinkingLevels, picked.name, picked.strength);
|
|
311
317
|
}
|
|
312
318
|
if (!configuredAny) continue;
|
|
313
319
|
await saveConfig(next, configPath);
|
package/src/spawn.ts
CHANGED
|
@@ -115,16 +115,18 @@ export const RESULT_ARTIFACT_MAX_FILES_PER_PROJECT = 50;
|
|
|
115
115
|
// Explicit current prefix plus the strict timestamp/token convention used by 1.1.0.
|
|
116
116
|
const RESULT_ARTIFACT_NAME = /^(?:pi-subagent-\d{13,}-[0-9a-f]{12}|\d{13,}-[a-z0-9]{6})-[\w.-]+\.md$/;
|
|
117
117
|
|
|
118
|
-
/** Name of the single
|
|
119
|
-
* holds every project-scoped artifact (sessions, worktrees, result excerpts).
|
|
120
|
-
* Nothing long-lived is written to the OS temp directory. */
|
|
118
|
+
/** Name of the single internal-state root under the pi agent directory. */
|
|
121
119
|
export const PROJECT_ROOTS_DIR_NAME = "ferris-pi-subagents";
|
|
122
120
|
|
|
121
|
+
export function getSubagentsRoot(configPath: string): string {
|
|
122
|
+
return join(dirname(configPath), PROJECT_ROOTS_DIR_NAME);
|
|
123
|
+
}
|
|
124
|
+
|
|
123
125
|
/** Per-project directory that groups every durable artifact of one checkout:
|
|
124
126
|
* `<pi home>/ferris-pi-subagents/<project-slug-hash>/{sessions,worktrees,results}`.
|
|
125
127
|
* Callers join the kind-specific subdirectory themselves. */
|
|
126
128
|
export function getProjectRoot(configPath: string, cwd?: string): string {
|
|
127
|
-
return join(
|
|
129
|
+
return join(getSubagentsRoot(configPath), resultArtifactProjectKey(cwd));
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
interface ResultArtifactRetentionOptions {
|