pi-subagents 0.64.0 → 0.65.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 (89) hide show
  1. package/CHANGELOG.md +33 -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 +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +2 -2
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +43 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/process-terminal.ts +1 -1
  34. package/src/runs/background/run-child-session.ts +613 -0
  35. package/src/runs/background/run-status.ts +0 -1
  36. package/src/runs/background/runner-aliases.ts +125 -0
  37. package/src/runs/background/runner-child-sessions.ts +31 -0
  38. package/src/runs/background/scheduled-runs.ts +18 -4
  39. package/src/runs/background/subagent-runner.ts +184 -901
  40. package/src/runs/foreground/async-steering-action.ts +1 -17
  41. package/src/runs/foreground/execution.ts +189 -377
  42. package/src/runs/foreground/foreground-control.ts +4 -0
  43. package/src/runs/foreground/subagent-executor.ts +100 -57
  44. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  45. package/src/runs/shared/abort-recovery.ts +3 -3
  46. package/src/runs/shared/capability-ceiling.ts +1 -2
  47. package/src/runs/shared/child-hooks.ts +25 -0
  48. package/src/runs/shared/child-identity.ts +13 -2
  49. package/src/runs/shared/child-launch.ts +314 -0
  50. package/src/runs/shared/child-lifecycle.ts +25 -0
  51. package/src/runs/shared/child-runtime-config.ts +126 -0
  52. package/src/runs/shared/child-session.ts +342 -0
  53. package/src/runs/shared/child-tool-plan.ts +530 -0
  54. package/src/runs/shared/claude-code-adapter.ts +5 -1
  55. package/src/runs/shared/completion-guard.ts +1 -1
  56. package/src/runs/shared/external-cli-preflight.ts +16 -0
  57. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  58. package/src/runs/shared/model-exclusions.ts +82 -14
  59. package/src/runs/shared/model-fallback.ts +47 -4
  60. package/src/runs/shared/nested-events.ts +29 -45
  61. package/src/runs/shared/nested-path.ts +0 -14
  62. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  63. package/src/runs/shared/parallel-utils.ts +0 -2
  64. package/src/runs/shared/permissions.ts +0 -13
  65. package/src/runs/shared/process-signal.ts +4 -1
  66. package/src/runs/shared/run-fanout-budget.ts +0 -13
  67. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  68. package/src/runs/shared/structured-output.ts +17 -4
  69. package/src/runs/shared/subagent-control.ts +2 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  71. package/src/runs/shared/tool-availability.ts +18 -62
  72. package/src/runs/shared/tool-budget.ts +0 -14
  73. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  74. package/src/runs/shared/worktree.ts +117 -30
  75. package/src/shared/child-session-name.ts +1 -1
  76. package/src/shared/jsonl-writer.ts +11 -0
  77. package/src/shared/thinking-ceiling.ts +0 -6
  78. package/src/shared/types.ts +55 -28
  79. package/src/shared/utils.ts +3 -4
  80. package/src/slash/slash-commands.ts +0 -6
  81. package/src/tui/fleet.ts +0 -1
  82. package/src/tui/render.ts +233 -31
  83. package/src/watchdog/child-status.ts +0 -1
  84. package/src/watchdog/register-child.ts +12 -12
  85. package/src/workflows/scripted-workflow.ts +48 -1
  86. package/src/runs/shared/child-protocol.ts +0 -415
  87. package/src/runs/shared/pi-args.ts +0 -1059
  88. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  89. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -1,1059 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import {
7
- encodeNestedPathEnv,
8
- parseNestedPathEnv,
9
- type NestedPathEntry,
10
- } from "./nested-path.ts";
11
- import {
12
- formatUnresolvedMcpDirectToolSelectors,
13
- resolveMcpDirectToolResolution,
14
- type McpConfig,
15
- type McpRuntimeSnapshotHost,
16
- type ResolvedMcpDirectToolSelection,
17
- } from "./mcp-direct-tool-allowlist.ts";
18
- import { resolvePiPackageRoot } from "./pi-spawn.ts";
19
- import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
20
- import { RUNTIME_EXTENSION_ACK_PATH_ENV } from "./runtime-acknowledged-extensions.ts";
21
- import {
22
- STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV,
23
- STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV,
24
- STRUCTURED_OUTPUT_CAPTURE_ENV,
25
- STRUCTURED_OUTPUT_SCHEMA_ENV,
26
- type StructuredOutputRuntime,
27
- } from "./structured-output.ts";
28
- import {
29
- TEMP_ROOT_DIR,
30
- type JsonSchemaObject,
31
- type LaunchResolvedChildExtensionsV1,
32
- type ResolvedToolBudget,
33
- type RunFanoutBudgetDescriptor,
34
- } from "../../shared/types.ts";
35
- import { THINKING_LEVELS } from "../../shared/model-info.ts";
36
- import { decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
37
- import { encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDGET_ENV } from "./run-fanout-budget.ts";
38
- import {
39
- TOOL_BUDGET_ENV,
40
- TOOL_BUDGET_ZERO_AUTH_ENV,
41
- encodeToolBudgetEnv,
42
- } from "./tool-budget.ts";
43
- import {
44
- CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
45
- MCP_DIRECT_CHILD_TOOLS_ENV,
46
- REQUIRED_CHILD_TOOLS_ENV,
47
- } from "./tool-availability.ts";
48
- import {
49
- CHILD_WATCHDOG_CONFIG_ENV,
50
- type ChildWatchdogConfig,
51
- } from "../../watchdog/child-status.ts";
52
- import { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
53
- import {
54
- PI_CODING_AGENT_PACKAGE_ROOT_ENV,
55
- getAgentDir,
56
- } from "../../shared/utils.ts";
57
- import {
58
- encodePermissionRules,
59
- PERMISSION_AUDIT_PATH_ENV,
60
- PERMISSION_POLICY_ENV,
61
- type PermissionRules,
62
- } from "./permissions.ts";
63
- import {
64
- SUBAGENT_CAPABILITY_CEILING_ENV,
65
- capabilityCeilingAgentRestrictionSources,
66
- decodeSubagentCapabilityCeiling,
67
- encodeSubagentCapabilityCeiling,
68
- intersectSubagentCapabilityCeilings,
69
- isAgentAllowedByCapabilityCeiling,
70
- type ResolvedSubagentCapabilityCeiling,
71
- type SubagentCapabilityAudit,
72
- } from "./capability-ceiling.ts";
73
-
74
- const TASK_ARG_LIMIT = 8000;
75
-
76
- /**
77
- * Env override for how the task text reaches the child process. Endpoint
78
- * protection (EDR) pre-execution command-line scanning may deny exec of
79
- * children whose argv embeds a long natural-language task, which surfaces
80
- * as an immediate zero-activity SIGKILL. File delivery keeps the task out
81
- * of argv entirely.
82
- */
83
- export const SUBAGENT_TASK_DELIVERY_ENV = "PI_SUBAGENT_TASK_DELIVERY";
84
-
85
- export type SubagentTaskDelivery = "auto" | "file";
86
-
87
- export function resolveSubagentTaskDelivery(
88
- env: NodeJS.ProcessEnv = process.env,
89
- ): SubagentTaskDelivery {
90
- return env[SUBAGENT_TASK_DELIVERY_ENV]?.trim().toLowerCase() === "file"
91
- ? "file"
92
- : "auto";
93
- }
94
-
95
- export function shouldDeliverTaskViaFile(
96
- task: string,
97
- delivery: SubagentTaskDelivery,
98
- platform: NodeJS.Platform = process.platform,
99
- ): boolean {
100
- return delivery === "file" || platform === "darwin" || task.length > TASK_ARG_LIMIT;
101
- }
102
- const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
103
- const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
104
- path.dirname(fileURLToPath(import.meta.url)),
105
- "subagent-prompt-runtime.ts",
106
- );
107
- const FANOUT_CHILD_EXTENSION_PATH = path.join(
108
- path.dirname(fileURLToPath(import.meta.url)),
109
- "..",
110
- "..",
111
- "extension",
112
- "fanout-child.ts",
113
- );
114
- const FAST_MODE_EXTENSION_PATH = path.join(
115
- path.dirname(fileURLToPath(import.meta.url)),
116
- "fast-mode-extension.ts",
117
- );
118
- const FAST_MODE_ALLOWED_MODELS = new Set([
119
- "openai-codex/gpt-5.6-luna",
120
- "openai-codex/gpt-5.6-sol",
121
- ]);
122
- const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
123
- export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
124
- export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
125
- "PI_SUBAGENT_ORCHESTRATOR_TARGET";
126
- export const SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV =
127
- "PI_SUBAGENT_ORCHESTRATOR_SESSION_ID";
128
- export const SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV =
129
- "PI_SUBAGENT_SUPERVISOR_CHANNEL_DIR";
130
- export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
131
- export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
132
- export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
133
- export const SUBAGENT_FANOUT_CHILD_ENV = "PI_SUBAGENT_FANOUT_CHILD";
134
- export const SUBAGENT_PARENT_EVENT_SINK_ENV = "PI_SUBAGENT_PARENT_EVENT_SINK";
135
- export const SUBAGENT_PARENT_CONTROL_INBOX_ENV =
136
- "PI_SUBAGENT_PARENT_CONTROL_INBOX";
137
- export const SUBAGENT_PARENT_ROOT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_ROOT_RUN_ID";
138
- export const SUBAGENT_PARENT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_RUN_ID";
139
- export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX";
140
- export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
141
- export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
142
- export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV =
143
- "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
144
- export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
145
- export const SUBAGENT_FORK_CACHE_KEY_ENV = "PI_SUBAGENT_FORK_CACHE_KEY";
146
- export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
147
- export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
148
- export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
149
- export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
150
- export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
151
- export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
152
-
153
- export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
154
- const parent = parentSessionId?.trim();
155
- if (!parent) return undefined;
156
- const digest = createHash("sha256").update(parent).digest("hex").slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH - "pi-fork:".length);
157
- return `pi-fork:${digest}`;
158
- }
159
-
160
- export interface BuildPiArgsInput {
161
- parentSessionId?: string;
162
- forkCacheKey?: string;
163
- baseArgs: string[];
164
- task: string;
165
- sessionEnabled: boolean;
166
- sessionDir?: string;
167
- sessionFile?: string;
168
- model?: string;
169
- thinking?: string | false;
170
- systemPromptMode?: "append" | "replace";
171
- inheritProjectContext: boolean;
172
- inheritGlobalContext: boolean;
173
- inheritSkills: boolean;
174
- requireReadTool?: boolean;
175
- tools?: string[];
176
- excludeTools?: string[];
177
- extensions?: string[];
178
- subagentOnlyExtensions?: string[];
179
- systemPrompt?: string | null;
180
- mcpDirectTools?: string[];
181
- mcpConfig?: McpConfig;
182
- runtimeServerNames?: string[];
183
- cwd?: string;
184
- promptFileStem?: string;
185
- intercomSessionName?: string;
186
- /** Human-readable display name for the child session (agent + task excerpt,
187
- * derived by the caller). Passed to the child as PI_SUBAGENT_SESSION_NAME;
188
- * the prompt runtime applies it via pi.setSessionName unless an intercom
189
- * target takes precedence. */
190
- sessionName?: string;
191
- orchestratorIntercomTarget?: string;
192
- runId?: string;
193
- childAgentName?: string;
194
- childIndex?: number;
195
- parentEventSink?: string;
196
- parentControlInbox?: string;
197
- parentRootRunId?: string;
198
- parentRunId?: string;
199
- parentChildIndex?: number;
200
- parentDepth?: number;
201
- parentPath?: NestedPathEntry[];
202
- parentCapabilityToken?: string;
203
- runFanoutBudget?: RunFanoutBudgetDescriptor;
204
- steerInboxDir?: string;
205
- steerCapabilityPath?: string;
206
- steerAckDir?: string;
207
- structuredOutput?: StructuredOutputRuntime;
208
- fast?: boolean;
209
- modelCandidates?: readonly string[];
210
- toolBudget?: ResolvedToolBudget;
211
- allowZeroToolBudget?: boolean;
212
- permissionRules?: PermissionRules;
213
- permissionAuditPath?: string;
214
- childWatchdog?: ChildWatchdogConfig;
215
- /**
216
- * Per-launch override of the task delivery mode. Startup-retry paths set
217
- * this to "file" after an unexplained zero-activity SIGKILL so the retry
218
- * keeps the task text out of the child's argv.
219
- */
220
- taskDelivery?: SubagentTaskDelivery;
221
- waitToolEnabled?: boolean;
222
- waitToolDefaultTimeoutMs?: number;
223
- allowNestedSubagents?: boolean;
224
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
225
- thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
226
- extensionBindings?: ExtensionBindings;
227
- runtimeSnapshotHost?: McpRuntimeSnapshotHost;
228
- }
229
-
230
- export interface BuildPiArgsResult {
231
- args: string[];
232
- env: Record<string, string | undefined>;
233
- warnings: string[];
234
- tempDir?: string;
235
- toolDiagnosticPath?: string;
236
- runtimeAcknowledgedExtensionsPath?: string;
237
- capabilityAudit?: SubagentCapabilityAudit;
238
- }
239
-
240
- function sanitizeSupervisorChannelSegment(value: string): string {
241
- return (
242
- value
243
- .trim()
244
- .replace(/[^A-Za-z0-9._-]+/g, "-")
245
- .replace(/^-+|-+$/g, "") || "unknown"
246
- );
247
- }
248
-
249
- function supervisorChannelDir(
250
- runId: string,
251
- agent: string,
252
- childIndex: number,
253
- ): string {
254
- return path.join(
255
- TEMP_ROOT_DIR,
256
- "supervisor-channels",
257
- `${sanitizeSupervisorChannelSegment(runId)}-${sanitizeSupervisorChannelSegment(agent)}-${childIndex}`,
258
- );
259
- }
260
-
261
- export function applyThinkingSuffix(
262
- model: string | undefined,
263
- thinking: string | false | undefined,
264
- replaceExisting = false,
265
- ): string | undefined {
266
- if (!model || !thinking) return model;
267
- const colonIdx = model.lastIndexOf(":");
268
- if (
269
- colonIdx !== -1 &&
270
- THINKING_LEVELS.some((level) => level === model.substring(colonIdx + 1))
271
- ) {
272
- return replaceExisting ? `${model.slice(0, colonIdx)}:${thinking}` : model;
273
- }
274
- return `${model}:${thinking}`;
275
- }
276
-
277
- function stripThinkingSuffix(model: string): string {
278
- const colonIdx = model.lastIndexOf(":");
279
- if (colonIdx === -1) return model;
280
- return THINKING_LEVELS.some((level) => level === model.substring(colonIdx + 1))
281
- ? model.slice(0, colonIdx)
282
- : model;
283
- }
284
-
285
- function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "modelCandidates" | "agentName">): string[] {
286
- if (!input.fast) return [];
287
- const candidates = (input.modelCandidates?.length ? input.modelCandidates : input.model ? [input.model] : [])
288
- .map(stripThinkingSuffix);
289
- if (candidates.length === 0) {
290
- throw new Error(`fast mode requires an explicit supported native OpenAI-Codex model${input.agentName ? ` for agent '${input.agentName}'` : ""}.`);
291
- }
292
- const unsupported = candidates.filter((model) => !FAST_MODE_ALLOWED_MODELS.has(model));
293
- if (unsupported.length > 0) {
294
- throw new Error(`fast mode supports only ${[...FAST_MODE_ALLOWED_MODELS].join(", ")}; unsupported model${unsupported.length === 1 ? "" : "s"}: ${unsupported.join(", ")}.`);
295
- }
296
- return [FAST_MODE_EXTENSION_PATH];
297
- }
298
-
299
- export interface ResolvePiLaunchToolPlanInput {
300
- tools?: string[];
301
- excludeTools?: string[];
302
- allowNestedSubagents?: boolean;
303
- extensions?: string[];
304
- subagentOnlyExtensions?: string[];
305
- mcpDirectTools?: string[];
306
- mcpConfig?: McpConfig;
307
- runtimeServerNames?: string[];
308
- cwd?: string;
309
- requireReadTool?: boolean;
310
- structuredOutput?:
311
- | boolean
312
- | {
313
- schema: JsonSchemaObject;
314
- schemaPath: string;
315
- outputPath: string;
316
- };
317
- fast?: boolean;
318
- model?: string;
319
- modelCandidates?: readonly string[];
320
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
321
- inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
322
- agentName?: string;
323
- permissionRules?: PermissionRules;
324
- runtimeSnapshotHost?: McpRuntimeSnapshotHost;
325
- }
326
-
327
- export interface PiLaunchToolPlan {
328
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
329
- requestedBuiltinTools: string[];
330
- declaredBuiltinTools: string[];
331
- excludeTools: string[];
332
- toolExtensionPaths: string[];
333
- resolvedMcpSelections: ResolvedMcpDirectToolSelection[];
334
- effectiveMcpSelections: ResolvedMcpDirectToolSelection[];
335
- effectiveMcpTools: string[];
336
- explicitToolAllowlist: boolean;
337
- internalTools: string[];
338
- effectiveToolAllowlist: string[];
339
- requiredChildTools: string[];
340
- fanoutAuthorized: boolean;
341
- runtimeExtensions: string[];
342
- configuredExtensions: string[];
343
- extensionArgs: string[];
344
- disableAmbientExtensions: boolean;
345
- capabilityAudit?: SubagentCapabilityAudit;
346
- /** Non-fatal launch warnings; they do not change behavior. */
347
- warnings: string[];
348
- mcpConfig?: McpConfig;
349
- runtimeServerNames?: string[];
350
- }
351
-
352
- function extensionIdentifier(value: string): string {
353
- return `sha256:${createHash("sha256").update(path.normalize(value.trim())).digest("hex").slice(0, 16)}`;
354
- }
355
-
356
- function boundedExtensionIdentifiers(values: string[]): {
357
- ids: string[];
358
- omitted: number;
359
- } {
360
- const ids = [...new Set(values.map(extensionIdentifier))];
361
- return {
362
- ids: ids.slice(0, MAX_LAUNCH_RESOLVED_EXTENSION_IDS),
363
- omitted: Math.max(0, ids.length - MAX_LAUNCH_RESOLVED_EXTENSION_IDS),
364
- };
365
- }
366
-
367
- function hasPermissionRules(rules: PermissionRules | undefined): boolean {
368
- return rules !== undefined && Object.keys(rules).length > 0;
369
- }
370
-
371
- function filterRuntimeMcpConfig(
372
- config: McpConfig,
373
- runtimeServerNames: readonly string[],
374
- effectiveRuntimeServerNames: readonly string[],
375
- ): McpConfig {
376
- const runtimeNames = new Set(runtimeServerNames);
377
- const effectiveNames = new Set(effectiveRuntimeServerNames);
378
- return {
379
- ...config,
380
- mcpServers: Object.fromEntries(
381
- Object.entries(config.mcpServers).filter(([name]) => !runtimeNames.has(name) || effectiveNames.has(name)),
382
- ),
383
- };
384
- }
385
-
386
- export function projectLaunchResolvedChildExtensions(
387
- toolPlan: Pick<
388
- PiLaunchToolPlan,
389
- | "runtimeExtensions"
390
- | "configuredExtensions"
391
- | "extensionArgs"
392
- | "disableAmbientExtensions"
393
- >,
394
- ): LaunchResolvedChildExtensionsV1 {
395
- const runtime = boundedExtensionIdentifiers(toolPlan.runtimeExtensions);
396
- const configured = boundedExtensionIdentifiers(toolPlan.configuredExtensions);
397
- const effective = boundedExtensionIdentifiers(toolPlan.extensionArgs);
398
- return {
399
- version: 1,
400
- source: "launch-resolved",
401
- disableAmbientExtensions: toolPlan.disableAmbientExtensions,
402
- runtime: runtime.ids,
403
- configured: configured.ids,
404
- effective: effective.ids,
405
- omitted: {
406
- runtime: runtime.omitted,
407
- configured: configured.omitted,
408
- effective: effective.omitted,
409
- },
410
- };
411
- }
412
-
413
- /**
414
- * Resolve the permission-system extension entry point when installed.
415
- * Returns the absolute path to the extension's main module, or undefined
416
- * when the package is not installed. Callers can check `autoInject` config
417
- * to decide whether to include it in child processes.
418
- */
419
- export function resolvePermissionSystemExtension(): string | undefined {
420
- const agentDir = getAgentDir();
421
- const candidates = [
422
- // npm-scoped package (most common)
423
- path.join(
424
- agentDir,
425
- "npm",
426
- "node_modules",
427
- "@gotgenes",
428
- "pi-permission-system",
429
- ),
430
- // direct extension directory (some layouts)
431
- path.join(agentDir, "extensions", "pi-permission-system"),
432
- ];
433
- const errors: Error[] = [];
434
- for (const extDir of candidates) {
435
- if (!fs.existsSync(extDir)) continue;
436
- const pkgPath = path.join(extDir, "package.json");
437
- if (!fs.existsSync(pkgPath)) {
438
- errors.push(new Error(`Permission-system package manifest is missing at ${pkgPath}.`));
439
- continue;
440
- }
441
- try {
442
- let pkg: { pi?: { extensions?: string[] } };
443
- const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
444
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
445
- throw new Error("manifest root must be an object");
446
- }
447
- pkg = parsed as typeof pkg;
448
- const extensions = pkg.pi?.extensions;
449
- const entry = Array.isArray(extensions) ? extensions[0] : undefined;
450
- if (typeof entry !== "string" || !entry.trim()) {
451
- throw new Error(
452
- `Permission-system package manifest at ${pkgPath} must declare pi.extensions[0] as a non-empty string.`,
453
- );
454
- }
455
- const resolved = path.resolve(extDir, entry);
456
- if (fs.existsSync(resolved)) return resolved;
457
- throw new Error(
458
- `Permission-system extension entry ${JSON.stringify(entry)} in ${pkgPath} does not exist at ${resolved}.`,
459
- );
460
- } catch (error) {
461
- const message = error instanceof Error ? error.message : String(error);
462
- errors.push(message.startsWith("Permission-system") ? new Error(message) : new Error(`Cannot read permission-system package manifest at ${pkgPath}: ${message}`));
463
- }
464
- }
465
- if (errors.length > 0) throw errors[0]!;
466
- return undefined;
467
- }
468
-
469
- export function resolvePiLaunchToolPlan(
470
- input: ResolvePiLaunchToolPlanInput,
471
- ): PiLaunchToolPlan {
472
- const capabilityCeiling = intersectSubagentCapabilityCeilings(
473
- input.capabilityCeiling,
474
- input.inheritedCapabilityCeiling,
475
- );
476
- const allowedToolSet =
477
- capabilityCeiling?.allowedTools === undefined
478
- ? undefined
479
- : new Set(capabilityCeiling.allowedTools);
480
- const requestedBuiltinTools =
481
- input.tools?.filter(
482
- (tool) =>
483
- !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
484
- ) ?? [];
485
- if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
486
- throw new Error(
487
- `Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`,
488
- );
489
- }
490
- const declaredBuiltinTools =
491
- input.tools === undefined
492
- ? allowedToolSet
493
- ? [...allowedToolSet]
494
- : []
495
- : (input.requireReadTool &&
496
- requestedBuiltinTools.length > 0 &&
497
- !requestedBuiltinTools.includes("read") &&
498
- !allowedToolSet
499
- ? ["read", ...requestedBuiltinTools]
500
- : requestedBuiltinTools
501
- ).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
502
- const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
503
- const excludedToolSet = new Set(excludeTools);
504
- const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
505
- const fanoutAuthorized = effectiveDeclaredBuiltinTools.includes("subagent") || (
506
- input.allowNestedSubagents === true &&
507
- !excludedToolSet.has("subagent") &&
508
- (!allowedToolSet || allowedToolSet.has("subagent"))
509
- );
510
- const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
511
- ? []
512
- : (input.tools ?? []).filter(
513
- (tool) =>
514
- !requestedBuiltinTools.includes(tool) &&
515
- (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
516
- );
517
- const mcpResolution = capabilityCeiling?.denyExtensions
518
- ? { selections: [], unresolvedSelectors: [] }
519
- : resolveMcpDirectToolResolution(input.mcpDirectTools, input.cwd, input.runtimeSnapshotHost, input.mcpConfig);
520
- if (mcpResolution.unresolvedSelectors.length > 0) {
521
- throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
522
- }
523
- const resolvedMcpSelections = mcpResolution.selections;
524
- const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
525
- const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
526
- const effectiveMcpSelections = resolvedMcpSelections.filter(
527
- (selection) =>
528
- !allowedToolSet ||
529
- allowedToolSet.has(selection.name) ||
530
- isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
531
- ).filter((selection) => !excludedToolSet.has(selection.name));
532
- const effectiveMcpTools = effectiveMcpSelections.map(
533
- (selection) => selection.name,
534
- );
535
- const runtimeServerNames = mcpResolution.runtimeServerNames ?? input.runtimeServerNames ?? [];
536
- const effectiveRuntimeServerNames = runtimeServerNames.filter((serverName) =>
537
- effectiveMcpSelections.some((selection) => selection.selector.startsWith(`${serverName}/`)),
538
- );
539
- const effectiveMcpConfig = mcpResolution.mcpConfig ?? input.mcpConfig;
540
- const filteredMcpConfig = effectiveMcpConfig
541
- ? filterRuntimeMcpConfig(effectiveMcpConfig, runtimeServerNames, effectiveRuntimeServerNames)
542
- : undefined;
543
- const explicitToolAllowlist =
544
- input.tools !== undefined ||
545
- (input.mcpDirectTools?.length ?? 0) > 0 ||
546
- allowedToolSet !== undefined;
547
- const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
548
- const effectiveToolAllowlist = [
549
- ...new Set([
550
- ...effectiveDeclaredBuiltinTools,
551
- ...effectiveMcpTools,
552
- ...internalTools,
553
- ]),
554
- ];
555
- // Supervisor-coordination names stay in the --tools allowlist but are never
556
- // strict requirements: children register contact_supervisor at runtime through
557
- // the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
558
- // appended intercom alongside contact_supervisor, so that exact pairing is
559
- // legacy plumbing, not a user demand for an external intercom provider;
560
- // a lone intercom entry stays strictly required (#1207).
561
- const legacySupervisorPairing = effectiveDeclaredBuiltinTools.includes("contact_supervisor");
562
- const requiredChildTools = explicitToolAllowlist
563
- ? [
564
- ...new Set([
565
- ...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
566
- ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
567
- ...internalTools,
568
- ].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
569
- ]
570
- : [];
571
- const permSystemExt = capabilityCeiling?.denyExtensions
572
- ? undefined
573
- : hasPermissionRules(input.permissionRules)
574
- ? resolvePermissionSystemExtension()
575
- : undefined;
576
- if (input.fast && capabilityCeiling?.denyExtensions) throw new Error("fast mode requires a child runtime extension, but this launch denies extensions.");
577
- const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, modelCandidates: input.modelCandidates, agentName: input.agentName });
578
- const runtimeExtensions = [
579
- PROMPT_RUNTIME_EXTENSION_PATH,
580
- ...fastModeExtensions,
581
- ...(fanoutAuthorized ? [FANOUT_CHILD_EXTENSION_PATH] : []),
582
- ...(permSystemExt ? [permSystemExt] : []),
583
- ];
584
- const disableAmbientExtensions =
585
- capabilityCeiling?.denyExtensions === true ||
586
- input.extensions !== undefined;
587
- const warnings: string[] = [];
588
- // An explicit empty list disables ambient extensions, including model providers.
589
- if (capabilityCeiling?.denyExtensions !== true && Array.isArray(input.extensions) && input.extensions.length === 0) {
590
- const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
591
- warnings.push(
592
- `extensions: [] override${agentLabel} disables ALL ambient extensions for this child (not just "adds nothing"), `
593
- + "including any model-provider extension needed to resolve a provider-qualified model. "
594
- + "List the extensions this child actually needs instead of an empty array.",
595
- );
596
- }
597
- const configuredExtensions = capabilityCeiling?.denyExtensions
598
- ? []
599
- : [
600
- ...toolExtensionPaths,
601
- ...(input.extensions ?? []),
602
- ...(input.subagentOnlyExtensions ?? []),
603
- ];
604
- const extensionArgs = disableAmbientExtensions
605
- ? [...new Set([...runtimeExtensions, ...configuredExtensions])]
606
- : [
607
- ...new Set([
608
- ...runtimeExtensions,
609
- ...toolExtensionPaths,
610
- ...(input.subagentOnlyExtensions ?? []),
611
- ]),
612
- ];
613
- const requestedToolNames =
614
- input.tools !== undefined
615
- ? [
616
- ...new Set([
617
- ...requestedBuiltinTools,
618
- ...resolvedMcpSelections.map((selection) => selection.name),
619
- ]),
620
- ]
621
- : undefined;
622
- const capabilityAudit = capabilityCeiling
623
- ? ({
624
- ceiling: capabilityCeiling,
625
- ...(requestedToolNames ? { requestedTools: requestedToolNames } : {}),
626
- effectiveTools: effectiveToolAllowlist,
627
- ...(excludeTools.length > 0 ? { excludeTools } : {}),
628
- removedTools:
629
- requestedToolNames?.filter(
630
- (tool) => !effectiveToolAllowlist.includes(tool),
631
- ) ?? [],
632
- internalTools,
633
- extensionsDenied: capabilityCeiling.denyExtensions,
634
- removedExtensionCount: capabilityCeiling.denyExtensions
635
- ? (input.extensions?.length ?? 0) +
636
- (input.subagentOnlyExtensions?.length ?? 0) +
637
- (input.tools ?? []).filter(
638
- (tool) =>
639
- tool.includes("/") ||
640
- tool.endsWith(".ts") ||
641
- tool.endsWith(".js"),
642
- ).length
643
- : 0,
644
- requestedMcpToolCount: input.mcpDirectTools?.length ?? 0,
645
- effectiveMcpTools,
646
- agentAllowed:
647
- input.agentName === undefined
648
- ? true
649
- : isAgentAllowedByCapabilityCeiling(
650
- input.agentName,
651
- capabilityCeiling,
652
- ),
653
- ...(capabilityCeilingAgentRestrictionSources(capabilityCeiling)
654
- ? {
655
- agentRestrictionSources:
656
- capabilityCeilingAgentRestrictionSources(capabilityCeiling),
657
- }
658
- : {}),
659
- } satisfies SubagentCapabilityAudit)
660
- : undefined;
661
- return {
662
- ...(capabilityCeiling ? { capabilityCeiling } : {}),
663
- requestedBuiltinTools,
664
- declaredBuiltinTools,
665
- excludeTools,
666
- toolExtensionPaths,
667
- resolvedMcpSelections,
668
- effectiveMcpSelections,
669
- effectiveMcpTools,
670
- explicitToolAllowlist,
671
- internalTools,
672
- effectiveToolAllowlist,
673
- requiredChildTools,
674
- fanoutAuthorized,
675
- runtimeExtensions,
676
- configuredExtensions,
677
- extensionArgs,
678
- disableAmbientExtensions,
679
- warnings,
680
- ...(effectiveRuntimeServerNames.length > 0 && filteredMcpConfig
681
- ? { mcpConfig: filteredMcpConfig, runtimeServerNames: effectiveRuntimeServerNames }
682
- : {}),
683
- ...(capabilityAudit ? { capabilityAudit } : {}),
684
- };
685
- }
686
-
687
- // Capability ceilings persisted before #1685 may still name hyphenated MCP server prefixes with underscores.
688
- function countLegacyUnderscoreMcpToolNames(selections: readonly ResolvedMcpDirectToolSelection[]): Map<string, number> {
689
- const counts = new Map<string, number>();
690
- for (const selection of selections) {
691
- const legacyName = legacyUnderscoreMcpToolName(selection);
692
- if (legacyName !== selection.name) counts.set(legacyName, (counts.get(legacyName) ?? 0) + 1);
693
- }
694
- return counts;
695
- }
696
-
697
- function isLegacyUnderscoreMcpToolAllowed(
698
- selection: ResolvedMcpDirectToolSelection,
699
- allowedToolSet: ReadonlySet<string>,
700
- resolvedMcpNames: ReadonlySet<string>,
701
- legacyMcpNameCounts: ReadonlyMap<string, number>,
702
- ): boolean {
703
- const legacyName = legacyUnderscoreMcpToolName(selection);
704
- return legacyMcpNameCounts.get(legacyName) === 1 && !resolvedMcpNames.has(legacyName) && allowedToolSet.has(legacyName);
705
- }
706
-
707
- function legacyUnderscoreMcpToolName(selection: ResolvedMcpDirectToolSelection): string {
708
- const slash = selection.selector.indexOf("/");
709
- if (slash < 1) return selection.name;
710
- const toolName = selection.selector.slice(slash + 1);
711
- const suffix = `_${toolName}`;
712
- if (!selection.name.endsWith(suffix)) return selection.name;
713
- const prefix = selection.name.slice(0, -suffix.length);
714
- return `${prefix.replace(/-/g, "_")}${suffix}`;
715
- }
716
-
717
- /** Escape XML-significant characters in a string for safe attribute interpolation. */
718
- function escapeXmlAttr(value: string): string {
719
- return value
720
- .replace(/&/g, "&amp;")
721
- .replace(/"/g, "&quot;")
722
- .replace(/</g, "&lt;")
723
- .replace(/>/g, "&gt;");
724
- }
725
-
726
- export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
727
- const args = [...input.baseArgs];
728
-
729
- if (input.sessionFile) {
730
- fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
731
- args.push("--session", input.sessionFile);
732
- } else {
733
- if (!input.sessionEnabled) {
734
- args.push("--no-session");
735
- }
736
- if (input.sessionDir) {
737
- fs.mkdirSync(input.sessionDir, { recursive: true });
738
- args.push("--session-dir", input.sessionDir);
739
- }
740
- }
741
-
742
- const modelArg = applyThinkingSuffix(input.model, input.thinking);
743
- if (modelArg) {
744
- args.push("--model", modelArg);
745
- }
746
-
747
- const toolPlan = resolvePiLaunchToolPlan({
748
- tools: input.tools,
749
- excludeTools: input.excludeTools,
750
- allowNestedSubagents: input.allowNestedSubagents,
751
- extensions: input.extensions,
752
- subagentOnlyExtensions: input.subagentOnlyExtensions,
753
- mcpDirectTools: input.mcpDirectTools,
754
- mcpConfig: input.mcpConfig,
755
- runtimeServerNames: input.runtimeServerNames,
756
- cwd: input.cwd,
757
- requireReadTool: input.requireReadTool,
758
- structuredOutput: input.structuredOutput,
759
- fast: input.fast,
760
- model: modelArg,
761
- modelCandidates: input.modelCandidates,
762
- capabilityCeiling: input.capabilityCeiling,
763
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(
764
- process.env[SUBAGENT_CAPABILITY_CEILING_ENV],
765
- ),
766
- agentName: input.childAgentName,
767
- permissionRules: input.permissionRules,
768
- runtimeSnapshotHost: input.runtimeSnapshotHost,
769
- });
770
- if (toolPlan.explicitToolAllowlist) {
771
- args.push(
772
- toolPlan.effectiveToolAllowlist.length > 0 ? "--tools" : "--no-tools",
773
- );
774
- if (toolPlan.effectiveToolAllowlist.length > 0)
775
- args.push(toolPlan.effectiveToolAllowlist.join(","));
776
- } else if (toolPlan.excludeTools.length > 0) {
777
- args.push("--exclude-tools", toolPlan.excludeTools.join(","));
778
- }
779
- if (toolPlan.disableAmbientExtensions) {
780
- args.push("--no-extensions");
781
- }
782
- for (const extPath of toolPlan.extensionArgs)
783
- args.push("--extension", extPath);
784
- let tempDir: string | undefined;
785
- if (toolPlan.mcpConfig && toolPlan.runtimeServerNames?.length) {
786
- if (!tempDir)
787
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
788
- const mcpConfigPath = path.join(tempDir, "mcp-config.json");
789
- fs.writeFileSync(mcpConfigPath, JSON.stringify(toolPlan.mcpConfig, null, 2), { mode: 0o600 });
790
- args.push("--mcp-config", mcpConfigPath);
791
- }
792
-
793
- if (!input.inheritProjectContext) {
794
- args.push("--no-context-files");
795
- }
796
- if (!input.inheritSkills) {
797
- args.push("--no-skills");
798
- }
799
-
800
- if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
801
- if (!tempDir)
802
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
803
- const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
804
- const promptPath = path.join(tempDir, `${stem}.md`);
805
- // Inject <active_agent> tag so @gotgenes/pi-permission-system can
806
- // resolve per-agent policy inside the child session.
807
- const taggedPrompt = input.childAgentName
808
- ? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
809
- : input.systemPrompt;
810
- fs.writeFileSync(promptPath, taggedPrompt, { mode: 0o600 });
811
- args.push(
812
- input.systemPromptMode === "replace"
813
- ? "--system-prompt"
814
- : "--append-system-prompt",
815
- promptPath,
816
- );
817
- }
818
-
819
- if (
820
- shouldDeliverTaskViaFile(
821
- input.task,
822
- input.taskDelivery ?? resolveSubagentTaskDelivery(),
823
- )
824
- ) {
825
- if (!tempDir) {
826
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
827
- }
828
- const taskFilePath = path.join(tempDir, "task.md");
829
- fs.writeFileSync(taskFilePath, `Task: ${input.task}`, { mode: 0o600 });
830
- args.push(`@${taskFilePath}`);
831
- } else {
832
- args.push(`Task: ${input.task}`);
833
- }
834
-
835
- const env: Record<string, string | undefined> = {};
836
- env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
837
- const piPackageRoot =
838
- process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
839
- if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
840
- if (!tempDir)
841
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
842
- const runtimeAcknowledgedExtensionsPath = path.join(
843
- tempDir,
844
- "runtime-acknowledged-extensions.json",
845
- );
846
- env[RUNTIME_EXTENSION_ACK_PATH_ENV] = runtimeAcknowledgedExtensionsPath;
847
- let toolDiagnosticPath: string | undefined;
848
- // Child launch environments are merged over process.env. Explicitly clear
849
- // parent-scoped diagnostics so a nested zero-tool child cannot validate
850
- // against, or overwrite, its parent's required-tool report.
851
- env[REQUIRED_CHILD_TOOLS_ENV] = undefined;
852
- env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = undefined;
853
- if (toolPlan.requiredChildTools.length > 0) {
854
- if (!tempDir)
855
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
856
- toolDiagnosticPath = path.join(tempDir, "tool-diagnostic.json");
857
- env[REQUIRED_CHILD_TOOLS_ENV] = JSON.stringify(toolPlan.requiredChildTools);
858
- env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV] = toolDiagnosticPath;
859
- }
860
- env[MCP_DIRECT_CHILD_TOOLS_ENV] =
861
- toolPlan.effectiveMcpTools.length > 0
862
- ? JSON.stringify(toolPlan.effectiveMcpTools)
863
- : undefined;
864
- env[SUBAGENT_CHILD_ENV] = "1";
865
- env[SUBAGENT_FANOUT_CHILD_ENV] = toolPlan.fanoutAuthorized ? "1" : "0";
866
- if (input.waitToolEnabled !== undefined) {
867
- env[WAIT_TOOL_ENABLED_ENV] = input.waitToolEnabled ? "true" : "false";
868
- }
869
- if (input.waitToolDefaultTimeoutMs !== undefined) {
870
- env[WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV] = String(input.waitToolDefaultTimeoutMs);
871
- }
872
- const inheritedNestedRoute = Boolean(
873
- process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] &&
874
- process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] &&
875
- process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV],
876
- );
877
- const parentRunId =
878
- input.parentRunId ??
879
- input.runId ??
880
- (inheritedNestedRoute ? process.env[SUBAGENT_RUN_ID_ENV] : undefined) ??
881
- process.env[SUBAGENT_PARENT_RUN_ID_ENV] ??
882
- "";
883
- const parentChildIndex =
884
- input.parentChildIndex !== undefined
885
- ? String(input.parentChildIndex)
886
- : input.childIndex !== undefined
887
- ? String(input.childIndex)
888
- : (process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV] ?? "");
889
- const inheritedDepth = Number(process.env[SUBAGENT_PARENT_DEPTH_ENV]);
890
- const parentDepth =
891
- input.parentDepth ??
892
- (inheritedNestedRoute && Number.isFinite(inheritedDepth)
893
- ? inheritedDepth + 1
894
- : 1);
895
- const parentPath = input.parentPath ?? [
896
- ...parseNestedPathEnv(process.env[SUBAGENT_PARENT_PATH_ENV]),
897
- ...(parentRunId
898
- ? [
899
- {
900
- runId: parentRunId,
901
- ...(parentChildIndex && /^\d+$/.test(parentChildIndex)
902
- ? { stepIndex: Number(parentChildIndex) }
903
- : {}),
904
- ...(input.childAgentName ? { agent: input.childAgentName } : {}),
905
- },
906
- ]
907
- : []),
908
- ];
909
- env[SUBAGENT_PARENT_EVENT_SINK_ENV] = toolPlan.fanoutAuthorized
910
- ? (input.parentEventSink ??
911
- process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ??
912
- "")
913
- : "";
914
- env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = toolPlan.fanoutAuthorized
915
- ? (input.parentControlInbox ??
916
- process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ??
917
- "")
918
- : "";
919
- env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
920
- ? (input.parentRootRunId ??
921
- process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ??
922
- input.runId ??
923
- "")
924
- : "";
925
- env[SUBAGENT_PARENT_RUN_ID_ENV] = toolPlan.fanoutAuthorized
926
- ? parentRunId
927
- : "";
928
- env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = toolPlan.fanoutAuthorized
929
- ? parentChildIndex
930
- : "";
931
- env[SUBAGENT_PARENT_DEPTH_ENV] = toolPlan.fanoutAuthorized
932
- ? String(parentDepth)
933
- : "";
934
- env[SUBAGENT_PARENT_PATH_ENV] = toolPlan.fanoutAuthorized
935
- ? encodeNestedPathEnv(parentPath)
936
- : "";
937
- env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = toolPlan.fanoutAuthorized
938
- ? (input.parentCapabilityToken ??
939
- process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ??
940
- "")
941
- : "";
942
- env[RUN_FANOUT_BUDGET_ENV] = toolPlan.fanoutAuthorized
943
- ? (input.runFanoutBudget ? encodeRunFanoutBudgetDescriptor(input.runFanoutBudget) : process.env[RUN_FANOUT_BUDGET_ENV])
944
- : undefined;
945
- env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext
946
- ? "1"
947
- : "0";
948
- env[SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV] = input.inheritGlobalContext ? "1" : "0";
949
- env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
950
- env[PI_INTERCOM_STABLE_ID_ENV] = input.intercomSessionName || undefined;
951
- env[PI_INTERCOM_SESSION_ID_ENV] = undefined;
952
- if (input.intercomSessionName) {
953
- env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
954
- }
955
- if (input.sessionName?.trim()) {
956
- env.PI_SUBAGENT_SESSION_NAME = input.sessionName.trim();
957
- }
958
- if (input.orchestratorIntercomTarget) {
959
- env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
960
- }
961
- if (input.parentSessionId) {
962
- env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV] = input.parentSessionId;
963
- }
964
- const encodedPermissionRules = encodePermissionRules(input.permissionRules);
965
- if (
966
- input.orchestratorIntercomTarget &&
967
- input.parentSessionId &&
968
- input.runId &&
969
- input.childAgentName
970
- ) {
971
- const childIndex = input.childIndex ?? 0;
972
- const channelDir = supervisorChannelDir(
973
- input.runId,
974
- input.childAgentName,
975
- childIndex,
976
- );
977
- fs.mkdirSync(path.join(channelDir, "requests"), { recursive: true });
978
- fs.mkdirSync(path.join(channelDir, "replies"), { recursive: true });
979
- env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV] = channelDir;
980
- }
981
- if (encodedPermissionRules)
982
- env[PERMISSION_AUDIT_PATH_ENV] =
983
- input.permissionAuditPath ?? path.join(tempDir, "permission-audit.jsonl");
984
- if (input.runId) {
985
- env[SUBAGENT_RUN_ID_ENV] = input.runId;
986
- }
987
- if (input.childAgentName) {
988
- env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName;
989
- }
990
- if (input.childIndex !== undefined) {
991
- env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
992
- }
993
- if (!toolPlan.capabilityCeiling && input.mcpDirectTools?.length)
994
- env.MCP_DIRECT_TOOLS = input.mcpDirectTools.join(",");
995
- else if (
996
- toolPlan.capabilityCeiling &&
997
- toolPlan.effectiveMcpSelections.length &&
998
- !toolPlan.capabilityCeiling.denyExtensions
999
- )
1000
- env.MCP_DIRECT_TOOLS = toolPlan.effectiveMcpSelections
1001
- .map((selection) => selection.selector)
1002
- .join(",");
1003
- else env.MCP_DIRECT_TOOLS = "__none__";
1004
- const encodedCapabilityCeiling = encodeSubagentCapabilityCeiling(
1005
- toolPlan.capabilityCeiling,
1006
- );
1007
- const thinkingCeiling = intersectThinkingCeilings(
1008
- input.thinkingCeiling,
1009
- decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
1010
- );
1011
- if (thinkingCeiling) env[SUBAGENT_THINKING_CEILING_ENV] = thinkingCeiling;
1012
- if (encodedCapabilityCeiling)
1013
- env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
1014
- if (encodedPermissionRules)
1015
- env[PERMISSION_POLICY_ENV] = encodedPermissionRules;
1016
- env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = undefined;
1017
- env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = undefined;
1018
- if (input.structuredOutput) {
1019
- env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
1020
- env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
1021
- if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
1022
- if (input.structuredOutput.acceptanceReportRequired) env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
1023
- }
1024
- if (input.steerInboxDir) {
1025
- env[SUBAGENT_STEER_INBOX_ENV] = input.steerInboxDir;
1026
- }
1027
- if (input.steerCapabilityPath)
1028
- env[SUBAGENT_STEER_CAPABILITY_ENV] = input.steerCapabilityPath;
1029
- if (input.steerAckDir) env[SUBAGENT_STEER_ACK_DIR_ENV] = input.steerAckDir;
1030
- const encodedToolBudget = encodeToolBudgetEnv(input.toolBudget);
1031
- if (encodedToolBudget) env[TOOL_BUDGET_ENV] = encodedToolBudget;
1032
- env[TOOL_BUDGET_ZERO_AUTH_ENV] = input.allowZeroToolBudget ? "1" : undefined;
1033
- if (input.childWatchdog) env[CHILD_WATCHDOG_CONFIG_ENV] = JSON.stringify(input.childWatchdog);
1034
-
1035
- env[SUBAGENT_PARENT_SESSION_ENV] =
1036
- input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
1037
- env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
1038
-
1039
- return {
1040
- args,
1041
- env,
1042
- warnings: toolPlan.warnings,
1043
- tempDir,
1044
- toolDiagnosticPath,
1045
- runtimeAcknowledgedExtensionsPath,
1046
- capabilityAudit: toolPlan.capabilityAudit,
1047
- };
1048
- }
1049
-
1050
- export const parseParentPathEnv = parseNestedPathEnv;
1051
-
1052
- export function cleanupTempDir(tempDir: string | null | undefined): void {
1053
- if (!tempDir) return;
1054
- try {
1055
- fs.rmSync(tempDir, { recursive: true, force: true });
1056
- } catch {
1057
- // Temp cleanup is best effort.
1058
- }
1059
- }