pi-subagents 0.64.0 → 0.65.1

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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +19 -17
  6. package/docs/configuration.md +23 -15
  7. package/docs/extension-api.md +18 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +15 -8
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +13 -3
  13. package/package.json +5 -4
  14. package/runner-server-preload.mjs +13 -0
  15. package/skills/pi-subagents/SKILL.md +2 -1
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  17. package/skills/pi-subagents/references/execution-controls.md +7 -3
  18. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  19. package/src/agents/agent-management.ts +47 -15
  20. package/src/api/capability-ceiling.ts +0 -1
  21. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  22. package/src/api/preflight.ts +7 -4
  23. package/src/extension/config.ts +4 -2
  24. package/src/extension/doctor.ts +2 -10
  25. package/src/extension/fanout-child.ts +9 -11
  26. package/src/extension/index.ts +58 -7
  27. package/src/extension/public-execution.ts +14 -0
  28. package/src/extension/rpc.ts +3 -2
  29. package/src/extension/schemas.ts +3 -2
  30. package/src/extension/tool-description.ts +13 -8
  31. package/src/integrations/pi-web-session-liveness.ts +73 -0
  32. package/src/intercom/native-supervisor-channel.ts +159 -95
  33. package/src/intercom/supervisor-ui.ts +244 -0
  34. package/src/missions/workflow-state.ts +37 -16
  35. package/src/runs/background/async-execution.ts +51 -25
  36. package/src/runs/background/async-job-tracker.ts +11 -0
  37. package/src/runs/background/async-resume.ts +14 -3
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/control-channel.ts +2 -204
  40. package/src/runs/background/notify.ts +2 -0
  41. package/src/runs/background/process-terminal.ts +1 -1
  42. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  43. package/src/runs/background/run-child-session.ts +614 -0
  44. package/src/runs/background/run-status.ts +0 -1
  45. package/src/runs/background/runner-aliases.ts +152 -0
  46. package/src/runs/background/runner-child-sessions.ts +31 -0
  47. package/src/runs/background/scheduled-runs.ts +18 -4
  48. package/src/runs/background/subagent-runner.ts +203 -901
  49. package/src/runs/foreground/async-steering-action.ts +1 -17
  50. package/src/runs/foreground/execution.ts +204 -378
  51. package/src/runs/foreground/foreground-control.ts +4 -0
  52. package/src/runs/foreground/foreground-history.ts +3 -1
  53. package/src/runs/foreground/prompt-audit.ts +9 -5
  54. package/src/runs/foreground/subagent-executor.ts +161 -91
  55. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  56. package/src/runs/shared/abort-recovery.ts +3 -3
  57. package/src/runs/shared/acceptance.ts +14 -1
  58. package/src/runs/shared/capability-ceiling.ts +1 -2
  59. package/src/runs/shared/child-hooks.ts +25 -0
  60. package/src/runs/shared/child-identity.ts +13 -2
  61. package/src/runs/shared/child-launch.ts +314 -0
  62. package/src/runs/shared/child-lifecycle.ts +25 -0
  63. package/src/runs/shared/child-runtime-config.ts +126 -0
  64. package/src/runs/shared/child-session.ts +337 -0
  65. package/src/runs/shared/child-tool-plan.ts +530 -0
  66. package/src/runs/shared/claude-code-adapter.ts +5 -1
  67. package/src/runs/shared/completion-guard.ts +1 -1
  68. package/src/runs/shared/external-cli-preflight.ts +16 -0
  69. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  71. package/src/runs/shared/model-exclusions.ts +84 -15
  72. package/src/runs/shared/model-fallback.ts +78 -6
  73. package/src/runs/shared/nested-events.ts +32 -48
  74. package/src/runs/shared/nested-path.ts +0 -14
  75. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  76. package/src/runs/shared/parallel-utils.ts +1 -2
  77. package/src/runs/shared/permissions.ts +0 -13
  78. package/src/runs/shared/process-signal.ts +4 -1
  79. package/src/runs/shared/run-fanout-budget.ts +0 -13
  80. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  81. package/src/runs/shared/structured-output.ts +17 -4
  82. package/src/runs/shared/subagent-control.ts +2 -0
  83. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  84. package/src/runs/shared/tool-availability.ts +18 -62
  85. package/src/runs/shared/tool-budget.ts +0 -14
  86. package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  87. package/src/runs/shared/worktree.ts +192 -40
  88. package/src/shared/child-session-name.ts +1 -1
  89. package/src/shared/jsonl-writer.ts +11 -0
  90. package/src/shared/model-response-aliases.ts +13 -0
  91. package/src/shared/thinking-ceiling.ts +0 -6
  92. package/src/shared/types.ts +63 -28
  93. package/src/shared/utils.ts +6 -4
  94. package/src/shared/watch-strategy.ts +2 -0
  95. package/src/slash/slash-commands.ts +0 -6
  96. package/src/tui/fleet-status.ts +1 -1
  97. package/src/tui/fleet.ts +0 -1
  98. package/src/tui/render.ts +254 -41
  99. package/src/watchdog/child-status.ts +0 -1
  100. package/src/watchdog/register-child.ts +12 -12
  101. package/src/workflows/scripted-workflow.ts +80 -7
  102. package/src/workflows/workflow-checklist.ts +4 -3
  103. package/src/runs/shared/child-protocol.ts +0 -415
  104. package/src/runs/shared/pi-args.ts +0 -1059
  105. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  106. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -1,20 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
