pi-subagents 0.53.0 → 0.55.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 (76) hide show
  1. package/CHANGELOG.md +66 -5
  2. package/README.md +1 -1
  3. package/agents/reviewer.md +12 -2
  4. package/docs/agents.md +4 -4
  5. package/docs/configuration.md +6 -6
  6. package/docs/extension-api.md +14 -4
  7. package/docs/models.md +41 -6
  8. package/docs/observability.md +14 -3
  9. package/docs/tool-reference.md +21 -6
  10. package/docs/workflows.md +7 -1
  11. package/index.ts +10 -1
  12. package/package.json +1 -1
  13. package/prompts/council.md +19 -7
  14. package/prompts/parallel-review.md +5 -1
  15. package/prompts/review-loop.md +9 -5
  16. package/skills/council-mode/SKILL.md +50 -26
  17. package/skills/pi-subagents/SKILL.md +5 -1
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  19. package/skills/pi-subagents/references/execution-controls.md +13 -4
  20. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  21. package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
  22. package/src/agents/agent-management.ts +115 -14
  23. package/src/agents/agent-serializer.ts +2 -2
  24. package/src/agents/agents.ts +195 -47
  25. package/src/api/external-job-provider.ts +10 -1
  26. package/src/api/preflight.ts +33 -19
  27. package/src/api/project-panes.ts +2 -0
  28. package/src/extension/doctor.ts +10 -0
  29. package/src/extension/fanout-child.ts +3 -2
  30. package/src/extension/index.ts +24 -4
  31. package/src/extension/public-execution.ts +12 -9
  32. package/src/extension/rpc.ts +77 -3
  33. package/src/extension/schemas.ts +2 -1
  34. package/src/extension/tool-description.ts +9 -6
  35. package/src/extension/tool-result.ts +19 -0
  36. package/src/inspectors/herdr/client.ts +3 -3
  37. package/src/inspectors/herdr/focus.ts +55 -0
  38. package/src/inspectors/herdr/project-panes.ts +228 -44
  39. package/src/integrations/herdr-status.ts +26 -4
  40. package/src/runs/background/async-execution.ts +112 -13
  41. package/src/runs/background/async-job-tracker.ts +33 -14
  42. package/src/runs/background/async-resume.ts +20 -2
  43. package/src/runs/background/async-retention.ts +1 -1
  44. package/src/runs/background/async-status.ts +10 -0
  45. package/src/runs/background/chain-root-attachment.ts +5 -0
  46. package/src/runs/background/control-channel.ts +98 -10
  47. package/src/runs/background/notify.ts +62 -1
  48. package/src/runs/background/result-watcher.ts +4 -3
  49. package/src/runs/background/run-status.ts +15 -1
  50. package/src/runs/background/stale-run-reconciler.ts +3 -0
  51. package/src/runs/background/subagent-runner.ts +251 -38
  52. package/src/runs/background/wait-completions.ts +2 -0
  53. package/src/runs/background/wait-tool.ts +4 -3
  54. package/src/runs/foreground/async-stop-action.ts +23 -2
  55. package/src/runs/foreground/execution.ts +110 -8
  56. package/src/runs/foreground/subagent-executor.ts +361 -45
  57. package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
  58. package/src/runs/shared/acceptance.ts +1 -0
  59. package/src/runs/shared/child-identity.ts +36 -0
  60. package/src/runs/shared/completion-guard.ts +50 -1
  61. package/src/runs/shared/external-job-bridge.ts +53 -37
  62. package/src/runs/shared/external-job-runner.ts +126 -23
  63. package/src/runs/shared/model-fallback.ts +46 -15
  64. package/src/runs/shared/model-scope.ts +106 -39
  65. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  66. package/src/runs/shared/parallel-utils.ts +12 -0
  67. package/src/runs/shared/pi-args.ts +31 -1
  68. package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
  69. package/src/runs/shared/tool-availability.ts +1 -3
  70. package/src/shared/launch-contract.ts +6 -5
  71. package/src/shared/thinking-ceiling.ts +52 -0
  72. package/src/shared/types.ts +72 -5
  73. package/src/tui/fleet-status.ts +66 -13
  74. package/src/tui/render.ts +5 -4
  75. package/src/watchdog/permission-arbiter.ts +59 -51
  76. package/src/workflows/scripted-workflow.ts +107 -10
