@ferris1225/pi-subagents 4.1.12 → 4.1.15

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/src/setup.ts CHANGED
@@ -2,20 +2,24 @@
2
2
  * Interactive configuration wizard for /subagents-setup.
3
3
  *
4
4
  * The wizard stays one level deep and exposes only what most users touch:
5
- * which agents run, the model each runs on, and the delegation directive
6
- * toggle. Everything else (per-agent thinking, agent scope, idle timeout,
5
+ * which agents run, the model and thinking strength each runs on, and the
6
+ * delegation directive toggle. Everything else (agent scope, idle timeout,
7
7
  * result lines, notifications) is config-file-only; model failures hand
8
8
  * directly to the current main model, and thinking defaults to capability-
9
9
  * aware Auto.
10
10
  */
11
11
 
12
12
  import { stat } from "node:fs/promises";
13
+ import type { Api, Model } from "@earendil-works/pi-ai";
13
14
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
15
+ import { discoverAgents } from "./agents.ts";
14
16
  import {
15
17
  BUILTIN_AGENT_NAMES,
16
18
  DEFAULT_CONFIG,
17
19
  DEFAULT_ENABLED_AGENTS,
20
+ DEFAULT_THINKING_LEVEL,
18
21
  type SubagentsConfig,
22
+ type ThinkingLevel,
19
23
  errorMessage,
20
24
  getConfigPath,
21
25
  loadConfig,
@@ -27,7 +31,10 @@ import {
27
31
  availableModelsInScope,
28
32
  buildModelPickerItems,
29
33
  currentModelRef,
34
+ findModelByRef,
30
35
  modelRef,
36
+ resolveThinkingLevel,
37
+ supportedThinkingLevels,
31
38
  } from "./models.ts";
32
39
  import { promptSelectMany, promptSelectOne } from "./ui.ts";
33
40
 
@@ -98,6 +105,76 @@ async function pickAgentModel(
98
105
  return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
99
106
  }
100
107
 
108
+ const AUTO_THINKING = "__auto_thinking__";
109
+
110
+ function actualAgentThinkingDefault(
111
+ ctx: ExtensionCommandContext,
112
+ config: SubagentsConfig,
113
+ agentName: string,
114
+ ): ThinkingLevel {
115
+ const { agents } = discoverAgents(ctx.cwd, {
116
+ scope: config.agentScope,
117
+ enabledNames: config.enabledAgents,
118
+ projectTrusted: ctx.isProjectTrusted(),
119
+ });
120
+ return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
121
+ }
122
+
123
+ const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
124
+ off: "no reasoning tokens",
125
+ minimal: "minimal reasoning",
126
+ low: "light reasoning",
127
+ medium: "balanced reasoning",
128
+ high: "deep reasoning",
129
+ xhigh: "extra-deep reasoning",
130
+ max: "strongest reasoning",
131
+ };
132
+
133
+ function effectiveModelForChoice(
134
+ ctx: ExtensionCommandContext,
135
+ choice: string,
136
+ ): Model<Api> | undefined {
137
+ if (choice === CURRENT_MAIN_MODEL) return ctx.model;
138
+ return findModelByRef(availableModelsInScope(ctx), choice);
139
+ }
140
+
141
+ /** Auto is the default. Manual rows are exactly the levels Pi exposes for the
142
+ * selected model; unsupported xhigh/max entries never appear. */
143
+ async function pickAgentStrength(
144
+ ctx: ExtensionCommandContext,
145
+ agentName: string,
146
+ model: Model<Api> | undefined,
147
+ current: ThinkingLevel | undefined,
148
+ agentDefault: ThinkingLevel,
149
+ escNote = "cancels this setup pass",
150
+ ): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
151
+ const supported = supportedThinkingLevels(model);
152
+ const automatic = resolveThinkingLevel(model, agentDefault);
153
+ // No model metadata, or a non-reasoning model whose only valid value is off:
154
+ // Auto is already the complete and least surprising choice.
155
+ if (supported.length <= 1) return AUTO_THINKING;
156
+
157
+ const currentEffective = current ? resolveThinkingLevel(model, current) : undefined;
158
+ const modelName = model ? modelRef(model) : "current main model";
159
+ const options = [
160
+ {
161
+ value: AUTO_THINKING,
162
+ label: `auto — ${automatic} for ${modelName}${current === undefined ? " (current, recommended)" : " (recommended)"}`,
163
+ },
164
+ ...supported.map((level) => ({
165
+ value: level,
166
+ label: `${level} — ${THINKING_LEVEL_HINTS[level]}${current !== undefined && currentEffective === level ? " (current)" : ""}`,
167
+ })),
168
+ ];
169
+ return promptSelectOne(
170
+ ctx,
171
+ `Thinking for "${agentName}"?`,
172
+ `Only levels supported by ${modelName} are shown • Enter selects • Esc ${escNote}`,
173
+ options,
174
+ current === undefined ? AUTO_THINKING : currentEffective,
175
+ ) as Promise<ThinkingLevel | typeof AUTO_THINKING | undefined>;
176
+ }
177
+
101
178
  async function pickAgentToConfigure(
102
179
  ctx: ExtensionCommandContext,
103
180
  enabledAgents: readonly string[],
@@ -117,10 +194,11 @@ async function pickAgentToConfigure(
117
194
  interface ConfiguredAgentChoice {
118
195
  name: string;
119
196
  model: string;
197
+ strength: ThinkingLevel | typeof AUTO_THINKING;
120
198
  }
121
199
 
122
- /** Configure agents while preserving the UI back stack: model selection returns
123
- * to the agent picker on Esc; agent-picker Esc ends this configuration pass. */
200
+ /** Configure one agent while preserving the UI back stack: thinking → model
201
+ * agent selection. Esc from agent selection ends this configuration pass. */
124
202
  async function configureOneAgent(
125
203
  ctx: ExtensionCommandContext,
126
204
  config: SubagentsConfig,
@@ -128,14 +206,27 @@ async function configureOneAgent(
128
206
  while (true) {
129
207
  const name = await pickAgentToConfigure(ctx, config.enabledAgents);
130
208
  if (name === undefined) return undefined;
131
- const model = await pickAgentModel(
132
- ctx,
133
- name,
134
- config.agentModels[name],
135
- "returns to agent selection",
136
- );
137
- if (model === undefined) continue;
138
- return { name, model };
209
+
210
+ while (true) {
211
+ const modelChoice = await pickAgentModel(
212
+ ctx,
213
+ name,
214
+ config.agentModels[name],
215
+ "returns to agent selection",
216
+ );
217
+ if (modelChoice === undefined) break;
218
+ const model = effectiveModelForChoice(ctx, modelChoice);
219
+ const strength = await pickAgentStrength(
220
+ ctx,
221
+ name,
222
+ model,
223
+ config.agentThinkingLevels[name],
224
+ actualAgentThinkingDefault(ctx, config, name),
225
+ "returns to model selection",
226
+ );
227
+ if (strength === undefined) continue;
228
+ return { name, model: modelChoice, strength };
229
+ }
139
230
  }
140
231
  }
141
232
 
@@ -186,7 +277,7 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
186
277
  while (true) {
187
278
  const choice = await ctx.ui.select("pi-subagents settings", [
188
279
  "Enable/disable agents",
189
- "Configure agent models",
280
+ "Configure an agent (model + thinking)",
190
281
  "Proactive injection",
191
282
  "Full re-setup",
192
283
  ]);
@@ -216,9 +307,9 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
216
307
  next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
217
308
  }
218
309
  }
219
- // Documenter intentionally follows the faster explorer route. Fresh
220
- // installs leave it unselected; enabling it later inherits any explorer
221
- // overrides instead of silently choosing a stronger model.
310
+ // Documenter intentionally follows the faster explorer route. When it
311
+ // is re-enabled after being explicitly disabled, it inherits any
312
+ // explorer overrides instead of silently choosing a stronger model.
222
313
  if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
223
314
  if (!next.agentModels.documenter && config.agentModels.explorer) {
224
315
  next.agentModels.documenter = config.agentModels.explorer;
@@ -230,14 +321,17 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
230
321
  next.agentModels = keepAgentEntries(next.agentModels, enabled);
231
322
  next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
232
323
  } else if (choice.startsWith("Configure")) {
233
- // Per-agent loop: model Esc returns to the agent picker; agent-picker
234
- // Esc saves completed choices and returns to this settings menu.
324
+ // Per-agent loop: thinking Esc returns to that agent's model picker;
325
+ // model Esc returns to the agent picker; agent-picker Esc saves completed
326
+ // choices and returns to this settings menu.
235
327
  let configuredAny = false;
236
328
  while (true) {
237
329
  const picked = await configureOneAgent(ctx, next);
238
330
  if (!picked) break;
239
331
  configuredAny = true;
240
332
  next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
333
+ if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
334
+ else next.agentThinkingLevels[picked.name] = picked.strength;
241
335
  }
242
336
  if (!configuredAny) continue;
243
337
  await saveConfig(next, configPath);
@@ -42,6 +42,7 @@ import {
42
42
  failedStartResult,
43
43
  formatCompletionBlock,
44
44
  modelLevelTakeoverNote,
45
+ reviewFailFollowUpNote,
45
46
  queuedResult,
46
47
  } from "./format.ts";
47
48
  import {
@@ -220,16 +221,19 @@ export function ownsResumeReservation(
220
221
  );
221
222
  }
222
223
 
223
- /** Fire-and-forget durable checkpoint write. The live session keeps working
224
- * when the manifest is unwritable; only cross-reload resume is degraded. */
224
+ /** Fire-and-forget durable checkpoint. Parked threads stay resumable across
225
+ * reloads; a settled thread drops its record so the manifest only exists
226
+ * while unfinished work needs it. The live session keeps working when the
227
+ * manifest is unwritable; only cross-reload resume is degraded. */
225
228
  export function persistThreadCheckpoint(
226
229
  runtime: SubagentRuntime,
227
230
  thread: SubagentThread,
228
231
  state: "parked" | "completed" | "failed",
229
232
  ): void {
230
- void upsertThreadRecord(runtime.configPath, threadRecordFromThread(thread, state)).catch(
231
- () => undefined,
232
- );
233
+ const write = state === "parked"
234
+ ? upsertThreadRecord(runtime.configPath, threadRecordFromThread(thread, state))
235
+ : removeThreadRecord(runtime.configPath, thread.id);
236
+ void write.catch(() => undefined);
233
237
  }
234
238
 
235
239
  const WORKTREE_ISOLATION_INSTRUCTIONS =
@@ -246,27 +250,6 @@ export function isWorktreeCapableAgent(agent: AgentConfig): boolean {
246
250
  return isWriteCapableAgent(agent);
247
251
  }
248
252
 
249
- /** A direct reviewer otherwise cannot infer enabled-role availability from its
250
- * isolated task. Managed internal gates receive the same contract in their
251
- * generated briefs. Advisory reviews still emit neither machine marker. */
252
- function withEnabledDocumenterReviewContract(agent: AgentConfig): AgentConfig {
253
- return {
254
- ...agent,
255
- systemPrompt: `${agent.systemPrompt.trimEnd()}\n\nRuntime workflow context: documenter is enabled. In gate reviews, documentation drift is non-gating: emit DOCUMENTATION: NEEDED with ## Documentation notes, or DOCUMENTATION: CLEAN when no sync is needed. Advisory reviews still emit neither VERDICT nor DOCUMENTATION markers.`.trim(),
256
- };
257
- }
258
-
259
- /** The dispatcher explicitly requested a report-only review: forbid the gate
260
- * markers at the source and (in dispatch) refuse to chain on them anyway. */
261
- function withAdvisoryReviewContract(agent: AgentConfig): AgentConfig {
262
- return {
263
- ...agent,
264
- systemPrompt: `${agent.systemPrompt.trimEnd()}
265
-
266
- Runtime workflow context: this dispatch is advisory. Report findings only; do not emit VERDICT or DOCUMENTATION markers — the runtime will not act on them.`.trim(),
267
- };
268
- }
269
-
270
253
  export interface DispatchEnvironment {
271
254
  ctx: ExtensionContext;
272
255
  config: SubagentsConfig;
@@ -296,7 +279,6 @@ export interface StartBackgroundOptions {
296
279
  environment?: DispatchEnvironment;
297
280
  seed?: SessionSeed;
298
281
  resumeReservation?: ResumeReservation;
299
- advisoryReview?: boolean;
300
282
  }
301
283
 
302
284
  export type StartBackgroundInternal = (
@@ -426,15 +408,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
426
408
  if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
427
409
  const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
428
410
  resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
429
- const resolvedAgent = resolveLiveAgentTools(discoveredAgent);
430
- const advisoryReview = startOptions.advisoryReview ?? existingThread?.advisoryReview ?? false;
431
- const agent = agentName === "reviewer"
432
- ? advisoryReview
433
- ? withAdvisoryReviewContract(resolvedAgent)
434
- : runAgents.some((candidate) => candidate.name === "documenter")
435
- ? withEnabledDocumenterReviewContract(resolvedAgent)
436
- : resolvedAgent
437
- : resolvedAgent;
411
+ const agent = resolveLiveAgentTools(discoveredAgent);
438
412
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
439
413
  return {
440
414
  ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
@@ -528,7 +502,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
528
502
  thread.executionCwd = executionCwd;
529
503
  thread.thinkingLevel = thinkingLevel;
530
504
  thread.isolation = isolation;
531
- thread.advisoryReview = advisoryReview;
532
505
  thread.worktree = worktree;
533
506
  thread.state = "queued";
534
507
  thread.control = control;
@@ -552,7 +525,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
552
525
  executionCwd,
553
526
  thinkingLevel,
554
527
  isolation,
555
- advisoryReview,
556
528
  worktree,
557
529
  state: "queued",
558
530
  control,
@@ -670,13 +642,13 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
670
642
  if (thread.lifecycleOperation === "stop") return;
671
643
 
672
644
  // A shutdown can win in the microtask gap after the top-level RPC
673
- // settles. Do not launch an obsolete documenter/reviewer or replace the
645
+ // settles. Do not launch an obsolete downstream gate or replace the
674
646
  // stable top-level session with an aborted downstream attempt.
675
647
  if (backgroundSignal.aborted || lifecycleInterrupted() || !runtime.sessionActive) return;
676
648
 
677
649
  if (thread.retireOnSettle) runtime.retireThreadSession(thread);
678
650
  let workflowOutcome: ManagedWorkflowOutcome | undefined;
679
- const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability, thread.advisoryReview);
651
+ const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability);
680
652
  if (workflowPlan && runtime.sessionActive) {
681
653
  // The continuation is runtime-initiated (gate review,
682
654
  // documentation sync): release this generation's
@@ -801,6 +773,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
801
773
  if (needsFullFinal) {
802
774
  block += `\n\n${formatCompletionBlock(result, runConfig.maxResultLines, originalCwd)}`;
803
775
  }
776
+ if (finalVerdict === "fail") {
777
+ block += `\n\n${reviewFailFollowUpNote()}`;
778
+ }
804
779
  if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
805
780
  runtime.sendCompletionGroup([{
806
781
  agent: `managed workflow (${result.agent})`,
@@ -816,7 +791,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
816
791
  agent: result.agent,
817
792
  block: modelLevel
818
793
  ? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
819
- : formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
794
+ : result.agent === "reviewer" && reviewVerdict(getResultOutput(result)) === "fail"
795
+ ? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${reviewFailFollowUpNote()}`
796
+ : formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
820
797
  triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
821
798
  usage: result.usage,
822
799
  };
@@ -1290,7 +1267,6 @@ function createRestoredThread(
1290
1267
  executionCwd: record.executionCwd,
1291
1268
  ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
1292
1269
  isolation: record.isolation,
1293
- advisoryReview: false,
1294
1270
  worktree,
1295
1271
  state,
1296
1272
  control: new RpcRunControl(record.task, record.generation),
@@ -1320,15 +1296,20 @@ function createRestoredThread(
1320
1296
  return thread;
1321
1297
  }
1322
1298
 
1323
- /** Rebuild parked and settled threads from the durable manifest after a reload
1324
- * or restart. Orphaned children recorded by the previous process are killed
1325
- * first; records whose retained session vanished drop out with their
1326
- * artifacts. Returns the restored run ids. */
1299
+ /** Rebuild interrupted (parked) threads from the durable manifest after a
1300
+ * reload or restart. Orphaned children recorded by the previous process are
1301
+ * killed first; records whose retained session vanished and settled records
1302
+ * left by older versions, which hold no work worth resuming — drop out with
1303
+ * their artifacts. Returns the restored run ids. */
1327
1304
  export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<number[]> {
1328
1305
  const records = await readThreadRecords(runtime.configPath);
1329
1306
  const restoredIds: number[] = [];
1330
1307
  for (const record of records) {
1331
1308
  if (runtime.threads.has(record.runId) || monitor.findRun(record.runId)) continue;
1309
+ if (record.state !== "parked") {
1310
+ await discardRestoredRecord(runtime, record);
1311
+ continue;
1312
+ }
1332
1313
  // A child orphaned by reload/crash may still hold the retained session.
1333
1314
  // The on-disk session checkpoint is what survives; kill the writer.
1334
1315
  for (const pid of record.childPids) {
@@ -1348,33 +1329,29 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
1348
1329
  // A worktree thread whose isolated filesystem is gone cannot continue its
1349
1330
  // isolation invariant; surface it as failed instead of pretending.
1350
1331
  const state: ThreadState = worktree
1351
- ? record.state
1332
+ ? "parked"
1352
1333
  : record.isolation === "worktree" && record.worktree
1353
1334
  ? "failed"
1354
- : record.state;
1335
+ : "parked";
1355
1336
  const thread = createRestoredThread(runtime, record, worktree, state);
1356
1337
  runtime.threads.set(record.runId, thread);
1357
1338
  runtime.sessionDirs.add(record.sessionDir!);
1358
- if (state === "parked") {
1359
- monitor.restoreRun({
1360
- id: record.runId,
1361
- agent: record.agentName,
1362
- task: record.task,
1363
- status: "parked",
1364
- elapsedMs: record.elapsedMs,
1365
- isolation: record.isolation,
1366
- ...(record.worktree
1367
- ? {
1368
- integrationStatus: record.worktree.state === "active"
1369
- ? ("pending" as const)
1370
- : record.worktree.state,
1371
- ...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
1372
- }
1373
- : {}),
1374
- });
1375
- } else if (thread.lastResult) {
1376
- runtime.registerRunResult(record.runId, thread.lastResult);
1377
- }
1339
+ monitor.restoreRun({
1340
+ id: record.runId,
1341
+ agent: record.agentName,
1342
+ task: record.task,
1343
+ status: "parked",
1344
+ elapsedMs: record.elapsedMs,
1345
+ isolation: record.isolation,
1346
+ ...(record.worktree
1347
+ ? {
1348
+ integrationStatus: record.worktree.state === "active"
1349
+ ? ("pending" as const)
1350
+ : record.worktree.state,
1351
+ ...(worktree ? { worktreeId: worktreeGroupId(worktree) } : {}),
1352
+ }
1353
+ : {}),
1354
+ });
1378
1355
  restoredIds.push(record.runId);
1379
1356
  }
1380
1357
  return restoredIds;
package/src/tools.ts CHANGED
@@ -106,7 +106,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
106
106
  const context = hadRetainedSession
107
107
  ? "the same retained session and prior context are preserved"
108
108
  : "no prior child session existed, so only the logical run and objective are continued";
109
- return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. Completion will arrive automatically.` }], details: {}, terminate: true };
109
+ return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. It runs in the background — keep working; the result resumes you automatically.` }], details: {} };
110
110
  }
111
111
  }
112
112
  } catch (error) {