@ferris1225/pi-subagents 4.1.2 → 4.1.4
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 +584 -506
- package/agents/cleaner.md +4 -4
- package/agents/documenter.md +46 -44
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +7 -4
- package/agents/worker.md +7 -5
- package/package.json +55 -55
- package/src/agents.ts +42 -1
- package/src/config.ts +5 -5
- package/src/dispatch.ts +647 -637
- package/src/fixloop.ts +90 -127
- package/src/monitor.ts +97 -27
- package/src/prompt.ts +3 -3
- package/src/rpc-run.ts +6 -3
- package/src/runtime.ts +5 -0
- package/src/setup.ts +151 -136
- package/src/spawn.ts +8 -2
- package/src/thread-lifecycle.ts +46 -14
- package/src/tools.ts +44 -30
- package/src/widget.ts +65 -19
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;
|
package/src/thread-lifecycle.ts
CHANGED
|
@@ -11,7 +11,12 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { rm } from "node:fs/promises";
|
|
13
13
|
import { resolve } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
discoverAgents,
|
|
16
|
+
isWriteCapableAgent,
|
|
17
|
+
resolveAgentTools,
|
|
18
|
+
type AgentConfig,
|
|
19
|
+
} from "./agents.ts";
|
|
15
20
|
import { completionTriggersTurn, type CompletionMessageItem } from "./completion.ts";
|
|
16
21
|
import {
|
|
17
22
|
DEFAULT_THINKING_LEVEL,
|
|
@@ -43,7 +48,7 @@ import {
|
|
|
43
48
|
resolveAgentModelRoute,
|
|
44
49
|
resolveThinkingLevel,
|
|
45
50
|
} from "./models.ts";
|
|
46
|
-
import { monitor, sumUsage } from "./monitor.ts";
|
|
51
|
+
import { monitor, sumUsage, type ContinuationKind } from "./monitor.ts";
|
|
47
52
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
48
53
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
49
54
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
@@ -185,6 +190,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
185
190
|
prompt?: string;
|
|
186
191
|
worktree?: WorktreeIsolation;
|
|
187
192
|
forkedFromRunId?: number;
|
|
193
|
+
continuationKind?: ContinuationKind;
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
interface ResumeReservation {
|
|
@@ -225,7 +231,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
225
231
|
cwd: string | undefined,
|
|
226
232
|
isolation: IsolationMode = "shared",
|
|
227
233
|
existingThread?: SubagentThread,
|
|
228
|
-
|
|
234
|
+
appendedObjectiveOnResume = false,
|
|
229
235
|
environment?: DispatchEnvironment,
|
|
230
236
|
seed?: SessionSeed,
|
|
231
237
|
resumeReservation?: ResumeReservation,
|
|
@@ -239,8 +245,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
239
245
|
const runCtx = environment?.ctx ?? ctx;
|
|
240
246
|
const runConfig = environment?.config ?? config;
|
|
241
247
|
const runAgents = environment?.agents ?? agents;
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
248
|
+
const discoveredAgent = runAgents.find((candidate) => candidate.name === agentName);
|
|
249
|
+
if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
250
|
+
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
251
|
+
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
252
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
244
253
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
245
254
|
return {
|
|
246
255
|
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
|
|
@@ -288,6 +297,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
288
297
|
const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
289
298
|
isolation,
|
|
290
299
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
300
|
+
...(seed?.continuationKind ? { continuationKind: seed.continuationKind } : {}),
|
|
291
301
|
});
|
|
292
302
|
const generation = (existingThread?.generation ?? 0) + 1;
|
|
293
303
|
const pending: SingleResult = {
|
|
@@ -302,7 +312,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
302
312
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
303
313
|
};
|
|
304
314
|
if (existingThread) {
|
|
305
|
-
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation
|
|
315
|
+
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation, {
|
|
316
|
+
elapsedMs: existingThread.elapsedMs,
|
|
317
|
+
continuationKind: appendedObjectiveOnResume ? "resume-appended" : "resume-retained",
|
|
318
|
+
});
|
|
306
319
|
runtime.settledRuns.delete(runId);
|
|
307
320
|
}
|
|
308
321
|
|
|
@@ -367,6 +380,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
367
380
|
control,
|
|
368
381
|
generationCompletion: Promise.resolve(),
|
|
369
382
|
lifecycleVersion: 0,
|
|
383
|
+
elapsedMs: 0,
|
|
370
384
|
sessionId: seed?.sessionId,
|
|
371
385
|
sessionDir: seed?.sessionDir,
|
|
372
386
|
forkedFromRunId: seed?.forkedFromRunId,
|
|
@@ -504,6 +518,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
504
518
|
});
|
|
505
519
|
};
|
|
506
520
|
|
|
521
|
+
const persistElapsedTime = (): void => {
|
|
522
|
+
thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
|
|
523
|
+
};
|
|
524
|
+
|
|
507
525
|
thread.park = async (): Promise<"queued" | "active"> => {
|
|
508
526
|
if (thread.retired) throw new Error(`Run #${runId} was retired by subagent_stop.`);
|
|
509
527
|
if (thread.lifecycleOperation) throw new Error(`Run #${runId} is already handling ${thread.lifecycleOperation}.`);
|
|
@@ -544,7 +562,14 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
544
562
|
thread.state = "parked";
|
|
545
563
|
thread.queueController = undefined;
|
|
546
564
|
runtime.runControllers.delete(runId);
|
|
565
|
+
const parkedRun = monitor.findRun(runId);
|
|
566
|
+
if (parkedRun?.managedWorkflow && parkedRun.task !== thread.task) {
|
|
567
|
+
// The active child row previously showed this stage objective. Once it
|
|
568
|
+
// disappears, keep the parked parent aligned with what resume retains.
|
|
569
|
+
monitor.setTask(runId, thread.task);
|
|
570
|
+
}
|
|
547
571
|
monitor.setStatus(runId, "parked");
|
|
572
|
+
persistElapsedTime();
|
|
548
573
|
return queued ? "queued" : "active";
|
|
549
574
|
} finally {
|
|
550
575
|
if (thread.lifecycleVersion === version && thread.lifecycleOperation === "park") {
|
|
@@ -822,6 +847,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
822
847
|
prompt: forkObjective ?? FORK_CONTINUATION_PROMPT,
|
|
823
848
|
worktree: childWorktree,
|
|
824
849
|
forkedFromRunId: runId,
|
|
850
|
+
continuationKind: forkObjective ? "fork-appended" : "fork-retained",
|
|
825
851
|
},
|
|
826
852
|
);
|
|
827
853
|
if (child.exitCode !== -1 || child.runId === undefined) {
|
|
@@ -877,6 +903,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
877
903
|
{
|
|
878
904
|
defaultCwd: executionCwd,
|
|
879
905
|
agent: route.agent,
|
|
906
|
+
resolveAgentForAttempt: resolveLiveAgentTools,
|
|
880
907
|
agentName,
|
|
881
908
|
task,
|
|
882
909
|
cwd: executionCwd,
|
|
@@ -891,7 +918,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
891
918
|
? {
|
|
892
919
|
sessionId: priorSessionId,
|
|
893
920
|
sessionDir: priorSessionDir,
|
|
894
|
-
stdinText: seed?.prompt ?? (
|
|
921
|
+
stdinText: seed?.prompt ?? (appendedObjectiveOnResume
|
|
895
922
|
? task
|
|
896
923
|
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
|
|
897
924
|
}
|
|
@@ -955,9 +982,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
955
982
|
const workflowPlan = getManagedWorkflowPlan(result, runConfig, workflowAvailability);
|
|
956
983
|
if (workflowPlan && runtime.sessionActive) {
|
|
957
984
|
thread.state = "running";
|
|
958
|
-
// The stable parent row
|
|
959
|
-
//
|
|
960
|
-
//
|
|
985
|
+
// The stable parent row now represents workflow ownership, not whichever
|
|
986
|
+
// model stage ran most recently. Internal rows own their exact role/model/
|
|
987
|
+
// thinking/timing telemetry and remain independently queryable.
|
|
988
|
+
monitor.setManagedWorkflow(runId, true);
|
|
961
989
|
monitor.setStatus(runId, "running");
|
|
962
990
|
monitor.setActivity(
|
|
963
991
|
runId,
|
|
@@ -978,8 +1006,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
978
1006
|
rememberLatest: (latest) => {
|
|
979
1007
|
if (runtime.threads.get(runId) !== thread || thread.generation !== generation) return;
|
|
980
1008
|
thread.lastResult = latest;
|
|
1009
|
+
// Retained control follows the newest child session, but the live parent
|
|
1010
|
+
// row keeps the original top-level role/model/usage. The active internal
|
|
1011
|
+
// row already owns the current stage's role and telemetry.
|
|
981
1012
|
thread.agentName = latest.agent;
|
|
982
|
-
monitor.setAgent(runId, latest.agent);
|
|
983
1013
|
thread.task = latest.task;
|
|
984
1014
|
thread.sessionId = latest.sessionId;
|
|
985
1015
|
thread.sessionDir = latest.sessionDir;
|
|
@@ -1021,9 +1051,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1021
1051
|
thread.lifecycleOperation === "settle" &&
|
|
1022
1052
|
!thread.retired;
|
|
1023
1053
|
try {
|
|
1024
|
-
// For isolated writers this is deliberately after the managed
|
|
1025
|
-
// and
|
|
1026
|
-
// lifecycle owner integrates the complete writer+docs state exactly once.
|
|
1054
|
+
// For isolated writers this is deliberately after the managed reviewer
|
|
1055
|
+
// and documentation stages: every child sees the same worktree, then one
|
|
1056
|
+
// lifecycle owner integrates the complete writer+fixes+docs state exactly once.
|
|
1027
1057
|
await thread.finalizeIsolation(generation, result);
|
|
1028
1058
|
if (!ownsSettlement()) return;
|
|
1029
1059
|
if (workflowOutcome && isolation === "worktree") {
|
|
@@ -1041,6 +1071,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1041
1071
|
// Stamp the terminal monitor state before projecting it. This gives every
|
|
1042
1072
|
// path a fixed endedAt even when the row is removed immediately.
|
|
1043
1073
|
monitor.setStatus(runId, failed ? "failed" : "done");
|
|
1074
|
+
persistElapsedTime();
|
|
1044
1075
|
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
1045
1076
|
|
|
1046
1077
|
const modelLevel = failed && isModelLevelFailure(result);
|
|
@@ -1175,6 +1206,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1175
1206
|
if (!ownsSettlement()) return;
|
|
1176
1207
|
thread.state = "failed";
|
|
1177
1208
|
monitor.setStatus(runId, "failed");
|
|
1209
|
+
persistElapsedTime();
|
|
1178
1210
|
finishRun(runId, "failed", { silent: true });
|
|
1179
1211
|
runtime.registerRunResult(runId, crashed);
|
|
1180
1212
|
runtime.runControllers.delete(runId);
|