pi-subagents 0.54.0 → 0.56.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/agents/reviewer.md +12 -2
  3. package/docs/agents.md +4 -4
  4. package/docs/configuration.md +4 -4
  5. package/docs/extension-api.md +14 -4
  6. package/docs/models.md +30 -3
  7. package/docs/observability.md +14 -3
  8. package/docs/tool-reference.md +21 -6
  9. package/docs/workflows.md +7 -1
  10. package/package.json +1 -1
  11. package/prompts/council.md +9 -0
  12. package/prompts/parallel-review.md +5 -1
  13. package/prompts/review-loop.md +9 -5
  14. package/skills/council-mode/SKILL.md +33 -10
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  16. package/skills/pi-subagents/references/execution-controls.md +11 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  18. package/skills/pi-subagents/references/prompting-and-roles.md +8 -8
  19. package/src/agents/agent-management.ts +30 -9
  20. package/src/agents/agent-serializer.ts +4 -2
  21. package/src/agents/agents.ts +168 -41
  22. package/src/api/external-job-provider.ts +10 -1
  23. package/src/api/preflight.ts +45 -17
  24. package/src/api/project-panes.ts +2 -0
  25. package/src/extension/index.ts +20 -1
  26. package/src/extension/public-execution.ts +12 -9
  27. package/src/extension/rpc.ts +77 -3
  28. package/src/extension/schemas.ts +7 -1
  29. package/src/extension/tool-description.ts +9 -6
  30. package/src/inspectors/herdr/focus.ts +55 -0
  31. package/src/inspectors/herdr/project-panes.ts +228 -44
  32. package/src/integrations/herdr-status.ts +26 -4
  33. package/src/runs/background/async-execution.ts +137 -13
  34. package/src/runs/background/async-job-tracker.ts +33 -14
  35. package/src/runs/background/async-resume.ts +23 -2
  36. package/src/runs/background/async-status.ts +10 -0
  37. package/src/runs/background/control-channel.ts +98 -10
  38. package/src/runs/background/notify.ts +62 -1
  39. package/src/runs/background/run-status.ts +15 -1
  40. package/src/runs/background/subagent-runner.ts +308 -47
  41. package/src/runs/foreground/async-stop-action.ts +23 -2
  42. package/src/runs/foreground/execution.ts +115 -12
  43. package/src/runs/foreground/foreground-history.ts +3 -2
  44. package/src/runs/foreground/subagent-executor.ts +404 -45
  45. package/src/runs/foreground/workflow-detach-reconcile.ts +18 -6
  46. package/src/runs/shared/acceptance.ts +11 -5
  47. package/src/runs/shared/agent-contract.ts +1 -1
  48. package/src/runs/shared/child-identity.ts +36 -0
  49. package/src/runs/shared/completion-guard.ts +52 -2
  50. package/src/runs/shared/dynamic-fanout.ts +1 -1
  51. package/src/runs/shared/extension-bindings.ts +78 -0
  52. package/src/runs/shared/external-cli-runner.ts +2 -0
  53. package/src/runs/shared/external-job-bridge.ts +53 -37
  54. package/src/runs/shared/external-job-runner.ts +126 -23
  55. package/src/runs/shared/fast-mode-extension.ts +10 -0
  56. package/src/runs/shared/model-exclusions.ts +2 -1
  57. package/src/runs/shared/model-fallback.ts +12 -7
  58. package/src/runs/shared/mutation-evidence.ts +145 -0
  59. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  60. package/src/runs/shared/parallel-utils.ts +15 -0
  61. package/src/runs/shared/pi-args.ts +56 -12
  62. package/src/runs/shared/structured-output.ts +18 -4
  63. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  64. package/src/runs/shared/tool-availability.ts +1 -3
  65. package/src/shared/launch-contract.ts +12 -5
  66. package/src/shared/settings.ts +6 -1
  67. package/src/shared/thinking-ceiling.ts +52 -0
  68. package/src/shared/types.ts +107 -2
  69. package/src/tui/fleet-status.ts +66 -13
  70. package/src/tui/render.ts +5 -4
  71. package/src/workflows/scripted-workflow.ts +156 -12