2
  import * as path from "node:path";
4
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
4
  import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
6
5
  import { readStatus } from "../../shared/utils.ts";
7
6
  import { steeringReceipt } from "../background/steering.ts";
8
- import {
9
- consumeSteerAckFromDir,
10
- readSteerCapability,
11
- steerAcksDir,
12
- steerCapabilityPath,
13
- stepSteerInboxDir,
14
- writeSteerRequestToExistingDir,
15
- type SteerDeliveryMode,
16
- type SteerRequest,
17
- } from "../background/control-channel.ts";
7
+ import type { SteerDeliveryMode } from "../background/control-channel.ts";
18
8
 
19
9
  export interface WorkflowForegroundSteeringTarget {
20
10
  control: ForegroundRunControl;
@@ -40,7 +30,6 @@ function activeWorkflowError(state: SubagentState, workflowRunId: string, asyncD
40
30
  function controlIsLiveInWorkflow(control: ForegroundRunControl, workflowRunId: string, sessionId: string): boolean {
41
31
  return control.parentWorkflowRunId === workflowRunId
42
32
  && control.sessionId === sessionId
43
- && Boolean(control.workflowSteeringDir)
44
33
  && (control.activeChildren?.size ?? 0) > 0;
45
34
  }
46
35
 
@@ -77,17 +66,18 @@ function managementError(message: string): AgentToolResult<Details> {
77
66
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
78
67
  }
79
68
 
69
+ /**
70
+ * Steer a live workflow-owned foreground child through its in-process session.
71
+ * `steer` (and `auto`) interrupt the child at its next safe point; `follow_up`
72
+ * queues the message until the current run settles.
73
+ */
80
74
  export async function steerWorkflowForegroundTarget(input: {
81
75
  target: WorkflowForegroundSteeringTarget;
82
76
  message: string;
83
77
  mode?: SteerDeliveryMode;
84
78
  index?: number;
85
- signal?: AbortSignal;
86
- ackTimeoutMs?: number;
87
79
  }): Promise<AgentToolResult<Details>> {
88
80
  const { control, sourceRunId } = input.target;
89
- const routeDir = control.workflowSteeringDir;
90
- if (!routeDir || !fs.existsSync(routeDir)) return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
91
81
  const activeIndexes = [...(control.activeChildren?.keys() ?? [])].sort((left, right) => left - right);
92
82
  const index = input.index ?? (activeIndexes.length === 1 ? activeIndexes[0] : undefined);
93
83
  if (index === undefined) {
@@ -95,94 +85,30 @@ export async function steerWorkflowForegroundTarget(input: {
95
85
  ? `Foreground run '${control.runId}' has no live child session.`
96
86
  : `Foreground run '${control.runId}' has ${activeIndexes.length} live child sessions; provide index.`);
97
87
  }
98
- if (!activeIndexes.includes(index)) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
99
- const capability = readSteerCapability(routeDir, index);
100
- if (capability?.supported === false) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
88
+ const child = control.activeChildren?.get(index);
89
+ if (!child) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
90
+ if (!child.steer) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
101
91
 
102
- const request: SteerRequest = {
103
- type: "steer",
104
- id: randomUUID(),
105
- ts: Date.now(),
106
- message: input.message.trim(),
107
- ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}),
108
- targetIndex: index,
109
- source: "steer-action",
110
- };
111
- try {
112
- writeSteerRequestToExistingDir(stepSteerInboxDir(routeDir, index), request);
113
- } catch (error) {
114
- if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
115
- return managementError(`Foreground run '${control.runId}' has no live workflow steering route.`);
116
- }
117
- return managementError(`Failed to queue steering for foreground run ${control.runId}: ${error instanceof Error ? error.message : String(error)}`);
118
- }
119
-
120
- const deadline = Date.now() + (input.ackTimeoutMs ?? 3_000);
121
- let ack;
122
- let routeRemoved = false;
123
- while (Date.now() <= deadline) {
124
- ack = consumeSteerAckFromDir(steerAcksDir(routeDir, index), request.id);
125
- if (ack || input.signal?.aborted) break;
126
- if (!fs.existsSync(routeDir)) {
127
- routeRemoved = true;
128
- break;
129
- }
130
- await new Promise<void>((resolve) => setTimeout(resolve, Math.min(50, Math.max(1, deadline - Date.now()))));
131
- }
132
- if (routeRemoved || (!ack && !input.signal?.aborted && !fs.existsSync(routeDir))) {
133
- return managementError(`Foreground run '${control.runId}' has no live child session.`);
134
- }
135
- const target = ack?.state === "delivered"
136
- ? { index, state: "delivered" as const, deliveredAt: ack.ts }
137
- : ack?.state === "queued"
92
+ const message = input.message.trim();
93
+ const requestId = randomUUID();
94
+ const outcome = await child.steer({ message, ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}) });
95
+ const target = outcome.state === "delivered"
96
+ ? { index, state: "delivered" as const, deliveredAt: Date.now() }
97
+ : outcome.state === "queued"
138
98
  ? { index, state: "queued" as const }
