pi-subagents 0.12.4 → 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.
@@ -364,6 +364,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
364
364
  }
365
365
  const id = randomUUID();
366
366
  const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
367
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
368
+ provider: m.provider,
369
+ id: m.id,
370
+ fullId: `${m.provider}/${m.id}`,
371
+ }));
367
372
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
368
373
 
369
374
  if (hasChain && params.chain) {
@@ -374,6 +379,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
374
379
  chain,
375
380
  agents,
376
381
  ctx: asyncCtx,
382
+ availableModels,
377
383
  cwd: params.cwd,
378
384
  maxOutput: params.maxOutput,
379
385
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -407,6 +413,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
407
413
  task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
408
414
  agentConfig: a,
409
415
  ctx: asyncCtx,
416
+ availableModels,
410
417
  cwd: params.cwd,
411
418
  maxOutput: params.maxOutput,
412
419
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -416,6 +423,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
416
423
  sessionFile: sessionFileForIndex(0),
417
424
  skills,
418
425
  output: effectiveOutput,
426
+ modelOverride: params.model as string | undefined,
419
427
  maxSubagentDepth,
420
428
  worktreeSetupHook: deps.config.worktreeSetupHook,
421
429
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -482,6 +490,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
482
490
  chain: asyncChain,
483
491
  agents,
484
492
  ctx: asyncCtx,
493
+ availableModels: ctx.modelRegistry.getAvailable().map((m) => ({
494
+ provider: m.provider,
495
+ id: m.id,
496
+ fullId: `${m.provider}/${m.id}`,
497
+ })),
485
498
  cwd: params.cwd,
486
499
  maxOutput: params.maxOutput,
487
500
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -514,6 +527,7 @@ interface ForegroundParallelRunInput {
514
527
  maxOutput?: MaxOutputConfig;
515
528
  paramsCwd?: string;
516
529
  maxSubagentDepths: number[];
530
+ availableModels: ModelInfo[];
517
531
  modelOverrides: (string | undefined)[];
518
532
  skillOverrides: (string[] | false | undefined)[];
519
533
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
@@ -616,6 +630,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
616
630
  maxOutput: input.maxOutput,
617
631
  maxSubagentDepth: input.maxSubagentDepths[index],
618
632
  modelOverride: input.modelOverrides[index],
633
+ availableModels: input.availableModels,
619
634
  skills: effectiveSkills === false ? [] : effectiveSkills,
620
635
  onUpdate: input.onUpdate
621
636
  ? (progressUpdate) => {
@@ -691,6 +706,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
691
706
  if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
692
707
  }
693
708
 
709
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
710
+ provider: m.provider,
711
+ id: m.id,
712
+ fullId: `${m.provider}/${m.id}`,
713
+ }));
694
714
  let taskTexts = tasks.map((t) => t.task);
695
715
  const modelOverrides: (string | undefined)[] = tasks.map((t) => t.model);
696
716
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
@@ -698,12 +718,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
698
718
  );
699
719
 
700
720
  if (params.clarify === true && ctx.hasUI) {
701
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
702
- provider: m.provider,
703
- id: m.id,
704
- fullId: `${m.provider}/${m.id}`,
705
- }));
706
-
707
721
  const behaviors = agentConfigs.map((c, i) =>
708
722
  resolveStepBehavior(c, { skills: skillOverrides[i] }),
709
723
  );
@@ -758,6 +772,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
758
772
  chain: [{ parallel: parallelTasks, worktree: params.worktree }],
759
773
  agents,
760
774
  ctx: asyncCtx,
775
+ availableModels,
761
776
  cwd: params.cwd,
762
777
  maxOutput: params.maxOutput,
763
778
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -807,6 +822,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
807
822
  artifactsDir,
808
823
  maxOutput: params.maxOutput,
809
824
  paramsCwd: params.cwd,
825
+ availableModels,
810
826
  modelOverrides,
811
827
  skillOverrides,
812
828
  behaviors,
@@ -884,6 +900,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
884
900
  };
885
901
  }
886
902
 
903
+ const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
904
+ provider: m.provider,
905
+ id: m.id,
906
+ fullId: `${m.provider}/${m.id}`,
907
+ }));
887
908
  let task = params.task!;
888
909
  let modelOverride: string | undefined = params.model as string | undefined;
889
910
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
@@ -893,12 +914,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
893
914
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
894
915
 
895
916
  if (params.clarify === true && ctx.hasUI) {
896
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
897
- provider: m.provider,
898
- id: m.id,
899
- fullId: `${m.provider}/${m.id}`,
900
- }));
901
-
902
917
  const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput, skills: skillOverride });