@@ -28,12 +28,14 @@ import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentMode
28
28
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
29
29
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
30
30
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
31
+ import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
31
32
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
32
33
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
33
34
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
34
35
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
35
36
  import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
36
37
  import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
38
+ import { validateImplementationToolContract } from "../shared/completion-guard.ts";
37
39
  import {
38
40
  type AcceptanceInput,
39
41
  type AgentContract,
@@ -72,6 +74,7 @@ import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
72
74
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
73
75
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
74
76
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
77
+ import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
75
78
 
76
79
  const require = createRequire(import.meta.url);
77
80
  const piPackageRoot = resolvePiPackageRoot();
@@ -173,6 +176,7 @@ interface AsyncChainParams {
173
176
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
174
177
  nestedRoute?: NestedRouteInfo;
175
178
  acceptance?: AcceptanceInput;
179
+ fast?: boolean;
176
180
  timeoutMs?: number;
177
181
  turnBudget?: ResolvedTurnBudget;
178
182
  toolBudget?: ResolvedToolBudget;
@@ -187,6 +191,7 @@ interface AsyncChainParams {
187
191
  /** Global cap on simultaneously-running subagent tasks within the async run. */
188
192
  globalConcurrencyLimit?: number;
189
193
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
194
+ thinkingCeiling?: ThinkingLevel;
190
195
  runFanoutBudget?: RunFanoutBudgetDescriptor;
191
196
  parentWorkflowRunId?: string;
192
197
  workflowKey?: string;
@@ -221,6 +226,7 @@ interface AsyncSingleParams {
221
226
  structuredOutputSchema?: JsonSchemaObject;
222
227
  modelOverride?: string;
223
228
  modelOverrideFromParent?: boolean;
229
+ fast?: boolean;
224
230
  thinkingOverride?: AgentConfig["thinking"];
225
231
  availableModels?: AvailableModelInfo[];
226
232
  maxSubagentDepth: number;
@@ -249,10 +255,20 @@ interface AsyncSingleParams {
249
255
  toolTimeoutMsEnv?: string | undefined;
250
256
  allowZeroToolBudget?: boolean;
251
257
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
258
+ thinkingCeiling?: ThinkingLevel;
252
259
  runFanoutBudget?: RunFanoutBudgetDescriptor;
253
260
  parentWorkflowRunId?: string;
254
261
  workflowKey?: string;
262
+ workflowAwaitAsync?: boolean;
255
263
  activeAsyncCapacity?: ActiveAsyncCapacityHandle;
264
+ externalJobFollowUp?: {
265
+ sourceRunId: string;
266
+ sourceStepIndex: number;
267
+ parentProviderJobId: string;
268
+ requestId: string;
269
+ requestDigest: string;
270
+ };
271
+ extensionBindings?: ExtensionBindings;
256
272
  }
257
273
 
258
274
  interface AsyncExecutionResult {
@@ -283,6 +299,7 @@ export interface AsyncRunnerStepBuildParams {
283
299
  asyncDir: string;
284
300
  outputBaseDir?: string;
285
301
  validateOutputBindings?: boolean;
302
+ fast?: boolean;
286
303
  toolBudget?: ResolvedToolBudget;
287
304
  configToolBudget?: ResolvedToolBudget;
288
305
  /** Optional per-call hard toolTimeoutMs override from the subagent invocation. */
@@ -292,6 +309,7 @@ export interface AsyncRunnerStepBuildParams {
292
309
  /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
293
310
  toolTimeoutMsEnv?: string | undefined;
294
311
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
312
+ thinkingCeiling?: ThinkingLevel;
295
313
  }
296
314
 
297
315
  export type AsyncRunnerStepBuildResult =
@@ -493,7 +511,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
493
511
  const cfgPath = getAsyncConfigPath(suffix);
494
512
  const runnerProcessInstanceId = randomUUID();
495
513
  const launchConfig = { ...cfg, runnerProcessInstanceId };
496
- fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
514
+ writePrivateAtomicJson(cfgPath, launchConfig);
497
515
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
498
516
  const nodeCommand = resolveNodeExecutable();
499
517
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
@@ -525,7 +543,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
525
543
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
526
544
  windowsHide: true,
527
545
  env: {
528
- ...process.env,
546
+ ...omitExtensionBindingsEnv(process.env),
529
547
  ...(piPackageRoot ? { [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot } : {}),
530
548
  },
531
549
  });
@@ -714,6 +732,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
714
732
  ...(s.progress !== undefined ? { progress: s.progress } : {}),
715
733
  ...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
716
734
  ...(s.model !== undefined ? { model: s.model } : {}),
735
+ ...(s.fast !== undefined ? { fast: s.fast } : {}),
717
736
  };
718
737
  };
719
738
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
@@ -726,6 +745,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
726
745
  if (s.outputSchema !== undefined) unsupported.push("structured output");
727
746
  if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
728
747
  if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
748
+ if ((s.fast ?? params.fast ?? a.fast) === true) unsupported.push("fast mode");
729
749
  if (params.contextForAgent?.(s.agent) === "fork") unsupported.push("fork context");
730
750
  if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
731
751
  }
@@ -792,19 +812,47 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
792
812
  const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
793
813
 
794
814
  const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
815
+ const primaryModelFromParent = inheritsParentModel(s.model, a.model, ctx.currentModel);
795
816
  const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
796
817
  s.model,
797
818
  a.model,
798
819
  ctx.currentModel,
799
820
  availableModels,
800
- ctx.currentModelProvider,
821
+ a.modelProvider ?? ctx.currentModelProvider,
801
822
  { scope: modelScopes },
802
823
  );
803
824
  const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
804
825
  const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
805
826
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
827
+ const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
828
+ params.thinkingCeiling,
829
+ a.maxThinking,
830
+ decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
831
+ );
832
+ if (!externalRunner) {
833
+ try {
834
+ assertThinkingWithinCeiling({ model, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
835
+ } catch (error) {
836
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
837
+ }
838
+ }
806
839
  const agentContract = s.agentContract ?? params.agentContract;
807
840
  const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
841
+ const modelCandidates = externalRunner ? [] : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
842
+ scope: modelScopes,
843
+ primaryModelFromParent,
844
+ }).flatMap((candidate) => {
845
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
846
+ return resolved ? [resolved] : [];
847
+ });
848
+ if (!externalRunner) {
849
+ try {
850
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
851
+ } catch (error) {
852
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
853
+ }
854
+ }
855
+ const fast = s.fast ?? params.fast ?? a.fast;
808
856
  const toolPlan = resolvePiLaunchToolPlan({
809
857
  tools: a.tools,
810
858
  extensions: a.extensions,
@@ -813,6 +861,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
813
861
  cwd: stepCwd,
814
862
  requireReadTool: Boolean(resolvedSkills.length),
815
863
  structuredOutput: Boolean(s.outputSchema),
864
+ fast,
865
+ model,
866
+ modelCandidates,
816
867
  capabilityCeiling: params.capabilityCeiling,
817
868
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
818
869
  agentName: a.name,
@@ -822,6 +873,20 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
822
873
  if (externalRunner && permissionRules) {
823
874
  throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
824
875
  }
876
+ if (!externalRunner) {
877
+ const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
878
+ const contractError = validateImplementationToolContract({
879
+ agent: a.name,
880
+ task,
881
+ tools: contractTools,
882
+ mcpDirectTools: toolPlan.effectiveMcpTools,
883
+ configuredExtensions: toolPlan.configuredExtensions,
884
+ requestedTools: toolPlan.requestedBuiltinTools,
885
+ acceptanceRole: a.acceptanceRole,
886
+ completionGuard: a.completionGuard,
887
+ });
888
+ if (contractError) throw new AsyncStartValidationError(contractError);
889
+ }
825
890
  return {
826
891
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
827
892
  permissionRules,
@@ -838,14 +903,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
838
903
  structured: Boolean(s.outputSchema),
839
904
  cwd: stepCwd,
840
905
  model,
906
+ ...(fast !== undefined ? { fast } : {}),
841
907
  thinking: resolveEffectiveThinking(model, effectiveThinking),
908
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
842
909
  launchResolvedExtensions,
843
- modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
844
- scope: modelScopes,
845
- primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
846
- }).map((candidate) =>
847
- applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
848
- ),
910
+ modelCandidates: externalRunner ? undefined : modelCandidates,
911
+ ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
912
+ ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
849
913
  tools: a.tools,
850
914
  extensions: a.extensions,
851
915
  subagentOnlyExtensions: a.subagentOnlyExtensions,
@@ -878,7 +942,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
878
942
  acceptanceRole: a.acceptanceRole,
879
943
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
880
944
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
881
- ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
945
+ ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: s.acceptance !== false }) } : {}),
882
946
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
883
947
  ...(s.worktree ? { worktree: true } : {}),