@@ -30,7 +30,7 @@ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/
30
30
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
31
31
  import { buildModelCandidates, inheritsParentModel, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
32
32
  import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
33
- import type { ModelScopeConfig } from "../shared/model-scope.ts";
33
+ import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
34
34
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
35
35
  import { recordRun } from "../shared/run-history.ts";
36
36
  import {
@@ -108,7 +108,10 @@ import { resultFilePath, writeAsyncResultFile } from "../background/result-files
108
108
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
109
109
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
110
110
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
111
+ import { isStoppableAsyncStatusStep, resolveAsyncStatusChild } from "../shared/child-identity.ts";
111
112
  import { inspectSubagentStatus } from "../background/run-status.ts";
113
+ import { getExternalJobProvider } from "../../api/external-job-provider.ts";
114
+ import { externalJobFollowUpRequestDigest, externalJobFollowUpRequestId, externalJobFollowUpRunId, externalJobPromptDigest, externalJobStableJson } from "../shared/external-job-runner.ts";
112
115
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
113
116
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
114
117
  import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
@@ -154,6 +157,7 @@ import {
154
157
  type ResolvedToolBudget,
155
158
  type RunFanoutBudgetDescriptor,
156
159
  type SingleResult,
160
+ type SubagentChildStatusEvent,
157
161
  type ToolBudgetConfig,
158
162
  type TurnBudgetConfig,
159
163
  type UsageBudgetConfig,
@@ -274,6 +278,7 @@ export interface SubagentParamsLike {
274
278
  dir?: string;
275
279
  handoffPath?: string;
276
280
  index?: number;
281
+ childId?: string;
277
282
  view?: "fleet" | "transcript";
278
283
  lines?: number;
279
284
  topic?: string;
@@ -298,12 +303,15 @@ export interface SubagentParamsLike {
298
303
  /** Set by the scheduler so this run's completion can name the schedule that produced it. */
299
304
  scheduleOrigin?: ScheduleOrigin;
300
305
  workflowChildAsyncId?: string;
306
+ workflowAwaitAsync?: boolean;
301
307
  workflowParentDeadlineAt?: number;
302
308
  suppressRoutineResultIntercom?: boolean;
303
309
  /** Internal inherited cumulative run-tree budget. */
304
310
  runFanoutBudget?: RunFanoutBudgetDescriptor;
305
311
  /** Internal workflow host admission proof. */
306
312
  runFanoutAdmitted?: boolean;
313
+ /** Internal inherited tool/agent ceiling for delegated child launches. */
314
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
307
315
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
308
316
  chain?: ChainStep[];
309
317
  tasks?: TaskParam[];
@@ -384,7 +392,7 @@ interface ExecutorDeps {
384
392
  tempArtifactsDir: string;
385
393
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
386
394
  expandTilde: (p: string) => string;
387
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig };
395
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig; maxThinking?: AgentConfig["maxThinking"] };
388
396
  allowMutatingManagementActions?: boolean;
389
397
  activateSupervisorTransport?: () => void;
390
398
  refreshResultDelivery?: () => void;
@@ -853,6 +861,10 @@ function isAsyncRunNotFound(error: unknown): boolean {
853
861
  return error instanceof Error && error.message.startsWith("Async run not found.");
854
862
  }
855
863
 
864
+ function isMissingExactAsyncStatusFile(error: unknown, requested: string): boolean {
865
+ return error instanceof Error && error.message === `Status file not found for async run '${requested}'.`;
866
+ }
867
+
856
868
  function isResumeAmbiguity(error: unknown): boolean {
857
869
  return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message);
858
870
  }
@@ -906,7 +918,7 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
906
918
  throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`);
907
919
  }
908
920
  if (foregroundTarget) {
909
- if (isExactResumeError(asyncError, "async", requested)) throw asyncError;
921
+ if (isExactResumeError(asyncError, "async", requested) && !isMissingExactAsyncStatusFile(asyncError, requested)) throw asyncError;
910
922
  if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError;
911
923
  return foregroundTarget;
912
924
  }
@@ -1456,6 +1468,31 @@ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "
1456
1468
  return { content: [{ type: "text", text: `Nested run ${run.id} is not a live async Pi child session with a steering inbox. action='steer' cannot target foreground nested runs.` }], isError: true, details: { mode: "management", results: [] } };
1457
1469
  }
1458
1470
 
1471
+ function externalJobOptionsEqual(left: Record<string, unknown>, right: Record<string, unknown>): boolean {
1472
+ return externalJobStableJson(left) === externalJobStableJson(right);
1473
+ }
1474
+
1475
+ function providerFollowUpSupport(providerName: string): { ok: true } | { ok: false; message: string } {
1476
+ try {
1477
+ const provider = getExternalJobProvider(providerName);
1478
+ if (!provider) return { ok: false, message: `External-job provider '${providerName}' is not registered. Load the provider package, then retry action='resume'.` };
1479
+ if (typeof provider.followUp !== "function") return { ok: false, message: `External-job provider '${providerName}' does not support follow-up. Update or reload the provider package, then retry action='resume'.` };
1480
+ return { ok: true };
1481
+ } catch (error) {
1482
+ return { ok: false, message: `External-job provider registry unavailable: ${error instanceof Error ? error.message : String(error)}` };
1483
+ }
1484
+ }
1485
+
1486
+ function externalJobFollowUpStarted(input: { sourceRunId: string; runId: string; asyncDir: string; duplicate?: boolean; interactive: boolean }): AgentToolResult<Details> {
1487
+ const lines = [
1488
+ input.duplicate ? `External-job follow-up already exists for ${input.sourceRunId}.` : `Started external-job follow-up for ${input.sourceRunId}.`,
1489
+ `Follow-up run: ${input.runId}`,
1490
+ `Async dir: ${input.asyncDir}`,
1491
+ `Status if needed: subagent({ action: "status", id: "${input.runId}" })`,
1492
+ ];
1493
+ return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.interactive) }], details: { mode: "single", results: [], asyncId: input.runId, asyncDir: input.asyncDir } };
1494
+ }
1495
+
1459
1496
  function externalRunnerControlError(asyncDir: string, action: "steer" | "resume"): AgentToolResult<Details> | undefined {
1460
1497
  const status = readStatus(asyncDir);
1461
1498
  if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job")) return undefined;
@@ -1465,6 +1502,116 @@ function externalRunnerControlError(asyncDir: string, action: "steer" | "resume"
1465
1502
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
1466
1503
  }
1467
1504
 
1505
+ async function resumeExternalJobFollowUp(input: {
1506
+ target: AsyncResumeSourceTarget;
1507
+ followUp: string;
1508
+ baseAgentConfig: AgentConfig;
1509
+ effectiveCwd: string;
1510
+ requestCwd: string;
1511
+ ctx: ExtensionContext;
1512
+ deps: ExecutorDeps;
1513
+ parentModel?: ParentModel;
1514
+ modelScope?: ModelScopeConfig;
1515
+ intercomBridge: IntercomBridgeState;
1516
+ parentSessionFile: string | null;
1517
+ absoluteDeadlineAt?: number;
1518
+ }): Promise<AgentToolResult<Details>> {
1519
+ if (input.target.kind === "live" || input.target.state === "running" || input.target.state === "queued") {
1520
+ return { content: [{ type: "text", text: `External-job run '${input.target.runId}' is still running. Wait for completion, then use subagent({ action: "resume", id: "${input.target.runId}", message: "..." }).` }], isError: true, details: { mode: "management", results: [] } };
1521
+ }
1522
+ const runner = input.target.runner;
1523
+ const externalJob = input.target.externalJob;
1524
+ if (runner?.type !== "external-job") return { content: [{ type: "text", text: "Internal error: external-job follow-up was requested for a non-external-job runner." }], isError: true, details: { mode: "management", results: [] } };
1525
+ if (!externalJob) return { content: [{ type: "text", text: `External-job run '${input.target.runId}' has no persisted provider metadata. Cannot follow up without the parent provider job id.` }], isError: true, details: { mode: "management", results: [] } };
1526
+ if (externalJob.provider !== runner.provider) return { content: [{ type: "text", text: `External-job run '${input.target.runId}' has mismatched provider metadata. Refusing to follow up.` }], isError: true, details: { mode: "management", results: [] } };
1527
+ if (!externalJobOptionsEqual(externalJob.options, runner.options)) return { content: [{ type: "text", text: `External-job run '${input.target.runId}' has mismatched provider options. Refusing to follow up.` }], isError: true, details: { mode: "management", results: [] } };
1528
+ if (!externalJob.providerJobId) return { content: [{ type: "text", text: `External-job run '${input.target.runId}' has no parent provider job id. Cannot follow up without reopening or redispatching, so this fails closed.` }], isError: true, details: { mode: "management", results: [] } };
1529
+ if (externalJob.state !== "completed") return { content: [{ type: "text", text: `External-job run '${input.target.runId}' provider state is ${externalJob.state}. Wait for completion, then use action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
1530
+ const support = providerFollowUpSupport(runner.provider);
1531
+ if (!support.ok) return { content: [{ type: "text", text: support.message }], isError: true, details: { mode: "management", results: [] } };
1532
+
1533
+ const promptDigest = externalJobPromptDigest(input.followUp);
1534
+ const requestDigest = externalJobFollowUpRequestDigest({ provider: runner.provider, parentProviderJobId: externalJob.providerJobId, promptDigest, options: runner.options });
1535
+ const requestId = externalJobFollowUpRequestId(requestDigest);
1536
+ const runId = externalJobFollowUpRunId(requestDigest);
1537
+ const asyncDir = path.join(DIRS.async, runId);
1538
+ const currentSessionId = input.deps.state.currentSessionId;
1539
+ if (!currentSessionId) return { content: [{ type: "text", text: "External-job follow-up requires an active parent session." }], isError: true, details: { mode: "management", results: [] } };
1540
+ if (fs.existsSync(asyncDir) || fs.existsSync(resultFilePath(DIRS.results, runId))) {
1541
+ return externalJobFollowUpStarted({ sourceRunId: input.target.runId, runId, asyncDir, duplicate: true, interactive: input.ctx.hasUI });
1542
+ }
1543
+
1544
+ const depthState = checkSubagentDepth(input.deps.config.maxSubagentDepth);
1545
+ if (depthState.blocked) {
1546
+ return { content: [{ type: "text", text: `Nested subagent resume blocked (depth=${depthState.depth}, max=${depthState.maxDepth}). Complete the follow-up directly instead.` }], isError: true, details: { mode: "management", results: [] } };
1547
+ }
1548
+ const topLevelResume = depthState.depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.deps.state.workflowControllers?.has(input.target.runId);
1549
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1550
+ try {
1551
+ activeAsyncCapacity = topLevelResume ? acquireActiveAsyncCapacity({
1552
+ sessionId: currentSessionId,
1553
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1554
+ runId,
1555
+ kind: "runner",
1556
+ asyncDir,
1557
+ }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []) }) : undefined;
1558
+ } catch (error) {
1559
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1560
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1561
+ }
1562
+
1563
+ const artifactConfig: ArtifactConfig = omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: true, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1564
+ const artifactsDir = getArtifactsDir(input.parentSessionFile, input.effectiveCwd, artifactConfig.dir);
1565
+ const parentModel = input.parentModel;
1566
+ const agentConfig: AgentConfig = {
1567
+ ...input.baseAgentConfig,
1568
+ runner: { type: "external-job", provider: runner.provider, options: runner.options },
1569
+ };
1570
+ const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1571
+ agent: input.target.agent,
1572
+ task: input.followUp,
1573
+ goal: input.followUp,
1574
+ agentConfig,
1575
+ recoveryAgentConfig: agentConfig,
1576
+ ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1577
+ pi: input.deps.pi,
1578
+ cwd: input.requestCwd,
1579
+ currentSessionId,
1580
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1581
+ currentModelProvider: parentModel?.provider,
1582
+ currentModel: parentModel,
1583
+ modelScope: input.modelScope,
1584
+ interactive: input.ctx.hasUI,
1585
+ permissions: input.deps.config.permissions,
1586
+ }),
1587
+ cwd: input.effectiveCwd,
1588
+ artifactsDir,
1589
+ artifactConfig,
1590
+ shareEnabled: false,
1591
+ ...(input.parentSessionFile ? { sessionRoot: input.deps.getSubagentSessionRoot(input.parentSessionFile) } : {}),
1592
+ maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1593
+ waitToolEnabled: input.deps.waitToolEnabled,
1594
+ worktreeSetupHook: input.deps.config.worktreeSetupHook,
1595
+ worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1596
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
1597
+ controlConfig: resolveControlConfig(input.deps.config.control, undefined),
1598
+ controlIntercomTarget: input.intercomBridge.active ? input.intercomBridge.orchestratorTarget : undefined,
1599
+ childIntercomTarget: input.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1600
+ availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo),
1601
+ outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1602
+ ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1603
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(currentSessionId),
1604
+ runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1605
+ activeAsyncCapacity,
1606
+ externalJobFollowUp: { sourceRunId: input.target.runId, sourceStepIndex: input.target.index, parentProviderJobId: externalJob.providerJobId, requestId, requestDigest },
1607
+ }));
1608
+ if (result.isError) {
1609
+ activeAsyncCapacity?.rollback();
1610
+ return result;
1611
+ }
1612
+ return externalJobFollowUpStarted({ sourceRunId: input.target.runId, runId: result.details.asyncId ?? runId, asyncDir: result.details.asyncDir ?? asyncDir, interactive: input.ctx.hasUI });
1613
+ }
1614
+
1468
1615
  async function resumeAsyncRun(input: {
1469
1616
  params: SubagentParamsLike;
1470
1617
  requestCwd: string;
@@ -1529,11 +1676,7 @@ async function resumeAsyncRun(input: {
1529
1676
  ];
1530
1677
  target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
1531
1678
  } else {
1532
- if (resolved?.kind === "async" && resolved.location.asyncDir) {
1533
- const unsupported = externalRunnerControlError(resolved.location.asyncDir, "resume");
1534
- if (unsupported) return unsupported;
1535
- }
1536
- target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: !attachChain });
1679
+ target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: false });
1537
1680
  }