903
918
  const availableSkills = discoverAvailableSkills(ctx.cwd);
904
919
 
@@ -944,6 +959,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
944
959
  task: params.context === "fork" ? wrapForkTask(task) : task,
945
960
  agentConfig,
946
961
  ctx: asyncCtx,
962
+ availableModels,
947
963
  cwd: params.cwd,
948
964
  maxOutput: params.maxOutput,
949
965
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -953,6 +969,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
953
969
  sessionFile: sessionFileForIndex(0),
954
970
  skills: skillOverride === false ? [] : skillOverride,
955
971
  output: effectiveOutput,
972
+ modelOverride,
956
973
  maxSubagentDepth,
957
974
  worktreeSetupHook: deps.config.worktreeSetupHook,
958
975
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -988,6 +1005,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
988
1005
  maxSubagentDepth,
989
1006
  onUpdate,
990
1007
  modelOverride,
1008
+ availableModels,
991
1009
  skills: effectiveSkills,
992
1010
  });
993
1011
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
@@ -9,6 +9,8 @@ import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-outpu
9
9
  import {
10
10
  type ArtifactConfig,
11
11
  type ArtifactPaths,
12
+ type ModelAttempt,
13
+ type Usage,
12
14
  DEFAULT_MAX_OUTPUT,
13
15
  type MaxOutputConfig,
14
16
  truncateOutput,
@@ -24,6 +26,7 @@ import {
24
26
  MAX_PARALLEL_CONCURRENCY,
25
27
  } from "./parallel-utils.ts";
26
28
  import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
29
+ import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
27
30
  import {
28
31
  cleanupWorktrees,
29
32
  createWorktrees,
@@ -59,6 +62,9 @@ interface StepResult {
59
62
  output: string;
60
63
  success: boolean;
61
64
  skipped?: boolean;
65
+ model?: string;
66
+ attemptedModels?: string[];
67
+ modelAttempts?: ModelAttempt[];
62
68
  artifactPaths?: ArtifactPaths;
63
69
  truncated?: boolean;
64
70
  }
@@ -112,6 +118,38 @@ function parseSessionTokens(sessionDir: string): TokenUsage | null {
112
118
  }
113
119
  }
114
120
 
121
+ function emptyUsage(): Usage {
122
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
123
+ }
124
+
125
+ function parseRunOutput(output: string): { usage: Usage; model?: string; error?: string } {
126
+ const usage = emptyUsage();
127
+ let model: string | undefined;
128
+ let error: string | undefined;
129
+ for (const line of output.split("\n")) {
130
+ if (!line.trim()) continue;
131
+ try {
132
+ const evt = JSON.parse(line) as { type?: string; message?: { role?: string; model?: string; errorMessage?: string; usage?: any } };
133
+ if (evt.type !== "message_end" || evt.message?.role !== "assistant") continue;
134
+ const msg = evt.message;
135
+ if (msg.model) model = msg.model;
136
+ if (msg.errorMessage) error = msg.errorMessage;
137
+ const u = msg.usage;
138
+ if (u) {
139
+ usage.turns++;
140
+ usage.input += u.input ?? u.inputTokens ?? 0;
141
+ usage.output += u.output ?? u.outputTokens ?? 0;
142
+ usage.cacheRead += u.cacheRead ?? 0;
143
+ usage.cacheWrite += u.cacheWrite ?? 0;
144
+ usage.cost += u.cost?.total ?? 0;
145
+ }
146
+ } catch {
147
+ // Ignore malformed stdout lines.
148
+ }
149
+ }
150
+ return { usage, model, error };
151
+ }
152
+
115
153
  function runPiStreaming(
116
154
  args: string[],
117
155
  cwd: string,
@@ -119,13 +157,14 @@ function runPiStreaming(
119
157
  env?: Record<string, string | undefined>,
120
158
  piPackageRoot?: string,
121
159
  maxSubagentDepth?: number,
122
- ): Promise<{ stdout: string; exitCode: number | null }> {
160
+ ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
123
161
  return new Promise((resolve) => {
124
162
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
125
163
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
126
164
  const spawnSpec = getPiSpawnCommand(args, piPackageRoot ? { piPackageRoot } : undefined);
127
165
  const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
128
166
  let stdout = "";
167
+ let stderr = "";
129
168
 
130
169
  child.stdout.on("data", (chunk: Buffer) => {
131
170
  const text = chunk.toString();
@@ -134,17 +173,19 @@ function runPiStreaming(
134
173
  });
135
174
 
136
175
  child.stderr.on("data", (chunk: Buffer) => {
137
- outputStream.write(chunk.toString());
176
+ const text = chunk.toString();
177
+ stderr += text;
178
+ outputStream.write(text);
138
179
  });
139
180
 
140
181
  child.on("close", (exitCode) => {
141
182
  outputStream.end();
142
- resolve({ stdout, exitCode });
183
+ resolve({ stdout, stderr, exitCode });
143
184
  });
144
185
 
145
186
  child.on("error", () => {
146
187
  outputStream.end();
147
- resolve({ stdout, exitCode: 1 });
188
+ resolve({ stdout, stderr, exitCode: 1 });
148
189
  });
149
190
  });
150
191
  }
@@ -305,26 +346,21 @@ interface SingleStepContext {
305
346
  async function runSingleStep(
306
347
  step: SubagentStep,
307
348
  ctx: SingleStepContext,
308
- ): Promise<{ agent: string; output: string; exitCode: number | null; artifactPaths?: ArtifactPaths }> {
349
+ ): Promise<{
350
+ agent: string;
351
+ output: string;
352
+ exitCode: number | null;
353
+ error?: string;
354
+ model?: string;
355
+ attemptedModels?: string[];
356
+ modelAttempts?: ModelAttempt[];
357
+ artifactPaths?: ArtifactPaths;
358
+ }> {
309
359
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
310
360
  const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
311
361
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
312
362
  const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
313
363
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
314
- const { args, env, tempDir } = buildPiArgs({
315
- baseArgs: ["-p"],
316
- task,
317
- sessionEnabled,
318
- sessionDir,
319
- sessionFile: step.sessionFile,
320
- model: step.model,
321
- tools: step.tools,
322
- extensions: step.extensions,
323
- skills: step.skills,
324
- systemPrompt: step.systemPrompt,
325
- mcpDirectTools: step.mcpDirectTools,
326
- promptFileStem: step.agent,
327
- });
328
364
 
329
365
  let artifactPaths: ArtifactPaths | undefined;
330
366
  if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
@@ -336,22 +372,71 @@ async function runSingleStep(
336
372
  }
337
373
  }
338
374
 
339
- const result = await runPiStreaming(args, step.cwd ?? ctx.cwd, ctx.outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
340
- cleanupTempDir(tempDir);
375
+ const candidates = step.modelCandidates && step.modelCandidates.length > 0
376
+ ? step.modelCandidates
377
+ : step.model
378
+ ? [step.model]
379
+ : [undefined];
380
+ const attemptedModels: string[] = [];
381
+ const modelAttempts: ModelAttempt[] = [];
382
+ const attemptNotes: string[] = [];
383
+ let finalResult:
384
+ | { stdout: string; stderr: string; exitCode: number | null; usage: Usage; model?: string; error?: string }
385
+ | undefined;
386
+
387
+ for (let index = 0; index < candidates.length; index++) {
388
+ const candidate = candidates[index];
389
+ const { args, env, tempDir } = buildPiArgs({
390
+ baseArgs: ["-p"],
391
+ task,
392
+ sessionEnabled,
393
+ sessionDir,
394
+ sessionFile: step.sessionFile,
395
+ model: candidate,
396
+ tools: step.tools,
397
+ extensions: step.extensions,
398
+ skills: step.skills,
399
+ systemPrompt: step.systemPrompt,
400
+ mcpDirectTools: step.mcpDirectTools,
401
+ promptFileStem: step.agent,
402
+ });
403
+ const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
404
+ const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
405
+ cleanupTempDir(tempDir);
406
+
407
+ const parsed = parseRunOutput(run.stdout);
408
+ const error = parsed.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
409
+ const attempt: ModelAttempt = {
410
+ model: candidate ?? parsed.model ?? step.model ?? "default",
411
+ success: run.exitCode === 0 && !error,
412
+ exitCode: run.exitCode,
413
+ error,
414
+ usage: parsed.usage,
415
+ };
416
+ modelAttempts.push(attempt);
417
+ if (candidate) attemptedModels.push(candidate);
418
+ finalResult = { ...run, usage: parsed.usage, model: candidate ?? parsed.model, error };
419
+ if (attempt.success) break;
420
+ if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
421
+ attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
422
+ }
341
423
 
342
- const rawOutput = (result.stdout || "").trim();
343
- const resolvedOutput = step.outputPath && result.exitCode === 0
424
+ const rawOutput = (finalResult?.stdout || "").trim();
425
+ const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
344
426
  ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
345
427
  : { fullOutput: rawOutput };
346
428
  const output = resolvedOutput.fullOutput;
347
429
  let outputForSummary = output;
430
+ if (attemptNotes.length > 0) {
431
+ outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
432
+ }
348
433
  if (resolvedOutput.savedPath) {
349
- outputForSummary = output
350
- ? `${output}\n\n📄 Output saved to: ${resolvedOutput.savedPath}`
434
+ outputForSummary = outputForSummary
435
+ ? `${outputForSummary}\n\n📄 Output saved to: ${resolvedOutput.savedPath}`
351
436
  : `📄 Output saved to: ${resolvedOutput.savedPath}`;
352
- } else if (resolvedOutput.saveError && step.outputPath && result.exitCode === 0) {
353
- outputForSummary = output
354
- ? `${output}\n\n⚠️ Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
437
+ } else if (resolvedOutput.saveError && step.outputPath && finalResult?.exitCode === 0) {
438
+ outputForSummary = outputForSummary
439
+ ? `${outputForSummary}\n\n⚠️ Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
355
440
  : `⚠️ Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`;
356
441
  }
357
442
 
@@ -366,7 +451,10 @@ async function runSingleStep(
366
451
  runId: ctx.id,
367
452
  agent: step.agent,
368
453
  task,
369
- exitCode: result.exitCode,
454
+ exitCode: finalResult?.exitCode,
455
+ model: finalResult?.model,
456
+ attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
457
+ modelAttempts,
370
458
  skills: step.skills,
371
459
  timestamp: Date.now(),
372
460
  }, null, 2),
@@ -375,7 +463,16 @@ async function runSingleStep(
375
463
  }
376
464
  }
377
465
 
378
- return { agent: step.agent, output: outputForSummary, exitCode: result.exitCode, artifactPaths };
466
+ return {
467
+ agent: step.agent,
468
+ output: outputForSummary,
469
+ exitCode: finalResult?.exitCode ?? 1,
470
+ error: finalResult?.error,
471
+ model: finalResult?.model,
472
+ attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
473
+ modelAttempts,
474
+ artifactPaths,
475
+ };
379
476
  }
380
477
 
381
478
  type RunnerStatusPayload = {
@@ -397,6 +494,10 @@ type RunnerStatusPayload = {
397
494
  exitCode?: number | null;
398
495
  tokens?: TokenUsage;
399
496
  skills?: string[];
497
+ model?: string;
498
+ attemptedModels?: string[];
499
+ modelAttempts?: ModelAttempt[];
500
+ error?: string;
400
501
  }>;
401
502
  artifactsDir?: string;
402
503
  sessionDir?: string;
@@ -530,7 +631,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
530
631
  pid: process.pid,
531
632
  cwd,
532
633
  currentStep: 0,
533
- steps: flatSteps.map((step) => ({ agent: step.agent, status: "pending", skills: step.skills })),
634
+ steps: flatSteps.map((step) => ({
635
+ agent: step.agent,
636
+ status: "pending",
637
+ skills: step.skills,
638
+ model: step.model,
639
+ attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
640
+ })),
534
641
  artifactsDir,
535
642
  sessionDir: config.sessionDir,
536
643
  outputFile: path.join(asyncDir, "output-0.log"),
@@ -664,6 +771,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
664
771
  statusPayload.steps[fi].endedAt = taskEndTime;
665
772
  statusPayload.steps[fi].durationMs = taskDuration;
666
773
  statusPayload.steps[fi].exitCode = singleResult.exitCode;
774
+ statusPayload.steps[fi].model = singleResult.model;
775
+ statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
776
+ statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
777
+ statusPayload.steps[fi].error = singleResult.error;
667
778
  statusPayload.lastUpdate = taskEndTime;
668
779
  writeJson(statusPath, statusPayload);
669
780
 
@@ -707,12 +818,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
707
818
  output: pr.output,
708
819
  success: pr.exitCode === 0,
709
820
  skipped: pr.skipped,
821
+ model: pr.model,
822
+ attemptedModels: pr.attemptedModels,
823
+ modelAttempts: pr.modelAttempts,
710
824
  artifactPaths: pr.artifactPaths,
711
825
  });
712
826
  }