884
948
  };
@@ -962,6 +1026,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
962
1026
  acceptanceRole: agent.acceptanceRole,
963
1027
  ...(s.agentContract ?? params.agentContract ? { agentContract: s.agentContract ?? params.agentContract } : {}),
964
1028
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1029
+ ...(parallel.thinkingCeiling ? { thinkingCeiling: parallel.thinkingCeiling } : {}),
965
1030
  };
966
1031
  }
967
1032
  const sequential = s as SequentialStep;
@@ -1090,12 +1155,14 @@ export function executeAsyncChain(
1090
1155
  waitToolEnabled: params.waitToolEnabled,
1091
1156
  worktreeBaseDir,
1092
1157
  asyncDir,
1158
+ fast: params.fast,
1093
1159
  toolBudget: params.toolBudget,
1094
1160
  configToolBudget: params.configToolBudget,
1095
1161
  callToolTimeoutMs: params.callToolTimeoutMs,
1096
1162
  configToolTimeoutMs: params.configToolTimeoutMs,
1097
1163
  toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
1098
1164
  capabilityCeiling,
1165
+ thinkingCeiling: params.thinkingCeiling,
1099
1166
  });
1100
1167
  if ("error" in built) {
1101
1168
  try {
@@ -1337,6 +1404,12 @@ export function executeAsyncSingle(
1337
1404
  nestedRoute,
1338
1405
  } = params;
1339
1406
  const task = params.task ?? "";
1407
+ let extensionBindings: ExtensionBindings | undefined;
1408
+ try {
1409
+ extensionBindings = normalizeExtensionBindings(params.extensionBindings)?.value;
1410
+ } catch (error) {
1411
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1412
+ }
1340
1413
  const acceptanceErrors = validateAcceptanceInput(params.acceptance);
1341
1414
  if (acceptanceErrors.length > 0) return formatAsyncStartError("single", acceptanceErrors.join(" "));
1342
1415
  const externalRunner = agentConfig.runner?.type === "external-cli" || agentConfig.runner?.type === "external-job";
@@ -1345,6 +1418,7 @@ export function executeAsyncSingle(
1345
1418
  if (externalRunner) {
1346
1419
  const unsupported: string[] = [];
1347
1420
  if (params.modelOverride !== undefined) unsupported.push("model override");
1421
+ if ((params.fast ?? agentConfig.fast) === true) unsupported.push("fast mode");
1348
1422
  if (params.thinkingOverride !== undefined) unsupported.push("thinking override");
1349
1423
  if (params.structuredOutputSchema !== undefined) unsupported.push("structured output");
1350
1424
  if (params.acceptance !== undefined || params.agentContract !== undefined) unsupported.push("acceptance/agent contract");
@@ -1352,6 +1426,7 @@ export function executeAsyncSingle(
1352
1426
  if (params.context === "fork") unsupported.push("fork context");
1353
1427
  if ((params.skills?.length ?? 0) > 0) unsupported.push("skills");
1354
1428
  if (permissionRules) unsupported.push("native Pi child permissions");
1429
+ if (extensionBindings !== undefined) unsupported.push("extension bindings");
1355
1430
  if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
1356
1431
  }
1357
1432
  const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
@@ -1432,6 +1507,18 @@ export function executeAsyncSingle(
1432
1507
  );
1433
1508
  const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
1434
1509
  const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
1510
+ const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
1511
+ params.thinkingCeiling,
1512
+ agentConfig.maxThinking,
1513
+ decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
1514
+ );
1515
+ if (!externalRunner) {
1516
+ try {
1517
+ assertThinkingWithinCeiling({ model, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1518
+ } catch (error) {
1519
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1520
+ }
1521
+ }
1435
1522
  const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
1436
1523
  const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, params.toolBudget ? "toolBudget" : agentConfig.toolBudget ? "agent.toolBudget" : "config.toolBudget");
1437
1524
  if (resolvedToolBudget.error) return formatAsyncStartError("single", resolvedToolBudget.error);
@@ -1452,11 +1539,11 @@ export function executeAsyncSingle(
1452
1539
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1453
1540
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1454
1541
  const structuredOutput = params.structuredOutputSchema
1455
- ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1542
+ ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: params.acceptance !== false })
1456
1543
  : undefined;
1457
1544
  const modelCandidates = externalRunner
1458
1545
  ? []
1459
- : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
1546
+ : buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1460
1547
  scope: modelScopes,
1461
1548
  primaryModelFromParent: params.modelOverrideFromParent,
1462
1549
  })
@@ -1464,6 +1551,13 @@ export function executeAsyncSingle(
1464
1551
  const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1465
1552
  return resolved ? [resolved] : [];
1466
1553
  });