139
- : ack?.state === "failed"
140
- ? { index, state: "failed" as const, reason: ack.message }
141
- : { index, state: "pending" as const };
99
+ : { index, state: "failed" as const, reason: outcome.reason };
142
100
  const steering = {
143
- requestId: request.id,
144
- state: ack?.state === "delivered" ? "delivered" as const : ack?.state === "failed" ? "failed" as const : "pending" as const,
145
- deliveryStatus: ack?.state === "delivered" ? "delivered" as const : "queued" as const,
101
+ requestId,
102
+ state: outcome.state === "delivered" ? "delivered" as const : outcome.state === "failed" ? "failed" as const : "pending" as const,
103
+ deliveryStatus: outcome.state === "delivered" ? "delivered" as const : "queued" as const,
146
104
  sourceRunId,
147
105
  targets: [target],
148
106
  };
149
- if (input.signal?.aborted) {
150
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering pending for foreground run ${control.runId} (request ${request.id}); caller aborted before acknowledgment.`) }], details: { mode: "management", results: [], steering } };
151
- }
152
- if (ack?.state === "delivered") {
153
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering delivered for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
154
- }
155
- if (ack?.state === "queued") {
156
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering queued for foreground run ${control.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering } };
107
+ if (outcome.state === "delivered") {
108
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering delivered for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
157
109
  }
158
- if (ack?.state === "failed") {
159
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering failed for foreground run ${control.runId} (request ${request.id}): ${ack.message}`) }], isError: true, details: { mode: "management", results: [], steering } };
110
+ if (outcome.state === "queued") {
111
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering queued for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
160
112
  }
