pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -2,33 +2,34 @@
2
2
  * Async execution logic for subagent tool
3
3
  */
4
4
 
5
- import { spawn } from "node:child_process";
5
+ import { spawn, type ChildProcess } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { createRequire } from "node:module";
10
+ import * as nodeModule from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
13
- import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
14
13
  import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
+ import { childCacheRetentionEnv } from "../../shared/child-cache-retention.ts";
15
+ import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
15
16
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
- import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
- import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
- import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
17
+ import { planChildLaunch, projectChainOutputSchemas, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
18
+ import { formatHerdrMachineRunnerUnsupported, resolveHerdrMachinePlacement } from "../shared/herdr-machine.ts";
19
+ import { applyThinkingSuffix, getHostBuiltinToolNames, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
20
+ import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
21
  import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
20
22
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
21
23
  import type { RunnerStep } from "../shared/parallel-utils.ts";
22
24
  import type { ContextMode } from "../shared/context-mode.ts";
23
- import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
25
+ import { PI_CODING_AGENT_PACKAGE, resolveBunPiExecutable, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
24
26
  import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
25
27
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
26
28
  import { resolveNodeExecutable } from "../../shared/node-executable.ts";
27
29
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
28
- import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
29
- import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
30
+ import { normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
30
31
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
31
- import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
32
+ import { resolveEffectiveSubagentModel, resolveModelOrigin, resolveModelSelection, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-resolution.ts";
32
33
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
33
34
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
34
35
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -48,6 +49,7 @@ import {
48
49
  type ArtifactConfig,
49
50
  type Details,
50
51
  type IntercomBridgeConfig,
52
+ type HerdrMachineReference,
51
53
  type JsonSchemaObject,
52
54
  type MaxOutputConfig,
53
55
  type NestedRouteInfo,
@@ -71,6 +73,7 @@ import type { ChildRuntimeConfig } from "../shared/child-runtime-config.ts";
71
73
  import { childSessionFactoryModule } from "../shared/child-session.ts";
72
74
  import { inheritedChildRuntime } from "../shared/child-launch.ts";
73
75
  import { resultFilePath } from "./result-files.ts";
76
+ import { updateActiveRunIndex } from "./active-run-index.ts";
74
77
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
75
78
  import { usageBudgetState } from "../shared/usage-budget.ts";
76
79
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
@@ -80,13 +83,29 @@ import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
80
83
  import { statusStepDescription } from "./chain-append.ts";
81
84
  import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
82
85
  import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
83
- import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
86
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
84
87
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
85
88
  import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
86
89
  import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
90
+ import { resolveRequiredChildExtensions, type RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
87
91
 
88
- const require = createRequire(import.meta.url);
89
- const piPackageRoot = resolvePiPackageRoot() ?? resolveInstalledPiPackageRoot();
92
+ const require = nodeModule.createRequire(import.meta.url);
93
+ const piPackageRoot = resolveAsyncPiPackageRoot();
94
+
95
+ /**
96
+ * The detached runner resolves the same host package the foreground
97
+ * `resolvePiCliScript` path resolves, so an explicit
98
+ * `PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT` override must be honored here
99
+ * too. Precedence mirrors the foreground resolver: argv-based discovery wins
100
+ * first (the foreground returns the argv script before any candidate), the
101
+ * environment override is consulted when that discovery cannot identify the
102
+ * host (wrapper installs, non-standard layouts), and the
103
+ * package-manager entry is last. Without the override, such hosts fail
104
+ * closed with "neither is available" while foreground children launch fine.
105
+ */
106
+ function resolveAsyncPiPackageRoot(env: NodeJS.ProcessEnv = process.env): string | undefined {
107
+ return resolvePiPackageRoot() || env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]?.trim() || resolveInstalledPiPackageRoot();
108
+ }
90
109
 
91
110
  function resolveJitiCliFromPackageJson(packageJsonPath: string): string | undefined {
92
111
  if (!fs.existsSync(packageJsonPath)) return undefined;
@@ -110,12 +129,12 @@ function resolveJitiCliPath(): string | undefined {
110
129
  const candidates: Array<() => string | undefined> = [
111
130
  () => require.resolve("jiti/package.json"),
112
131
  () => piPackageRoot
113
- ? createRequire(path.join(piPackageRoot, "package.json")).resolve("jiti/package.json")
132
+ ? nodeModule.createRequire(path.join(piPackageRoot, "package.json")).resolve("jiti/package.json")
114
133
  : undefined,
115
134
  () => {
116
135
  if (!process.argv[1]) return undefined;
117
136
  const piEntry = fs.realpathSync(process.argv[1]);
118
- return createRequire(piEntry).resolve("jiti/package.json");
137
+ return nodeModule.createRequire(piEntry).resolve("jiti/package.json");
119
138
  },
120
139
  () => piPackageRoot ? path.join(piPackageRoot, "node_modules", "jiti", "package.json") : undefined,
121
140
  ];
@@ -133,6 +152,17 @@ function resolveJitiCliPath(): string | undefined {
133
152
  }
134
153
 
135
154
  const jitiCliPath = resolveJitiCliPath();
155
+ const asyncRunnerSourcePath = path.join(
156
+ path.dirname(fileURLToPath(import.meta.url)),
157
+ `subagent-runner${path.extname(fileURLToPath(import.meta.url))}`,
158
+ );
159
+ const sourceUnderNodeModules = asyncRunnerSourcePath.split(path.sep).some((segment) => segment.toLowerCase() === "node_modules");
160
+ function supportsNativeRunner(nodeExecutable: string): boolean {
161
+ return Boolean(process.features.typescript)
162
+ && typeof nodeModule.registerHooks === "function"
163
+ && nodeExecutable === process.execPath
164
+ && !sourceUnderNodeModules;
165
+ }
136
166
 
137
167
  interface AsyncExecutionContext {
138
168
  pi: ExtensionAPI;
@@ -169,6 +199,9 @@ interface AsyncChainParams {
169
199
  availableModels?: AvailableModelInfo[];
170
200
  cwd?: string;
171
201
  maxOutput?: MaxOutputConfig;
202
+ machine?: string;
203
+ /** Launch cwd as typed when a machine is set: a path on that machine, never resolved locally. */
204
+ machineCwd?: string;
172
205
  artifactsDir?: string;
173
206
  artifactConfig: ArtifactConfig;
174
207
  shareEnabled: boolean;
@@ -224,9 +257,13 @@ interface AsyncSingleParams {
224
257
  agentConfig: AgentConfig;
225
258
  /** Agent contract before per-run bridge injection, used only for recovery persistence. */
226
259
  recoveryAgentConfig?: AgentConfig;
260
+ requiredExtensions?: RequiredChildExtensionSnapshot;
227
261
  ctx: AsyncExecutionContext;
228
262
  cwd?: string;
229
263
  requestedCwd?: string;
264
+ machine?: string;
265
+ /** Launch cwd as typed when a machine is set: a path on that machine, never resolved locally. */
266
+ machineCwd?: string;
230
267
  maxOutput?: MaxOutputConfig;
231
268
  artifactsDir?: string;
232
269
  artifactConfig: ArtifactConfig;
@@ -270,6 +307,8 @@ interface AsyncSingleParams {
270
307
  absoluteDeadlineAt?: number;
271
308
  /** Optional per-call hard toolTimeoutMs override (highest precedence). */
272
309
  toolTimeoutMs?: number;
310
+ /** Steer the child to checkpoint and stop this many ms before the run deadline (resolved call param ?? config). */
311
+ checkpointBeforeDeadlineMs?: number;
273
312
  toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
274
313
  usageBudget?: UsageBudgetConfig;
275
314
  configToolBudget?: ResolvedToolBudget;
@@ -313,6 +352,8 @@ export interface AsyncRunnerStepBuildParams {
313
352
  ctx: AsyncExecutionContext;
314
353
  availableModels?: AvailableModelInfo[];
315
354
  cwd?: string;
355
+ machine?: string;
356
+ machineCwd?: string;
316
357
  chainSkills?: string[];
317
358
  sessionFilesByFlatIndex?: (string | undefined)[];
318
359
  thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
@@ -369,11 +410,9 @@ export function formatAsyncStartedMessage(headline: string, interactive: boolean
369
410
  return [headline, "", ...guidance].join("\n");
370
411
  }
371
412
 
372
- /**
373
- * Check if jiti is available for async execution
374
- */
413
+ /** Check whether detached async execution has a supported runtime. */
375
414
  export function isAsyncAvailable(): boolean {
376
- return jitiCliPath !== undefined;
415
+ return resolveBunPiExecutable() !== undefined || supportsNativeRunner(resolveNodeExecutable()) || jitiCliPath !== undefined;
377
416
  }
378
417
 
379
418
  export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
@@ -400,24 +439,14 @@ function closeFd(fd: number | undefined): void {
400
439
  * Spawn the async runner process
401
440
  */
402
441
  const RUNNER_STARTUP_TIMEOUT_MS = 10_000;
403
- const RUNNER_STARTUP_WAIT_BUFFER = typeof SharedArrayBuffer !== "undefined" ? new SharedArrayBuffer(4) : undefined;
404
- const RUNNER_STARTUP_WAIT_VIEW = RUNNER_STARTUP_WAIT_BUFFER ? new Int32Array(RUNNER_STARTUP_WAIT_BUFFER) : undefined;
405
-
406
- type RunnerStartupState = "ready" | "acknowledged";
442
+ type RunnerStartupState = "ready" | "acknowledged" | "confirmed";
407
443
 
408
444
  type RunnerStartupWaitResult =
409
445
  | { ok: true; token: string }
410
446
  | { ok: false; error: string; startupDidNotProceed?: boolean };
411
447
 
412
- function waitForStartupInterval(delayMs = 20): void {
413
- if (RUNNER_STARTUP_WAIT_VIEW) {
414
- Atomics.wait(RUNNER_STARTUP_WAIT_VIEW, 0, 0, delayMs);
415
- return;
416
- }
417
- const waitUntil = Date.now() + delayMs;
418
- while (Date.now() < waitUntil) {
419
- // Startup handshakes are synchronous so resume rejects before reporting a run as started.
420
- }
448
+ function waitForStartupInterval(delayMs = 20): Promise<void> {
449
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
421
450
  }
422
451
 
423
452
  function readRunnerStartup(startupPath: string, expectedState: RunnerStartupState, expectedToken?: string): RunnerStartupWaitResult | undefined {
@@ -435,13 +464,32 @@ function readRunnerStartup(startupPath: string, expectedState: RunnerStartupStat
435
464
  }
436
465
  }
437
466
 
438
- function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupState, timeoutMs: number, expectedToken?: string): RunnerStartupWaitResult {
467
+ async function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupState, timeoutMs: number, expectedToken?: string, processClosed?: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>): Promise<RunnerStartupWaitResult> {
468
+ const confirmOpen = async (result: RunnerStartupWaitResult): Promise<RunnerStartupWaitResult> => {
469
+ if (!processClosed || result.ok === false) return result;
470
+ const outcome = await Promise.race([processClosed.then((exit) => ({ exit })), new Promise<undefined>((resolve) => setImmediate(() => resolve(undefined)))]);
471
+ if (!outcome) return result;
472
+ const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
473
+ if (finalResult?.ok === false) return finalResult;
474
+ const { exitCode, signal } = outcome.exit;
475
+ return { ok: false, error: `Async runner exited before startup state '${expectedState}' (exit code ${exitCode ?? "unknown"}, signal ${signal ?? "none"}).`, startupDidNotProceed: true };
476
+ };
439
477
  const deadline = Date.now() + timeoutMs;
440
- for (;;) {
478
+ while (Date.now() <= deadline) {
441
479
  const result = readRunnerStartup(startupPath, expectedState, expectedToken);
442
- if (result) return result;
443
- if (Date.now() >= deadline) break;
444
- waitForStartupInterval(Math.min(20, Math.max(1, deadline - Date.now())));
480
+ if (result) return await confirmOpen(result);
481
+ const delay = waitForStartupInterval(Math.min(20, Math.max(1, deadline - Date.now())));
482
+ if (processClosed) {
483
+ const outcome = await Promise.race([delay.then(() => undefined), processClosed.then((exit) => ({ exit }))]);
484
+ if (outcome) {
485
+ const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
486
+ if (finalResult?.ok === false) return finalResult;
487
+ const { exitCode, signal } = outcome.exit;
488
+ return { ok: false, error: `Async runner exited before startup state '${expectedState}' (exit code ${exitCode ?? "unknown"}, signal ${signal ?? "none"}).`, startupDidNotProceed: true };
489
+ }
490
+ } else {
491
+ await delay;
492
+ }
445
493
  }
446
494
  const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
447
495
  if (finalResult) return finalResult;
@@ -450,7 +498,7 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
450
498
 
451
499
  const writePrivateStartupControlJson = createAtomicJsonWriter({ mode: 0o600, ignoreCleanupErrorAfterSuccess: true });
452
500
 
453
- function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
501
+ function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "confirm" | "proceed"; token: string }): void {
454
502
  // Delegate to the shared atomic JSON writer (temp file + rename, retrying
455
503
  // transient Windows EPERM/EBUSY/EACCES locks and cleaning up the temp file
456
504
  // on failure), so the startup handshake gets the same locking resilience as
@@ -468,6 +516,13 @@ function runnerIsAlive(pid: number): boolean {
468
516
  }
469
517
  }
470
518
 
519
+ function waitForStartupIntervalSync(delayMs = 20): void {
520
+ const waitUntil = Date.now() + delayMs;
521
+ while (Date.now() < waitUntil) {
522
+ // Pre-handshake failures happen before the runner can be observed asynchronously.
523
+ }
524
+ }
525
+
471
526
  function terminateRunnerBeforeProceed(pid: number): boolean {
472
527
  for (const signal of ["SIGTERM", "SIGKILL"] as const) {
473
528
  if (!runnerIsAlive(pid)) return true;
@@ -477,11 +532,88 @@ function terminateRunnerBeforeProceed(pid: number): boolean {
477
532
  if (!runnerIsAlive(pid)) return true;
478
533
  }
479
534
  const deadline = Date.now() + 1000;
480
- while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupInterval();
535
+ while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupIntervalSync();
481
536
  }
482
537
  return !runnerIsAlive(pid);
483
538
  }
484
539
 
540
+ async function terminateRunnerBeforeProceedAsync(proc: ChildProcess, processClosed: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>): Promise<boolean> {
541
+ for (const signal of ["SIGTERM", "SIGKILL"] as const) {
542
+ if (proc.pid === undefined || !runnerIsAlive(proc.pid)) return true;
543
+ try { process.kill(proc.pid, signal); } catch {
544
+ if (proc.pid === undefined || !runnerIsAlive(proc.pid)) return true;
545
+ }
546
+ const exited = await Promise.race([processClosed.then(() => true), waitForStartupInterval(1000).then(() => false)]);
547
+ if (exited || proc.pid === undefined || !runnerIsAlive(proc.pid)) return true;
548
+ }
549
+ return proc.pid === undefined || !runnerIsAlive(proc.pid);
550
+ }
551
+
552
+ async function completeRunnerStartupHandshake(
553
+ startupPath: string,
554
+ startupAckPath: string,
555
+ startupProceedPath: string,
556
+ proc: ChildProcess,
557
+ processClosed: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>,
558
+ getObservedProcessExit: () => { exitCode: number | null; signal: NodeJS.Signals | null } | undefined,
559
+ runnerProcessInstanceId: string,
560
+ persistStartupFailure: (message: string) => void,
561
+ ): Promise<SpawnRunnerResult> {
562
+ try {
563
+ const ready = await waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS, undefined, processClosed);
564
+ if (ready.ok === false) {
565
+ persistStartupFailure(ready.error);
566
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
567
+ return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: true };
568
+ }
569
+ try { writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token }); } catch (error) {
570
+ const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
571
+ persistStartupFailure(message);
572
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
573
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
574
+ }
575
+ const acknowledged = await waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token, processClosed);
576
+ if (acknowledged.ok === false) {
577
+ persistStartupFailure(acknowledged.error);
578
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
579
+ return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: true };
580
+ }
581
+ try { writeRunnerStartupControl(startupAckPath, { action: "confirm", token: ready.token }); } catch (error) {
582
+ const message = `Failed to confirm async runner startup: ${error instanceof Error ? error.message : String(error)}`;
583
+ persistStartupFailure(message);
584
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
585
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
586
+ }
587
+ const confirmed = await waitForRunnerStartup(startupPath, "confirmed", RUNNER_STARTUP_TIMEOUT_MS, ready.token, processClosed);
588
+ if (confirmed.ok === false) {
589
+ persistStartupFailure(confirmed.error);
590
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
591
+ return { pid: proc.pid, runnerProcessInstanceId, error: confirmed.error, terminationObserved, startupDidNotProceed: true };
592
+ }
593
+ const closedAtCommit = await Promise.race([
594
+ processClosed.then((exit) => exit),
595
+ new Promise<undefined>((resolve) => setImmediate(() => resolve(undefined))),
596
+ ]);
597
+ const observedExit = closedAtCommit ?? getObservedProcessExit();
598
+ if (observedExit) {
599
+ const message = `Async runner exited after startup state 'acknowledged' before proceed (exit code ${observedExit.exitCode ?? "unknown"}, signal ${observedExit.signal ?? "none"}).`;
600
+ persistStartupFailure(message);
601
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
602
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
603
+ }
604
+ try { writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token }); } catch (error) {
605
+ const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
606
+ persistStartupFailure(message);
607
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
608
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
609
+ }
610
+ try { fs.rmSync(startupPath, { force: true }); } catch {}
611
+ return { pid: proc.pid, runnerProcessInstanceId };
612
+ } finally {
613
+ await new Promise<void>((resolve) => setImmediate(resolve));
614
+ }
615
+ }
616
+
485
617
  function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, completionOwnerId: string | undefined, message: string): void {
486
618
  const now = Date.now();
487
619
  try {
@@ -490,6 +622,8 @@ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runner
490
622
  try {
491
623
  status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as Partial<AsyncStatus>;
492
624
  } catch {}
625
+ const existingProcessTerminal = status.processTerminal?.state === "observed" || status.processTerminal?.state === "unknown"
626
+ ? status.processTerminal : undefined;
493
627
  writePrivateAtomicJson(statusPath, {
494
628
  ...status,
495
629
  runId,
@@ -498,7 +632,7 @@ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runner
498
632
  state: "failed",
499
633
  lastUpdate: now,
500
634
  error: message,
501
- processTerminal: {
635
+ processTerminal: existingProcessTerminal ?? {
502
636
  version: 1,
503
637
  state: "not-started",
504
638
  runId,
@@ -538,19 +672,34 @@ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unkn
538
672
  }
539
673
  }
540
674
 
541
- function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, onBeforeProceed?: (runnerProcessInstanceId: string) => void, requestedCwd = cwd): SpawnRunnerResult {
675
+ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, onBeforeProceed?: (runnerProcessInstanceId: string) => void, requestedCwd = cwd): SpawnRunnerResult | Promise<SpawnRunnerResult> {
542
676
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
543
677
  if (cwdError) return { error: cwdError };
544
678
 
545
- if (!jitiCliPath) {
546
- return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
679
+ // The compiled host exposes its SDK only through Pi's extension loader.
680
+ const binaryHost = resolveBunPiExecutable();
681
+ const nodeExecutable = resolveNodeExecutable();
682
+ const configuredExtensions = (cfg as { steps?: Array<{ extensions?: unknown[]; subagentOnlyExtensions?: unknown[]; requiredExtensions?: unknown[] }> }).steps?.some(
683
+ (step) => (step.extensions?.length ?? 0) > 0 || (step.subagentOnlyExtensions?.length ?? 0) > 0 || (step.requiredExtensions?.length ?? 0) > 0,
684
+ ) ?? false;
685
+ // Keep provider/tool extensions in Jiti's host-alias boundary; the native preload
686
+ // only certifies the runner's imports, not a separately loaded extension graph.
687
+ const nativeRunnerSupported = supportsNativeRunner(nodeExecutable) && !configuredExtensions;
688
+ const runner = asyncRunnerSourcePath;
689
+ const runnerIsJavaScript = path.extname(runner) === ".js";
690
+ const bootstrap = path.join(path.dirname(runner), `binary-bootstrap${path.extname(fileURLToPath(import.meta.url))}`);
691
+ if (binaryHost && !fs.existsSync(bootstrap)) return { error: `Background runner bootstrap not found: ${bootstrap}` };
692
+ if (!binaryHost && !runnerIsJavaScript && !nativeRunnerSupported && !jitiCliPath) {
693
+ return { error: sourceUnderNodeModules
694
+ ? "Background runner source is installed under node_modules, so upstream jiti is required for TypeScript execution."
695
+ : "Background runner requires enabled native TypeScript with synchronous module hooks, or installed upstream jiti for TypeScript execution." };
547
696
  }
548
- if (!piPackageRoot) {
549
- return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}); a standalone pi binary has no package directory, so the async runner cannot create child sessions. Run this child in the foreground (async: false) or install pi from npm.` };
697
+ if (!binaryHost && !piPackageRoot) {
698
+ return { error: `Background children require a supported standalone Pi host or the installed npm package (${PI_CODING_AGENT_PACKAGE}); neither is available.` };
550
699
  }
551
- const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
700
+ const hostPeerAliases = !binaryHost && piPackageRoot ? resolveHostPeerAliases(piPackageRoot) : { aliases: {}, missing: [] };
552
701
  if (hostPeerAliases.missing.length > 0) {
553
- return { error: `Background children require pi installed as the npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}, so the async runner cannot create child sessions. A standalone pi binary cannot run background children.` };
702
+ return { error: `Background children require the host npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}.` };
554
703
  }
555
704
 
556
705
  fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
@@ -560,8 +709,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
560
709
  const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
561
710
  const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
562
711
  writePrivateAtomicJson(cfgPath, launchConfig);
563
- const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
564
- const nodeCommand = resolveNodeExecutable();
712
+ const command = binaryHost ?? nodeExecutable;
565
713
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
566
714
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
567
715
  const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
@@ -587,26 +735,49 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
587
735
  stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
588
736
  stderrFd = fs.openSync(logPaths.stderrPath, "a");
589
737
  }
590
- const preload = hostPeerAliases.supplemental.length > 0
591
- ? ["--import", new URL("../../../runner-server-preload.mjs", import.meta.url).href]
738
+ const preload = Object.keys(hostPeerAliases.aliases).length > 0
739
+ ? ["--import", new URL("../../../runner-peer-preload.mjs", import.meta.url).href]
592
740
  : [];
593
- const proc = spawn(nodeCommand, [...preload, jitiCliPath, runner, cfgPath], {
741
+ const args = binaryHost
742
+ ? ["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session", "--mode", "rpc", "--extension", bootstrap]
743
+ : runnerIsJavaScript
744
+ ? [...preload, runner, cfgPath]
745
+ : nativeRunnerSupported
746
+ ? [...preload, "--experimental-strip-types", runner, cfgPath]
747
+ : [...preload, jitiCliPath!, runner, cfgPath];
748
+ const proc = spawn(command, args, {
594
749
  cwd,
595
750
  ...backgroundProcessOptions(),
596
751
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
597
752
  env: {
598
753
  ...omitExtensionBindingsEnv(process.env),
599
- [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
600
- [JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
754
+ // Unset leaves the inherited parent value in place. See childCacheRetention.
755
+ ...childCacheRetentionEnv(),
756
+ [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: binaryHost ? undefined : piPackageRoot,
757
+ // npm must override inherited bundled layouts (#2071); binaries retain release assets.
758
+ PI_PACKAGE_DIR: binaryHost ? process.env.PI_PACKAGE_DIR : piPackageRoot,
759
+ [JITI_ALIAS_ENV]: binaryHost ? undefined : JSON.stringify(hostPeerAliases.aliases),
760
+ PI_ASYNC_NATIVE_RUNNER: !binaryHost && (runnerIsJavaScript || nativeRunnerSupported) ? "1" : "0",
761
+ PI_ASYNC_COMPILED_RUNNER: !binaryHost && runnerIsJavaScript ? "1" : "0",
762
+ PI_SUBAGENT_RUNNER_CONFIG: binaryHost ? cfgPath : undefined,
601
763
  },
602
764
  });
765
+ let observedProcessExit: { exitCode: number | null; signal: NodeJS.Signals | null } | undefined;
766
+ proc.once("exit", (exitCode, signal) => { observedProcessExit = { exitCode, signal }; });
767
+ const processClosed = new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>((resolve) => {
768
+ proc.once("close", (exitCode, signal) => {
769
+ observedProcessExit ??= { exitCode, signal };
770
+ resolve({ exitCode, signal });
771
+ });
772
+ });
603
773
  closeFd(stdoutFd);
604
774
  closeFd(stderrFd);
605
775
  proc.on("error", (error) => {
606
776
  console.error(`[pi-subagents] async spawn failed: ${error.message}`);
607
777
  });
608
778
  proc.once("close", (exitCode, signal) => {
609
- const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
779
+ const finalize = () => {
780
+ const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
610
781
  const asyncDir = launch.asyncDir;
611
782
  const runId = launch.id;
612
783
  if (typeof asyncDir !== "string" || typeof runId !== "string") return;
@@ -654,6 +825,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
654
825
  }
655
826
  }
656
827
  onProcessTerminal?.(persisted);
828
+ };
829
+ if (startupPath) setImmediate(finalize);
830
+ else finalize();
657
831
  });
658
832
  if (typeof proc.pid !== "number") {
659
833
  return { error: `async runner did not produce a pid for cwd: ${cwd}` };
@@ -664,6 +838,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
664
838
  pid: proc.pid,
665
839
  processTerminal: { version: 1, state: "pending", runId: initialStatus.runId, runnerProcessInstanceId },
666
840
  });
841
+ // Aggregate waits must see the launch before the runner's first status update.
842
+ updateActiveRunIndex(path.dirname(initialStatusPath), initialStatus.state, initialStatus.toolCallId);
667
843
  } catch (error) {
668
844
  const message = `Failed to persist initial async status: ${error instanceof Error ? error.message : String(error)}`;
669
845
  if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
@@ -702,39 +878,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
702
878
  const persistStartupFailure = (message: string) => {
703
879
  if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
704
880
  };
705
- const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
706
- if (ready.ok === false) {
707
- persistStartupFailure(ready.error);
708
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
709
- return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: ready.startupDidNotProceed };
710
- }
711
- try {
712
- writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token });
713
- } catch (error) {
714
- const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
715
- persistStartupFailure(message);
716
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
717
- return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
718
- }
719
- const acknowledged = waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token);
720
- if (acknowledged.ok === false) {
721
- persistStartupFailure(acknowledged.error);
722
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
723
- return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: acknowledged.startupDidNotProceed };
724
- }
725
- try {
726
- writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token });
727
- } catch (error) {
728
- const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
729
- persistStartupFailure(message);
730
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
731
- return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
732
- }
733
- try {
734
- fs.rmSync(startupPath, { force: true });
735
- } catch {
736
- // Proceed is the commit point; handshake cleanup cannot turn a running revival into a start error.
737
- }
881
+ return completeRunnerStartupHandshake(startupPath, startupAckPath, startupProceedPath, proc, processClosed, () => observedProcessExit ?? (proc.exitCode !== null || proc.signalCode !== null ? { exitCode: proc.exitCode, signal: proc.signalCode } : undefined), runnerProcessInstanceId, persistStartupFailure);
738
882
  }
739
883
  return { pid: proc.pid, runnerProcessInstanceId };
740
884
  } catch (error) {
@@ -776,6 +920,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
776
920
  const chainSkills = params.chainSkills ?? [];
777
921
  const availableModels = params.availableModels;
778
922
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
923
+ const launchMachine = params.machine;
779
924
  let managedWorktreeProvider: "native" | "worktrunk" | undefined;
780
925
  try {
781
926
  if (chain.some((step) => "worktree" in step && step.worktree === true)) {
@@ -810,8 +955,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
810
955
  if (error instanceof ChainOutputValidationError) return { error: error.message };
811
956
  throw error;
812
957
  }
813
- const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
814
-
815
958
  const diagnosticContext = params.unknownAgentDiagnosticContext
816
959
  ?? unknownAgentDiagnosticContext(discoverAgents(path.resolve(runnerCwd), "both"));
817
960
  for (const s of chain) {
@@ -824,6 +967,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
824
967
  if (!agents.find((x) => x.name === agentName)) return { error: formatUnknownAgentError(agentName, diagnosticContext) };
825
968
  }
826
969
  }
970
+ const graphSteps = projectChainOutputSchemas(graphChain, agents) as ChainStep[];
971
+ const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphSteps });
827
972
 
828
973
  let progressInstructionCreated = false;
829
974
  const buildStepOverrides = (s: SequentialStep): StepOverrides => {
@@ -836,16 +981,32 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
836
981
  ...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
837
982
  ...(s.model !== undefined ? { model: s.model } : {}),
838
983
  ...(s.fast !== undefined ? { fast: s.fast } : {}),
984
+ ...(s.outputSchema !== undefined ? { outputSchema: s.outputSchema } : {}),
839
985
  };
840
986
  };
841
987
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
842
988
  const a = agents.find((x) => x.name === s.agent)!;
989
+ const effectiveBehavior = resolvedBehavior ?? suppressProgressForReadOnlyTask(resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
990
+ const requestedMachine = s.machine ?? launchMachine ?? a.machine;
843
991
  const externalRunner = a.runner?.type === "external-cli" || a.runner?.type === "external-job";
844
992
  const externalRunnerType = a.runner?.type;
993
+ const machineUnsupported = formatHerdrMachineRunnerUnsupported({ machine: requestedMachine, agentName: a.name, runnerType: a.runner?.type, adapter: a.runner?.type === "external-cli" ? a.runner.adapter : undefined, worktree: s.worktree });
994
+ if (machineUnsupported) throw new AsyncStartValidationError(machineUnsupported);
995
+ let machine: HerdrMachineReference | undefined;
996
+ let machineEnv: Record<string, string> | undefined;
997
+ if (requestedMachine) {
998
+ try {
999
+ const placement = resolveHerdrMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: s.cwd ?? params.machineCwd });
1000
+ machine = placement.machine;
1001
+ machineEnv = placement.env;
1002
+ } catch (error) {
1003
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
1004
+ }
1005
+ }
845
1006
  if (externalRunner) {
846
1007
  const unsupported: string[] = [];
847
1008
  if (s.model !== undefined) unsupported.push("model override");
848
- if (s.outputSchema !== undefined) unsupported.push("structured output");
1009
+ if (effectiveBehavior.outputSchema !== undefined) unsupported.push("structured output");
849
1010
  if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
850
1011
  if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
851
1012
  if ((s.fast ?? params.fast ?? a.fast) === true) unsupported.push("fast mode");
@@ -876,10 +1037,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
876
1037
  runtimeCwd: ctx.cwd,
877
1038
  stepCwdInput: s.cwd,
878
1039
  behaviorCwd,
1040
+ ...(machine ? { machineCwd: machine.cwd } : {}),
879
1041
  chainSkills,
880
1042
  outputBaseDir,
881
1043
  parallelOutputNamespace,
882
- resolvedBehavior,
1044
+ resolvedBehavior: effectiveBehavior,
883
1045
  });
884
1046
  const { stepCwd, instructionCwd, readExistenceCwd, behavior, namespaceOutputPath, outputPath, skillNames } = launchPlan;
885
1047
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
@@ -891,22 +1053,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
891
1053
  );
892
1054
  if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
893
1055
 
894
- let systemPrompt = a.systemPrompt?.trim() ?? "";
895
- if (resolvedSkills.length > 0) {
896
- const injection = buildSkillInjection(resolvedSkills);
897
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
898
- }
899
- const memoryInjection = buildAgentMemoryInjection(a, stepCwd);
900
- if (memoryInjection) {
901
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
902
- }
903
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
1056
+ // A namespaced parallel output is injected by the runner, not the prompt.
1057
+ const systemPrompt = buildEffectiveSystemPrompt({ agent: a, resolvedSkills, cwd: stepCwd, ...(!namespaceOutputPath && outputPath ? { outputPath } : {}) });
904
1058
 
905
1059
  const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
906
1060
  const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
907
1061
  if (behavior.progress) progressInstructionCreated = true;
908
1062
  const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
909
- if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
910
1063
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
911
1064
  if (validationError) throw new AsyncStartValidationError(validationError);
912
1065
  let taskTemplate = s.task ?? "{previous}";
@@ -944,43 +1097,46 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
944
1097
  }
945
1098
  const agentContract = s.agentContract ?? params.agentContract;
946
1099
  const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
947
- let modelCandidates: string[] = [];
1100
+ let selectedModel = model;
1101
+ let requestedModel: string | undefined;
948
1102
  if (!externalRunner) {
949
1103
  try {
950
- modelCandidates = buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
1104
+ const modelEvidence = resolveModelSelection(primaryModel, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
951
1105
  scope: modelScopes,
952
1106
  primaryModelFromParent,
953
1107
  origin: modelOrigin,
954
- }).flatMap((candidate) => {
955
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
956
- return resolved ? [resolved] : [];
957
1108
  });
958
- for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
1109
+ requestedModel = modelEvidence.requestedModel;
1110
+ selectedModel = applyThinkingSuffix(modelEvidence.model, effectiveThinking, thinkingOverride !== undefined);
1111
+ assertThinkingWithinCeiling({ model: selectedModel, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
959
1112
  } catch (error) {
960
1113
  throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
961
1114
  }
962
1115
  }
963
- const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
1116
+ const launchRuleError = applyWatchdogLaunchRules({ cwd: machine ? runnerCwd : stepCwd, agent: a.name, model: selectedModel, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
964
1117
  if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
965
1118
  const fast = s.fast ?? params.fast ?? a.fast;
1119
+ const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
1120
+ const requiredExtensions = externalRunner ? [] : ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(ctx.parentSessionId ?? ctx.currentSessionId ?? undefined);
966
1121
  const toolPlan = resolvePiLaunchToolPlan({
967
1122
  tools: a.tools,
968
1123
  excludeTools: a.excludeTools,
969
1124
  allowNestedSubagents: a.allowNestedSubagents,
970
1125
  extensions: a.extensions,
971
1126
  subagentOnlyExtensions: a.subagentOnlyExtensions,
1127
+ requiredExtensions,
972
1128
  mcpDirectTools: a.mcpDirectTools,
973
1129
  cwd: stepCwd,
974
1130
  requireReadTool: Boolean(resolvedSkills.length),
975
- structuredOutput: Boolean(s.outputSchema),
1131
+ structuredOutput: Boolean(behavior.outputSchema),
976
1132
  fast,
977
- model,
978
- modelCandidates,
1133
+ model: selectedModel,
979
1134
  capabilityCeiling: params.capabilityCeiling,
980
1135
  inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
981
1136
  agentName: a.name,
982
1137
  permissionRules,
983
1138
  runtimeSnapshotHost: ctx.pi,
1139
+ hostAvailableBuiltins,
984
1140
  });
985
1141
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
986
1142
  if (externalRunner && permissionRules) {
@@ -1008,21 +1164,23 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1008
1164
  agent: s.agent,
1009
1165
  task,
1010
1166
  ...(a.runner ? { runner: a.runner } : {}),
1167
+ ...(machine ? { machine } : {}),
1168
+ ...(machineEnv ? { machineEnv } : {}),
1011
1169
  ...(params.contextForAgent ? { context: params.contextForAgent(s.agent) } : {}),
1012
1170
  ...(agentContract ? { agentContract } : {}),
1013
1171
  phase: s.phase,
1014
1172
  label: s.label,
1015
1173
  outputName: s.as,
1016
- structured: Boolean(s.outputSchema),
1174
+ structured: Boolean(behavior.outputSchema),
1017
1175
  cwd: stepCwd,
1018
- requestedCwd: s.cwd ?? stepCwd,
1019
- model,
1176
+ requestedCwd: machine ? machine.cwd : s.cwd ?? stepCwd,
1177
+ model: selectedModel,
1020
1178
  ...(contextLimit !== undefined ? { contextLimit } : {}),
1021
1179
  ...(fast !== undefined ? { fast } : {}),
1022
- thinking: resolveEffectiveThinking(model, effectiveThinking),
1180
+ thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
1023
1181
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1024
1182
  launchResolvedExtensions,
1025
- modelCandidates: externalRunner ? undefined : modelCandidates,
1183
+ ...(requestedModel ? { requestedModel } : {}),
1026
1184
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
1027
1185
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1028
1186
  ...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
@@ -1031,6 +1189,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1031
1189
  allowNestedSubagents: a.allowNestedSubagents,
1032
1190
  extensions: a.extensions,
1033
1191
  subagentOnlyExtensions: a.subagentOnlyExtensions,
1192
+ ...(!externalRunner ? { requiredExtensions } : {}),
1034
1193
  mcpDirectTools: a.mcpDirectTools,
1035
1194
  mutationTools: a.mutationTools,
1036
1195
  completionGuard: a.completionGuard,
@@ -1062,8 +1221,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1062
1221
  acceptanceInput: s.acceptance,
1063
1222
  acceptanceRole: a.acceptanceRole,
1064
1223
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1065
- ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
1066
- ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(s.acceptance) }) } : {}),
1224
+ ...(behavior.outputSchema ? { structuredOutputSchema: behavior.outputSchema } : {}),
1225
+ ...(behavior.outputSchema ? { structuredOutput: createStructuredOutputRuntime(behavior.outputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(s.acceptance) }) } : {}),
1067
1226
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1068
1227
  ...(s.worktree ? { worktree: true } : {}),
1069
1228
  };
@@ -1107,7 +1266,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1107
1266
  }
1108
1267
  }
1109
1268
  const staticStep = nextFlatStep();
1110
- return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex }, resultMode === "parallel" ? `tasks[${taskIndex}]` : `chain[${stepIndex}].parallel[${taskIndex}]`);
1269
+ return buildSeqStep({ ...t, machine: t.machine ?? s.machine, worktree: s.worktree, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex }, resultMode === "parallel" ? `tasks[${taskIndex}]` : `chain[${stepIndex}].parallel[${taskIndex}]`);
1111
1270
  }),
1112
1271
  concurrency: s.concurrency,
1113
1272
  failFast: s.failFast,
@@ -1236,11 +1395,9 @@ export function executeAsyncChain(
1236
1395
  } = params;
1237
1396
  const resultMode = params.resultMode ?? "chain";
1238
1397
  const acceptanceErrors = validateExecutionAcceptance({
1239
- chain: chain.map((step) => {
1240
- if (isParallelStep(step)) return { parallel: step.parallel };
1241
- if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
1242
- return { acceptance: step.acceptance, outputSchema: step.outputSchema };
1243
- }),
1398
+ chain: projectChainOutputSchemas(chain, agents,
1399
+ (step, outputSchema) => ({ acceptance: step.acceptance, outputSchema }),
1400
+ (step, parallel) => Array.isArray(step.parallel) ? { parallel } : { acceptance: "acceptance" in step ? step.acceptance : undefined, parallel }),
1244
1401
  });
1245
1402
  if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
1246
1403
  const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
@@ -1274,6 +1431,8 @@ export function executeAsyncChain(
1274
1431
  availableModels: params.availableModels,
1275
1432
  cwd,
1276
1433
  chainSkills: params.chainSkills,
1434
+ machine: params.machine,
1435
+ machineCwd: params.machineCwd,
1277
1436
  sessionFilesByFlatIndex,
1278
1437
  thinkingOverridesByFlatIndex,
1279
1438
  contextForAgent: params.contextForAgent,
@@ -1377,6 +1536,7 @@ export function executeAsyncChain(
1377
1536
  deadlineAt,
1378
1537
  globalConcurrencyLimit: params.globalConcurrencyLimit,
1379
1538
  runFanoutBudget,
1539
+ hostAvailableBuiltins: getHostBuiltinToolNames(ctx.pi),
1380
1540
  workflowGraph,
1381
1541
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1382
1542
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
@@ -1407,7 +1567,7 @@ export function executeAsyncChain(
1407
1567
  path.join(asyncDir, "status.json"),
1408
1568
  (proof) => emitProcessTerminalEvent(ctx, proof),
1409
1569
  (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1410
- );
1570
+ ) as SpawnRunnerResult;
1411
1571
  } catch (error) {
1412
1572
  params.activeAsyncCapacity?.rollback();
1413
1573
  const message = error instanceof Error ? error.message : String(error);
@@ -1547,7 +1707,7 @@ export function workflowAwaitedAsyncResultPath(asyncDir: string): string {
1547
1707
  export function executeAsyncSingle(
1548
1708
  id: string,
1549
1709
  params: AsyncSingleParams,
1550
- ): AsyncExecutionResult {
1710
+ ): AsyncExecutionResult | Promise<AsyncExecutionResult> {
1551
1711
  const {
1552
1712
  agent,
1553
1713
  agentConfig,
@@ -1611,6 +1771,20 @@ export function executeAsyncSingle(
1611
1771
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1612
1772
  }
1613
1773
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1774
+ const requestedMachine = params.machine ?? agentConfig.machine;
1775
+ const machineUnsupported = formatHerdrMachineRunnerUnsupported({ machine: requestedMachine, agentName: agentConfig.name, runnerType: agentConfig.runner?.type, adapter: agentConfig.runner?.type === "external-cli" ? agentConfig.runner.adapter : undefined, worktree: params.worktree });
1776
+ if (machineUnsupported) return formatAsyncStartError("single", machineUnsupported);
1777
+ let machine: HerdrMachineReference | undefined;
1778
+ let machineEnv: Record<string, string> | undefined;
1779
+ if (requestedMachine) {
1780
+ try {
1781
+ const placement = resolveHerdrMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: params.machineCwd });
1782
+ machine = placement.machine;
1783
+ machineEnv = placement.env;
1784
+ } catch (error) {
1785
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1786
+ }
1787
+ }
1614
1788
  let managedWorktreeProvider: "native" | "worktrunk" | undefined;
1615
1789
  if (params.worktree === true) {
1616
1790
  try {
@@ -1624,7 +1798,7 @@ export function executeAsyncSingle(
1624
1798
  ? WORKTREE_AGENT_CWD_PLACEHOLDER
1625
1799
  : params.worktree === true && managedWorktreeProvider === "native"
1626
1800
  ? resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s0`, 0, worktreeBaseDir)
1627
- : runnerCwd;
1801
+ : machine?.cwd ?? runnerCwd;
1628
1802
  const readExistenceCwd = params.worktree === true ? runnerCwd : instructionCwd;
1629
1803
  const skillNames = params.skills ?? agentConfig.skills ?? [];
1630
1804
  const availableModels = params.availableModels;
@@ -1636,16 +1810,6 @@ export function executeAsyncSingle(
1636
1810
  agentConfig.filePath ? path.dirname(agentConfig.filePath) : runnerCwd,
1637
1811
  );
1638
1812
  if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
1639
- let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
1640
- if (resolvedSkills.length > 0) {
1641
- const injection = buildSkillInjection(resolvedSkills);
1642
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
1643
- }
1644
- const memoryInjection = buildAgentMemoryInjection(agentConfig, runnerCwd);
1645
- if (memoryInjection) {
1646
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
1647
- }
1648
- systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
1649
1813
 
1650
1814
  const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
1651
1815
  const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
@@ -1668,7 +1832,7 @@ export function executeAsyncSingle(
1668
1832
 
1669
1833
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
1670
1834
  const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1671
- systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
1835
+ const systemPrompt = buildEffectiveSystemPrompt({ agent: agentConfig, resolvedSkills, cwd: runnerCwd, ...(outputPath ? { outputPath } : {}) });
1672
1836
  const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
1673
1837
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1674
1838
  if (validationError) return formatAsyncStartError("single", validationError);
@@ -1677,7 +1841,11 @@ export function executeAsyncSingle(
1677
1841
  // absolute paths pass through; relative paths resolve against the child cwd.
1678
1842
  const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
1679
1843
  const readPaths = Array.isArray(reads)
1680
- ? managedWorktreeProvider === "worktrunk"
1844
+ ? machine
1845
+ ? externalRunner
1846
+ ? reads.map((read) => read === "~" || read.startsWith("~/") || path.posix.isAbsolute(read) ? read : path.posix.resolve(instructionCwd, read))
1847
+ : []
1848
+ : managedWorktreeProvider === "worktrunk"
1681
1849
  ? resolveExistingReadInstructionPaths(reads, instructionCwd, readExistenceCwd)
1682
1850
  : resolveExistingReadPaths(reads, readExistenceCwd)
1683
1851
  : [];
@@ -1743,40 +1911,43 @@ export function executeAsyncSingle(
1743
1911
  const structuredOutput = params.structuredOutputSchema
1744
1912
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
1745
1913
  : undefined;
1746
- let modelCandidates: string[] = [];
1914
+ let selectedModel = model;
1915
+ let requestedModel: string | undefined;
1747
1916
  if (!externalRunner) {
1748
1917
  try {
1749
- modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1918
+ const modelEvidence = resolveModelSelection(primaryModel, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1750
1919
  scope: modelScopes,
1751
1920
  primaryModelFromParent: modelOrigin === "inherited",
1752
1921
  origin: modelOrigin,
1753
- }).flatMap((candidate) => {
1754
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1755
- return resolved ? [resolved] : [];
1756
1922
  });
1757
- for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1923
+ requestedModel = modelEvidence.requestedModel;
1924
+ selectedModel = applyThinkingSuffix(modelEvidence.model, effectiveThinking, params.thinkingOverride !== undefined);
1925
+ assertThinkingWithinCeiling({ model: selectedModel, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1758
1926
  } catch (error) {
1759
1927
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1760
1928
  }
1761
1929
  }
1930
+ const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
1931
+ const requiredExtensions = externalRunner ? [] : params.requiredExtensions ?? ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(ctx.parentSessionId ?? ctx.currentSessionId ?? undefined);
1762
1932
  const toolPlan = resolvePiLaunchToolPlan({
1763
1933
  tools: agentConfig.tools,
1764
1934
  excludeTools: agentConfig.excludeTools,
1765
1935
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1766
1936
  extensions: agentConfig.extensions,
1767
1937
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1938
+ requiredExtensions,
1768
1939
  mcpDirectTools: agentConfig.mcpDirectTools,
1769
1940
  cwd: runnerCwd,
1770
1941
  requireReadTool: Boolean(resolvedSkills.length),
1771
1942
  structuredOutput: Boolean(params.structuredOutputSchema),
1772
1943
  fast: params.fast ?? agentConfig.fast,
1773
- model,
1774
- modelCandidates,
1944
+ model: selectedModel,
1775
1945
  capabilityCeiling,
1776
1946
  inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
1777
1947
  agentName: agentConfig.name,
1778
1948
  permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
1779
1949
  runtimeSnapshotHost: ctx.pi,
1950
+ hostAvailableBuiltins,
1780
1951
  });
1781
1952
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1782
1953
  if (!externalRunner) {
@@ -1793,24 +1964,17 @@ export function executeAsyncSingle(
1793
1964
  });
1794
1965
  if (contractError) return formatAsyncStartError("single", contractError);
1795
1966
  }
1796
- const launchContractDigest = launchBindingDigest({
1797
- definitionDigest: agentDefinitionDigest(agentConfig),
1967
+ const fast = params.fast ?? agentConfig.fast;
1968
+ const launchThinking = resolveEffectiveThinking(selectedModel, effectiveThinking);
1969
+ const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
1970
+ agent: agentConfig,
1798
1971
  task,
1799
- ...(model ? { model } : {}),
1800
- modelCandidates,
1801
- ...((params.fast ?? agentConfig.fast) !== undefined ? { fast: params.fast ?? agentConfig.fast } : {}),
1802
- ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1803
- ...(thinkingCeiling ? { thinkingCeiling } : {}),
1972
+ model: selectedModel,
1973
+ ...(fast !== undefined ? { fast } : {}),
1974
+ ...(launchThinking ? { thinking: launchThinking } : {}),
1804
1975
  systemPrompt,
1805
- systemPromptMode: agentConfig.systemPromptMode,
1806
- inheritProjectContext: agentConfig.inheritProjectContext,
1807
- inheritGlobalContext: agentConfig.inheritGlobalContext,
1808
- inheritSkills: agentConfig.inheritSkills,
1809
1976
  skills: resolvedSkills.map((skill) => skill.name),
1810
- tools: toolPlan.effectiveToolAllowlist,
1811
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1812
- extensions: toolPlan.extensionArgs,
1813
- mcpDirectTools: toolPlan.effectiveMcpTools,
1977
+ toolPlan,
1814
1978
  ...(outputPath ? { outputPath } : {}),
1815
1979
  outputMode,
1816
1980
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
@@ -1832,6 +1996,7 @@ export function executeAsyncSingle(
1832
1996
  ...(lane ? { lane } : {}),
1833
1997
  launchContractDigest,
1834
1998
  ...(extensionBindings ? { extensionBindings } : {}),
1999
+ ...(requiredExtensions.length > 0 ? { requiredExtensions } : {}),
1835
2000
  runFanoutBudget,
1836
2001
  sourceRunId: id,
1837
2002
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
@@ -1839,13 +2004,12 @@ export function executeAsyncSingle(
1839
2004
  launchResolvedExtensions,
1840
2005
  ...(sessionFile ? { sessionFile } : {}),
1841
2006
  cwd: runnerCwd,
1842
- ...(model ? { model } : {}),
2007
+ ...(selectedModel ? { model: selectedModel } : {}),
1843
2008
  ...(params.fast ?? recoveryAgentConfig.fast ? { fast: params.fast ?? recoveryAgentConfig.fast } : {}),
1844
2009
  ...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
1845
2010
  ...(modelOrigin === "inherited" ? { modelOverrideFromParent: true } : {}),
1846
2011
  modelOrigin,
1847
- ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1848
- ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
2012
+ ...(effectiveThinking ? { thinking: resolveEffectiveThinking(selectedModel, effectiveThinking) } : {}),
1849
2013
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1850
2014
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1851
2015
  ...(recoveryAgentConfig.excludeTools ? { excludeTools: [...recoveryAgentConfig.excludeTools] } : {}),
@@ -1889,11 +2053,11 @@ export function executeAsyncSingle(
1889
2053
  return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
1890
2054
  }
1891
2055
  }
1892
- let spawnResult: SpawnRunnerResult = {};
2056
+ let spawnResultOrPromise: SpawnRunnerResult | Promise<SpawnRunnerResult> = {};
1893
2057
  const initialStatusAt = Date.now();
1894
2058
  const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
1895
2059
  try {
1896
- spawnResult = spawnRunner(
2060
+ spawnResultOrPromise = spawnRunner(
1897
2061
  {
1898
2062
  id,
1899
2063
  steps: [
@@ -1904,16 +2068,19 @@ export function executeAsyncSingle(
1904
2068
  agent,
1905
2069
  task: taskText,
1906
2070
  ...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
2071
+ ...(machine ? { machine } : {}),
2072
+ ...(!externalRunner && machine && params.reads !== undefined ? { remoteReads: params.reads } : {}),
2073
+ ...(machineEnv ? { machineEnv } : {}),
1907
2074
  ...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
1908
2075
  ...(params.context ? { context: params.context } : {}),
1909
- cwd: runnerCwd,
1910
- requestedCwd: params.requestedCwd ?? runnerCwd,
1911
- model,
2076
+ cwd: machine?.cwd ?? runnerCwd,
2077
+ requestedCwd: machine?.cwd ?? params.requestedCwd ?? runnerCwd,
2078
+ model: selectedModel,
1912
2079
  ...(contextLimit !== undefined ? { contextLimit } : {}),
1913
2080
  ...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
1914
- thinking: resolveEffectiveThinking(model, effectiveThinking),
2081
+ thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
1915
2082
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1916
- modelCandidates,
2083
+ ...(requestedModel ? { requestedModel } : {}),
1917
2084
  ...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
1918
2085
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1919
2086
  ...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
@@ -1922,6 +2089,7 @@ export function executeAsyncSingle(
1922
2089
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1923
2090
  extensions: agentConfig.extensions,
1924
2091
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
2092
+ ...(!externalRunner ? { requiredExtensions } : {}),
1925
2093
  mcpDirectTools: agentConfig.mcpDirectTools,
1926
2094
  mutationTools: agentConfig.mutationTools,
1927
2095
  completionGuard: agentConfig.completionGuard,
@@ -1939,7 +2107,7 @@ export function executeAsyncSingle(
1939
2107
  waitToolEnabled: params.waitToolEnabled,
1940
2108
  waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
1941
2109
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
1942
- definitionDigest: agentDefinitionDigest(agentConfig),
2110
+ definitionDigest,
1943
2111
  launchBindingTask: task,
1944
2112
  launchContractDigest,
1945
2113
  ...(extensionBindings ? { extensionBindings } : {}),
@@ -1980,6 +2148,7 @@ export function executeAsyncSingle(
1980
2148
  timeoutMs,
1981
2149
  deadlineAt,
1982
2150
  toolTimeoutMs,
2151
+ checkpointBeforeDeadlineMs: params.checkpointBeforeDeadlineMs,
1983
2152
  toolBudget: params.toolBudget,
1984
2153
  usageBudget: params.usageBudget,
1985
2154
  controlIntercomTarget,
@@ -1988,6 +2157,7 @@ export function executeAsyncSingle(
1988
2157
  launchContractDigest,
1989
2158
  launchResolvedExtensions,
1990
2159
  runFanoutBudget,
2160
+ hostAvailableBuiltins,
1991
2161
  ...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
1992
2162
  ...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
1993
2163
  ...(lane ? { lane } : {}),
@@ -2026,7 +2196,7 @@ export function executeAsyncSingle(
2026
2196
  const message = error instanceof Error ? error.message : String(error);
2027
2197
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${message}`);
2028
2198
  }
2029
-
2199
+ const finishSpawnResult = (spawnResult: SpawnRunnerResult): AsyncExecutionResult => {
2030
2200
  if (spawnResult.error) {
2031
2201
  if (spawnResult.startupDidNotProceed) {
2032
2202
  if (!spawnResult.runnerProcessInstanceId || params.activeAsyncCapacity?.rollbackBeforeRunnerProceed(spawnResult.runnerProcessInstanceId) !== true) params.activeAsyncCapacity?.rollback();
@@ -2103,4 +2273,6 @@ export function executeAsyncSingle(
2103
2273
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
2104
2274
  details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, launchResolvedExtensions, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.toolBudget ? { toolBudget: resolvedToolBudget.budget ?? params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) } as Details,
2105
2275
  };
2276
+ };
2277
+ return spawnResultOrPromise instanceof Promise ? spawnResultOrPromise.then(finishSpawnResult) : finishSpawnResult(spawnResultOrPromise);
2106
2278
  }