1554
+ if (!externalRunner) {
1555
+ try {
1556
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1557
+ } catch (error) {
1558
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1559
+ }
1560
+ }
1467
1561
  const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
1468
1562
  const toolPlan = resolvePiLaunchToolPlan({
1469
1563
  tools: agentConfig.tools,
@@ -1473,18 +1567,37 @@ export function executeAsyncSingle(
1473
1567
  cwd: runnerCwd,
1474
1568
  requireReadTool: Boolean(resolvedSkills.length),
1475
1569
  structuredOutput: Boolean(params.structuredOutputSchema),
1570
+ fast: params.fast ?? agentConfig.fast,
1571
+ model,
1572
+ modelCandidates,
1476
1573
  capabilityCeiling,
1477
1574
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1478
1575
  agentName: agentConfig.name,
1479
1576
  permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1480
1577
  });
1481
1578
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1579
+ if (!externalRunner) {
1580
+ const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
1581
+ const contractError = validateImplementationToolContract({
1582
+ agent: agentConfig.name,
1583
+ task: taskText,
1584
+ tools: contractTools,
1585
+ mcpDirectTools: toolPlan.effectiveMcpTools,
1586
+ configuredExtensions: toolPlan.configuredExtensions,
1587
+ requestedTools: toolPlan.requestedBuiltinTools,
1588
+ acceptanceRole: agentConfig.acceptanceRole,
1589
+ completionGuard: agentConfig.completionGuard,
1590
+ });
1591
+ if (contractError) return formatAsyncStartError("single", contractError);
1592
+ }
1482
1593
  const launchContractDigest = launchBindingDigest({
1483
1594
  definitionDigest: agentDefinitionDigest(agentConfig),
1484
1595
  task,
1485
1596
  ...(model ? { model } : {}),
1486
1597
  modelCandidates,
1598
+ ...((params.fast ?? agentConfig.fast) !== undefined ? { fast: params.fast ?? agentConfig.fast } : {}),
1487
1599
  ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1600
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
1488
1601
  systemPrompt: effectiveSystemPrompt,
1489
1602
  systemPromptMode: agentConfig.systemPromptMode,
1490
1603
  inheritProjectContext: agentConfig.inheritProjectContext,
@@ -1496,6 +1609,7 @@ export function executeAsyncSingle(
1496
1609
  ...(outputPath ? { outputPath } : {}),
1497
1610
  outputMode,
1498
1611
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1612
+ ...(extensionBindings ? { extensionBindings } : {}),
1499
1613
  });
1500
1614
  const resolvedAcceptance = resolveEffectiveAcceptance({
1501
1615
  explicit: params.acceptance,
@@ -1510,6 +1624,7 @@ export function executeAsyncSingle(
1510
1624
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1511
1625
  version: 1,
1512
1626
  launchContractDigest,
1627
+ ...(extensionBindings ? { extensionBindings } : {}),
1513
1628
  runFanoutBudget,
1514
1629
  sourceRunId: id,
1515
1630
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
@@ -1518,9 +1633,12 @@ export function executeAsyncSingle(
1518
1633
  ...(sessionFile ? { sessionFile } : {}),
1519
1634
  cwd: runnerCwd,
1520
1635
  ...(model ? { model } : {}),
1636
+ ...(params.fast ?? recoveryAgentConfig.fast ? { fast: params.fast ?? recoveryAgentConfig.fast } : {}),
1637
+ ...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
1521
1638
  ...(params.modelOverrideFromParent ? { modelOverrideFromParent: true } : {}),
1522
1639
  ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1523
1640
  ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1641
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
1524
1642
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1525
1643
  ...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
1526
1644
  ...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
@@ -1572,11 +1690,16 @@ export function executeAsyncSingle(
1572
1690
  agent,
1573
1691
  task: taskText,
1574
1692
  ...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
1693
+ ...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
1575
1694
  ...(params.context ? { context: params.context } : {}),
1576
1695
  cwd: runnerCwd,
1577
1696
  model,
1697
+ ...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
1578
1698
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1699
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
1579
1700
  modelCandidates,
1701
+ ...(params.modelOverrideFromParent ? { skipPrimaryModelVerification: true } : {}),
1702
+ ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1580
1703
  tools: agentConfig.tools,
1581
1704
  extensions: agentConfig.extensions,
1582
1705
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
@@ -1596,6 +1719,7 @@ export function executeAsyncSingle(
1596
1719
  definitionDigest: agentDefinitionDigest(agentConfig),
1597
1720
  launchBindingTask: task,
1598
1721
  launchContractDigest,
1722
+ ...(extensionBindings ? { extensionBindings } : {}),
1599
1723
  launchResolvedExtensions,
1600
1724
  effectiveAcceptance: resolvedAcceptance,
1601
1725
  ...(structuredOutput ? { structuredOutput } : {}),
@@ -1604,7 +1728,7 @@ export function executeAsyncSingle(
1604
1728
  ...(params.worktree === true ? { worktree: true } : {}),
1605
1729
  },
1606
1730
  ],
1607
- resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
1731
+ resultPath: params.parentWorkflowRunId !== undefined && (params.revivalLease !== undefined || params.workflowAwaitAsync === true)
1608
1732
  ? workflowAwaitedAsyncResultPath(asyncDir)
1609
1733
  : inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
1610
1734
  cwd: runnerCwd,
@@ -8,12 +8,14 @@ import {
8
8
  type AsyncStartedEvent,
9
9
  type ControlEvent,
10
10
  type SteeringNotice,
11
+ type SubagentChildStatusEvent,
11
12
  type SubagentState,
12
- POLL_INTERVAL_MS,
13
13
  DIRS,
14
+ SUBAGENT_CHILD_STATUS_EVENT,
14
15
  SUBAGENT_CONTROL_EVENT,
15
16
  SUBAGENT_CONTROL_INTERCOM_EVENT,
16
17
  SUBAGENT_STEERING_NOTICE_EVENT,
18
+ WIDGET_ANIMATION_INTERVAL_MS,
17
19
  } from "../../shared/types.ts";
18
20
  import { readStatus, resolveWatchPath } from "../../shared/utils.ts";
19
21
  import { normalizeParallelGroups } from "./parallel-groups.ts";
@@ -71,6 +73,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
71
73
  const runningJobIds = new Set<string>();
72
74
  let rootWatcher: fs.FSWatcher | undefined;
73
75
  let nextLivenessAt = Date.now() + livenessIntervalMs;
76
+ let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
74
77
  const watch = options.watch ?? fs.watch;
75
78
  const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
76
79
  const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
@@ -93,17 +96,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
93
96
  }
94
97
  };
95
98
  const requestLastWidgetRender = () => {
96
- const ctx = state.lastUiContext;
97
- if (!ctx || state.widgetsSuspended || options.widgetEnabled === false) return;
98
- try {
99
- if (ctx.hasUI) (ctx.ui as { requestRender?: () => void }).requestRender?.();
100
- } catch (error) {
101
- if (error instanceof Error && error.message.includes("extension ctx is stale")) {
102
- state.lastUiContext = null;
103
- return;
104
- }
105
- throw error;
106
- }
99
+ if (options.widgetEnabled === false) return;
100
+ rerenderLastWidget();
107
101
  };
108
102
  const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
109
103
  const restoredControlEventCursor = (asyncDir: string) => {
@@ -227,6 +221,28 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
227
221
  return;
228
222
  }
229
223
  if (!parsed || typeof parsed !== "object") return;
224
+ if ((parsed as { type?: unknown }).type === "subagent.child-status") {
225
+ const event = parsed as Partial<SubagentChildStatusEvent>;
226
+ if (event.version !== 1 || typeof event.runId !== "string" || typeof event.childId !== "string" || (event.status !== "stopping" && event.status !== "stopped") || typeof event.ts !== "number") return;
227
+ pi.events.emit(SUBAGENT_CHILD_STATUS_EVENT, {
228
+ type: "subagent.child-status",
229
+ version: 1,
230
+ runId: event.runId,
231
+ childId: event.childId,
232
+ status: event.status,
233
+ ts: event.ts,
234
+ ...(typeof event.reason === "string" ? { reason: event.reason } : {}),
235
+ source: event.source === "rpc" ? "rpc" : "async",
236
+ asyncDir: job.asyncDir,
237
+ ...(typeof event.stepIndex === "number" ? { stepIndex: event.stepIndex } : {}),
238
+ ...(typeof event.agent === "string" ? { agent: event.agent } : {}),
239
+ ...(typeof event.childRunId === "string" ? { childRunId: event.childRunId } : {}),
240
+ ...(typeof event.workflowKey === "string" ? { workflowKey: event.workflowKey } : {}),
241
+ ...(typeof event.phase === "string" ? { phase: event.phase } : {}),
242
+ ...(typeof event.label === "string" ? { label: event.label } : {}),
243
+ } satisfies SubagentChildStatusEvent);
244
+ return;
245
+ }
230
246
  if ((parsed as { type?: unknown }).type === "subagent.steering.notice") {
231
247
  const notice = parsed as Partial<SteeringNotice>;
232
248
  if (typeof notice.requestId !== "string" || typeof notice.runId !== "string" || (notice.state !== "failed" && notice.state !== "partial" && notice.state !== "recovered") || typeof notice.message !== "string") return;
@@ -556,8 +572,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
556
572
  }
557
573
  if (widgetChanged) rerenderLastWidget();
558
574
  }
559
- if (runningJobIds.size > 0) requestLastWidgetRender();
560
- }, Math.min(POLL_INTERVAL_MS, livenessIntervalMs));
575
+ if (runningJobIds.size > 0 && now >= nextWidgetAnimationAt) {
576
+ nextWidgetAnimationAt = now + WIDGET_ANIMATION_INTERVAL_MS;
577
+ requestLastWidgetRender();
578
+ }
579
+ }, Math.min(WIDGET_ANIMATION_INTERVAL_MS, livenessIntervalMs));
561
580
  state.poller.unref?.();
