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
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
9
  import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchdog/child-status.ts";
11
10
  import type { ThinkingLevel } from "../../shared/model-info.ts";
12
11
  import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
@@ -15,6 +14,7 @@ import {
15
14
  type LaunchResolvedChildExtensions,
16
15
  type ResolvedToolBudget,
17
16
  type RunFanoutBudgetDescriptor,
17
+ type HerdrMachineReference,
18
18
  } from "../../shared/types.ts";
19
19
  import type { NestedPathEntry } from "./nested-path.ts";
20
20
  import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
@@ -35,6 +35,8 @@ import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
35
35
  import { createCapturedChildHooks, withChildSessionErrorReporting } from "./child-hooks.ts";
36
36
  import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
37
37
  import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
38
+ import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
39
+ import { resolveRequiredChildExtensions, type RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
38
40
 
39
41
  /** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
40
42
  export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
@@ -44,7 +46,7 @@ export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
44
46
  * launches inherits. Serialized into the background runner config; the
45
47
  * foreground path passes the executor's full `ChildRuntimeConfig`.
46
48
  */
47
- export type InheritedChildRuntime = Pick<ChildRuntimeConfig, "depth" | "maxDepth" | "nestedRoute" | "nestedParent" | "capabilityCeiling" | "thinkingCeiling" | "runFanoutBudget">;
49
+ export type InheritedChildRuntime = Pick<ChildRuntimeConfig, "depth" | "maxDepth" | "nestedRoute" | "nestedParent" | "capabilityCeiling" | "thinkingCeiling" | "runFanoutBudget" | "requiredExtensions">;
48
50
 
49
51
  export function inheritedChildRuntime(config: ChildRuntimeConfig | undefined): InheritedChildRuntime | undefined {
50
52
  if (!config) return undefined;
@@ -56,10 +58,14 @@ export function inheritedChildRuntime(config: ChildRuntimeConfig | undefined): I
56
58
  ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
57
59
  ...(config.thinkingCeiling ? { thinkingCeiling: config.thinkingCeiling } : {}),
58
60
  ...(config.runFanoutBudget ? { runFanoutBudget: config.runFanoutBudget } : {}),
61
+ ...(config.requiredExtensions ? { requiredExtensions: config.requiredExtensions } : {}),
59
62
  };
60
63
  }
61
64
 