1538
1681
  } catch (error) {
1539
1682
  const message = error instanceof Error ? error.message : String(error);
@@ -1591,6 +1734,7 @@ async function resumeAsyncRun(input: {
1591
1734
  inheritSkills: recoveryDescriptor.inheritSkills,
1592
1735
  source: "project",
1593
1736
  filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
1737
+ ...(discovered.maxThinking ? { maxThinking: discovered.maxThinking } : {}),
1594
1738
  } : undefined);
1595
1739
  if (!baseAgentConfig) {
1596
1740
  return {
@@ -1599,6 +1743,26 @@ async function resumeAsyncRun(input: {
1599
1743
  details: { mode: "management", results: [] },
1600
1744
  };
1601
1745
  }
1746
+ if (target.source === "async" && target.runner?.type === "external-cli") {
1747
+ return { content: [{ type: "text", text: "External runners do not persist Pi sessions and cannot be resumed." }], isError: true, details: { mode: "management", results: [] } };
1748
+ }
1749
+ if (target.source === "async" && target.runner?.type === "external-job") {
1750
+ if (attachChain) return { content: [{ type: "text", text: "External-job follow-up does not support chain attachment. Use action='resume' with message instead." }], isError: true, details: { mode: "management", results: [] } };
1751
+ return resumeExternalJobFollowUp({
1752
+ target,
1753
+ followUp,
1754
+ baseAgentConfig,
1755
+ effectiveCwd,
1756
+ requestCwd: input.requestCwd,
1757
+ ctx: input.ctx,
1758
+ deps: input.deps,
1759
+ parentModel: input.parentModel,
1760
+ modelScope,
1761
+ intercomBridge,
1762
+ parentSessionFile,
1763
+ absoluteDeadlineAt: input.absoluteDeadlineAt,
1764
+ });
1765
+ }
1602
1766
 
1603
1767
  if (attachChain) {
1604
1768
  if (target.source !== "async") {
@@ -1683,6 +1847,7 @@ async function resumeAsyncRun(input: {
1683
1847
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1684
1848
  runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1685
1849
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1850
+ thinkingCeiling: target.thinkingCeiling,
1686
1851
  activeAsyncCapacity,
1687
1852
  }));
1688
1853
  if (result.isError) {
@@ -1705,7 +1870,7 @@ async function resumeAsyncRun(input: {
1705
1870
  const effectiveFollowUp = [...queuedBriefs.map(({ request }) => request.message), followUp].filter(Boolean).join("\n\n");
1706
1871
  const revivalSessionFile = target.sessionFile;
1707
1872
  if (!revivalSessionFile) {
1708
- return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1873
+ return { content: [{ type: "text", text: `Async run '${target.runId}' child ${target.index} does not have a persisted session file to resume from.` }], isError: true, details: { mode: "management", results: [] } };
1709
1874
  }
1710
1875
  if (target.source === "async" && asyncReviveRequiresRecoveryDescriptor(target)) {
1711
1876
  return { content: [{ type: "text", text: `Async child '${target.runId}' is missing its required run fan-out recovery identity. Start a new run instead.` }], isError: true, details: { mode: "management", results: [] } };
@@ -1775,6 +1940,7 @@ async function resumeAsyncRun(input: {
1775
1940
  modelOverride: recoveryDescriptor?.model ?? target.model,
1776
1941
  modelOverrideFromParent: recoveryDescriptor?.modelOverrideFromParent,
1777
1942
  thinkingOverride: recoveryDescriptor?.thinking ?? target.thinking,
1943
+ thinkingCeiling: recoveryDescriptor?.thinkingCeiling ?? ("thinkingCeiling" in target ? target.thinkingCeiling : undefined),
1778
1944
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1779
1945
  maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1780
1946
  waitToolEnabled: input.deps.waitToolEnabled,
@@ -1866,6 +2032,7 @@ async function resumeAsyncRun(input: {
1866
2032
  ...(completed.model ? { model: completed.model } : {}),
1867
2033
  ...(completed.attemptedModels ? { attemptedModels: completed.attemptedModels } : {}),
1868
2034
  ...(completed.modelAttempts ? { modelAttempts: completed.modelAttempts } : {}),
2035
+ ...(completed.contextOverflow ? { contextOverflow: true } : {}),
1869
2036
  ...(completed.structuredOutput !== undefined ? { structuredOutput: completed.structuredOutput } : {}),
1870
2037
  ...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
1871
2038
  ...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
@@ -2652,6 +2819,7 @@ function resolveStaticLaunchSummary(input: {
2652
2819
  }): StaticLaunchSummary {
2653
2820
  const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2654
2821
  const externalRunner = agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job";
2822
+ const modelScopes = resolveModelScopesForAgent(input.modelScope, input.agent, input.parentModel);
2655
2823
  const model = externalRunner
2656
2824
  ? undefined
2657
2825
  : resolveEffectiveSubagentModel(
@@ -2659,8 +2827,8 @@ function resolveStaticLaunchSummary(input: {
2659
2827
  agentConfig?.model,
2660
2828
  input.parentModel,
2661
2829
  input.availableModels,
2662
- input.currentProvider,
2663
- input.modelScope === undefined ? {} : { scope: input.modelScope },
2830
+ agentConfig?.modelProvider ?? input.currentProvider,
2831
+ modelScopes.length === 0 ? {} : { scope: modelScopes },
2664
2832
  );
2665
2833
  const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
2666
2834
  const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
@@ -2820,7 +2988,68 @@ function preflightForkSessionsForStaticTasks(
2820
2988
  }
2821
2989
  }
2822
2990
 
2823
- function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
2991
+ async function waitForWorkflowAsyncSingleResult(
2992
+ params: SubagentParamsLike,
2993
+ launchResult: AgentToolResult<Details>,
2994
+ options: { runId: string; task: string; signal?: AbortSignal; state: SubagentState; kill?: ExecutorDeps["kill"] },
2995
+ ): Promise<AgentToolResult<Details>> {
2996
+ if (params.workflowAwaitAsync !== true || !launchResult.details.asyncDir) return launchResult;
2997
+ const asyncDir = launchResult.details.asyncDir;
2998
+ const resultPath = workflowAwaitedAsyncResultPath(asyncDir);
2999
+ const stopOnAbort = () => { stopAsyncRun(options.state, options.runId, options.kill, { asyncDir, resolvedId: options.runId }); };
3000
+ if (options.signal?.aborted) stopOnAbort();
3001
+ else options.signal?.addEventListener("abort", stopOnAbort, { once: true });
3002
+ let completed: Awaited<ReturnType<typeof waitForImportedAsyncRoot>>;
3003
+ try {
3004
+ completed = await waitForImportedAsyncRoot({ runId: options.runId, asyncDir, resultPath, index: 0 }, omitUndefinedProperties({
3005
+ shouldAbort: () => options.signal?.aborted === true,
3006
+ timeoutMessage: "Workflow stopped before async child completed.",
3007
+ }));
3008
+ } finally {
3009
+ options.signal?.removeEventListener("abort", stopOnAbort);
3010
+ }
3011
+ fs.rmSync(resultPath, { force: true });
3012
+ const totalCost = completed.totalCost;
3013
+ const childResult: SingleResult = omitUndefinedProperties({
3014
+ index: 0,
3015
+ agent: completed.agent,
3016
+ task: options.task,
3017
+ exitCode: completed.exitCode,
3018
+ usage: {
3019
+ input: totalCost?.inputTokens ?? 0,
3020
+ output: totalCost?.outputTokens ?? 0,
3021
+ cacheRead: 0,
3022
+ cacheWrite: 0,
3023
+ cost: totalCost?.costUsd ?? 0,
3024
+ turns: 0,
3025
+ },
3026
+ finalOutput: completed.output,
3027
+ outputState: completed.output.trim() ? "present" as const : "absent" as const,
3028
+ ...(completed.error ? { error: completed.error } : {}),
3029
+ ...(completed.timedOut ? { timedOut: true } : {}),
3030
+ ...(completed.stopped ? { stopped: true } : {}),
3031
+ ...(completed.sessionFile ? { sessionFile: completed.sessionFile } : {}),
3032
+ ...(completed.model ? { model: completed.model } : {}),
3033
+ ...(completed.attemptedModels ? { attemptedModels: completed.attemptedModels } : {}),
3034
+ ...(completed.modelAttempts ? { modelAttempts: completed.modelAttempts } : {}),
3035
+ ...(completed.contextOverflow ? { contextOverflow: true } : {}),
3036
+ ...(completed.structuredOutput !== undefined ? { structuredOutput: completed.structuredOutput } : {}),
3037
+ ...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
3038
+ ...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
3039
+ ...(completed.acceptance ? { acceptance: completed.acceptance } : {}),
3040
+ });
3041
+ return {
3042
+ content: [{ type: "text", text: completed.output || completed.error || `Async workflow child ${options.runId} completed without output.` }],
3043
+ ...(completed.success ? {} : { isError: true }),
3044
+ details: {
3045
+ ...launchResult.details,
3046
+ runId: options.runId,
3047
+ results: [childResult],
3048
+ },
3049
+ };
3050
+ }
3051
+
3052
+ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details> | null> {
2824
3053
  const {
2825
3054
  params,
2826
3055
  effectiveCwd,
@@ -2890,11 +3119,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2890
3119
  const externalRunnerWithoutExplicitModel = (a.runner?.type === "external-cli" || a.runner?.type === "external-job")
2891
3120
  && params.model === undefined
2892
3121
  && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
3122
+ const modelScopes = resolveModelScopesForAgent(data.modelScope, a.name, parentModel);
2893
3123
  const modelOverride = a.runner?.type === "external-cli" || a.runner?.type === "external-job"
2894
3124
  ? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
2895
- : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3125
+ : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, a.modelProvider ?? currentProvider, modelScopes.length === 0 ? {} : { scope: modelScopes });
2896
3126
  const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, a.model, parentModel);
2897
- return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3127
+ const asyncResult = executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2898
3128
  agent: params.agent!,
2899
3129
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2900
3130
  goal: params.task ?? "",
@@ -2919,6 +3149,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2919
3149
  modelOverride,
2920
3150
  modelOverrideFromParent,
2921
3151
  thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
3152
+ thinkingCeiling: a.maxThinking,
2922
3153
  maxSubagentDepth,
2923
3154
  waitToolEnabled: deps.waitToolEnabled,
2924
3155
  ...(params.worktree === true ? { worktree: true } : {}),
@@ -2944,7 +3175,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2944
3175
  runFanoutBudget: data.runFanoutBudget,
2945
3176
  parentWorkflowRunId: params.workflowParentRunId,
2946
3177
  workflowKey: params.workflowKey,
3178
+ workflowAwaitAsync: params.workflowAwaitAsync,
2947
3179
  }));
3180
+ return waitForWorkflowAsyncSingleResult(params, asyncResult, { runId: id, task: params.task ?? "", signal: data.signal, state: deps.state, kill: deps.kill });
2948
3181
  }
2949
3182
 
2950
3183
  return null;
@@ -2998,9 +3231,7 @@ function resolveWorkflowAggregateOutputPath(
2998
3231
  ctxCwd: string,
2999
3232
  workflowCwd: string,
3000
3233
  outputBaseDir: string,
3001
- configuredOutputBaseDir: string | undefined,
3002
3234
  ): string | undefined {
3003
- if (typeof output === "string" && output && !path.isAbsolute(output) && !configuredOutputBaseDir) return path.resolve(workflowCwd, output);
3004
3235
  return resolveSingleOutputPath(output, ctxCwd, workflowCwd, outputBaseDir);
3005
3236
  }
3006
3237
 
@@ -3036,6 +3267,7 @@ function resolveWorkflowChildOutputPath(input: {
3036
3267
  aggregateOutputPath?: string;
3037
3268
  configuredOutputBaseDir?: string;
3038
3269
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3270
+ agents: AgentConfig[];
3039
3271
  workflowAgentScope?: unknown;
3040
3272
  state?: SubagentState;
3041
3273
  key: string;
@@ -3054,8 +3286,10 @@ function resolveWorkflowChildOutputPath(input: {
3054
3286
  const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
3055
3287
  const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
3056
3288
  const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
3057
- const agents = input.discoverAgents(childCwd, agentScope).agents;
3058
- const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3289
+ const discoveredAgents = input.discoverAgents(childCwd, agentScope).agents;
3290
+ const agent = typeof input.params.agent === "string"
3291
+ ? resolveAgentName(input.params.agent, discoveredAgents).agent ?? resolveAgentName(input.params.agent, input.agents).agent
3292
+ : undefined;
3059
3293
  const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3060
3294
  const output = rawOutput === true || rawOutput === "true"
3061
3295
  ? agentOutput
@@ -3065,7 +3299,7 @@ function resolveWorkflowChildOutputPath(input: {
3065
3299
  ? workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key)
3066
3300
  : agentOutput;
3067
3301
  return {
3068
- path: resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir),
3302
+ path: resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir ?? path.join(input.artifactsDir, "outputs", sanitizeRunPathSegment(input.workflowRunId))),
3069
3303
  inherited: !hasExplicitOutput && !input.aggregateOutputPath && agentOutput !== undefined,
3070
3304
  };
3071
3305
  }
@@ -3078,6 +3312,7 @@ function workflowChildOutputClaims(input: {
3078
3312
  aggregateOutputPath?: string;
3079
3313
  configuredOutputBaseDir?: string;
3080
3314
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3315
+ agents: AgentConfig[];
3081
3316
  workflowAgentScope?: unknown;
3082
3317
  state: SubagentState;
3083
3318
  claimedOutputPaths: Map<string, string>;
@@ -3126,9 +3361,10 @@ function prepareWorkflowChildLaunchParams(input: {
3126
3361
  aggregateOutputPath?: string;
3127
3362
  configuredOutputBaseDir?: string;
3128
3363
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3364
+ agents: AgentConfig[];
3129
3365
  workflowAgentScope?: unknown;
3130
3366
  outputOverride?: string;
3131
- options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number };
3367
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number; capabilityCeiling?: ResolvedSubagentCapabilityCeiling };
3132
3368
  }): SubagentParamsLike {
3133
3369
  let childParams = input.childParams;
3134
3370
  if (input.childParams.output === undefined && input.childParams.resume === undefined && input.outputOverride !== undefined) {
@@ -3136,10 +3372,17 @@ function prepareWorkflowChildLaunchParams(input: {
3136
3372
  } else if (input.childParams.output === undefined && input.childParams.resume === undefined && input.aggregateOutputPath !== undefined) {
3137
3373
  childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3138
3374
  } else if (input.childParams.resume === undefined) {
3139
- const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
3375
+ const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, agents: input.agents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
3140
3376
  if (resolvedOutput.path) childParams = { ...input.childParams, output: resolvedOutput.path };
3141
3377
  }
3142
- return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3378
+ const childCwd = typeof childParams.cwd === "string" ? resolveChildCwd(input.workflowCwd, childParams.cwd) : input.workflowCwd;
3379
+ const agentScope = resolveExecutionAgentScope(childParams.agentScope ?? input.workflowAgentScope);
3380
+ const discoveredAgents = input.discoverAgents(childCwd, agentScope).agents;
3381
+ const agent = typeof childParams.agent === "string"
3382
+ ? resolveAgentName(childParams.agent, discoveredAgents).agent ?? resolveAgentName(childParams.agent, input.agents).agent
3383
+ : undefined;
3384
+ const externalAsyncRequired = agent?.runner?.type === "external-cli" || agent?.runner?.type === "external-job";
3385
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, { ...input.options, externalAsyncRequired });
3143
3386
  }
3144
3387
 
3145
3388
  function finalizeSingleWorktreeHandoff(input: {
@@ -3234,14 +3477,15 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3234
3477
  const parentModel = data.parentModel;
3235
3478
  const currentProvider = parentModel?.provider;
3236
3479
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
3480
+ const modelScopes = resolveModelScopesForAgent(data.modelScope, agentConfig.name, parentModel);
3237
3481
  let task = params.task ?? "";
3238
3482
  let modelOverride: string | undefined = resolveEffectiveSubagentModel(
3239
3483
  params.model as string | undefined,
3240
3484
  agentConfig.model,
3241
3485
  parentModel,
3242
3486
  availableModels,
3243
- currentProvider,
3244
- data.modelScope === undefined ? {} : { scope: data.modelScope },
3487
+ agentConfig.modelProvider ?? currentProvider,
3488
+ modelScopes.length === 0 ? {} : { scope: modelScopes },
3245
3489
  );
3246
3490
  const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, agentConfig.model, parentModel);
3247
3491
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
@@ -3382,9 +3626,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3382
3626
  modelOverride,
3383
3627
  modelOverrideFromParent,
3384
3628
  thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
3629
+ thinkingCeiling: agentConfig.maxThinking,
3385
3630
  availableModels,
3386
3631
  preferredModelProvider: currentProvider,
3387
- modelScope: data.modelScope,
3632
+ modelScope: modelScopes,
3388
3633
  skills: effectiveSkills,
3389
3634
  structuredOutput: structuredRuntime,
3390
3635
  agentContract: params.agentContract,
@@ -3649,7 +3894,8 @@ function workflowChildResult(
3649
3894
  ? result.details.results[0].finalOutput
3650
3895
  : receiptOutput;
3651
3896
  const detached = result.details.results.some((child) => child.detached);
3652
- const ok = result.isError !== true && !detached;
3897
+ const stopped = result.details.results.some((child) => child.stopped);
3898
+ const ok = result.isError !== true && !detached && !stopped;
3653
3899
  const artifactPaths = new Set<string>();
3654
3900
  if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
3655
3901
  if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
@@ -3689,6 +3935,7 @@ function workflowChildResult(
3689
3935
  output,
3690
3936
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
3691
3937
  ...(detached ? { detached: true } : {}),
3938
+ ...(stopped ? { stopped: true } : {}),
3692
3939
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
3693
3940
  ...(requestedContext ? { requestedContext } : {}),
3694
3941
  ...(resolvedContext ? { resolvedContext } : {}),
@@ -3813,8 +4060,9 @@ export function prepareWorkflowLaunchParams(
3813
4060
  childParams: Record<string, unknown>,
3814
4061
  parentWorkflowRunId: string,
3815
4062
  workflowKey: string,
3816
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number } = {},
4063
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number; externalAsyncRequired?: boolean; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } = {},
3817
4064
  ): SubagentParamsLike {
4065
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(workflowDefaults.capabilityCeiling, options.capabilityCeiling);
3818
4066
  const parentTimeoutMs = options.parentDeadlineAt === undefined
3819
4067
  || childParams.timeoutMs !== undefined
3820
4068
  || childParams.maxRuntimeMs !== undefined
@@ -3845,18 +4093,21 @@ export function prepareWorkflowLaunchParams(
3845
4093
  ...(turnBudget !== undefined ? { turnBudget: turnBudget as TurnBudgetConfig } : {}),
3846
4094
  ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
3847
4095
  ...(intercomBridge !== undefined ? { intercomBridge: intercomBridge as IntercomBridgeConfig } : {}),
4096
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
3848
4097
  };
3849
4098
  }
3850
4099
  const launchParams = {
3851
4100
  ...workflowDefaults,
3852
- async: false,
4101
+ async: options.externalAsyncRequired === true && childParams.async === undefined && workflowDefaults.async === undefined ? true : false,
3853
4102
  ...childParams,
4103
+ ...(options.externalAsyncRequired === true && childParams.async === undefined && workflowDefaults.async === undefined ? { workflowAwaitAsync: true } : {}),
3854
4104
  ...(options.missionDetached ? { mission: false } : {}),
3855
4105
  workflowParentRunId: parentWorkflowRunId,
3856
4106
  workflowKey,
3857
4107
  ...(parentTimeoutMs !== undefined ? { workflowParentDeadlineAt: options.parentDeadlineAt } : {}),
3858
4108
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
3859
4109
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4110
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
3860
4111
  } as SubagentParamsLike;
3861
4112
  const normalizedGate = normalizeGateParams(launchParams);
3862
4113
  if (!normalizedGate.ok) throw new Error(normalizedGate.error);
@@ -3914,6 +4165,7 @@ function createScheduledOwnerState(source: SubagentState, ownerSessionId: string
3914
4165
  parentSessionFile: ctx.sessionManager.getSessionFile() ?? null,
3915
4166
  subagentInProgress: false,
3916
4167
  ...(ownerSpawns ? { subagentSpawns: ownerSpawns } : { subagentSpawns: undefined }),
4168
+ herdrProjectPanes: new Map(),
3917
4169
  asyncJobs: new Map(),
3918
4170
  fleetJobs: new Map(),
3919
4171
  foregroundRuns: new Map(),
@@ -3927,6 +4179,7 @@ function createScheduledOwnerState(source: SubagentState, ownerSessionId: string
3927
4179
  watcherRestartTimer: null,
3928
4180
  waitSubscriptions: new Map(),
3929
4181
  workflowControllers: new Map(),
4182
+ workflowChildStops: new Map(),
3930
4183
  };
3931
4184
  }
3932
4185
 
@@ -3997,6 +4250,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3997
4250
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
3998
4251
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
3999
4252
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4253
+ const workflowAgents = deps.discoverAgents(workflowCwd, resolveExecutionAgentScope(requestParams.agentScope)).agents;
4000
4254
  const workflowArtifactConfig: ArtifactConfig = omitUndefinedProperties({
4001
4255
  ...DEFAULT_ARTIFACT_CONFIG,
4002
4256
  enabled: requestParams.artifacts !== false,
@@ -4093,6 +4347,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4093
4347
  const eventsPath = path.join(asyncDir, "events.jsonl");
4094
4348
  const startedAt = Date.now();
4095
4349
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4350
+ const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(currentSessionId));
4096
4351
  const completionOwnerId = deps.state.completionOwnerId ?? currentCompletionOwnerId();
4097
4352
  deps.state.completionOwnerId = completionOwnerId;
4098
4353
  try {
@@ -4105,6 +4360,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4105
4360
  }
4106
4361
  const controller = new AbortController();
4107
4362
  deps.state.workflowControllers ??= new Map();
4363
+ deps.state.workflowChildStops ??= new Map();
4108
4364
  deps.state.workflowControllers.set(workflowRunId, controller);
4109
4365
  workflowCapacity?.markWorkflowStarted();
4110
4366
  if (workflowCapacity) deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers.keys()) });
@@ -4272,7 +4528,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4272
4528
  const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
4273
4529
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
4274
4530
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4275
- const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
4531
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId));
4276
4532
  const claimedOutputPaths = new Map<string, string>();
4277
4533
  const childOutputOverrides = new Map<string, string>();
4278
4534
  const producedChildOutputPaths = new Set<string>();
@@ -4333,6 +4589,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4333
4589
  status.steps?.push(step);
4334
4590
  workflowSteps.set(entry.key, step);
4335
4591
  }
4592
+ const projectedStep = workflowSteps.get(entry.key);
4593
+ if (entry.state === "stopped" && projectedStep) {
4594
+ appendWorkflowEvent({
4595
+ type: "subagent.child-status",
4596
+ version: 1,
4597
+ childId: entry.key,
4598
+ status: "stopped",
4599
+ reason: "user",
4600
+ source: "async",
4601
+ stepIndex: status.steps?.indexOf(projectedStep),
4602
+ agent: projectedStep.agent,
4603
+ ...(projectedStep.runId ? { childRunId: projectedStep.runId } : {}),
4604
+ workflowKey: entry.key,
4605
+ ...(projectedStep.phase ? { phase: projectedStep.phase } : {}),
4606
+ ...(projectedStep.label ? { label: projectedStep.label } : {}),
4607
+ });
4608
+ }
4336
4609
  }
4337
4610
  projectedTraceLength = trace.length;
4338
4611
  projectedTraceTail = trace.at(-1);
@@ -4345,10 +4618,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4345
4618
  script: workflowScript,
4346
4619
  timeoutMs: timeout,
4347
4620
  signal: controller.signal,
4621
+ registerStopChild: (stop) => {
4622
+ if (stop) deps.state.workflowChildStops?.set(workflowRunId, stop);
4623
+ else deps.state.workflowChildStops?.delete(workflowRunId);
4624
+ },
4348
4625
  ...(workflowState ? { state: workflowState } : {}),
4349
4626
  onTrace: updateTrace,
4350
4627
  admit: (calls) => {
4351
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4628
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4352
4629
  if (outputClaims.error) throw new Error(outputClaims.error);
4353
4630
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4354
4631
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
@@ -4376,7 +4653,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4376
4653
  });
4377
4654
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4378
4655
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4379
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
4656
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
4380
4657
  missionBinding,
4381
4658
  deps.asyncByDefault,
4382
4659
  );
@@ -4463,7 +4740,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4463
4740
  } catch (receiptError) {
4464
4741
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4465
4742
  }
4466
- 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, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4743
+ 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.stopped ? { stopped: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4467
4744
  persist();
4468
4745
  persistClosed = true;
4469
4746
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
@@ -4503,13 +4780,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4503
4780
  } catch (receiptError) {
4504
4781
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4505
4782
  }
4506
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : 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 && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4783
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : 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.stopped ? { stopped: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4507
4784
  persist();
4508
4785
  persistClosed = true;
4509
4786
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
4510
4787
  } finally {
4511
4788
  persistClosed = true;
4512
4789
  deps.state.workflowControllers?.delete(workflowRunId);
4790
+ deps.state.workflowChildStops?.delete(workflowRunId);
4513
4791
  deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
4514
4792
  ?? deps.state.activeAsyncCapacity;
4515
4793
  }
@@ -4522,7 +4800,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4522
4800
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
4523
4801
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
4524
4802
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4525
- const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
4803
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id));
4526
4804
  const claimedOutputPaths = new Map<string, string>();
4527
4805
  const childOutputOverrides = new Map<string, string>();
4528
4806
  const producedChildOutputPaths = new Set<string>();
@@ -4530,6 +4808,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4530
4808
  const workflowChildRunIds = new Map<string, string>();
4531
4809
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4532
4810
  const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
4811
+ const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager)));
4533
4812
  const sendWorkflowProgress = () => {
4534
4813
  const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow);