562
581
  };
563
582
 
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
4
4
  import type { AgentConfig } from "../../agents/agents.ts";
5
+ import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
5
6
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
7
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
7
8
  import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -11,6 +12,7 @@ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
11
12
  import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
12
13
  import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
13
14
  import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
15
+ import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
14
16
 
15
17
  export interface AsyncResumeParams {
16
18
  id?: string;
@@ -43,9 +45,12 @@ export type AsyncResumeTarget = {
43
45
  sessionFile?: string;
44
46
  model?: string;
45
47
  thinking?: string;
48
+ thinkingCeiling?: ThinkingLevel;
46
49
  recoveryDescriptor?: SteeringRecoveryDescriptor;
47
50
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
48
51
  launchContractDigest?: string;
52
+ runner?: NonNullable<AsyncStatus["steps"]>[number]["runner"];
53
+ externalJob?: NonNullable<AsyncStatus["steps"]>[number]["externalJob"];
49
54
  };
50
55
 
51
56
  interface AsyncResultFile {
@@ -265,6 +270,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
265
270
  if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
266
271
  if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
267
272
  if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
273
+ if (stepRecord.thinkingCeiling !== undefined) stepRecord.thinkingCeiling = parseThinkingLevel(stepRecord.thinkingCeiling, `async status '${source}' steps[${index}].thinkingCeiling`);
268
274
  if (stepRecord.launchContractDigest !== undefined && typeof stepRecord.launchContractDigest !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].launchContractDigest must be a string.`);
269
275
  if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
270
276
  });
@@ -318,11 +324,12 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
318
324
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
319
325
  const parsed = value as Record<string, unknown>;
320
326
  const allowedFields = new Set([
321
- "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
327
+ "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "extensions",
322
328
  "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
323
329
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
324
330
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
325
331
  "launchResolvedExtensions", "runFanoutBudget",
332
+ "extensionBindings",
326
333
  ]);
327
334
  for (const field of Object.keys(parsed)) {
328
335
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -338,6 +345,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
338
345
  throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
339
346
  }
340
347
  if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
348
+ if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
349
+ if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
341
350
  if (parsed.agentContract !== undefined) {
342
351
  if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
343
352
  const contract = parsed.agentContract as Record<string, unknown>;
@@ -356,7 +365,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
356
365
  if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
357
366
  }
358
367
  if (parsed.systemPrompt !== undefined && typeof parsed.systemPrompt !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPrompt must be a string.`);
359
- for (const field of ["launchContractDigest", "sessionFile", "model", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
368
+ for (const field of ["launchContractDigest", "sessionFile", "model", "modelProvider", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
360
369
  if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
361
370
  }
362
371
  if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
@@ -487,7 +496,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
487
496
  model: selectedStep.model,
488
497
  thinking: selectedStep.thinking,
489
498
  launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
499
+ ...(selectedStep.runner ? { runner: selectedStep.runner } : {}),
500
+ ...(selectedStep.externalJob ? { externalJob: selectedStep.externalJob } : {}),
490
501
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
502
+ ...(selectedStep.thinkingCeiling ? { thinkingCeiling: selectedStep.thinkingCeiling } : {}),
491
503
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
492
504
  };
493
505
  }
@@ -515,7 +527,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
515
527
  model: selected.step.model,
516
528
  thinking: selected.step.thinking,
517
529
  launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
530
+ ...(selected.step.runner ? { runner: selected.step.runner } : {}),
531
+ ...(selected.step.externalJob ? { externalJob: selected.step.externalJob } : {}),
518
532
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
533
+ ...(selected.step.thinkingCeiling ? { thinkingCeiling: selected.step.thinkingCeiling } : {}),
519
534
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
520
535
  };
521
536
  }
