pi-subagents 0.12.5 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/execution.ts CHANGED
@@ -14,8 +14,10 @@ import {
14
14
  import {
15
15
  type AgentProgress,
16
16
  type ArtifactPaths,
17
+ type ModelAttempt,
17
18
  type RunSyncOptions,
18
19
  type SingleResult,
20
+ type Usage,
19
21
  DEFAULT_MAX_OUTPUT,
20
22
  truncateOutput,
21
23
  getSubagentDepthEnv,
@@ -31,81 +33,87 @@ import { buildSkillInjection, resolveSkills } from "./skills.ts";
31
33
  import { getPiSpawnCommand } from "./pi-spawn.ts";
32
34
  import { createJsonlWriter } from "./jsonl-writer.ts";
33
35
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.ts";
34
- import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
35
-
36
- /**
37
- * Run a subagent synchronously (blocking until complete)
38
- */
39
- export async function runSync(
40
- runtimeCwd: string,
41
- agents: AgentConfig[],
42
- agentName: string,
43
- task: string,
44
- options: RunSyncOptions,
45
- ): Promise<SingleResult> {
46
- const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index, modelOverride } = options;
47
- const agent = agents.find((a) => a.name === agentName);
48
- if (!agent) {
49
- return {
50
- agent: agentName,
51
- task,
52
- exitCode: 1,
53
- messages: [],
54
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
55
- error: `Unknown agent: ${agentName}`,
56
- };
57
- }
36
+ import { captureSingleOutputSnapshot, resolveSingleOutput, type SingleOutputSnapshot } from "./single-output.ts";
37
+ import {
38
+ buildModelCandidates,
39
+ formatModelAttemptNote,
40
+ isRetryableModelFailure,
41
+ } from "./model-fallback.ts";
58
42
 
59
- const shareEnabled = options.share === true;
60
- const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
61
- const effectiveModel = modelOverride ?? agent.model;
62
- const modelArg = applyThinkingSuffix(effectiveModel, agent.thinking);
63
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
43
+ function emptyUsage(): Usage {
44
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
45
+ }
64
46
 
65
- const skillNames = options.skills ?? agent.skills ?? [];
66
- const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
47
+ function sumUsage(target: Usage, source: Usage): void {
48
+ target.input += source.input;
49
+ target.output += source.output;
50
+ target.cacheRead += source.cacheRead;
51
+ target.cacheWrite += source.cacheWrite;
52
+ target.cost += source.cost;
53
+ target.turns += source.turns;
54
+ }
67
55
 
68
- let systemPrompt = agent.systemPrompt?.trim() || "";
69
- if (resolvedSkills.length > 0) {
70
- const skillInjection = buildSkillInjection(resolvedSkills);
71
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
56
+ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
57
+ if (lines.length === 0) return;
58
+ progress.recentOutput.push(...lines.filter((line) => line.trim()));
59
+ if (progress.recentOutput.length > 50) {
60
+ progress.recentOutput.splice(0, progress.recentOutput.length - 50);
72
61
  }
62
+ }
73
63
 
64
+ async function runSingleAttempt(
65
+ runtimeCwd: string,
66
+ agent: AgentConfig,
67
+ task: string,
68
+ model: string | undefined,
69
+ options: RunSyncOptions,
70
+ shared: {
71
+ sessionEnabled: boolean;
72
+ systemPrompt: string;
73
+ skillNames: string[];
74
+ resolvedSkillNames?: string[];
75
+ skillsWarning?: string;
76
+ jsonlPath?: string;
77
+ attemptNotes: string[];
78
+ outputSnapshot?: SingleOutputSnapshot;
79
+ },
80
+ ): Promise<SingleResult> {
81
+ const modelArg = applyThinkingSuffix(model, agent.thinking);
74
82
  const { args, env: sharedEnv, tempDir } = buildPiArgs({
75
83
  baseArgs: ["--mode", "json", "-p"],
76
84
  task,
77
- sessionEnabled,
85
+ sessionEnabled: shared.sessionEnabled,
78
86
  sessionDir: options.sessionDir,
79
87
  sessionFile: options.sessionFile,
80
- model: effectiveModel,
88
+ model,
81
89
  thinking: agent.thinking,
82
90
  tools: agent.tools,
83
91
  extensions: agent.extensions,
84
- skills: skillNames,
85
- systemPrompt,
92
+ skills: shared.skillNames,
93
+ systemPrompt: shared.systemPrompt,
86
94
  mcpDirectTools: agent.mcpDirectTools,
87
95
  promptFileStem: agent.name,
88
96
  });
89
97
 
90
98
  const result: SingleResult = {
91
- agent: agentName,
99
+ agent: agent.name,
92
100
  task,
93
101
  exitCode: 0,
94
102
  messages: [],
95
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
103
+ usage: emptyUsage(),
96
104
  model: modelArg,
97
- skills: resolvedSkills.length > 0 ? resolvedSkills.map((s) => s.name) : undefined,
98
- skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
105
+ skills: shared.resolvedSkillNames,
106
+ skillsWarning: shared.skillsWarning,
99
107
  };
100
108
 
101
109
  const progress: AgentProgress = {
102
- index: index ?? 0,
103
- agent: agentName,
110
+ index: options.index ?? 0,
111
+ agent: agent.name,
104
112
  status: "running",
105
113
  task,
106
- skills: resolvedSkills.length > 0 ? resolvedSkills.map((s) => s.name) : undefined,
114
+ skills: shared.resolvedSkillNames,
107
115
  recentTools: [],
108
- recentOutput: [],
116
+ recentOutput: [...shared.attemptNotes],
109
117
  toolCount: 0,
110
118
  tokens: 0,
111
119
  durationMs: 0,
@@ -113,40 +121,25 @@ export async function runSync(
113
121
  result.progress = progress;
114
122
 
115
123
  const startTime = Date.now();
116
-
117
- let artifactPathsResult: ArtifactPaths | undefined;
118
- let jsonlPath: string | undefined;
119
- if (artifactsDir && artifactConfig?.enabled !== false) {
120
- artifactPathsResult = getArtifactPaths(artifactsDir, runId, agentName, index);
121
- ensureArtifactsDir(artifactsDir);
122
- if (artifactConfig?.includeInput !== false) {
123
- writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
124
- }
125
- if (artifactConfig?.includeJsonl !== false) {
126
- jsonlPath = artifactPathsResult.jsonlPath;
127
- }
128
- }
129
-
130
124
  const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
131
125
 
132
126
  let closeJsonlWriter: (() => Promise<void>) | undefined;
133
127
  const exitCode = await new Promise<number>((resolve) => {
134
128
  const spawnSpec = getPiSpawnCommand(args);
135
129
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
136
- cwd: cwd ?? runtimeCwd,
130
+ cwd: options.cwd ?? runtimeCwd,
137
131
  env: spawnEnv,
138
132
  stdio: ["ignore", "pipe", "pipe"],
139
133
  });
140
- const jsonlWriter = createJsonlWriter(jsonlPath, proc.stdout);
134
+ const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
141
135
  closeJsonlWriter = () => jsonlWriter.close();
142
136
  let buf = "";
143
-
144
137
  let processClosed = false;
145
138
 
146
139
  const fireUpdate = () => {
147
- if (!onUpdate || processClosed) return;
140
+ if (!options.onUpdate || processClosed) return;
148
141
  progress.durationMs = Date.now() - startTime;
149
- onUpdate({
142
+ options.onUpdate({
150
143
  content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
151
144
  details: { mode: "single", results: [result], progress: [progress] },
152
145
  });
@@ -195,37 +188,14 @@ export async function runSync(
195
188
  }
196
189
  if (!result.model && evt.message.model) result.model = evt.message.model;
197
190
  if (evt.message.errorMessage) result.error = evt.message.errorMessage;
198
-
199
- const text = extractTextFromContent(evt.message.content);
200
- if (text) {
201
- const lines = text
202
- .split("\n")
203
- .filter((l) => l.trim())
204
- .slice(-10);
205
- // Append to existing recentOutput (keep last 50 total) - mutate in place for efficiency
206
- progress.recentOutput.push(...lines);
207
- if (progress.recentOutput.length > 50) {
208
- progress.recentOutput.splice(0, progress.recentOutput.length - 50);
209
- }
210
- }
191
+ appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
211
192
  }
212
193
  fireUpdate();
213
194
  }
195
+
214
196
  if (evt.type === "tool_result_end" && evt.message) {
215
197
  result.messages.push(evt.message);
216
- // Also capture tool result text in recentOutput for streaming display
217
- const toolText = extractTextFromContent(evt.message.content);
218
- if (toolText) {
219
- const toolLines = toolText
220
- .split("\n")
221
- .filter((l) => l.trim())
222
- .slice(-10);
223
- // Append to existing recentOutput (keep last 50 total) - mutate in place for efficiency
224
- progress.recentOutput.push(...toolLines);
225
- if (progress.recentOutput.length > 50) {
226
- progress.recentOutput.splice(0, progress.recentOutput.length - 50);
227
- }
228
- }
198
+ appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
229
199
  fireUpdate();
230
200
  }
231
201
  } catch {
@@ -254,13 +224,13 @@ export async function runSync(
254
224
  });
255
225
  proc.on("error", () => resolve(1));
256
226
 
257
- if (signal) {
227
+ if (options.signal) {
258
228
  const kill = () => {
259
229
  proc.kill("SIGTERM");
260
230
  setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
261
231
  };
262
- if (signal.aborted) kill();
263
- else signal.addEventListener("abort", kill, { once: true });
232
+ if (options.signal.aborted) kill();
233
+ else options.signal.addEventListener("abort", kill, { once: true });
264
234
  }
265
235
  });
266
236
 
@@ -294,7 +264,6 @@ export async function runSync(
294
264
  }
295
265
  }
296
266
 
297
- result.progress = progress;
298
267
  result.progressSummary = {
299
268
  toolCount: progress.toolCount,
300
269
  tokens: progress.tokens,
@@ -303,29 +272,150 @@ export async function runSync(
303
272
 
304
273
  let fullOutput = getFinalOutput(result.messages);
305
274
  if (options.outputPath && result.exitCode === 0) {
306
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, outputSnapshot);
275
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
307
276
  fullOutput = resolvedOutput.fullOutput;
308
277
  result.savedOutputPath = resolvedOutput.savedPath;
309
278
  result.outputSaveError = resolvedOutput.saveError;
310
279
  }
311
280
  result.finalOutput = fullOutput;
281
+ return result;
282
+ }
312
283
 
313
- if (artifactPathsResult && artifactConfig?.enabled !== false) {
314
- result.artifactPaths = artifactPathsResult;
284
+ /**
285
+ * Run a subagent synchronously (blocking until complete)
286
+ */
287
+ export async function runSync(
288
+ runtimeCwd: string,
289
+ agents: AgentConfig[],
290
+ agentName: string,
291
+ task: string,
292
+ options: RunSyncOptions,
293
+ ): Promise<SingleResult> {
294
+ const agent = agents.find((a) => a.name === agentName);
295
+ if (!agent) {
296
+ return {
297
+ agent: agentName,
298
+ task,
299
+ exitCode: 1,
300
+ messages: [],
301
+ usage: emptyUsage(),
302
+ error: `Unknown agent: ${agentName}`,
303
+ };
304
+ }
305
+
306
+ const shareEnabled = options.share === true;
307
+ const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
308
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
309
+ const skillNames = options.skills ?? agent.skills ?? [];
310
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
311
+ let systemPrompt = agent.systemPrompt?.trim() || "";
312
+ if (resolvedSkills.length > 0) {
313
+ const skillInjection = buildSkillInjection(resolvedSkills);
314
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
315
+ }
315
316
 
316
- if (artifactConfig?.includeOutput !== false) {
317
- writeArtifact(artifactPathsResult.outputPath, fullOutput);
317
+ const candidates = buildModelCandidates(
318
+ options.modelOverride ?? agent.model,
319
+ agent.fallbackModels,
320
+ options.availableModels,
321
+ );
322
+ const attemptedModels: string[] = [];
323
+ const modelAttempts: ModelAttempt[] = [];
324
+ const aggregateUsage = emptyUsage();
325
+ const attemptNotes: string[] = [];
326
+ let totalToolCount = 0;
327
+ let totalDurationMs = 0;
328
+
329
+ let artifactPathsResult: ArtifactPaths | undefined;
330
+ let jsonlPath: string | undefined;
331
+ if (options.artifactsDir && options.artifactConfig?.enabled !== false) {
332
+ artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
333
+ ensureArtifactsDir(options.artifactsDir);
334
+ if (options.artifactConfig?.includeInput !== false) {
335
+ writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
336
+ }
337
+ if (options.artifactConfig?.includeJsonl !== false) {
338
+ jsonlPath = artifactPathsResult.jsonlPath;
318
339
  }
319
- if (artifactConfig?.includeMetadata !== false) {
340
+ }
341
+
342
+ let lastResult: SingleResult | undefined;
343
+ const modelsToTry = candidates.length > 0 ? candidates : [undefined];
344
+ for (let i = 0; i < modelsToTry.length; i++) {
345
+ const candidate = modelsToTry[i];
346
+ if (candidate) attemptedModels.push(candidate);
347
+ const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
348
+ sessionEnabled,
349
+ systemPrompt,
350
+ skillNames,
351
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
352
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
353
+ jsonlPath,
354
+ attemptNotes,
355
+ outputSnapshot,
356
+ });
357
+ lastResult = result;
358
+ sumUsage(aggregateUsage, result.usage);
359
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
360
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
361
+ const attempt: ModelAttempt = {
362
+ model: candidate ?? result.model ?? agent.model ?? "default",
363
+ success: result.exitCode === 0,
364
+ exitCode: result.exitCode,
365
+ error: result.error,
366
+ usage: { ...result.usage },
367
+ };
368
+ modelAttempts.push(attempt);
369
+ if (result.exitCode === 0) {
370
+ break;
371
+ }
372
+ if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
373
+ break;
374
+ }
375
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
376
+ }
377
+
378
+ const result = lastResult ?? {
379
+ agent: agentName,
380
+ task,
381
+ exitCode: 1,
382
+ messages: [],
383
+ usage: emptyUsage(),
384
+ error: "Subagent did not produce a result.",
385
+ } satisfies SingleResult;
386
+
387
+ result.usage = aggregateUsage;
388
+ result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
389
+ result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
390
+ result.progressSummary = {
391
+ toolCount: totalToolCount,
392
+ tokens: aggregateUsage.input + aggregateUsage.output,
393
+ durationMs: totalDurationMs,
394
+ };
395
+ if (attemptNotes.length > 0 && result.progress) {
396
+ result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
397
+ if (result.progress.recentOutput.length > 50) {
398
+ result.progress.recentOutput.splice(50);
399
+ }
400
+ }
401
+
402
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
403
+ result.artifactPaths = artifactPathsResult;
404
+ if (options.artifactConfig?.includeOutput !== false) {
405
+ writeArtifact(artifactPathsResult.outputPath, result.finalOutput ?? "");
406
+ }
407
+ if (options.artifactConfig?.includeMetadata !== false) {
320
408
  writeMetadata(artifactPathsResult.metadataPath, {
321
- runId,
409
+ runId: options.runId,
322
410
  agent: agentName,
323
411
  task,
324
412
  exitCode: result.exitCode,
325
413
  usage: result.usage,
326
414
  model: result.model,
327
- durationMs: progress.durationMs,
328
- toolCount: progress.toolCount,
415
+ attemptedModels: result.attemptedModels,
416
+ modelAttempts: result.modelAttempts,
417
+ durationMs: result.progressSummary?.durationMs,
418
+ toolCount: result.progressSummary?.toolCount,
329
419
  error: result.error,
330
420
  skills: result.skills,
331
421
  skillsWarning: result.skillsWarning,
@@ -333,19 +423,15 @@ export async function runSync(
333
423
  });
334
424
  }
335
425
 
336
- if (maxOutput) {
337
- const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
338
- const truncationResult = truncateOutput(fullOutput, config, artifactPathsResult.outputPath);
339
- if (truncationResult.truncated) {
340
- result.truncation = truncationResult;
341
- }
342
- }
343
- } else if (maxOutput) {
344
- const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
345
- const truncationResult = truncateOutput(fullOutput, config);
346
- if (truncationResult.truncated) {
347
- result.truncation = truncationResult;
426
+ if (options.maxOutput) {
427
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
428
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
429
+ if (truncationResult.truncated) result.truncation = truncationResult;
348
430
  }
431
+ } else if (options.maxOutput) {
432
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
433
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config);
434
+ if (truncationResult.truncated) result.truncation = truncationResult;
349
435
  }
350
436
 
351
437
  if (shareEnabled) {
@@ -353,8 +439,6 @@ export async function runSync(
353
439
  ?? (options.sessionDir ? findLatestSessionFile(options.sessionDir) : null);
354
440
  if (sessionFile) {
355
441
  result.sessionFile = sessionFile;
356
- // HTML export disabled - module resolution issues with global pi installation
357
- // Users can still access the session file directly
358
442
  }
359
443
  }
360
444
 
@@ -0,0 +1,100 @@
1
+ import type { Usage } from "./types.ts";
2
+
3
+ export interface AvailableModelInfo {
4
+ provider: string;
5
+ id: string;
6
+ fullId: string;
7
+ }
8
+
9
+ export interface ModelAttemptSummary {
10
+ model: string;
11
+ success: boolean;
12
+ exitCode?: number | null;
13
+ error?: string;
14
+ usage?: Usage;
15
+ }
16
+
17
+ function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
18
+ const colonIdx = model.lastIndexOf(":");
19
+ if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
20
+ return {
21
+ baseModel: model.substring(0, colonIdx),
22
+ thinkingSuffix: model.substring(colonIdx),
23
+ };
24
+ }
25
+
26
+ export function resolveModelCandidate(
27
+ model: string | undefined,
28
+ availableModels: AvailableModelInfo[] | undefined,
29
+ ): string | undefined {
30
+ if (!model) return undefined;
31
+ if (model.includes("/")) return model;
32
+ if (!availableModels || availableModels.length === 0) return model;
33
+
34
+ const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
35
+ const matches = availableModels.filter((entry) => entry.id === baseModel);
36
+ if (matches.length !== 1) return model;
37
+ return `${matches[0]!.fullId}${thinkingSuffix}`;
38
+ }
39
+
40
+ export function buildModelCandidates(
41
+ primaryModel: string | undefined,
42
+ fallbackModels: string[] | undefined,
43
+ availableModels: AvailableModelInfo[] | undefined,
44
+ ): string[] {
45
+ const seen = new Set<string>();
46
+ const candidates: string[] = [];
47
+ for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
48
+ if (!raw) continue;
49
+ const normalized = resolveModelCandidate(raw.trim(), availableModels);
50
+ if (!normalized || seen.has(normalized)) continue;
51
+ seen.add(normalized);
52
+ candidates.push(normalized);
53
+ }
54
+ return candidates;
55
+ }
56
+
57
+ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
58
+ /rate\s*limit/i,
59
+ /too many requests/i,
60
+ /\b429\b/,
61
+ /quota/i,
62
+ /billing/i,
63
+ /credit/i,
64
+ /auth(?:entication)?/i,
65
+ /unauthori[sz]ed/i,
66
+ /forbidden/i,
67
+ /api key/i,
68
+ /token expired/i,
69
+ /invalid key/i,
70
+ /provider.*unavailable/i,
71
+ /model.*unavailable/i,
72
+ /model.*disabled/i,
73
+ /model.*not found/i,
74
+ /unknown model/i,
75
+ /overloaded/i,
76
+ /service unavailable/i,
77
+ /temporar(?:ily)? unavailable/i,
78
+ /connection refused/i,
79
+ /fetch failed/i,
80
+ /network error/i,
81
+ /socket hang up/i,
82
+ /upstream/i,
83
+ /timed? out/i,
84
+ /timeout/i,
85
+ /\b502\b/,
86
+ /\b503\b/,
87
+ /\b504\b/,
88
+ ];
89
+
90
+ export function isRetryableModelFailure(error: string | undefined): boolean {
91
+ if (!error) return false;
92
+ return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
93
+ }
94
+
95
+ export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
96
+ const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
97
+ return nextModel
98
+ ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`
99
+ : `[fallback] ${attempt.model} failed: ${failure}.`;
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.12.5",
3
+ "version": "0.13.0",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
package/parallel-utils.ts CHANGED
@@ -10,6 +10,7 @@ export interface RunnerSubagentStep {
10
10
  task: string;
11
11
  cwd?: string;
12
12
  model?: string;
13
+ modelCandidates?: string[];
13
14
  tools?: string[];
14
15
  extensions?: string[];
15
16
  mcpDirectTools?: string[];
@@ -83,6 +84,8 @@ export interface ParallelTaskResult {
83
84
  output: string;
84
85
  exitCode: number | null;
85
86
  error?: string;
87
+ model?: string;
88
+ attemptedModels?: string[];
86
89
  outputTargetPath?: string;
87
90
  outputTargetExists?: boolean;
88
91
  }
package/pi-args.ts CHANGED
@@ -51,10 +51,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
51
51
 
52
52
  const modelArg = applyThinkingSuffix(input.model, input.thinking);
53
53
  if (modelArg) {
54
- // Use --models (not --model) because pi CLI silently ignores --model
55
- // without a companion --provider flag. --models resolves the provider
56
- // automatically via resolveModelScope. See: #8
57
- args.push("--models", modelArg);
54
+ args.push("--model", modelArg);
58
55
  }
59
56
 
60
57
  const toolExtensionPaths: string[] = [];
package/render.ts CHANGED
@@ -265,6 +265,9 @@ export function renderSubagentResult(
265
265
  if (r.skillsWarning) {
266
266
  c.addChild(new Text(truncLine(theme.fg("warning", `⚠️ ${r.skillsWarning}`), w), 0, 0));
267
267
  }
268
+ if (r.attemptedModels && r.attemptedModels.length > 1) {
269
+ c.addChild(new Text(truncLine(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`), w), 0, 0));
270
+ }
268
271
  c.addChild(new Text(truncLine(theme.fg("dim", formatUsage(r.usage, r.model)), w), 0, 0));
269
272
  if (r.sessionFile) {
270
273
  c.addChild(new Text(truncLine(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), w), 0, 0));
@@ -427,6 +430,9 @@ export function renderSubagentResult(
427
430
  if (r.skillsWarning) {
428
431
  c.addChild(new Text(truncLine(theme.fg("warning", ` ⚠️ ${r.skillsWarning}`), w), 0, 0));
429
432
  }
433
+ if (r.attemptedModels && r.attemptedModels.length > 1) {
434
+ c.addChild(new Text(truncLine(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`), w), 0, 0));
435
+ }
430
436
 
431
437
  if (rRunning && rProg) {
432
438
  if (rProg.skills?.length) {