@ferris1225/pi-subagents 4.1.2 → 4.1.3
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 +561 -506
- package/agents/cleaner.md +2 -2
- package/agents/documenter.md +46 -44
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +5 -2
- package/agents/worker.md +5 -3
- package/package.json +55 -55
- package/src/agents.ts +42 -1
- package/src/completion.ts +160 -160
- package/src/dispatch.ts +634 -637
- package/src/fixloop.ts +15 -32
- package/src/models.ts +189 -189
- package/src/monitor.ts +97 -27
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +6 -3
- package/src/runtime.ts +5 -0
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +151 -136
- package/src/spawn.ts +8 -2
- package/src/thread-lifecycle.ts +43 -11
- package/src/tools.ts +44 -30
- package/src/widget.ts +65 -19
package/src/session-fork.ts
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/** Pi SessionManager-backed cloning of a retained sub-agent session branch. */
|
|
2
|
-
|
|
3
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
export interface ForkedSession {
|
|
10
|
-
sessionDir: string;
|
|
11
|
-
sessionId: string;
|
|
12
|
-
sessionFile: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Locate one retained session by its authoritative header id. */
|
|
16
|
-
export async function findRetainedSessionFile(
|
|
17
|
-
sessionDir: string,
|
|
18
|
-
sessionId: string,
|
|
19
|
-
): Promise<string> {
|
|
20
|
-
// The retained header may point at a worktree that has since been removed.
|
|
21
|
-
// The session id is authoritative inside this explicit private directory;
|
|
22
|
-
// listing the directory directly avoids a stale-cwd filter rejecting it.
|
|
23
|
-
const sessions = await SessionManager.listAll(sessionDir);
|
|
24
|
-
const matches = sessions.filter((session) => session.id === sessionId);
|
|
25
|
-
if (matches.length === 0) {
|
|
26
|
-
throw new Error(`Retained session ${sessionId} was not found in ${sessionDir}.`);
|
|
27
|
-
}
|
|
28
|
-
if (matches.length > 1) {
|
|
29
|
-
throw new Error(`Retained session id ${sessionId} is ambiguous in ${sessionDir}.`);
|
|
30
|
-
}
|
|
31
|
-
return matches[0].path;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Copy only the source file's active branch into a new isolated temp session
|
|
36
|
-
* directory. SessionManager performs all JSONL/tree handling; source state is
|
|
37
|
-
* never mutated.
|
|
38
|
-
*/
|
|
39
|
-
export async function forkRetainedSession(options: {
|
|
40
|
-
/** Cwd stored in the source session header (used for exact lookup). */
|
|
41
|
-
cwd: string;
|
|
42
|
-
/** Optional cwd for the cloned session header and future child tools. */
|
|
43
|
-
targetCwd?: string;
|
|
44
|
-
sessionDir: string;
|
|
45
|
-
sessionId: string;
|
|
46
|
-
}): Promise<ForkedSession> {
|
|
47
|
-
const sourceSessionFile = await findRetainedSessionFile(
|
|
48
|
-
options.sessionDir,
|
|
49
|
-
options.sessionId,
|
|
50
|
-
);
|
|
51
|
-
const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-session-fork-"));
|
|
52
|
-
try {
|
|
53
|
-
// Supplying the new directory makes createBranchedSession write there.
|
|
54
|
-
// cwdOverride rewrites the cloned header so a settled isolated session can
|
|
55
|
-
// safely continue in its fresh worktree instead of a removed old path.
|
|
56
|
-
const manager = SessionManager.open(
|
|
57
|
-
sourceSessionFile,
|
|
58
|
-
sessionDir,
|
|
59
|
-
options.targetCwd ?? options.cwd,
|
|
60
|
-
);
|
|
61
|
-
const leafId = manager.getLeafId();
|
|
62
|
-
if (!leafId) throw new Error(`Retained session ${options.sessionId} has no active branch to fork.`);
|
|
63
|
-
const sessionFile = manager.createBranchedSession(leafId);
|
|
64
|
-
if (!sessionFile) throw new Error("Pi SessionManager did not create a persistent fork.");
|
|
65
|
-
// Pi defers branch files that contain no assistant response. Such a file
|
|
66
|
-
// cannot be resumed by RPC without creating a blank session, so reject
|
|
67
|
-
// rather than pretending context was preserved.
|
|
68
|
-
if (!existsSync(sessionFile)) {
|
|
69
|
-
throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
sessionDir,
|
|
73
|
-
sessionId: manager.getSessionId(),
|
|
74
|
-
sessionFile,
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
await rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
1
|
+
/** Pi SessionManager-backed cloning of a retained sub-agent session branch. */
|
|
2
|
+
|
|
3
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export interface ForkedSession {
|
|
10
|
+
sessionDir: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
sessionFile: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Locate one retained session by its authoritative header id. */
|
|
16
|
+
export async function findRetainedSessionFile(
|
|
17
|
+
sessionDir: string,
|
|
18
|
+
sessionId: string,
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
// The retained header may point at a worktree that has since been removed.
|
|
21
|
+
// The session id is authoritative inside this explicit private directory;
|
|
22
|
+
// listing the directory directly avoids a stale-cwd filter rejecting it.
|
|
23
|
+
const sessions = await SessionManager.listAll(sessionDir);
|
|
24
|
+
const matches = sessions.filter((session) => session.id === sessionId);
|
|
25
|
+
if (matches.length === 0) {
|
|
26
|
+
throw new Error(`Retained session ${sessionId} was not found in ${sessionDir}.`);
|
|
27
|
+
}
|
|
28
|
+
if (matches.length > 1) {
|
|
29
|
+
throw new Error(`Retained session id ${sessionId} is ambiguous in ${sessionDir}.`);
|
|
30
|
+
}
|
|
31
|
+
return matches[0].path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Copy only the source file's active branch into a new isolated temp session
|
|
36
|
+
* directory. SessionManager performs all JSONL/tree handling; source state is
|
|
37
|
+
* never mutated.
|
|
38
|
+
*/
|
|
39
|
+
export async function forkRetainedSession(options: {
|
|
40
|
+
/** Cwd stored in the source session header (used for exact lookup). */
|
|
41
|
+
cwd: string;
|
|
42
|
+
/** Optional cwd for the cloned session header and future child tools. */
|
|
43
|
+
targetCwd?: string;
|
|
44
|
+
sessionDir: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
}): Promise<ForkedSession> {
|
|
47
|
+
const sourceSessionFile = await findRetainedSessionFile(
|
|
48
|
+
options.sessionDir,
|
|
49
|
+
options.sessionId,
|
|
50
|
+
);
|
|
51
|
+
const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-session-fork-"));
|
|
52
|
+
try {
|
|
53
|
+
// Supplying the new directory makes createBranchedSession write there.
|
|
54
|
+
// cwdOverride rewrites the cloned header so a settled isolated session can
|
|
55
|
+
// safely continue in its fresh worktree instead of a removed old path.
|
|
56
|
+
const manager = SessionManager.open(
|
|
57
|
+
sourceSessionFile,
|
|
58
|
+
sessionDir,
|
|
59
|
+
options.targetCwd ?? options.cwd,
|
|
60
|
+
);
|
|
61
|
+
const leafId = manager.getLeafId();
|
|
62
|
+
if (!leafId) throw new Error(`Retained session ${options.sessionId} has no active branch to fork.`);
|
|
63
|
+
const sessionFile = manager.createBranchedSession(leafId);
|
|
64
|
+
if (!sessionFile) throw new Error("Pi SessionManager did not create a persistent fork.");
|
|
65
|
+
// Pi defers branch files that contain no assistant response. Such a file
|
|
66
|
+
// cannot be resumed by RPC without creating a blank session, so reject
|
|
67
|
+
// rather than pretending context was preserved.
|
|
68
|
+
if (!existsSync(sessionFile)) {
|
|
69
|
+
throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
sessionDir,
|
|
73
|
+
sessionId: manager.getSessionId(),
|
|
74
|
+
sessionFile,
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
await rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -63,7 +63,7 @@ const MODULE_HINTS: Record<string, string> = {
|
|
|
63
63
|
explorer: "read-only codebase recon (fast model)",
|
|
64
64
|
worker: "implement / fix / refactor / test (full tools)",
|
|
65
65
|
cleaner: "apply proven cleanup and deduplicate code (full tools)",
|
|
66
|
-
documenter: "sync diff or whole-codebase comments/docs (
|
|
66
|
+
documenter: "sync diff or whole-codebase comments/docs (docs write)",
|
|
67
67
|
reviewer: "read-only audits and pre-commit gates",
|
|
68
68
|
};
|
|
69
69
|
|
|
@@ -89,7 +89,7 @@ async function pickEnabledAgents(
|
|
|
89
89
|
return promptSelectMany(
|
|
90
90
|
ctx,
|
|
91
91
|
"Enable which sub-agents?",
|
|
92
|
-
"Space toggles • Enter confirms • Esc
|
|
92
|
+
"Space toggles • Enter confirms • Esc returns to settings",
|
|
93
93
|
items,
|
|
94
94
|
current,
|
|
95
95
|
);
|
|
@@ -120,7 +120,7 @@ async function pickAgentModel(
|
|
|
120
120
|
ctx: ExtensionCommandContext,
|
|
121
121
|
agentName: string,
|
|
122
122
|
currentRef: string | undefined,
|
|
123
|
-
escNote = "cancels setup",
|
|
123
|
+
escNote = "cancels this setup pass",
|
|
124
124
|
): Promise<string | undefined> {
|
|
125
125
|
return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
|
|
126
126
|
}
|
|
@@ -151,7 +151,7 @@ async function pickAgentStrength(
|
|
|
151
151
|
model: Model<Api> | undefined,
|
|
152
152
|
current: ThinkingLevel | undefined,
|
|
153
153
|
agentDefault: ThinkingLevel,
|
|
154
|
-
escNote = "cancels setup",
|
|
154
|
+
escNote = "cancels this setup pass",
|
|
155
155
|
): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
|
|
156
156
|
const supported = supportedThinkingLevels(model);
|
|
157
157
|
const automatic = resolveThinkingLevel(model, agentDefault);
|
|
@@ -191,44 +191,48 @@ async function pickAgentToConfigure(
|
|
|
191
191
|
return promptSelectOne(
|
|
192
192
|
ctx,
|
|
193
193
|
"Configure which agent?",
|
|
194
|
-
"Type to filter • ↑/↓ • Enter selects • Esc
|
|
194
|
+
"Type to filter • ↑/↓ • Enter selects • Esc returns to settings",
|
|
195
195
|
enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) })),
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
interface ConfiguredAgentChoice {
|
|
200
|
+
name: string;
|
|
201
|
+
model: string;
|
|
202
|
+
strength: ThinkingLevel | typeof AUTO_THINKING;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Configure one agent while preserving the UI back stack: thinking → model →
|
|
206
|
+
* agent selection. Esc from agent selection ends this configuration pass. */
|
|
201
207
|
async function configureOneAgent(
|
|
202
208
|
ctx: ExtensionCommandContext,
|
|
203
209
|
config: SubagentsConfig,
|
|
204
|
-
): Promise<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (strength === undefined) return undefined;
|
|
231
|
-
return { name, model: modelChoice, strength };
|
|
210
|
+
): Promise<ConfiguredAgentChoice | undefined> {
|
|
211
|
+
while (true) {
|
|
212
|
+
const name = await pickAgentToConfigure(ctx, config.enabledAgents);
|
|
213
|
+
if (name === undefined) return undefined;
|
|
214
|
+
|
|
215
|
+
while (true) {
|
|
216
|
+
const modelChoice = await pickAgentModel(
|
|
217
|
+
ctx,
|
|
218
|
+
name,
|
|
219
|
+
config.agentModels[name],
|
|
220
|
+
"returns to agent selection",
|
|
221
|
+
);
|
|
222
|
+
if (modelChoice === undefined) break;
|
|
223
|
+
const model = effectiveModelForChoice(ctx, modelChoice);
|
|
224
|
+
const strength = await pickAgentStrength(
|
|
225
|
+
ctx,
|
|
226
|
+
name,
|
|
227
|
+
model,
|
|
228
|
+
config.agentThinkingLevels[name],
|
|
229
|
+
actualAgentThinkingDefault(ctx, config, name),
|
|
230
|
+
"returns to model selection",
|
|
231
|
+
);
|
|
232
|
+
if (strength === undefined) continue;
|
|
233
|
+
return { name, model: modelChoice, strength };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Promise<boolean | undefined> {
|
|
@@ -280,21 +284,21 @@ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string
|
|
|
280
284
|
return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
|
|
281
285
|
}
|
|
282
286
|
|
|
283
|
-
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<
|
|
287
|
+
async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
|
|
284
288
|
const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
|
|
285
|
-
if (enabled === undefined) return
|
|
289
|
+
if (enabled === undefined) return false;
|
|
286
290
|
|
|
287
291
|
let agentModels = keepAgentEntries(base.agentModels, enabled);
|
|
288
292
|
for (const agentName of enabled) {
|
|
289
293
|
const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
|
|
290
|
-
if (choice === undefined) return
|
|
294
|
+
if (choice === undefined) return false;
|
|
291
295
|
agentModels = applyAgentModelChoice(agentModels, agentName, choice);
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
const injection = await pickInjection(ctx, base.proactiveInjection);
|
|
295
|
-
if (injection === undefined) return
|
|
299
|
+
if (injection === undefined) return false;
|
|
296
300
|
const scope = await pickScope(ctx, base.agentScope);
|
|
297
|
-
if (scope === undefined) return
|
|
301
|
+
if (scope === undefined) return false;
|
|
298
302
|
const maxConcurrency = await pickCount(
|
|
299
303
|
ctx,
|
|
300
304
|
"Max sub-agents running at once?",
|
|
@@ -302,7 +306,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
302
306
|
base.maxConcurrency,
|
|
303
307
|
DEFAULT_MAX_CONCURRENCY,
|
|
304
308
|
);
|
|
305
|
-
if (maxConcurrency === undefined) return
|
|
309
|
+
if (maxConcurrency === undefined) return false;
|
|
306
310
|
const maxFixRounds = await pickCount(
|
|
307
311
|
ctx,
|
|
308
312
|
"Reviewer worker-fix rounds? (0 = no automatic fixes)",
|
|
@@ -310,7 +314,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
310
314
|
base.maxFixRounds,
|
|
311
315
|
DEFAULT_MAX_FIX_ROUNDS,
|
|
312
316
|
);
|
|
313
|
-
if (maxFixRounds === undefined) return
|
|
317
|
+
if (maxFixRounds === undefined) return false;
|
|
314
318
|
const idleTimeoutSec = await pickCount(
|
|
315
319
|
ctx,
|
|
316
320
|
"Idle timeout in seconds? (0 = disabled)",
|
|
@@ -318,7 +322,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
318
322
|
base.idleTimeoutSec,
|
|
319
323
|
DEFAULT_IDLE_TIMEOUT_SEC,
|
|
320
324
|
);
|
|
321
|
-
if (idleTimeoutSec === undefined) return
|
|
325
|
+
if (idleTimeoutSec === undefined) return false;
|
|
322
326
|
|
|
323
327
|
const next: SubagentsConfig = {
|
|
324
328
|
enabledAgents: enabled,
|
|
@@ -342,114 +346,123 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
342
346
|
};
|
|
343
347
|
await saveConfig(next, configPath);
|
|
344
348
|
ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
|
|
349
|
+
return true;
|
|
345
350
|
}
|
|
346
351
|
|
|
347
352
|
async function updateRuntimeSetting(
|
|
348
353
|
ctx: ExtensionCommandContext,
|
|
349
354
|
config: SubagentsConfig,
|
|
350
355
|
): Promise<SubagentsConfig | undefined> {
|
|
351
|
-
|
|
352
|
-
"
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
356
|
+
while (true) {
|
|
357
|
+
const choice = await ctx.ui.select("Runtime setting", [
|
|
358
|
+
"Proactive injection",
|
|
359
|
+
"Agent scope",
|
|
360
|
+
"Max concurrency",
|
|
361
|
+
"Reviewer worker-fix rounds",
|
|
362
|
+
"Idle timeout",
|
|
363
|
+
]);
|
|
364
|
+
if (choice === undefined) return undefined;
|
|
365
|
+
const next = { ...config };
|
|
366
|
+
if (choice.startsWith("Proactive")) {
|
|
367
|
+
const value = await pickInjection(ctx, config.proactiveInjection);
|
|
368
|
+
if (value === undefined) continue;
|
|
369
|
+
next.proactiveInjection = value;
|
|
370
|
+
} else if (choice.startsWith("Agent scope")) {
|
|
371
|
+
const value = await pickScope(ctx, config.agentScope);
|
|
372
|
+
if (value === undefined) continue;
|
|
373
|
+
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
|
+
} else {
|
|
383
|
+
const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
|
|
384
|
+
if (value === undefined) continue;
|
|
385
|
+
next.idleTimeoutSec = value;
|
|
386
|
+
}
|
|
387
|
+
return next;
|
|
380
388
|
}
|
|
381
|
-
return next;
|
|
382
389
|
}
|
|
383
390
|
|
|
384
391
|
async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
|
|
385
|
-
|
|
386
|
-
"
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
392
|
+
while (true) {
|
|
393
|
+
const choice = await ctx.ui.select("pi-subagents settings", [
|
|
394
|
+
"Enable/disable agents",
|
|
395
|
+
"Configure an agent (model + thinking)",
|
|
396
|
+
"Runtime settings",
|
|
397
|
+
"Full re-setup",
|
|
398
|
+
]);
|
|
399
|
+
if (choice === undefined) return;
|
|
400
|
+
if (choice.startsWith("Full")) {
|
|
401
|
+
if (await runFullSetup(ctx, configPath, config)) return;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
393
404
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
405
|
+
let next: SubagentsConfig = {
|
|
406
|
+
...config,
|
|
407
|
+
agentModels: { ...config.agentModels },
|
|
408
|
+
agentThinkingLevels: { ...config.agentThinkingLevels },
|
|
409
|
+
};
|
|
410
|
+
if (choice.startsWith("Enable")) {
|
|
411
|
+
const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
|
|
412
|
+
if (enabled === undefined) continue;
|
|
413
|
+
next.enabledAgents = enabled;
|
|
414
|
+
// Newly enabling cleaner inherits the reviewer's configured model and
|
|
415
|
+
// thinking level, so the file reflects what cleaner will actually run
|
|
416
|
+
// instead of silently falling back to the current main model.
|
|
417
|
+
if (!config.enabledAgents.includes("cleaner") && enabled.includes("cleaner")) {
|
|
418
|
+
if (!next.agentModels.cleaner && config.agentModels.reviewer) {
|
|
419
|
+
next.agentModels.cleaner = config.agentModels.reviewer;
|
|
420
|
+
}
|
|
421
|
+
if (!next.agentThinkingLevels.cleaner && config.agentThinkingLevels.reviewer) {
|
|
422
|
+
next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
|
|
423
|
+
}
|
|
412
424
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
425
|
+
// Documenter intentionally follows the faster explorer route. Fresh
|
|
426
|
+
// installs leave it unselected; enabling it later inherits any explorer
|
|
427
|
+
// overrides instead of silently choosing a stronger model.
|
|
428
|
+
if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
|
|
429
|
+
if (!next.agentModels.documenter && config.agentModels.explorer) {
|
|
430
|
+
next.agentModels.documenter = config.agentModels.explorer;
|
|
431
|
+
}
|
|
432
|
+
if (!next.agentThinkingLevels.documenter && config.agentThinkingLevels.explorer) {
|
|
433
|
+
next.agentThinkingLevels.documenter = config.agentThinkingLevels.explorer;
|
|
434
|
+
}
|
|
420
435
|
}
|
|
421
|
-
|
|
422
|
-
|
|
436
|
+
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
437
|
+
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
438
|
+
} else if (choice.startsWith("Configure")) {
|
|
439
|
+
// Per-agent loop: thinking Esc returns to that agent's model picker;
|
|
440
|
+
// model Esc returns to the agent picker; agent-picker Esc saves completed
|
|
441
|
+
// choices and returns to this settings menu.
|
|
442
|
+
let configuredAny = false;
|
|
443
|
+
while (true) {
|
|
444
|
+
const picked = await configureOneAgent(ctx, next);
|
|
445
|
+
if (!picked) break;
|
|
446
|
+
configuredAny = true;
|
|
447
|
+
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
448
|
+
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
449
|
+
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
423
450
|
}
|
|
451
|
+
if (!configuredAny) continue;
|
|
452
|
+
await saveConfig(next, configPath);
|
|
453
|
+
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
454
|
+
config = next;
|
|
455
|
+
continue;
|
|
456
|
+
} else {
|
|
457
|
+
const updated = await updateRuntimeSetting(ctx, next);
|
|
458
|
+
if (updated === undefined) continue;
|
|
459
|
+
next = updated;
|
|
424
460
|
}
|
|
425
|
-
next.agentModels = keepAgentEntries(next.agentModels, enabled);
|
|
426
|
-
next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
|
|
427
|
-
} else if (choice.startsWith("Configure")) {
|
|
428
|
-
// Per-agent loop: model (+ thinking when the model exposes a choice), then
|
|
429
|
-
// back to the agent picker so several agents can be set in one pass. Esc
|
|
430
|
-
// at any step ends the loop; agents already configured in this pass are kept.
|
|
431
|
-
let configuredAny = false;
|
|
432
|
-
while (true) {
|
|
433
|
-
const picked = await configureOneAgent(ctx, next);
|
|
434
|
-
if (picked === undefined) break;
|
|
435
|
-
configuredAny = true;
|
|
436
|
-
next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
|
|
437
|
-
if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
|
|
438
|
-
else next.agentThinkingLevels[picked.name] = picked.strength;
|
|
439
|
-
}
|
|
440
|
-
if (!configuredAny) return notifyCancelled(ctx);
|
|
441
|
-
} else {
|
|
442
|
-
const updated = await updateRuntimeSetting(ctx, next);
|
|
443
|
-
if (updated === undefined) return notifyCancelled(ctx);
|
|
444
|
-
next = updated;
|
|
445
|
-
}
|
|
446
461
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
function notifyCancelled(ctx: ExtensionCommandContext): void {
|
|
452
|
-
ctx.ui.notify("pi-subagents setup cancelled.", "info");
|
|
462
|
+
await saveConfig(next, configPath);
|
|
463
|
+
ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
453
466
|
}
|
|
454
467
|
|
|
455
468
|
/** Entry point for the /subagents-setup command. */
|
|
@@ -462,7 +475,9 @@ export async function runSetup(ctx: ExtensionCommandContext, configPath: string
|
|
|
462
475
|
const exists = await configExists(configPath);
|
|
463
476
|
const config = await loadConfig(configPath);
|
|
464
477
|
if (exists) await runMenu(ctx, configPath, config);
|
|
465
|
-
else await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] })
|
|
478
|
+
else if (!(await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] }))) {
|
|
479
|
+
ctx.ui.notify("pi-subagents setup cancelled.", "info");
|
|
480
|
+
}
|
|
466
481
|
} catch (error) {
|
|
467
482
|
ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
|
|
468
483
|
}
|
package/src/spawn.ts
CHANGED
|
@@ -327,7 +327,7 @@ export function getResultOutput(result: SingleResult): string {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
export function buildResumePrompt(task: string, reason: string): string {
|
|
330
|
-
return `You are resuming an earlier sub-agent session after ${reason}. Your earlier work — searches, reads, edits, and reasoning — is preserved in this session's history above; review it before acting.
|
|
330
|
+
return `You are resuming an earlier sub-agent session after ${reason}. Your earlier work — searches, reads, edits, and reasoning — is preserved in this session's history above; review it before acting. Current objective: ${task}. Pick up exactly where you left off and finish it. Do NOT redo searches, reads, or edits you already completed unless a step clearly failed. Continue now.`;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
export function buildFallbackResumeReason(fromModel?: string): string {
|
|
@@ -351,6 +351,9 @@ export interface RunSingleOptions {
|
|
|
351
351
|
sessionId?: string;
|
|
352
352
|
/** Initial RPC prompt. Kept under the old name to limit caller churn. */
|
|
353
353
|
stdinText?: string;
|
|
354
|
+
/** Refresh parent-derived tools immediately before every startup retry and
|
|
355
|
+
* selected-to-main fallback process is spawned. */
|
|
356
|
+
resolveAgentForAttempt?: (agent: AgentConfig) => AgentConfig;
|
|
354
357
|
signal?: AbortSignal;
|
|
355
358
|
onLive?: (event: SubagentLiveEvent) => void;
|
|
356
359
|
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
@@ -496,7 +499,10 @@ export async function runSingleAgentWithMainFallback(
|
|
|
496
499
|
}
|
|
497
500
|
const start = Date.now();
|
|
498
501
|
try {
|
|
499
|
-
|
|
502
|
+
const attemptOptions = opts.resolveAgentForAttempt
|
|
503
|
+
? { ...opts, agent: opts.resolveAgentForAttempt(opts.agent) }
|
|
504
|
+
: opts;
|
|
505
|
+
lastResult = await runSingleAgent(attemptOptions);
|
|
500
506
|
} catch (error) {
|
|
501
507
|
const failed = await dispatchFailure(error);
|
|
502
508
|
return controlledDisposition(opts, failed) ?? failed;
|