@@ -537,6 +552,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
537
552
  const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
538
553
  const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
539
554
  const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
555
+ const thinkingCeiling = statusSteps[index]?.thinkingCeiling ?? (stepCount === 1 ? recoveryDescriptor?.thinkingCeiling : undefined);
540
556
  const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
541
557
  const managedWorktreeCwd = location.asyncDir
542
558
  ? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
@@ -556,7 +572,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
556
572
  ...(stepModel ? { model: stepModel } : {}),
557
573
  ...(stepThinking ? { thinking: stepThinking } : {}),
558
574
  launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
575
+ ...(statusSteps[index]?.runner ? { runner: statusSteps[index]!.runner } : {}),
576
+ ...(statusSteps[index]?.externalJob ? { externalJob: statusSteps[index]!.externalJob } : {}),
559
577
  ...(capabilityCeiling ? { capabilityCeiling } : {}),
578
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
560
579
  ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
561
580
  };
562
581
  }
@@ -565,8 +584,10 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
565
584
  return {
566
585
  ...agentConfig,
567
586
  model: descriptor.model,
587
+ modelProvider: descriptor.modelProvider,
568
588
  fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
569
589
  thinking: descriptor.thinking,
590
+ maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
570
591
  tools: descriptor.tools ? [...descriptor.tools] : undefined,
571
592
  extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
572
593
  subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,