713
827
 
714
828
  previousOutput = aggregateParallelOutputs(
715
- parallelResults.map((r) => ({ agent: r.agent, output: r.output, exitCode: r.exitCode })),
829
+ parallelResults.map((r) => ({
830
+ agent: r.agent,
831
+ output: r.output,
832
+ exitCode: r.exitCode,
833
+ error: r.error,
834
+ model: r.model,
835
+ attemptedModels: r.attemptedModels,
836
+ })),
716
837
  );
717
838
  previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
718
839
 
@@ -768,6 +889,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
768
889
  agent: singleResult.agent,
769
890
  output: singleResult.output,
770
891
  success: singleResult.exitCode === 0,
892
+ model: singleResult.model,
893
+ attemptedModels: singleResult.attemptedModels,
894
+ modelAttempts: singleResult.modelAttempts,
771
895
  artifactPaths: singleResult.artifactPaths,
772
896
  });
773
897
 
@@ -788,6 +912,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
788
912
  statusPayload.steps[flatIndex].endedAt = stepEndTime;
789
913
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
790
914
  statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
915
+ statusPayload.steps[flatIndex].model = singleResult.model;
916
+ statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
917
+ statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
918
+ statusPayload.steps[flatIndex].error = singleResult.error;
791
919
  if (stepTokens) {
792
920
  statusPayload.steps[flatIndex].tokens = stepTokens;
793
921
  statusPayload.totalTokens = { ...previousCumulativeTokens };
@@ -915,6 +1043,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
915
1043
  output: r.output,
916
1044
  success: r.success,
917
1045
  skipped: r.skipped || undefined,
1046
+ model: r.model,
1047
+ attemptedModels: r.attemptedModels,
1048
+ modelAttempts: r.modelAttempts,
918
1049
  artifactPaths: r.artifactPaths,
919
1050
  truncated: r.truncated,
920
1051
  })),