4535
4814
  if (update) onUpdate?.(update);
@@ -4545,7 +4824,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4545
4824
  sendWorkflowProgress();
4546
4825
  },
4547
4826
  admit: (calls) => {
4548
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4827
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4549
4828
  if (outputClaims.error) throw new Error(outputClaims.error);
4550
4829
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4551
4830
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
@@ -4570,7 +4849,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4570
4849
  });
4571
4850
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4572
4851
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4573
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
4852
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
4574
4853
  missionBinding,
4575
4854
  deps.asyncByDefault,
4576
4855
  );
@@ -4693,7 +4972,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4693
4972
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4694
4973
  return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
4695
4974
  }
4696
- return handleHerdrProjectPaneAction(action as (typeof HERDR_PROJECT_PANE_ACTIONS)[number], paramsWithResolvedCwd, { cwd: requestCwd, signal });
4975
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4976
+ return handleHerdrProjectPaneAction(action as (typeof HERDR_PROJECT_PANE_ACTIONS)[number], paramsWithResolvedCwd, { cwd: requestCwd, state: deps.state, signal });
4697
4977
  }
4698
4978
  if ((HERDR_INSPECTOR_ACTIONS as readonly string[]).includes(action)) {
4699
4979
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
@@ -5104,6 +5384,40 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5104
5384
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5105
5385
  const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
5106
5386
  if (workflowController) {
5387
+ if (paramsWithResolvedCwd.childId !== undefined) {
5388
+ const workflowRunId = targetRunId!;
5389
+ const asyncJob = deps.state.asyncJobs.get(workflowRunId);
5390
+ if (!asyncJob?.asyncDir) return { content: [{ type: "text", text: `Status file not found for async workflow '${workflowRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
5391
+ const status = readStatus(asyncJob.asyncDir);
5392
+ if (!status) return { content: [{ type: "text", text: `Status file not found for async workflow '${workflowRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
5393
+ const resolution = resolveAsyncStatusChild(status, paramsWithResolvedCwd.childId);
5394
+ if (!resolution.ok) return { content: [{ type: "text", text: resolution.message }], isError: true, details: { mode: "management", results: [] } };
5395
+ if (!isStoppableAsyncStatusStep(resolution.child.step)) return { content: [{ type: "text", text: `Child '${paramsWithResolvedCwd.childId}' in async run '${targetRunId}' is ${resolution.child.step.status}; stop only supports pending or running children.` }], isError: true, details: { mode: "management", results: [] } };
5396
+ const stopChild = deps.state.workflowChildStops?.get(targetRunId!);
5397
+ if (!stopChild) return { content: [{ type: "text", text: `Workflow ${targetRunId} child stop is unavailable in this extension runtime.` }], isError: true, details: { mode: "management", results: [] } };
5398
+ if (!stopChild(resolution.child.id)) return { content: [{ type: "text", text: `Child '${paramsWithResolvedCwd.childId}' in workflow ${workflowRunId} is not available to stop.` }], isError: true, details: { mode: "management", results: [] } };
5399
+ try {
5400
+ fs.appendFileSync(path.join(asyncJob.asyncDir, "events.jsonl"), `${JSON.stringify({
5401
+ type: "subagent.child-status",
5402
+ version: 1,
5403
+ runId: workflowRunId,
5404
+ childId: resolution.child.id,
5405
+ status: "stopping",
5406
+ ts: Date.now(),
5407
+ reason: "user",
5408
+ source: "async",
5409
+ stepIndex: resolution.child.index,
5410
+ agent: resolution.child.step.agent,
5411
+ ...(resolution.child.step.runId ? { childRunId: resolution.child.step.runId } : {}),
5412
+ ...(resolution.child.step.workflowKey ? { workflowKey: resolution.child.step.workflowKey } : {}),
5413
+ ...(resolution.child.step.phase ? { phase: resolution.child.step.phase } : {}),
5414
+ ...(resolution.child.step.label ? { label: resolution.child.step.label } : {}),
5415
+ } satisfies SubagentChildStatusEvent)}\n`, "utf-8");
5416
+ } catch (error) {
5417
+ console.error(`Failed to append child status event for workflow ${workflowRunId}:`, error);
5418
+ }
5419
+ return { content: [{ type: "text", text: `Stop requested for child ${resolution.child.id} in async workflow ${workflowRunId}.` }], details: { mode: "management", results: [] } };
5420
+ }
5107
5421
  workflowController.abort(new Error("Workflow stopped by user."));
5108
5422
  return { content: [{ type: "text", text: `Stop requested for async workflow ${targetRunId}.` }], details: { mode: "management", results: [] } };
5109
5423
  }
@@ -5115,7 +5429,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5115
5429
  if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
5116
5430
  return { content: [{ type: "text", text: `Workflow ${existingStatus.runId} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
5117
5431
  }
5118
- const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
5432
+ const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location, paramsWithResolvedCwd.childId);
5119
5433
  return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
5120
5434
  } catch (error) {
5121
5435
  const text = error instanceof Error ? error.message : String(error);
@@ -5142,6 +5456,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5142
5456
  resolved?.kind === "async" ? resolved.id : targetRunId,
5143
5457
  deps.kill,
5144
5458
  resolved?.kind === "async" ? resolved.location : undefined,
5459
+ paramsWithResolvedCwd.childId,
5145
5460
  );
5146
5461
  if (stopResult) return stopResult;
5147
5462
  return {
@@ -5333,13 +5648,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5333
5648
  agentConfig?.model,
5334
5649
  parentModel,
5335
5650
  forkAvailableModels,
5336
- parentModel?.provider,
5651
+ agentConfig?.modelProvider ?? parentModel?.provider,
5337
5652
  );
5338
5653
  const candidates = buildModelCandidates(
5339
5654
  primaryModel,
5340
5655
  agentConfig?.fallbackModels,
5341
5656
  forkAvailableModels,
5342
- parentModel?.provider,
5657
+ agentConfig?.modelProvider ?? parentModel?.provider,
5343
5658
  { primaryModelFromParent: modelOverrideFromParent ?? inheritsParentModel(modelOverride, agentConfig?.model, parentModel) },
5344
5659
  );
5345
5660
  forkThinkingRequirements.set(
@@ -5356,9 +5671,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5356
5671
  } catch (error) {
5357
5672
  return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5358
5673
  }
5359
- const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
5360
- const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
5361
- const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
5362
5674
  const selectedAgentNames = hasSingle
5363
5675
  ? [effectiveParams.agent!]
5364
5676
  : hasTasks
@@ -5367,6 +5679,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5367
5679
  const externalAgent = selectedAgentNames
5368
5680
  .map((name) => agents.find((agent) => agent.name === name))
5369
5681
  .find((agent) => agent?.runner?.type === "external-cli" || agent?.runner?.type === "external-job");
5682
+ const externalAsyncRequired = Boolean(externalAgent) && effectiveParams.async === undefined && effectiveParams.clarify !== true && effectiveParams.foregroundOnly !== true;
5683
+ const requestedAsync = externalAsyncRequired ? true : effectiveParams.async ?? deps.asyncByDefault;
5684
+ const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
5685
+ const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
5370
5686
  if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
5371
5687
  return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='${externalAgent.runner?.type}', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
5372
5688
  }
@@ -5571,7 +5887,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5571
5887
  parentModel: requestParentModel,
5572
5888
  parentSessionId: requestSessionId,
5573
5889
  parentPiSessionId: requestPiSessionId,
5574
- capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
5890
+ capabilityCeiling: intersectSubagentCapabilityCeilings(effectiveParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(requestSessionId)),
5575
5891
  runFanoutBudget,
5576
5892
  topLevelAsyncCapacityEligible,
5577
5893
  activeAsyncCapacity,
@@ -5755,7 +6071,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5755
6071
  workflowLaunchObserver(launch);
5756
6072
  }
5757
6073
  }
5758
- const asyncResult = runAsyncPath(execData, deps);
6074
+ const asyncResult = await runAsyncPath(execData, deps);
5759
6075
  if (asyncResult) {
5760
6076
  asyncLaunchFailed = asyncResult.isError === true;
5761
6077
  return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));