62
65
  export interface BuildInProcessChildLaunchInput {
66
+ machine?: HerdrMachineReference;
67
+ remoteSkillNames?: string[];
68
+ remoteReads?: string[] | false;
63
69
  parentSessionId?: string;
64
70
  forkCacheKey?: string;
65
71
  sessionEnabled: boolean;
@@ -76,6 +82,8 @@ export interface BuildInProcessChildLaunchInput {
76
82
  excludeTools?: string[];
77
83
  extensions?: string[];
78
84
  subagentOnlyExtensions?: string[];
85
+ /** Serialized launch snapshot; omitted only for a top-level parent-process lookup. */
86
+ requiredExtensions?: RequiredChildExtensionSnapshot;
79
87
  systemPrompt?: string | null;
80
88
  mcpDirectTools?: string[];
81
89
  extensionBindings?: ExtensionBindings;
@@ -90,7 +98,6 @@ export interface BuildInProcessChildLaunchInput {
90
98
  runFanoutBudget?: RunFanoutBudgetDescriptor;
91
99
  structuredOutput?: StructuredOutputRuntime;
92
100
  fast?: boolean;
93
- modelCandidates?: readonly string[];
94
101
  toolBudget?: ResolvedToolBudget;
95
102
  permissionRules?: PermissionRules;
96
103
  permissionAuditPath?: string;
@@ -112,13 +119,21 @@ export interface BuildInProcessChildLaunchInput {
112
119
  * them and exposes the child environment external extensions read.
113
120
  */
114
121
  host: "parent" | "runner";
122
+ /**
123
+ * Pi core tool names the host runtime provides. When set, child tool plans
124
+ * intersect known core slots with this set; declared non-core names remain
125
+ * for child startup validation. Review/scout lanes fail closed when a requested,
126
+ * still-permitted repository inspection tool is missing from that set.
127
+ */
128
+ hostAvailableBuiltins?: readonly string[];
115
129
  }
116
130
 
117
131
  export interface InProcessChildCapture {
118
- completionIntentContext?(): Pick<ExtensionContext, "model" | "modelRegistry"> | undefined;
132
+ completionIntentContext?(): ArbiterModelContext | undefined;
119
133
  structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
120
134
  toolDiagnostic(): ChildToolDiagnostic | undefined;
121
135
  runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensions | undefined;
136
+ finalDrainHeld(): boolean;
122
137
  }
123
138
 
124
139
  export interface InProcessChildLaunch {
@@ -174,24 +189,26 @@ function childStorage(input: BuildInProcessChildLaunchInput): ChildSessionStorag
174
189
  }
175
190
 
176
191
  export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput): InProcessChildLaunch {
192
+ const requiredExtensions = input.requiredExtensions ?? input.inherited?.requiredExtensions ?? resolveRequiredChildExtensions(input.parentSessionId);
177
193
  const toolPlan = resolvePiLaunchToolPlan({
178
194
  tools: input.tools,
179
195
  excludeTools: input.excludeTools,
180
196
  allowNestedSubagents: input.allowNestedSubagents,
181
197
  extensions: input.extensions,
182
198
  subagentOnlyExtensions: input.subagentOnlyExtensions,
199
+ requiredExtensions,
183
200
  mcpDirectTools: input.mcpDirectTools,
184
201
  cwd: input.cwd,
185
202
  requireReadTool: input.requireReadTool,
186
203
  structuredOutput: Boolean(input.structuredOutput),
187
204
  fast: input.fast,
188
205
  model: input.model,
189
- modelCandidates: input.modelCandidates,
190
206
  capabilityCeiling: input.capabilityCeiling,
191
207
  inheritedCapabilityCeiling: inheritedCapabilityCeiling(input.inherited),
192
208
  agentName: input.childAgentName,
193
209
  permissionRules: input.permissionRules,
194
210
  runtimeSnapshotHost: input.runtimeSnapshotHost,
211
+ hostAvailableBuiltins: input.hostAvailableBuiltins,
195
212
  });
196
213
 
197
214
  const inherited = input.inherited;
@@ -239,6 +256,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
239
256
  maxDepth: childDepth.maxDepth,
240
257
  ...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
241
258
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
259
+ ...(toolPlan.requiredExtensions.length > 0 ? { requiredExtensions: toolPlan.requiredExtensions } : {}),
242
260
  inheritProjectContext: input.inheritProjectContext,
243
261
  inheritGlobalContext: input.inheritGlobalContext,
244
262
  inheritSkills: input.inheritSkills,
@@ -280,17 +298,21 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
280
298
  configuredExtensions: toolPlan.configuredExtensions,
281
299
  extensionArgs: toolPlan.extensionArgs,
282
300
  disableAmbientExtensions: !ambientExtensions,
301
+ requiredExtensions: toolPlan.requiredExtensions,
283
302
  });
284
303
  const taggedPrompt = input.systemPrompt !== undefined && input.systemPrompt !== null
285
304
  ? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
286
305
  : undefined;
287
306
  const session: Omit<ChildSessionLaunch, "onExtensionError"> = {
288
307
  cwd: input.cwd,
308
+ ...(input.machine ? { machine: input.machine } : {}),
309
+ ...(input.machine ? { remoteResources: { agent: input.childAgentName, ...(input.remoteSkillNames ? { skills: input.remoteSkillNames } : {}), ...(input.remoteReads !== undefined ? { reads: input.remoteReads } : {}), ...(toolPlan.explicitToolAllowlist ? { toolCeiling: [...toolPlan.effectiveToolAllowlist] } : toolPlan.capabilityCeiling?.allowedTools ? { toolCeiling: [...toolPlan.capabilityCeiling.allowedTools] } : {}) } } : {}),
289
310
  storage: childStorage(input),
290
311
  ...(input.model ? { model: input.model } : {}),
291
312
  ...(toolPlan.explicitToolAllowlist ? { tools: toolPlan.effectiveToolAllowlist } : {}),
292
313
  ...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
293
314
  extensionPaths,
315
+ requiredExtensions: toolPlan.requiredExtensions,
294
316
  ambientExtensions,
295
317
  hooks: capturedHooks.hooks,
296
318
  ...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
@@ -311,6 +333,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
311
333
  structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
312
334
  toolDiagnostic: capturedHooks.toolDiagnostic,
313
335
  runtimeAcknowledgedExtensions: capturedHooks.runtimeAcknowledgedExtensions,
336
+ finalDrainHeld: capturedHooks.finalDrainHeld,
314
337
  },
315
338
  launchResolvedExtensions,
316
339
  warnings: toolPlan.warnings,
@@ -14,11 +14,14 @@ export function projectChildLifecycle(event: { type?: string; willRetry?: unknow
14
14
  if (state) state.compactionRetryActive = event.willRetry === true;
15
15
  return event.willRetry === true ? "cancel-drain" : "none";
16
16
  }
17
- if (event.type === "agent_start" || event.type === "auto_retry_start") {
17
+ if (event.type === "agent_start" || event.type === "auto_retry_start" || event.type === "turn_start") {
18
18
  if (state) state.compactionRetryActive = false;
19
+ return "cancel-drain";
20
+ }
21
+ if (event.type === "agent_end") {
22
+ if (event.willRetry !== true && state) state.compactionRetryActive = false;
23
+ return "cancel-drain";
19
24
  }
20
- if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
21
- if (event.type === "agent_end" && state) state.compactionRetryActive = false;
22
25
  if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
23
26
  if (terminalAssistantStop) return "start-drain";
24
27
  return "none";
@@ -1,4 +1,4 @@
1
- import type { JsonSchemaObject, ResolvedToolBudget, RunFanoutBudgetDescriptor } from "../../shared/types.ts";
1
+ import type { JsonSchemaObject, ResolvedToolBudget, RunFanoutBudgetDescriptor, SubagentState } from "../../shared/types.ts";
2
2
  import type { ThinkingLevel } from "../../shared/model-info.ts";
3
3
  import type { NestedPathEntry } from "./nested-path.ts";
4
4
  import type { PermissionRules } from "./permissions.ts";
@@ -6,6 +6,7 @@ import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchd
6
6
  import type { ResolvedWaitToolConfig } from "../background/wait-config.ts";
7
7
  import type { ChildToolDiagnostic } from "./tool-availability.ts";
8
8
  import type { ResolvedSubagentCapabilityCeiling } from "./capability-ceiling.ts";
9
+ import type { RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
9
10
 
10
11
  /**
11
12
  * Set in processes that host child sessions (the async runner). The extension
@@ -75,6 +76,8 @@ export interface ChildRuntimeConfig {
75
76
  depth: number;
76
77
  maxDepth?: number;
77
78
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
79
+ /** Immutable root-parent host policy propagated to nested native launches. */
80
+ requiredExtensions?: RequiredChildExtensionSnapshot;
78
81
  thinkingCeiling?: ThinkingLevel;
79
82
  inheritProjectContext?: boolean;
80
83
  inheritGlobalContext?: boolean;
@@ -86,6 +89,10 @@ export interface ChildRuntimeConfig {
86
89
  /** Receives child watchdog status events. */
87
90
  watchdogStatus?: (event: ChildWatchdogStatusEvent) => void;
88
91
  waitTool: ResolvedWaitToolConfig;
92
+ runtimeState?: SubagentState;
93
+ holdFinalDrain?: (held: boolean) => void;
94
+ /** Installation-local downward owner-channel barrier; never inherited or serialized into descendants. */
95
+ hasPendingSupervisorRequest?: () => boolean;
89
96
  structuredOutput?: ChildStructuredOutput;
90
97
  requiredTools?: string[];
91
98
  mcpDirectTools?: string[];
@@ -5,21 +5,15 @@
5
5
  * parent pi process for foreground children, the detached runner process for
6
6
  * background children. The factory is injectable so tests can script a child
7
7
  * without the real runtime; the default implementation wraps
8
- * `createAgentSession` from a pi package module and shares one `ModelRuntime`
9
- * across every child it creates.
8
+ * `createAgentSession` from a pi package module.
10
9
  */
11
10
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
12
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { pinChildCacheRetention } from "../../shared/child-cache-retention.ts";
13
13
  import { getAgentDir } from "../../shared/utils.ts";
14
14
  import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
15
- import { prepareReadonlySessionEvidence } from "./readonly-session-evidence.ts";
16
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
17
-
18
- // Private runtime authority for host continuation planning; injected factories have none.
19
- const readonlyModels = new WeakMap<ChildSession, { current: ModelInfo; resolve(reference: string): ModelInfo | undefined; requestBytes: number }>();
20
- export function getReadonlyChildModels(child: ChildSession) {
21
- return readonlyModels.get(child);
22
- }
15
+ import type { RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
16
+ import type { HerdrMachineReference, HerdrRemoteGitStatus } from "../../shared/types.ts";
23
17
 
24
18
  export interface ChildSessionEvent {
25
19
  type: string;
@@ -54,6 +48,12 @@ export type ChildSessionStorage =
54
48
 
55
49
  export interface ChildSessionLaunch {
56
50
  cwd: string;
51
+ /** Resolved pane-native placement. Local launches omit this field. */
52
+ machine?: HerdrMachineReference;
53
+ /** Process-local provider source owned by the invoking foreground parent. */
54
+ parentProviderRegistry?: ParentProviderRegistry;
55
+ /** Logical names resolved only by the remote ambient package. */
56
+ remoteResources?: { agent: string; skills?: string[]; toolCeiling?: string[]; reads?: string[] | false };
57
57
  storage: ChildSessionStorage;
58
58
  /** Model reference as the agent config names it (`provider/id`, optionally `:thinking`). */
59
59
  model?: string;
@@ -62,6 +62,8 @@ export interface ChildSessionLaunch {
62
62
  excludeTools?: string[];
63
63
  /** Extension files loaded for this child in addition to the inline hooks. */
64
64
  extensionPaths: string[];
65
+ /** Canonical required paths and safe evidence identities for fail-closed loading. */
66
+ requiredExtensions?: RequiredChildExtensionSnapshot;
65
67
  /**
66
68
  * Discover the ambient extensions (agent dir, project, settings) the way a
67
69
  * `pi` process would. False loads only `extensionPaths` and `hooks`.
@@ -93,16 +95,29 @@ export interface ChildSession {
93
95
  abort(): Promise<void>;
94
96
  /** Emits `session_shutdown` to the child's extensions and disposes the session; resolves once that shutdown work is done. */
95
97
  dispose(): Promise<void>;
98
+ /** True while Pi still has steering or follow-up input that has not started a turn. */
99
+ hasQueuedMessages?(): boolean;
96
100
  readonly messages: readonly AgentMessage[];
97
101
  readonly sessionFile: string | undefined;
98
102
  readonly sessionId: string;
99
103
  readonly modelId: string | undefined;
104
+ readonly machineEvidence?: { machineId: string; initial?: HerdrRemoteGitStatus; final?: HerdrRemoteGitStatus };
105
+ /** Event-updated pane-native status; reading it performs no network work. */
106
+ readonly placementSnapshot?: unknown;
100
107
  /** Set by the foreground host once the run detached; `factory.dispose()` leaves such children running. */
101
108
  detached?: boolean;
102
109
  /** Set by `factory.dispose()` before it aborts the child, so the host can report the stop truthfully. */
103
110
  shutDown?: boolean;
104
111
  }
105
112
 
113
+ export function childSessionHasQueuedMessages(session: ChildSession | undefined): boolean {
114
+ try {
115
+ return session?.hasQueuedMessages?.() === true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
106
121
  export interface ChildSessionFactory {
107
122
  create(launch: ChildSessionLaunch): Promise<ChildSession>;
108
123
  /** Abort and dispose every live attached child; detached children keep running and hold the shared runtime. */
@@ -124,6 +139,49 @@ export interface DefaultChildSessionFactoryOptions {
124
139
 
125
140
  type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
126
141
 
142
+ export type ParentProviderRegistry = Pick<ModelRuntimeInstance, "getRegisteredProviderIds" | "getRegisteredProviderConfig" | "getRegisteredNativeProvider">;
143
+
144
+ function inheritParentProviders(modelRuntime: ModelRuntimeInstance, parentProviders: ParentProviderRegistry, claimedProviderIds: ReadonlySet<string>, onError: ((error: ChildSessionExtensionError) => void) | undefined): boolean {
145
+ let providerIds: readonly string[];
146
+ try {
147
+ providerIds = parentProviders.getRegisteredProviderIds();
148
+ } catch (error) {
149
+ onError?.({ extensionPath: "<parent-providers>", event: "inherit_provider", error });
150
+ throw new Error(`Failed to enumerate parent providers: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
151
+ }
152
+ let registered = false;
153
+ for (const providerId of new Set(providerIds)) {
154
+ if (claimedProviderIds.has(providerId)) continue;
155
+ try {
156
+ const native = parentProviders.getRegisteredNativeProvider(providerId);
157
+ const config = native ? undefined : parentProviders.getRegisteredProviderConfig(providerId);
158
+ if (native) modelRuntime.registerNativeProvider(native);
159
+ else if (config) modelRuntime.registerProvider(providerId, config);
160
+ else throw new Error(`Parent provider '${providerId}' has no registered native provider or config.`);
161
+ registered = true;
162
+ } catch (error) {
163
+ onError?.({ extensionPath: `<parent-provider:${providerId}>`, event: "inherit_provider", error });
164
+ throw new Error(`Failed to inherit parent provider '${providerId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
165
+ }
166
+ }
167
+ return registered;
168
+ }
169
+
170
+ const CHILD_PROMPT_RUNTIME_EXTENSION_PATH = "<inline:pi-subagents:prompt-runtime>";
171
+
172
+ /** The prompt runtime filters parent-only context before ambient extensions inspect
173
+ * the child prompt. Other inline hooks keep their normal position after ambient
174
+ * extensions, and ambient extension order stays unchanged. */
175
+ function prioritizeChildPromptRuntime<T extends { extensions: Array<{ path: string }> }>(result: T): T {
176
+ const index = result.extensions.findIndex(({ path }) => path === CHILD_PROMPT_RUNTIME_EXTENSION_PATH);
177
+ if (index <= 0) return result;
178
+ const extensions = [...result.extensions];
179
+ const [promptRuntime] = extensions.splice(index, 1);
180
+ if (!promptRuntime) return result;
181
+ extensions.unshift(promptRuntime);
182
+ return { ...result, extensions };
183
+ }
184
+
127
185
  /** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
128
186
  let loading: Promise<unknown> = Promise.resolve();
129
187
 
@@ -148,34 +206,39 @@ function applyProcessEnv(values: Record<string, string | undefined> | undefined)
148
206
  }
149
207
  }
150
208
 
151
- async function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined): Promise<void> {
152
- if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return;
209
+ function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined, requiredPaths: ReadonlySet<string>): { claimedProviderIds: Set<string>; registered: boolean } {
210
+ const claimedProviderIds = new Set<string>();
211
+ if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return { claimedProviderIds, registered: false };
153
212
  const { runtime } = loader.getExtensions();
154
213
  let registered = false;
155
214
  for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations ?? []) {
215
+ claimedProviderIds.add(name);
156
216
  try {
157
217
  modelRuntime.registerProvider(name, config);
158
218
  registered = true;
159
219
  } catch (error) {
160
220
  onError?.({ extensionPath, event: "register_provider", error });
221
+ if (requiredPaths.has(extensionPath)) throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
161
222
  }
162
223
  }
163
224
  if (Array.isArray(runtime.pendingProviderRegistrations)) runtime.pendingProviderRegistrations = [];
164
225
  for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations ?? []) {
226
+ claimedProviderIds.add(provider.id);
165
227
  try {
166
228
  modelRuntime.registerNativeProvider(provider);
167
229
  registered = true;
168
230
  } catch (error) {
169
231
  onError?.({ extensionPath, event: "register_provider", error });
232
+ if (requiredPaths.has(extensionPath)) throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
170
233
  }
171
234
  }
172
235
  if (Array.isArray(runtime.pendingNativeProviderRegistrations)) runtime.pendingNativeProviderRegistrations = [];
173
- if (registered) await modelRuntime.refresh({ allowNetwork: false });
236
+ return { claimedProviderIds, registered };
174
237
  }
175
238
 
176
239
  /**
177
- * Default factory: real pi sessions sharing one `ModelRuntime`, created lazily
178
- * on the first child launch and dropped on `dispose()`.
240
+ * Default factory: detached/background sessions retain the existing shared
241
+ * runtime; each parent-bound foreground launch gets an isolated runtime.
179
242
  */
180
243
  export function createDefaultChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
181
244
  const loadPiCodingAgent = options.loadPiCodingAgent ?? (() => import("@earendil-works/pi-coding-agent"));
@@ -193,13 +256,18 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
193
256
  };
194
257
  return {
195
258
  async create(launch) {
196
- const observeReadonly = prepareReadonlySessionEvidence(launch);
197
259
  const pi = await loadPiCodingAgent();
198
- const modelRuntime = await sharedRuntime(pi);
260
+ const modelRuntime = launch.parentProviderRegistry
261
+ ? await pi.ModelRuntime.create()
262
+ : await sharedRuntime(pi);
199
263
  const agentDir = getAgentDir();
200
264
  const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
201
- // Headless sessions skip Pi's CLI theme setup; extensions still need ctx.ui.theme.
202
- if (typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
265
+ // Foreground children share Pi's global theme with the parent, so reinitializing it
266
+ // would overwrite the parent's active light/dark appearance. Detached runners have
267
+ // no initialized theme and must initialize one for headless extension renderers.
268
+ const themeKey = Symbol.for("@earendil-works/pi-coding-agent:theme");
269
+ const themeInitialized = Boolean((globalThis as Record<symbol, unknown>)[themeKey]);
270
+ if (!themeInitialized && typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
203
271
  const loader = new pi.DefaultResourceLoader({
204
272
  cwd: launch.cwd,
205
273
  agentDir,
@@ -211,17 +279,30 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
211
279
  noContextFiles: launch.noContextFiles,
212
280
  additionalExtensionPaths: launch.extensionPaths,
213
281
  extensionFactories: launch.hooks,
282
+ extensionsOverride: prioritizeChildPromptRuntime,
214
283
  ...(launch.systemPrompt !== undefined ? { systemPrompt: launch.systemPrompt } : {}),
215
284
  ...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
216
285
  });
217
286
  const open = async () => {
287
+ const requiredPaths = new Set((launch.requiredExtensions ?? []).map(({ path }) => path));
218
288
  applyProcessEnv(launch.processEnv);
219
289
  if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
220
- observeReadonly?.loadingHooks(true);
221
- try { await loader.reload(); } finally { observeReadonly?.loadingHooks(false); }
222
- await flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
223
- // No await between receipt validation and the SDK's permissive file open.
224
- observeReadonly?.beforeOpen();
290
+ await loader.reload();
291
+ const loadErrors = requiredPaths.size > 0
292
+ ? loader.getExtensions().errors.filter(({ path }) => requiredPaths.has(path)) : [];
293
+ if (loadErrors.length > 0) throw new Error(`Required child extension failed to load: ${loadErrors.map(({ path, error }) => `${path}: ${error}`).join("; ")}`);
294
+ const queued = flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError, requiredPaths);
295
+ const inherited = launch.parentProviderRegistry
296
+ ? inheritParentProviders(modelRuntime, launch.parentProviderRegistry, queued.claimedProviderIds, launch.onExtensionError)
297
+ : false;
298
+ if (queued.registered || inherited) {
299
+ try {
300
+ await modelRuntime.refresh({ allowNetwork: false });
301
+ } catch (error) {
302
+ launch.onExtensionError?.({ extensionPath: "<provider-refresh>", event: "refresh_providers", error });
303
+ throw new Error(`Failed to refresh child providers: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
304
+ }
305
+ }
225
306
  const sessionManager = launch.storage.kind === "file"
226
307
  ? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
227
308
  : launch.storage.kind === "dir"
@@ -229,7 +310,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
229
310
  : launch.storage.kind === "memory"
230
311
  ? pi.SessionManager.inMemory(launch.cwd)
231
312
  : pi.SessionManager.create(launch.cwd);
232
- observeReadonly?.opened(sessionManager);
233
313
  const resolvedModel = launch.model
234
314
  ? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
235
315
  : undefined;
@@ -247,6 +327,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
247
327
  settingsManager,
248
328
  sessionStartEvent: { type: "session_start", reason: "startup" },
249
329
  });
330
+ pinChildCacheRetention(session.agent);
250
331
  try {
251
332
  await session.bindExtensions({
252
333
  mode: "print",
@@ -261,9 +342,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
261
342
  const opened = loading.catch(() => {}).then(open);
262
343
  loading = opened;
263
344
  const session = await opened;
264
- let evidence: ReturnType<NonNullable<typeof observeReadonly>["observe"]>;
265
- try { evidence = observeReadonly?.observe(pi, modelRuntime, session); }
266
- catch (error) { session.dispose(); throw error; }
267
345
  let pending: Promise<void> | undefined;
268
346
  // pi's own hosts emit `session_shutdown` before disposing a session so the
269
347
  // extensions loaded into it (ambient extensions included) release their
@@ -272,28 +350,21 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
272
350
  try {
273
351
  const runner = session.extensionRunner;
274
352
  if (runner.hasHandlers("session_shutdown")) {
275
- evidence?.beforeShutdown();
276
- const settled = await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }).then(() => true), new Promise<false>((resolve) => setTimeout(() => resolve(false), shutdownTimeoutMs).unref?.())]);
277
- if (!settled) evidence?.invalidate();
353
+ await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }), new Promise<void>((resolve) => setTimeout(resolve, shutdownTimeoutMs).unref?.())]);
278
354
  }
279
355
  } catch (error) {
280
- evidence?.invalidate();
281
356
  launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
282
357
  } finally {
283
358
  session.dispose();
284
- evidence?.finish(child);
285
359
  }
286
360
  };
287
361
  const child: ChildSession = {
288
362
  subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
289
- prompt: (text) => {
290
- if (!evidence) return session.prompt(text);
291
- try { evidence.start(); } catch (error) { return Promise.reject(error); }
292
- return session.prompt(text).then(() => evidence?.settled(), (error) => { evidence?.invalidate(); throw error; });
293
- },
294
- steer: (text) => { evidence?.invalidate(); return session.steer(text); },
295
- followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
296
- abort: () => { evidence?.invalidate(); return session.abort(); },
363
+ prompt: (text) => session.prompt(text),
364
+ steer: (text) => session.steer(text),
365
+ followUp: (text) => session.followUp(text),
366
+ abort: () => session.abort(),
367
+ hasQueuedMessages: () => session.agent?.hasQueuedMessages?.() === true,
297
368
  dispose: () => {
298
369
  if (!pending) {
299
370
  live.delete(child);
@@ -309,16 +380,6 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
309
380
  get sessionId() { return session.sessionId; },
310
381
  get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
311
382
  };
312
- if (evidence && session.model) readonlyModels.set(child, {
313
- current: toModelInfo(session.model),
314
- requestBytes: Buffer.byteLength(session.systemPrompt) + Buffer.byteLength(JSON.stringify(session.agent.state.tools)),
315
- resolve(reference) {
316
- try {
317
- const resolved = pi.resolveCliModel({ cliModel: reference, modelRuntime });
318
- return !resolved.error && resolved.model ? toModelInfo(resolved.model) : undefined;
319
- } catch { return undefined; }
320
- },
321
- });
322
383
  live.add(child);
323
384
  return child;
324
385
  },
@@ -340,10 +401,24 @@ let activeFactoryModule: string | undefined;
340
401
 
341
402
  /** The process-wide factory foreground runs use unless a run passes its own. */
342
403
  export function childSessionFactory(): ChildSessionFactory {
343
- activeFactory ??= createDefaultChildSessionFactory();
404
+ activeFactory ??= createLazyPlacementFactory(createDefaultChildSessionFactory());
344
405
  return activeFactory;
345
406
  }
346
407
 
408
+ function createLazyPlacementFactory(local: ChildSessionFactory): ChildSessionFactory {
409
+ let placed: ChildSessionFactory | undefined;
410
+ const factory = async () => placed ??= (await import("./herdr-placed-run.ts")).createPlacementAwareChildSessionFactory(local);
411
+ return {
412
+ async create(launch) { return launch.machine ? (await factory()).create(launch) : local.create(launch); },
413
+ async dispose() { if (placed) await placed.dispose(); else await local.dispose(); },
414
+ };
415
+ }
416
+
417
+ /** Default factory including pane-native placement; detached runners use the same boundary. */
418
+ export function createPlacementChildSessionFactory(options: DefaultChildSessionFactoryOptions = {}): ChildSessionFactory {
419
+ return createLazyPlacementFactory(createDefaultChildSessionFactory(options));
420
+ }
421
+
347
422
  /**
348
423
  * Replace the process-wide factory. Tests install a scripted factory; passing
349
424
  * undefined restores the default on next use.