@@ -158,10 +158,17 @@ export class SubagentsStatusComponent implements Component {
158
158
  row(`cwd: ${truncateToWidth(shortenPath(run.cwd ?? run.asyncDir), innerW - 5)}`, width, this.theme),
159
159
  ];
160
160
  for (const step of run.steps) {
161
+ const model = step.model ? ` | ${step.model}` : "";
162
+ const attempts = step.attemptedModels && step.attemptedModels.length > 1
163
+ ? ` | attempts ${step.attemptedModels.length}`
164
+ : "";
161
165
  const duration = step.durationMs !== undefined ? ` | ${formatDuration(step.durationMs)}` : "";
162
166
  const tokens = step.tokens ? ` | ${formatTokens(step.tokens.total)} tok` : "";
163
- const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${duration}${tokens}`;
167
+ const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${model}${attempts}${duration}${tokens}`;
164
168
  lines.push(row(truncateToWidth(line, innerW), width, this.theme));
169
+ if (step.error) {
170
+ lines.push(row(truncateToWidth(` ${step.error}`, innerW), width, this.theme));
171
+ }
165
172
  }
166
173
  if (run.steps.length === 0) {
167
174
  lines.push(row(this.theme.fg("dim", " No step details available yet."), width, this.theme));
package/types.ts CHANGED
@@ -82,6 +82,14 @@ export interface ProgressSummary {
82
82
  // Results
83
83
  // ============================================================================
84
84
 
85
+ export interface ModelAttempt {
86
+ model: string;
87
+ success: boolean;
88
+ exitCode?: number | null;
89
+ error?: string;
90
+ usage?: Usage;
91
+ }
92
+
85
93
  export interface SingleResult {
86
94
  agent: string;
87
95
  task: string;
@@ -89,6 +97,8 @@ export interface SingleResult {
89
97
  messages: Message[];
90
98
  usage: Usage;
91
99
  model?: string;
100
+ attemptedModels?: string[];
101
+ modelAttempts?: ModelAttempt[];
92
102
  error?: string;
93
103
  sessionFile?: string;
94
104
  skills?: string[];
@@ -159,7 +169,17 @@ export interface AsyncStatus {
159
169
  lastUpdate?: number;
160
170
  cwd?: string;
161
171
  currentStep?: number;
162
- steps?: Array<{ agent: string; status: string; durationMs?: number; tokens?: TokenUsage; skills?: string[] }>;
172
+ steps?: Array<{
173
+ agent: string;
174
+ status: string;
175
+ durationMs?: number;
176
+ tokens?: TokenUsage;
177
+ skills?: string[];
178
+ model?: string;
179
+ attemptedModels?: string[];
180
+ modelAttempts?: ModelAttempt[];
181
+ error?: string;
182
+ }>;
163
183
  sessionDir?: string;
164
184
  outputFile?: string;
165
185
  totalTokens?: TokenUsage;
@@ -237,6 +257,8 @@ export interface RunSyncOptions {
237
257
  maxSubagentDepth?: number;
238
258
  /** Override the agent's default model (format: "provider/id" or just "id") */
239
259
  modelOverride?: string;
260
+ /** Registry models available for heuristic bare-model resolution */
261
+ availableModels?: Array<{ provider: string; id: string; fullId: string }>;
240
262
  /** Skills to inject (overrides agent default if provided) */
241
263
  skills?: string[];
242
264
  }