pi-subagents 0.49.0 → 0.50.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 (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/agents/oracle.md +7 -5
  3. package/agents/researcher.md +1 -1
  4. package/agents/reviewer.md +2 -2
  5. package/agents/scout.md +1 -1
  6. package/agents/worker.md +1 -1
  7. package/docs/agents.md +2 -0
  8. package/docs/configuration.md +47 -3
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +1 -1
  11. package/docs/observability.md +16 -0
  12. package/docs/tool-reference.md +19 -2
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  16. package/skills/pi-subagents/references/execution-controls.md +5 -4
  17. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +10 -0
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +12 -6
  23. package/src/extension/config.ts +33 -0
  24. package/src/extension/index.ts +48 -5
  25. package/src/extension/public-execution.ts +3 -1
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +2 -1
  28. package/src/extension/tool-description.ts +4 -2
  29. package/src/inspectors/herdr/actions.ts +3 -7
  30. package/src/inspectors/herdr/project-panes.ts +2 -6
  31. package/src/inspectors/herdr/shell-command.ts +16 -0
  32. package/src/intercom/intercom-bridge.ts +2 -3
  33. package/src/intercom/native-supervisor-channel.ts +19 -42
  34. package/src/missions/goal-driver.ts +3 -1
  35. package/src/runs/background/active-run-index.ts +71 -1
  36. package/src/runs/background/async-execution.ts +49 -7
  37. package/src/runs/background/async-job-tracker.ts +1 -0
  38. package/src/runs/background/async-resume.ts +3 -5
  39. package/src/runs/background/async-status-snapshot.ts +277 -0
  40. package/src/runs/background/async-status.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +2 -2
  42. package/src/runs/background/completion-replay.ts +11 -1
  43. package/src/runs/background/fleet-view.ts +3 -1
  44. package/src/runs/background/result-files.ts +437 -0
  45. package/src/runs/background/result-watcher.ts +188 -41
  46. package/src/runs/background/retained-children.ts +69 -20
  47. package/src/runs/background/run-id-resolver.ts +30 -24
  48. package/src/runs/background/run-status.ts +7 -2
  49. package/src/runs/background/scheduled-runs.ts +54 -28
  50. package/src/runs/background/stale-run-reconciler.ts +27 -13
  51. package/src/runs/background/subagent-runner.ts +294 -32
  52. package/src/runs/background/subagent-wait.ts +2 -0
  53. package/src/runs/background/wait-completions.ts +5 -2
  54. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  55. package/src/runs/foreground/chain-execution.ts +16 -0
  56. package/src/runs/foreground/execution.ts +219 -15
  57. package/src/runs/foreground/subagent-executor.ts +114 -28
  58. package/src/runs/shared/completion-guard.ts +96 -6
  59. package/src/runs/shared/external-cli-runner.ts +4 -0
  60. package/src/runs/shared/model-fallback.ts +16 -2
  61. package/src/runs/shared/nested-events.ts +66 -62
  62. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  63. package/src/runs/shared/parallel-utils.ts +2 -0
  64. package/src/runs/shared/subagent-control.ts +15 -0
  65. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  66. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  67. package/src/runs/shared/tool-timeout.ts +93 -0
  68. package/src/shared/types.ts +29 -2
  69. package/src/slash/slash-commands.ts +34 -25
  70. package/src/slash/slash-live-state.ts +3 -0
  71. package/src/tui/fleet-status.ts +160 -45
  72. package/src/tui/fleet.ts +125 -15
  73. package/src/tui/render.ts +41 -10
  74. package/src/workflows/chat-progress.ts +6 -4
  75. package/src/workflows/scripted-workflow.ts +227 -70
@@ -101,6 +101,7 @@ import { stopAsyncRun } from "./async-stop-action.ts";
101
101
  import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
102
102
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
103
103
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
104
+ import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
104
105
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
105
106
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
106
107
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
@@ -108,7 +109,7 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
108
109
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
109
110
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
110
111
  import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
111
- import { updateMission } from "../../missions/store.ts";
112
+ import { MissionNotFoundError, updateMission } from "../../missions/store.ts";
112
113
  import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
113
114
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
114
115
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
@@ -307,6 +308,8 @@ export interface SubagentParamsLike {
307
308
  foregroundOnly?: boolean;
308
309
  timeoutMs?: number;
309
310
  maxRuntimeMs?: number;
311
+ /** Optional hard per-tool-call timeout (ms). Known-fast tools also have a default. */
312
+ toolTimeoutMs?: number;
310
313
  turnBudget?: TurnBudgetConfig;
311
314
  /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
312
315
  enforceHardTurnLimit?: boolean;
@@ -409,6 +412,8 @@ interface ExecutionContextData {
409
412
  nestedRoute?: NestedRouteInfo;
410
413
  timeoutMs?: number;
411
414
  deadlineAt?: number;
415
+ /** Raw global config.toolTimeoutMs, for per-step resolution in async runners. */
416
+ configToolTimeoutMs?: number;
412
417
  turnBudget?: ResolvedTurnBudget;
413
418
  toolBudget?: ResolvedToolBudget;
414
419
  usageBudget?: UsageBudgetConfig;
@@ -2506,16 +2511,19 @@ function resolveStaticLaunchSummary(input: {
2506
2511
  thinkingOverrideForTask: ForkThinkingOverrideForTask;
2507
2512
  }): StaticLaunchSummary {
2508
2513
  const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2509
- const model = resolveEffectiveSubagentModel(
2510
- input.explicitModel,
2511
- agentConfig?.model,
2512
- input.parentModel,
2513
- input.availableModels,
2514
- input.currentProvider,
2515
- input.modelScope === undefined ? {} : { scope: input.modelScope },
2516
- );
2517
- const thinkingOverride = input.thinkingOverrideForTask(input.agent, input.index, model);
2518
- const thinking = resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2514
+ const externalRunner = agentConfig?.runner?.type === "external-cli";
2515
+ const model = externalRunner
2516
+ ? undefined
2517
+ : resolveEffectiveSubagentModel(
2518
+ input.explicitModel,
2519
+ agentConfig?.model,
2520
+ input.parentModel,
2521
+ input.availableModels,
2522
+ input.currentProvider,
2523
+ input.modelScope === undefined ? {} : { scope: input.modelScope },
2524
+ );
2525
+ const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
2526
+ const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2519
2527
  return {
2520
2528
  agent: input.agent,
2521
2529
  ...(model ? { model } : {}),
@@ -2798,6 +2806,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2798
2806
  toolBudget: data.toolBudget,
2799
2807
  usageBudget: data.usageBudget,
2800
2808
  configToolBudget: data.configToolBudget,
2809
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
2810
+ configToolTimeoutMs: data.configToolTimeoutMs,
2801
2811
  capabilityCeiling: data.capabilityCeiling,
2802
2812
  runFanoutBudget: data.runFanoutBudget,
2803
2813
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2845,6 +2855,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2845
2855
  toolBudget: data.toolBudget,
2846
2856
  usageBudget: data.usageBudget,
2847
2857
  configToolBudget: data.configToolBudget,
2858
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
2859
+ configToolTimeoutMs: data.configToolTimeoutMs,
2848
2860
  capabilityCeiling: data.capabilityCeiling,
2849
2861
  runFanoutBudget: data.runFanoutBudget,
2850
2862
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2871,8 +2883,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2871
2883
  const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2872
2884
  && params.model === undefined
2873
2885
  && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2874
- const modelOverride = externalRunnerWithoutExplicitModel
2875
- ? undefined
2886
+ const modelOverride = a.runner?.type === "external-cli"
2887
+ ? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
2876
2888
  : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2877
2889
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2878
2890
  agent: params.agent!,
@@ -2916,6 +2928,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2916
2928
  toolBudget: data.toolBudget,
2917
2929
  usageBudget: data.usageBudget,
2918
2930
  configToolBudget: data.configToolBudget,
2931
+ toolTimeoutMs: data.params?.toolTimeoutMs,
2932
+ configToolTimeoutMs: data.configToolTimeoutMs,
2919
2933
  capabilityCeiling: data.capabilityCeiling,
2920
2934
  runFanoutBudget: data.runFanoutBudget,
2921
2935
  parentWorkflowRunId: params.workflowParentRunId,
@@ -3005,6 +3019,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3005
3019
  toolBudget: data.toolBudget,
3006
3020
  usageBudget: data.usageBudget,
3007
3021
  configToolBudget: data.configToolBudget,
3022
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
3023
+ configToolTimeoutMs: data.configToolTimeoutMs,
3008
3024
  permissions: deps.config.permissions,
3009
3025
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3010
3026
  capabilityCeiling: data.capabilityCeiling,
@@ -3072,6 +3088,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3072
3088
  toolBudget: data.toolBudget,
3073
3089
  usageBudget: data.usageBudget,
3074
3090
  configToolBudget: data.configToolBudget,
3091
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
3092
+ configToolTimeoutMs: data.configToolTimeoutMs,
3075
3093
  capabilityCeiling: data.capabilityCeiling,
3076
3094
  runFanoutBudget: data.runFanoutBudget,
3077
3095
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -3159,6 +3177,10 @@ interface ForegroundParallelRunInput {
3159
3177
  worktreeSetup?: WorktreeSetup;
3160
3178
  timeoutMs?: number;
3161
3179
  deadlineAt?: number;
3180
+ /** Per-call per-tool timeout, resolved separately for each task's agent. */
3181
+ toolTimeoutMs?: number;
3182
+ /** Raw global config.toolTimeoutMs, used by each task's resolver. */
3183
+ configToolTimeoutMs?: number;
3162
3184
  turnBudget?: ResolvedTurnBudget;
3163
3185
  usageBudget?: UsageBudgetConfig;
3164
3186
  toolBudgets: (ResolvedToolBudget | undefined)[];
@@ -3535,6 +3557,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3535
3557
  share: input.shareEnabled || undefined,
3536
3558
  maxOutput: input.maxOutput,
3537
3559
  timeoutMs: input.timeoutMs,
3560
+ toolTimeoutMs: input.toolTimeoutMs,
3561
+ configToolTimeoutMs: input.configToolTimeoutMs,
3538
3562
  turnBudget: input.turnBudget,
3539
3563
  ...(model ? { model } : {}),
3540
3564
  ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
@@ -3614,6 +3638,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3614
3638
  acceptanceContext: { mode: "parallel" },
3615
3639
  onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3616
3640
  timeoutMs: input.timeoutMs,
3641
+ toolTimeoutMs: input.toolTimeoutMs,
3642
+ configToolTimeoutMs: input.configToolTimeoutMs,
3617
3643
  deadlineAt: input.deadlineAt,
3618
3644
  turnBudget: input.turnBudget,
3619
3645
  toolBudget: input.toolBudgets[index],
@@ -3741,7 +3767,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3741
3767
  ...(task.model !== undefined ? { model: task.model } : {}),
3742
3768
  }));
3743
3769
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
3744
- resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
3770
+ agentConfigs[i]?.runner?.type === "external-cli"
3771
+ ? undefined
3772
+ : resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
3745
3773
  );
3746
3774
 
3747
3775
  if (params.clarify === true && ctx.hasUI) {
@@ -3776,7 +3804,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3776
3804
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
3777
3805
  const override = result.behaviorOverrides[i];
3778
3806
  if (override?.model !== undefined) {
3779
- modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3807
+ modelOverrides[i] = agentConfigs[i]?.runner?.type === "external-cli"
3808
+ ? undefined
3809
+ : resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3780
3810
  behaviorOverrides[i]!.model = override.model;
3781
3811
  }
3782
3812
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -3863,6 +3893,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3863
3893
  usageBudget: data.usageBudget,
3864
3894
  toolBudget: data.toolBudget,
3865
3895
  configToolBudget: data.configToolBudget,
3896
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
3897
+ configToolTimeoutMs: data.configToolTimeoutMs,
3866
3898
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3867
3899
  runFanoutBudget: data.runFanoutBudget,
3868
3900
  }));