161
- return { content: [{ type: "text", text: steeringReceipt(request.message, `Steering pending for foreground run ${control.runId} (request ${request.id}); no acknowledgment was received.`) }], details: { mode: "management", results: [], steering } };
162
- }
163
-
164
- export function workflowForegroundSteeringDir(asyncDirRoot: string, workflowRunId: string, childRunId: string): string {
165
- return path.join(asyncDirRoot, workflowRunId, "control", "workflow-foreground", childRunId);
166
- }
167
-
168
- export function removeWorkflowForegroundSteeringRoute(control: ForegroundRunControl): void {
169
- if (!control.workflowSteeringDir) return;
170
- try {
171
- fs.rmSync(control.workflowSteeringDir, { recursive: true, force: true });
172
- } catch (error) {
173
- console.warn(`[pi-subagents] Failed to remove workflow foreground steering route '${control.workflowSteeringDir}': ${error instanceof Error ? error.message : String(error)}`);
174
- }
175
- }
176
-
177
- export function workflowForegroundSteeringLaunchOptions(control: ForegroundRunControl | undefined, index: number): Pick<import("../../shared/types.ts").RunSyncOptions, "steerInboxDir" | "steerCapabilityPath" | "steerAckDir"> {
178
- if (!control?.workflowSteeringDir) return {};
179
- const steerInboxDir = stepSteerInboxDir(control.workflowSteeringDir, index);
180
- const steerAckDir = steerAcksDir(control.workflowSteeringDir, index);
181
- fs.mkdirSync(steerInboxDir, { recursive: true });
182
- fs.mkdirSync(steerAckDir, { recursive: true });
183
- return {
184
- steerInboxDir,
185
- steerCapabilityPath: steerCapabilityPath(control.workflowSteeringDir, index),
186
- steerAckDir,
187
- };
113
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering failed for foreground run ${control.runId} (request ${requestId}): ${outcome.reason ?? "unknown error"}`) }], isError: true, details: { mode: "management", results: [], steering } };
188
114
  }
@@ -101,9 +101,9 @@ export function planAbortRecovery(input: {
101
101
  if (input.alreadyResumed) return settle("resume already attempted");
102
102
  if (!input.sessionAvailable) return settle("retained session unavailable");
103
103
  if (input.stopped || input.interrupted) return settle("explicit stop or interrupt");
104
- // A compaction abort can leave the child process open until the runner's
105
- // terminal drain sends SIGTERM. The compaction + settlement markers make
106
- // that cleanup signal non-authoritative; explicit stop/interrupt still wins.
104
+ // An external-job runner reports a process signal when its terminal drain
105
+ // ends a child left open by a compaction abort. The compaction + settlement
106
+ // markers make that cleanup signal non-authoritative; explicit stop/interrupt still wins.
107
107
  if (input.processSignal && !compactionAbortCandidate) return settle("process terminated by signal");
108
108
  if (input.timedOut) return settle("elapsed timeout");
109
109
  if (input.toolBudgetExhausted || input.usageBudgetExhausted) return settle("budget exhausted");
@@ -202,10 +202,23 @@ export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInp
202
202
  return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
203
203
  }
204
204
  if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
205
- if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
205
+ if (acceptance !== undefined && acceptance !== false) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(gate, acceptance) };
206
206
  return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
207
207
  }
208
208
 
209
+ export function describeGateAcceptanceConflict(gate: unknown, acceptance: unknown): string {
210
+ const render = (value: unknown): string => {
211
+ let encoded: string;
212
+ try {
213
+ encoded = JSON.stringify(value) ?? String(value);
214
+ } catch {
215
+ encoded = String(value);
216
+ }
217
+ return encoded.length > 120 ? `${encoded.slice(0, 120)}...` : encoded;
218
+ };
219
+ return ` Both fields were present: gate=${render(gate)} acceptance=${render(acceptance)}.`;
220
+ }
221
+
209
222
  function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
210
223
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
211
224
  }
@@ -2,7 +2,6 @@ import { Buffer } from "node:buffer";
2
2
 
3
3
  export const SUBAGENT_CAPABILITY_CEILING_VERSION = 1 as const;
4
4
  export const SUBAGENT_CAPABILITY_CEILING_REGISTRY_KEY = "pi-subagents.capability-ceiling.v1";
5
- export const SUBAGENT_CAPABILITY_CEILING_ENV = "PI_SUBAGENT_CAPABILITY_CEILING_V1";
6
5
 
7
6
  export type SubagentCapabilityCeiling =
8
7
  | { allowedTools: readonly string[]; allowedAgents?: readonly string[]; denyExtensions?: boolean }
@@ -167,7 +166,7 @@ export function resolveSubagentCapabilityCeiling(sessionId: string | undefined,
167
166
  }
168
167
 
169
168
  export function resolveCurrentSubagentCapabilityCeiling(sessionId: string | undefined): ResolvedSubagentCapabilityCeiling | undefined {
170
- return resolveSubagentCapabilityCeiling(sessionId, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
169
+ return resolveSubagentCapabilityCeiling(sessionId, undefined);
171
170
  }
172
171
 
173
172
  export function isAgentAllowedByCapabilityCeiling(agentName: string, ceiling: ResolvedSubagentCapabilityCeiling | undefined): boolean {
@@ -0,0 +1,25 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import registerFanoutChildSubagentExtension from "../../extension/fanout-child.ts";
3
+ import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
4
+ import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
5
+ import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
6
+
7
+ /** Inline extension shape accepted by pi's resource loader (`extensionFactories`). */
8
+ export interface ChildHookExtension {
9
+ name: string;
10
+ factory: (pi: ExtensionAPI) => void;
11
+ }
12
+
13
+ /**
14
+ * The child-side hooks pi-subagents installs in every child, keyed off the
15
+ * launch config. The registrations live in `subagent-prompt-runtime.ts`,
16
+ * `fast-mode-extension.ts`, and `fanout-child.ts`.
17
+ */
18
+ export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension[] {
19
+ const hooks: ChildHookExtension[] = [
20
+ { name: "pi-subagents:prompt-runtime", factory: (pi) => registerSubagentPromptRuntime(pi, config) },
21
+ ];
22
+ if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
23
+ if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, config) });
24
+ return hooks;
25
+ }
@@ -14,11 +14,11 @@ export type AsyncStatusChildResolution =
14
14
  | { ok: false; code: "not_found" | "ambiguous"; message: string };
15
15
 
16
16
  export function asyncStatusChildIdentity(step: AsyncStatusStep, index: number): string {
17
- return step.workflowKey ?? step.runId ?? `step:${index}`;
17
+ return asyncStatusChildIdentityCandidates(step, index)[0]!;
18
18
  }
19
19
 
20
20
  export function asyncStatusChildIdentityCandidates(step: AsyncStatusStep, index: number): string[] {
21
- return [...new Set([step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
21
+ return [...new Set([step.childId, step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
22
22
  }
23
23
 
24
24
  export function resolveAsyncStatusChild(
@@ -49,3 +49,14 @@ export function resolveAsyncStatusChild(
49
49
  export function isStoppableAsyncStatusStep(step: AsyncStatusStep): boolean {
50
50
  return step.status === "pending" || step.status === "running";
51
51
  }
52
+
53
+ export function stopStoppableAsyncStatusChildren(
54
+ status: Pick<AsyncStatus, "steps">,
55
+ stopChild: ((childId: string, message?: string) => boolean) | undefined,
56
+ message: string,
57
+ ): void {
58
+ if (!stopChild) return;
59
+ for (const [index, step] of (status.steps ?? []).entries()) {
60
+ if (isStoppableAsyncStatusStep(step)) stopChild(asyncStatusChildIdentity(step, index), message);
61
+ }
62
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Builds the in-process launch for one child: the tool plan, the typed child
3
+ * runtime config the hooks read, and the session launch input. Foreground
4
+ * children are built in the parent process; background children are built in
5
+ * the detached runner from the step it received in its config.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchdog/child-status.ts";
10
+ import type { ThinkingLevel } from "../../shared/model-info.ts";
11
+ import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
12
+ import {
13
+ resolveChildDepth,
14
+ type LaunchResolvedChildExtensionsV1,
15
+ type ResolvedToolBudget,
16
+ type RunFanoutBudgetDescriptor,
17
+ } from "../../shared/types.ts";
18
+ import type { NestedPathEntry } from "./nested-path.ts";
19
+ import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
20
+ import type { PermissionRules } from "./permissions.ts";
21
+ import type { StructuredOutputRuntime } from "./structured-output.ts";
22
+ import type { ChildToolDiagnostic } from "./tool-availability.ts";
23
+ import type { RuntimeAcknowledgedChildExtensionsV1 } from "../../shared/types.ts";
24
+ import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
25
+ import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
26
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "./capability-ceiling.ts";
27
+ import {
28
+ isSubagentRuntimeExtensionPath,
29
+ projectLaunchResolvedChildExtensions,
30
+ resolvePiLaunchToolPlan,
31
+ supervisorChannelDir,
32
+ type PiLaunchToolPlan,
33
+ } from "./child-tool-plan.ts";
34
+ import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
35
+ import { createChildHooks } from "./child-hooks.ts";
36
+ import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
37
+
38
+ /** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
39
+ export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
40
+
41
+ /**
42
+ * The parts of the launching executor's own child runtime that a child it
43
+ * launches inherits. Serialized into the background runner config; the
44
+ * foreground path passes the executor's full `ChildRuntimeConfig`.
45
+ */
46
+ export type InheritedChildRuntime = Pick<ChildRuntimeConfig, "depth" | "maxDepth" | "nestedRoute" | "nestedParent" | "capabilityCeiling" | "thinkingCeiling" | "runFanoutBudget">;
47
+
48
+ export function inheritedChildRuntime(config: ChildRuntimeConfig | undefined): InheritedChildRuntime | undefined {
49
+ if (!config) return undefined;
50
+ return {
51
+ depth: config.depth,
52
+ ...(config.maxDepth !== undefined ? { maxDepth: config.maxDepth } : {}),
53
+ ...(config.nestedRoute ? { nestedRoute: config.nestedRoute } : {}),
54
+ ...(config.nestedParent ? { nestedParent: config.nestedParent } : {}),
55
+ ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
56
+ ...(config.thinkingCeiling ? { thinkingCeiling: config.thinkingCeiling } : {}),
57
+ ...(config.runFanoutBudget ? { runFanoutBudget: config.runFanoutBudget } : {}),
58
+ };
59
+ }
60
+
61
+ export interface BuildInProcessChildLaunchInput {
62
+ parentSessionId?: string;
63
+ forkCacheKey?: string;
64
+ sessionEnabled: boolean;
65
+ sessionDir?: string;
66
+ sessionFile?: string;
67
+ /** Model reference with the thinking suffix already applied. */
68
+ model?: string;
69
+ systemPromptMode?: "append" | "replace";
70
+ inheritProjectContext: boolean;
71
+ inheritGlobalContext: boolean;
72
+ inheritSkills: boolean;
73
+ requireReadTool?: boolean;
74
+ tools?: string[];
75
+ excludeTools?: string[];
76
+ extensions?: string[];
77
+ subagentOnlyExtensions?: string[];
78
+ systemPrompt?: string | null;
79
+ mcpDirectTools?: string[];
80
+ extensionBindings?: ExtensionBindings;
81
+ cwd: string;
82
+ intercomSessionName?: string;
83
+ sessionName?: string;
84
+ orchestratorIntercomTarget?: string;
85
+ runId?: string;
86
+ childAgentName: string;
87
+ childIndex: number;
88
+ nestedRoute?: { rootRunId: string; eventSink: string; controlInbox: string; capabilityToken: string };
89
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
90
+ structuredOutput?: StructuredOutputRuntime;
91
+ fast?: boolean;
92
+ modelCandidates?: readonly string[];
93
+ toolBudget?: ResolvedToolBudget;
94
+ permissionRules?: PermissionRules;
95
+ permissionAuditPath?: string;
96
+ childWatchdog?: ChildWatchdogConfig;
97
+ watchdogStatus?: (event: ChildWatchdogStatusEvent) => void;
98
+ waitToolEnabled?: boolean;
99
+ waitToolDefaultTimeoutMs?: number;
100
+ allowNestedSubagents?: boolean;
101
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
102
+ thinkingCeiling?: ThinkingLevel;
103
+ maxSubagentDepth?: number;
104
+ runtimeSnapshotHost?: McpRuntimeSnapshotHost;
105
+ /** The launching executor's own child runtime when it is itself an in-process child. */
106
+ inherited?: InheritedChildRuntime;
107
+ /**
108
+ * Which process hosts the session. The parent never loads ambient extensions
109
+ * or writes child environment values (it shares its process with the parent
110
+ * session); the runner loads ambient extensions when the tool plan allows
111
+ * them and exposes the child environment external extensions read.
112
+ */
113
+ host: "parent" | "runner";
114
+ }
115
+
116
+ export interface InProcessChildCapture {
117
+ structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
118
+ toolDiagnostic(): ChildToolDiagnostic | undefined;
119
+ runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensionsV1 | undefined;
120
+ }
121
+
122
+ export interface InProcessChildLaunch {
123
+ toolPlan: PiLaunchToolPlan;
124
+ config: ChildRuntimeConfig;
125
+ session: Omit<ChildSessionLaunch, "onExtensionError">;
126
+ capture: InProcessChildCapture;
127
+ launchResolvedExtensions: LaunchResolvedChildExtensionsV1;
128
+ warnings: string[];
129
+ capabilityAudit?: SubagentCapabilityAudit;
130
+ }
131
+
132
+ /** Escape XML-significant characters in a string for safe attribute interpolation. */
133
+ function escapeXmlAttr(value: string): string {
134
+ return value
135
+ .replace(/&/g, "&amp;")
136
+ .replace(/"/g, "&quot;")
137
+ .replace(/</g, "&lt;")
138
+ .replace(/>/g, "&gt;");
139
+ }
140
+
141
+ function inheritedCapabilityCeiling(inherited: InheritedChildRuntime | undefined): ResolvedSubagentCapabilityCeiling | undefined {
142
+ return inherited?.capabilityCeiling;
143
+ }
144
+
145
+ /** Environment values external child extensions read; only the runner applies them. */
146
+ function childProcessEnv(input: BuildInProcessChildLaunchInput, toolPlan: PiLaunchToolPlan): Record<string, string | undefined> {
147
+ const env: Record<string, string | undefined> = {};
148
+ env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
149
+ if (!toolPlan.capabilityCeiling && input.mcpDirectTools?.length) env[MCP_DIRECT_TOOLS_ENV] = input.mcpDirectTools.join(",");
150
+ else if (toolPlan.capabilityCeiling && toolPlan.effectiveMcpSelections.length && !toolPlan.capabilityCeiling.denyExtensions) {
151
+ env[MCP_DIRECT_TOOLS_ENV] = toolPlan.effectiveMcpSelections.map((selection) => selection.selector).join(",");
152
+ } else env[MCP_DIRECT_TOOLS_ENV] = "__none__";
153
+ return env;
154
+ }
155
+
156
+ function childStorage(input: BuildInProcessChildLaunchInput): ChildSessionStorage {
157
+ if (input.sessionFile) {
158
+ fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
159
+ return { kind: "file", sessionFile: input.sessionFile };
160
+ }
161
+ if (!input.sessionEnabled) return { kind: "memory" };
162
+ if (input.sessionDir) {
163
+ fs.mkdirSync(input.sessionDir, { recursive: true });
164
+ return { kind: "dir", sessionDir: input.sessionDir };
165
+ }
166
+ return { kind: "default" };
167
+ }
168
+
169
+ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput): InProcessChildLaunch {
170
+ const toolPlan = resolvePiLaunchToolPlan({
171
+ tools: input.tools,
172
+ excludeTools: input.excludeTools,
173
+ allowNestedSubagents: input.allowNestedSubagents,
174
+ extensions: input.extensions,
175
+ subagentOnlyExtensions: input.subagentOnlyExtensions,
176
+ mcpDirectTools: input.mcpDirectTools,
177
+ cwd: input.cwd,
178
+ requireReadTool: input.requireReadTool,
179
+ structuredOutput: Boolean(input.structuredOutput),
180
+ fast: input.fast,
181
+ model: input.model,
182
+ modelCandidates: input.modelCandidates,
183
+ capabilityCeiling: input.capabilityCeiling,
184
+ inheritedCapabilityCeiling: inheritedCapabilityCeiling(input.inherited),
185
+ agentName: input.childAgentName,
186
+ permissionRules: input.permissionRules,
187
+ runtimeSnapshotHost: input.runtimeSnapshotHost,
188
+ });
189
+
190
+ const inherited = input.inherited;
191
+ const fanout = toolPlan.fanoutAuthorized;
192
+ const inheritedRoute = inherited?.nestedRoute;
193
+ const parentRunId = input.runId ?? inherited?.nestedParent?.parentRunId ?? "";
194
+ const parentChildIndex = input.childIndex;
195
+ const parentDepth = inheritedRoute && inherited?.nestedParent ? inherited.nestedParent.depth + 1 : 1;
196
+ const parentPath: NestedPathEntry[] = [
197
+ ...(inherited?.nestedParent?.path ?? []),
198
+ ...(parentRunId ? [{ runId: parentRunId, stepIndex: parentChildIndex, agent: input.childAgentName }] : []),
199
+ ];
200
+ const nestedRoute = fanout ? (input.nestedRoute ?? inheritedRoute) : undefined;
201
+ const childDepth = resolveChildDepth(input.maxSubagentDepth, inherited);
202
+ const permissions = input.permissionRules && Object.keys(input.permissionRules).length > 0
203
+ ? { rules: input.permissionRules, ...(input.permissionAuditPath ? { auditPath: input.permissionAuditPath } : {}) }
204
+ : undefined;
205
+ let supervisorDir: string | undefined;
206
+ if (input.orchestratorIntercomTarget && input.parentSessionId && input.runId) {
207
+ supervisorDir = supervisorChannelDir(input.runId, input.childAgentName, input.childIndex);
208
+ fs.mkdirSync(path.join(supervisorDir, "requests"), { recursive: true });
209
+ fs.mkdirSync(path.join(supervisorDir, "replies"), { recursive: true });
210
+ }
211
+ const thinkingCeiling = intersectThinkingCeilings(input.thinkingCeiling, inherited?.thinkingCeiling);
212
+
213
+ let structuredValue: unknown;
214
+ let structuredAcceptanceReport: unknown;
215
+ let structuredCalled = false;
216
+ let structuredAcceptanceProvided = false;
217
+ let toolDiagnostic: ChildToolDiagnostic | undefined;
218
+ let acknowledgedIds: string[] | undefined;
219
+
220
+ const config: ChildRuntimeConfig = {
221
+ ...(input.runId ? { runId: input.runId } : {}),
222
+ agent: input.childAgentName,
223
+ childIndex: input.childIndex,
224
+ fanoutChild: fanout,
225
+ ...(input.sessionName?.trim() ? { sessionName: input.sessionName.trim() } : {}),
226
+ ...(input.intercomSessionName ? { intercomSessionName: input.intercomSessionName } : {}),
227
+ ...(input.orchestratorIntercomTarget ? { orchestratorTarget: input.orchestratorIntercomTarget } : {}),
228
+ ...(input.parentSessionId ? { orchestratorSessionId: input.parentSessionId, parentSessionId: input.parentSessionId } : {}),
229
+ ...(supervisorDir ? { supervisorChannelDir: supervisorDir } : {}),
230
+ ...(nestedRoute ? { nestedRoute } : {}),
231
+ ...(fanout && parentRunId ? { nestedParent: { parentRunId, parentChildIndex, depth: parentDepth, path: parentPath } } : {}),
232
+ ...(fanout && (input.runFanoutBudget ?? inherited?.runFanoutBudget) ? { runFanoutBudget: input.runFanoutBudget ?? inherited?.runFanoutBudget } : {}),
233
+ depth: childDepth.depth,
234
+ maxDepth: childDepth.maxDepth,
235
+ ...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
236
+ ...(thinkingCeiling ? { thinkingCeiling } : {}),
237
+ inheritProjectContext: input.inheritProjectContext,
238
+ inheritGlobalContext: input.inheritGlobalContext,
239
+ inheritSkills: input.inheritSkills,
240
+ ...(input.forkCacheKey?.trim() ? { forkCacheKey: input.forkCacheKey.trim() } : {}),
241
+ ...(permissions ? { permissions } : {}),
242
+ ...(input.toolBudget ? { toolBudget: input.toolBudget } : {}),
243
+ ...(input.childWatchdog ? { childWatchdog: input.childWatchdog } : {}),
244
+ ...(input.watchdogStatus ? { watchdogStatus: input.watchdogStatus } : {}),
245
+ waitTool: {
246
+ enabled: input.waitToolEnabled ?? true,
247
+ ...(input.waitToolDefaultTimeoutMs !== undefined ? { defaultTimeoutMs: input.waitToolDefaultTimeoutMs } : {}),
248
+ },
249
+ ...(input.structuredOutput
250
+ ? {
251
+ structuredOutput: {
252
+ schema: input.structuredOutput.schema,
253
+ ...(input.structuredOutput.acceptanceReportPath
254
+ ? { acceptanceReport: input.structuredOutput.acceptanceReportRequired ? "required" as const : "optional" as const }
255
+ : {}),
256
+ capture: (value: unknown, acceptanceReport: unknown) => {
257
+ structuredCalled = true;
258
+ structuredValue = value;
259
+ structuredAcceptanceProvided = acceptanceReport !== undefined;
260
+ structuredAcceptanceReport = acceptanceReport;
261
+ },
262
+ },
263
+ }
264
+ : {}),
265
+ ...(toolPlan.requiredChildTools.length > 0 ? { requiredTools: toolPlan.requiredChildTools } : {}),
266
+ ...(toolPlan.effectiveMcpTools.length > 0 ? { mcpDirectTools: toolPlan.effectiveMcpTools } : {}),
267
+ toolDiagnostic: (diagnostic) => { toolDiagnostic = diagnostic; },
268
+ runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
269
+ fast: input.fast === true,
270
+ };
271
+
272
+ const extensionPaths = toolPlan.extensionArgs.filter((extensionPath) => !isSubagentRuntimeExtensionPath(extensionPath));
273
+ const ambientExtensions = input.host === "runner" && !toolPlan.disableAmbientExtensions;
274
+ const launchResolvedExtensions = projectLaunchResolvedChildExtensions({
275
+ runtimeExtensions: toolPlan.runtimeExtensions,
276
+ configuredExtensions: toolPlan.configuredExtensions,
277
+ extensionArgs: toolPlan.extensionArgs,
278
+ disableAmbientExtensions: !ambientExtensions,
279
+ });
280
+ const taggedPrompt = input.systemPrompt !== undefined && input.systemPrompt !== null
281
+ ? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
282
+ : undefined;
283
+ const session: Omit<ChildSessionLaunch, "onExtensionError"> = {
284
+ cwd: input.cwd,
285
+ storage: childStorage(input),
286
+ ...(input.model ? { model: input.model } : {}),
287
+ ...(toolPlan.explicitToolAllowlist ? { tools: toolPlan.effectiveToolAllowlist } : {}),
288
+ ...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
289
+ extensionPaths,
290
+ ambientExtensions,
291
+ hooks: createChildHooks(config),
292
+ ...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
293
+ runtime: config,
294
+ noSkills: !input.inheritSkills,
295
+ noContextFiles: !input.inheritProjectContext,
296
+ ...(taggedPrompt !== undefined
297
+ ? input.systemPromptMode === "replace" ? { systemPrompt: taggedPrompt } : { appendSystemPrompt: taggedPrompt }
298
+ : {}),
299
+ };
300
+
301
+ return {
302
+ toolPlan,
303
+ config,
304
+ session,
305
+ capture: {
306
+ structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
307
+ toolDiagnostic: () => toolDiagnostic,
308
+ runtimeAcknowledgedExtensions: () => (acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined),
309
+ },
310
+ launchResolvedExtensions,
311
+ warnings: toolPlan.warnings,
312
+ ...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
313
+ };
314
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Child lifecycle projection: when the observed child session events mean
3
+ * the run is settling and the observer may start (or must cancel) its final
4
+ * drain window.
5
+ */
6
+ export type ChildLifecycleAction = "start-drain" | "cancel-drain" | "none";
7
+
8
+ export interface ChildLifecycleState {
9
+ compactionRetryActive: boolean;
10
+ }
11
+
12
+ export function projectChildLifecycle(event: { type?: string; willRetry?: unknown }, terminalAssistantStop = false, state?: ChildLifecycleState): ChildLifecycleAction {
13
+ if (event.type === "compaction_end") {
14
+ if (state) state.compactionRetryActive = event.willRetry === true;
15
+ return event.willRetry === true ? "cancel-drain" : "none";
16
+ }
17
+ if (event.type === "agent_start" || event.type === "auto_retry_start") {
18
+ if (state) state.compactionRetryActive = false;
19
+ }
20
+ if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
21
+ if (event.type === "agent_end" && state) state.compactionRetryActive = false;
22
+ if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
23
+ if (terminalAssistantStop) return "start-drain";
24
+ return "none";
25
+ }