@@ -3983,6 +4015,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3983
4015
  worktreeSetup,
3984
4016
  timeoutMs: data.timeoutMs,
3985
4017
  deadlineAt,
4018
+ toolTimeoutMs: data.params?.toolTimeoutMs,
4019
+ configToolTimeoutMs: data.configToolTimeoutMs,
3986
4020
  turnBudget: data.turnBudget,
3987
4021
  usageBudget: data.usageBudget,
3988
4022
  toolBudgets,
@@ -4377,6 +4411,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4377
4411
  },
4378
4412
  timeoutMs: data.timeoutMs,
4379
4413
  deadlineAt,
4414
+ toolTimeoutMs: params.toolTimeoutMs,
4415
+ configToolTimeoutMs: data.configToolTimeoutMs,
4380
4416
  turnBudget: data.turnBudget,
4381
4417
  enforceHardTurnLimit: params.enforceHardTurnLimit,
4382
4418
  toolBudget: effectiveToolBudget.toolBudget,
@@ -4455,8 +4491,15 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4455
4491
  }
4456
4492
 
4457
4493
  if (r.detached) {
4494
+ const statusRecovery = `subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
4495
+ const blockingRecovery = `subagent_wait({ id: "${runId}" }). Use ${statusRecovery}`;
4496
+ const message = r.detachedReason === "intercom coordination"
4497
+ ? `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with ${blockingRecovery}`
4498
+ : r.detachedReason === "user request"
4499
+ ? `Detached at user request: ${params.agent}. The child continues independently. Register a completion wake-up with subagent_wait({ id: "${runId}", nonBlocking: true }), or use ${statusRecovery}`
4500
+ : `Detached before task completion: ${params.agent}. Wait with ${blockingRecovery}`;
4458
4501
  return {
4459
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
4502
+ content: [{ type: "text", text: message }],
4460
4503
  details,
4461
4504
  };
4462
4505
  }
@@ -4500,17 +4543,28 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4500
4543
 
4501
4544
  const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
4502
4545
 
4546
+ /**
4547
+ * Terminal-mission retention can remove a mission while its children still
4548
+ * report. Remember it to prevent later heartbeats from warning repeatedly.
4549
+ */
4550
+ const missionsMissingFromStore = new Set<string>();
4551
+
4503
4552
  function recordMissionWorkflowChild(
4504
4553
  binding: MissionLaunchBinding | undefined,
4505
4554
  workflowRunId: string,
4506
4555
  key: string,
4507
4556
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4508
4557
  ): void {
4509
- if (!binding) return;
4558
+ if (!binding || missionsMissingFromStore.has(binding.missionId)) return;
4510
4559
  const { task: _task, ...durableUpdate } = update;
4511
4560
  try {
4512
4561
  updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4513
4562
  } catch (error) {
4563
+ if (error instanceof MissionNotFoundError) {
4564
+ missionsMissingFromStore.add(binding.missionId);
4565
+ console.warn(`[pi-subagents] Mission '${binding.missionId}' is no longer in the mission store; stopped recording its workflow children. Terminal-mission retention can prune a mission while its run is still active.`);
4566
+ return;
4567
+ }
4514
4568
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4515
4569
  }
4516
4570
  }
@@ -4580,6 +4634,7 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
4580
4634
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
4581
4635
  output,
4582
4636
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
4637
+ ...(detached ? { detached: true } : {}),
4583
4638
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
4584
4639
  artifactPaths: [...artifactPaths],
4585
4640
  results: result.details.results,
@@ -4888,7 +4943,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4888
4943
  if (workflowRunId) {
4889
4944
  const toolCallId = _id;
4890
4945
  const asyncDir = path.join(DIRS.async, workflowRunId);
4891
- const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4946
+ const resultPath = resultFilePath(DIRS.results, workflowRunId);
4892
4947
  const statusPath = path.join(asyncDir, "status.json");
4893
4948
  const eventsPath = path.join(asyncDir, "events.jsonl");
4894
4949
  const startedAt = Date.now();
@@ -4927,7 +4982,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4927
4982
  status.lastUpdate = Date.now();
4928
4983
  writeAtomicJson(statusPath, status);
4929
4984
  if (indexedState !== status.state) {
4930
- updateActiveRunIndex(asyncDir, status.state);
4985
+ updateActiveRunIndex(asyncDir, status.state, status.toolCallId);
4931
4986
  indexedState = status.state;
4932
4987
  }
4933
4988
  const job = deps.state.asyncJobs.get(workflowRunId);
@@ -4952,6 +5007,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4952
5007
  job.workflow = status.workflow;
4953
5008
  }
4954
5009
  };
5010
+ const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
5011
+ try {
5012
+ writeAsyncResultFile(resultPath, payload);
5013
+ return true;
5014
+ } catch (error) {
5015
+ const message = `Failed to write async workflow result ${resultPath}: ${error instanceof Error ? error.message : String(error)}`;
5016
+ console.error(message, error);
5017
+ appendWorkflowEvent({ type: "subagent.workflow.result_write_failed", error: message });
5018
+ return false;
5019
+ }
5020
+ };
4955
5021
  const projectWorkflowActivity = () => {
4956
5022
  const steps = status.steps ?? [];
4957
5023
  const runningSteps = steps.filter((step) => step.status === "running");
@@ -4973,7 +5039,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4973
5039
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
4974
5040
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
4975
5041
  };
4976
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
5042
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4977
5043
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4978
5044
  deps.state.fleetJobs ??= new Map();
4979
5045
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -5012,12 +5078,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5012
5078
  ? "completed"
5013
5079
  : entry.state === "stopped"
5014
5080
  ? "stopped"
5015
- : "failed";
5081
+ : entry.state === "detached"
5082
+ ? "paused"
5083
+ : "failed";
5016
5084
  if (existing) {
5017
5085
  existing.status = mapped;
5018
5086
  if (entry.agent) existing.agent = entry.agent;
5019
5087
  if (entry.runId) existing.runId = entry.runId;
5020
5088
  if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
5089
+ if (entry.state === "detached") existing.activityState = "needs_attention";
5090
+ else if (existing.status !== "running") delete existing.activityState;
5021
5091
  if (entry.state === "stopped") existing.stopped = true;
5022
5092
  else delete existing.stopped;
5023
5093
  if (entry.error === undefined) delete existing.error;
@@ -5034,6 +5104,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5034
5104
  startedAt: Date.now(),
5035
5105
  ...(entry.runId ? { runId: entry.runId } : {}),
5036
5106
  ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
5107
+ ...(entry.state === "detached" ? { activityState: "needs_attention" as const } : {}),
5037
5108
  ...(entry.state === "stopped" ? { stopped: true } : {}),
5038
5109
  };
5039
5110
  status.steps?.push(step);
@@ -5156,18 +5227,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5156
5227
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
5157
5228
  const workflowUsage = sumResultsUsage(workflowResults);
5158
5229
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
5159
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5230
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5160
5231
  persist();
5161
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
5232
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
5162
5233
  } catch (error) {
5163
5234
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
5164
5235
  const stopped = controller.signal.aborted;
5165
- status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5166
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? "Workflow failed.");
5167
- const resultSummary = appendWorkflowOutputWarning(status.error ?? "Workflow failed.", outputWarning);
5168
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5236
+ const detachedChildKeys = new Set(partial.children.filter((child) => child.detached).map((child) => child.key));
5237
+ const hasRealFailedChild = partial.children.some((child) => !child.ok && !child.detached);
5238
+ const pauseForDetached = !stopped && error instanceof WorkflowScriptError && error.errorKind === "detached-child" && detachedChildKeys.size > 0 && !hasRealFailedChild;
5239
+ const state = stopped ? "stopped" : pauseForDetached ? "paused" : "failed";
5240
+ for (const step of status.steps ?? []) {
5241
+ if (step.workflowKey && detachedChildKeys.has(step.workflowKey)) {
5242
+ step.status = "paused";
5243
+ step.activityState = "needs_attention";
5244
+ }
5245
+ }
5246
+ status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5247
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."));
5248
+ const resultSummary = appendWorkflowOutputWarning(status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."), outputWarning);
5249
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.detached ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5169
5250
  persist();
5170
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
5251
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error, ...(status.activityState ? { activityState: status.activityState } : {}) });
5171
5252
  } finally {
5172
5253
  deps.state.workflowControllers?.delete(workflowRunId);
5173
5254
  deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
@@ -6013,6 +6094,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6013
6094
  const parentModel = requestParentModel;
6014
6095
  prepareForkThinking = (agentName, index, modelOverride) => {
6015
6096
  const agentConfig = agents.find((agent) => agent.name === agentName);
6097
+ if (agentConfig?.runner?.type === "external-cli") {
6098
+ forkThinkingRequirements.set(index, true);
6099
+ return;
6100
+ }
6016
6101
  const primaryModel = resolveEffectiveSubagentModel(
6017
6102
  modelOverride,
6018
6103
  agentConfig?.model,
@@ -6249,6 +6334,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6249
6334
  usageBudget: usageBudget.budget,
6250
6335
  allowZeroToolBudget,
6251
6336
  configToolBudget: configToolBudget.toolBudget,
6337
+ configToolTimeoutMs: deps.config.toolTimeoutMs,
6252
6338
  contextPolicy,
6253
6339
  modelScope,
6254
6340
  parentModel: requestParentModel,
@@ -22,9 +22,22 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
22
22
  const CURSOR_FILE_MUTATION_THINKING =
23
23
  /(?:^|\n)\s*Cursor (?:edit|write)\s*:/i;
24
24
 
25
- const IMPLEMENTATION_CHALLENGE_TASK_PATTERN = /^You are reviving a previous subagent conversation\.\n\nOriginal run: .+\nOriginal agent: .+(?:\nOriginal session file: .+)?\n\nUse the stored session context as background\. Answer the orchestrator's follow-up below\. Do not assume the original child process is still alive\.\n\nFollow-up:\nRun implementation challenge pass (?:one|two|\d+) and implement any better current-scope change\.$/;
26
- const NO_BETTER_CHANGE_NEEDED_PATTERN = /^\s*no (?:better|further|additional) (?:current[- ]scope )?(?:code |source |file )?(?:change|changes|edit|edits|patch|patches) (?:is|are) needed[.!]?\s*$/i;
27
- const NO_BETTER_CHANGE_QUALIFIER_PATTERN = /\b(?:do\s+not|don't|dont|not|never|cannot|can't|cant|unable|uncertain|unsure|unclear|maybe|might|may|\w+n['’]t)\b/i;
25
+ const REVIVED_TASK_PREFIX = /^You are reviving a previous subagent conversation\.\n\nOriginal run: .+\nOriginal agent: .+(?:\nOriginal session file: .+)?\n\nUse the stored session context as background\. Answer the orchestrator's follow-up below\. Do not assume the original child process is still alive\.\n\nFollow-up:\n/;
26
+ const IMPLEMENTATION_CHALLENGE_FOLLOW_UP_PATTERN = /^(?:Run )?implementation challenge pass (?:one|two|\d+)(?:\s+and implement any better current[- ]scope change\.?|\s+for the accepted candidate\.\s+Reconsider it and implement any better current[- ]scope change\.?)?$/i;
27
+ const NO_BETTER_CHANGE_NEEDED_PATTERN = /^\s*no (?:better|further|additional) (?:current[- ]scope )?(?:code |source |file )?(?:change|changes|edit|edits|patch|patches) (?:is|are) needed\b/i;
28
+ const KEPT_CURRENT_IMPLEMENTATION_PATTERN = /\b(?:kept (?:the )?current (?:implementation|candidate|shape)|(?:the )?current (?:implementation|candidate|shape) was kept)\b/i;
29
+ const NO_CHANGE_MADE_PATTERN = /\bno (?:new )?(?:(?:code|source|file|test)(?:\s*(?:,\s*or|,|\/|or)\s*(?:code|source|file|test))* changes?|changes?) (?:were|was) made\b/i;
30
+ const NO_BETTER_CHANGE_QUALIFIER_PATTERN = /\b(?:do\s+not|don't|dont|not|never|cannot|can't|cant|unable|uncertain|unsure|unclear|disagree|wrong|false|reject|rejected|rejecting|maybe|might|may|\w+n['’]t)\b/i;
31
+ const REPORT_SENTENCE_PATTERN = /[^.!?]+(?:[.!?]+(?=(?:["”'’]?\s)|["”'’]?$)|$)/g;
32
+ const QUOTED_CLAIM_PATTERN = /["“]([^"”]+)["”]([.!?]?\s*[^.!?]*)/g;
33
+ const QUOTED_CLAIM_REFERENCE_PATTERN = /\b(?:that|this|it|claim|report|message|disagree)\b/i;
34
+ const CLAIM_CONTRADICTION_PATTERN = /\bbut\b[^.!?]*\b(?:uncertain|unsure|unclear|disagree|wrong|false|reject|rejected|rejecting)\b/i;
35
+ const IMPLEMENTATION_RETRACTION_PATTERN = /\b(?:found|identified)\s+(?:(?:a|an|the)\s+)?(?:required|necessary|additional|better)?\s*(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:(?:implementation|code|source|file|test)\s+)?work\s+(?:remains?|is\s+(?:needed|required))\b|\b(?:(?:a|an|the)\s+)?(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\s+(?:are|is)\s+(?:needed|required)\b|\b(?:required|necessary|additional|better)\s+(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:I|we)?\s*need\s+(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:need|needs|require|requires)\s+(?:to\s+)?(?:implement|make|apply)\b/i;
36
+ const IMPLEMENTATION_RETRACTION_GLOBAL_PATTERN = new RegExp(IMPLEMENTATION_RETRACTION_PATTERN.source, "gi");
37
+ const DIRECT_CLAIM_RETRACTION_PATTERN = /^\s*(?:I\s+)?(?:disagree|reject|retract)\b|\b(?:disagree|retract|retracted|retracting|reject|rejected|rejecting)\b[^.!?]*\b(?:that|this|it|claim|report|message)\b|\b(?:that|this|it|claim|report|message)\b[^.!?]*\b(?:is|was)\s+(?:rejected|retracted)\b/i;
38
+ const NEGATED_RETRACTION_PREFIX_PATTERN = /\b(?:no|not|\w+n['’]t)\b(?:\s+\w+){0,2}\s*$/i;
39
+ const REPORTED_CLAIM_PREFIX_PATTERN = /["“]|\b(?:prior|previous)\s+(?:message|report)\b|\b(?:said|stated|reported)\b/i;
40
+ const POST_CLAIM_SEPARATOR_PATTERN = /^[\s,;:.—–'"“‘’-]+/;
28
41
 
29
42
  interface CompletionMutationGuardInput {
30
43
  agent: string;
@@ -40,6 +53,11 @@ interface CompletionMutationGuardResult {
40
53
  triggered: boolean;
41
54
  }
42
55
 
56
+ interface ReportSentence {
57
+ text: string;
58
+ offset: number;
59
+ }
60
+
43
61
  export function hasMutationToolCapability(tools: string[] | undefined, mcpDirectTools: string[] | undefined): boolean {
44
62
  if ((mcpDirectTools?.length ?? 0) > 0) return true;
45
63
  if (tools === undefined) return true;
@@ -86,14 +104,86 @@ export function hasMutationToolCall(messages: Message[]): boolean {
86
104
  return false;
87
105
  }
88
106
 
107
+ function hasImplementationRetraction(text: string): boolean {
108
+ return Array.from(text.matchAll(IMPLEMENTATION_RETRACTION_GLOBAL_PATTERN)).some((retraction) =>
109
+ !NEGATED_RETRACTION_PREFIX_PATTERN.test(text.slice(0, retraction.index!)));
110
+ }
111
+
112
+ function hasDirectClaimRetraction(text: string): boolean {
113
+ return DIRECT_CLAIM_RETRACTION_PATTERN.test(text.replace(POST_CLAIM_SEPARATOR_PATTERN, ""));
114
+ }
115
+
116
+ function isInsideQuotedText(report: string, index: number): boolean {
117
+ let quote: "double" | "single" | undefined;
118
+ for (const char of report.slice(0, index)) {
119
+ if (quote === "double") {
120
+ if (char === "\"" || char === "”") quote = undefined;
121
+ } else if (quote === "single") {
122
+ if (char === "’") quote = undefined;
123
+ } else if (char === "\"" || char === "“") {
124
+ quote = "double";
125
+ } else if (char === "‘") {
126
+ quote = "single";
127
+ }
128
+ }
129
+ return quote !== undefined;
130
+ }
131
+
132
+ function unqualifiedClaimIndex(report: string, sentences: ReportSentence[], pattern: RegExp): number {
133
+ return sentences.findIndex((sentence) => {
134
+ const claim = sentence.text.match(pattern);
135
+ if (claim === null) return false;
136
+ const before = sentence.text.slice(0, claim.index!);
137
+ const after = sentence.text.slice(claim.index! + claim[0].length);
138
+ const previous = before.trimEnd().at(-1);
139
+ return previous !== "'"
140
+ && previous !== "‘"
141
+ && !isInsideQuotedText(report, sentence.offset + claim.index!)
142
+ && !REPORTED_CLAIM_PREFIX_PATTERN.test(before)
143
+ && !NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(before)
144
+ && !CLAIM_CONTRADICTION_PATTERN.test(after)
145
+ && !hasImplementationRetraction(after)
146
+ && !hasDirectClaimRetraction(after);
147
+ });
148
+ }
149
+
150
+ function hasLaterClaimRetraction(sentences: ReportSentence[], claimIndex: number): boolean {
151
+ return sentences.slice(claimIndex + 1).some((sentence) => hasImplementationRetraction(sentence.text)
152
+ || hasDirectClaimRetraction(sentence.text));
153
+ }
154
+
155
+ function explicitlyRejectsQuotedClaim(report: string): boolean {
156
+ for (const [, claim, response] of report.matchAll(QUOTED_CLAIM_PATTERN)) {
157
+ if ((KEPT_CURRENT_IMPLEMENTATION_PATTERN.test(claim!) || NO_CHANGE_MADE_PATTERN.test(claim!))
158
+ && NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(response!)
159
+ && QUOTED_CLAIM_REFERENCE_PATTERN.test(response!)) return true;
160
+ }
161
+ return false;
162
+ }
163
+
164
+ function isImplementationChallengeTask(task: string): boolean {
165
+ const followUp = task.replace(REVIVED_TASK_PREFIX, "");
166
+ return followUp !== task && IMPLEMENTATION_CHALLENGE_FOLLOW_UP_PATTERN.test(followUp.trim());
167
+ }
168
+
89
169
  function reportsNoBetterChallengeChange(messages: Message[]): boolean {
90
170
  const report = messages
91
171
  .filter((message) => message.role === "assistant")
92
172
  .flatMap((message) => message.content)
93
173
  .flatMap((part) => part.type === "text" ? [part.text] : [])
94
174
  .join("\n");
95
- return NO_BETTER_CHANGE_NEEDED_PATTERN.test(report)
96
- && !NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(report);
175
+ if (explicitlyRejectsQuotedClaim(report)) return false;
176
+ const sentences = Array.from(report.matchAll(REPORT_SENTENCE_PATTERN), (match) => ({
177
+ text: match[0],
178
+ offset: match.index!,
179
+ }));
180
+ const noBetterIndex = unqualifiedClaimIndex(report, sentences, NO_BETTER_CHANGE_NEEDED_PATTERN);
181
+ if (noBetterIndex >= 0) return !hasLaterClaimRetraction(sentences, noBetterIndex);
182
+ const keptIndex = unqualifiedClaimIndex(report, sentences, KEPT_CURRENT_IMPLEMENTATION_PATTERN);
183
+ const noChangeIndex = unqualifiedClaimIndex(report, sentences, NO_CHANGE_MADE_PATTERN);
184
+ return keptIndex >= 0
185
+ && noChangeIndex >= 0
186
+ && !hasLaterClaimRetraction(sentences, Math.min(keptIndex, noChangeIndex));
97
187
  }
98
188
 
99
189
  export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
@@ -101,7 +191,7 @@ export function evaluateCompletionMutationGuard(input: CompletionMutationGuardIn
101
191
  ? expectsImplementationMutation(input.agent, input.task)
102
192
  : false;
103
193
  const attemptedMutation = hasMutationToolCall(input.messages);
104
- const noEditChallengeComplete = IMPLEMENTATION_CHALLENGE_TASK_PATTERN.test(input.task)
194
+ const noEditChallengeComplete = isImplementationChallengeTask(input.task)
105
195
  && reportsNoBetterChallengeChange(input.messages);
106
196
  return {
107
197
  expectedMutation,
@@ -35,6 +35,8 @@ export function runExternalCli(input: {
35
35
  timeoutMessage?: string;
36
36
  stopMessage?: string;
37
37
  onProcess?: (process: ExternalProcessStatus) => void;
38
+ onStdout?: (chunk: Buffer) => void;
39
+ onStderr?: (chunk: Buffer) => void;
38
40
  }): Promise<ExternalCliRunResult> {
39
41
  return new Promise((resolve, reject) => {
40
42
  const startedAt = Date.now();
@@ -65,11 +67,13 @@ export function runExternalCli(input: {
65
67
  input.onProcess?.(initialProcess);
66
68
  child.stdout.on("data", (chunk: Buffer) => {
67
69
  stdoutStream.write(chunk);
70
+ input.onStdout?.(chunk);
68
71
  stdoutTail = Buffer.concat([stdoutTail, chunk]);
69
72
  if (stdoutTail.length > MAX_OUTPUT_TAIL_BYTES) stdoutTail = stdoutTail.subarray(stdoutTail.length - MAX_OUTPUT_TAIL_BYTES);
70
73
  });
71
74
  child.stderr.on("data", (chunk: Buffer) => {
72
75
  stderrStream.write(chunk);
76
+ input.onStderr?.(chunk);
73
77
  stderrTail = Buffer.concat([stderrTail, chunk]);
74
78
  if (stderrTail.length > MAX_ERROR_TAIL_BYTES) stderrTail = stderrTail.subarray(stderrTail.length - MAX_ERROR_TAIL_BYTES);
75
79
  });
@@ -163,6 +163,20 @@ export function resolveModelCandidate(
163
163
  return model;
164
164
  }
165
165
 
166
+ function resolveRequiredSubagentModelCandidate(
167
+ model: string,
168
+ availableModels: AvailableModelInfo[] | undefined,
169
+ preferredProvider?: string,
170
+ ): string {
171
+ if (!availableModels || availableModels.length === 0) return model;
172
+ const resolvedWhole = resolveBaseModelCandidate(model, availableModels, preferredProvider);
173
+ if (resolvedWhole) return resolvedWhole;
174
+ const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
175
+ const resolvedBase = thinkingSuffix ? resolveBaseModelCandidate(baseModel, availableModels, preferredProvider) : undefined;
176
+ if (resolvedBase) return `${resolvedBase}${thinkingSuffix}`;
177
+ throw new Error(`Unknown subagent model '${model}' in the active Pi model registry.`);
178
+ }
179
+
166
180
  export interface ResolveSubagentModelOverrideOptions {
167
181
  /** When set with `enforce: true`, out-of-scope models are rejected. */
168
182
  scope?: ModelScopeConfig;
@@ -207,7 +221,7 @@ export function resolveSubagentModelOverride(
207
221
  if (explicit === undefined) {
208
222
  resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
209
223
  } else {
210
- resolved = resolveModelCandidate(explicit, availableModels, preferredProvider);
224
+ resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
211
225
  }
212
226
  if (resolved && options?.scope?.enforce) {
213
227
  const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
@@ -264,7 +278,7 @@ export function buildModelCandidates(
264
278
  for (let index = 0; index < rawCandidates.length; index++) {
265
279
  const raw = rawCandidates[index];
266
280
  if (!raw) continue;
267
- const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
281
+ const normalized = resolveRequiredSubagentModelCandidate(raw.trim(), availableModels, preferredProvider);
268
282
  if (!normalized || seen.has(normalized)) continue;
269
283
  if ((index > 0 || options?.scope?.strict === true) && options?.scope?.enforce) {
270
284
  const violation = checkModelScope(normalized, options.scope, "inherited");