pi-subagents 0.57.0 → 0.58.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/docs/agents.md +13 -6
  3. package/docs/extension-api.md +39 -0
  4. package/docs/models.md +3 -3
  5. package/docs/tool-reference.md +3 -2
  6. package/docs/workflows.md +24 -0
  7. package/package.json +1 -1
  8. package/skills/pi-subagents/SKILL.md +2 -0
  9. package/skills/pi-subagents/references/execution-controls.md +1 -1
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  12. package/src/agents/agent-management.ts +23 -0
  13. package/src/agents/agent-serializer.ts +5 -0
  14. package/src/agents/agents.ts +38 -3
  15. package/src/agents/runtime-agent-events.ts +70 -0
  16. package/src/agents/runtime-agent-registry.ts +10 -2
  17. package/src/api/agents.ts +10 -5
  18. package/src/api/preflight.ts +26 -1
  19. package/src/extension/index.ts +15 -4
  20. package/src/extension/rpc.ts +35 -14
  21. package/src/extension/schemas.ts +2 -2
  22. package/src/extension/tool-description.ts +8 -4
  23. package/src/integrations/herdr-status.ts +5 -0
  24. package/src/runs/background/async-execution.ts +29 -12
  25. package/src/runs/background/async-resume.ts +6 -2
  26. package/src/runs/background/async-status.ts +2 -0
  27. package/src/runs/background/chain-append.ts +2 -0
  28. package/src/runs/background/notify.ts +26 -3
  29. package/src/runs/background/result-delivery-ownership.ts +45 -0
  30. package/src/runs/background/result-watcher.ts +27 -10
  31. package/src/runs/background/subagent-runner.ts +71 -21
  32. package/src/runs/foreground/execution.ts +20 -1
  33. package/src/runs/foreground/subagent-executor.ts +57 -26
  34. package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
  35. package/src/runs/shared/completion-guard.ts +4 -3
  36. package/src/runs/shared/dynamic-fanout.ts +3 -3
  37. package/src/runs/shared/fast-mode-extension.ts +5 -5
  38. package/src/runs/shared/launch-cwd.ts +16 -0
  39. package/src/runs/shared/long-running-guard.ts +2 -1
  40. package/src/runs/shared/mcp-config-sources.ts +386 -0
  41. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  42. package/src/runs/shared/model-exclusions.ts +12 -0
  43. package/src/runs/shared/model-fallback.ts +16 -4
  44. package/src/runs/shared/parallel-utils.ts +8 -1
  45. package/src/runs/shared/pi-args.ts +61 -6
  46. package/src/runs/shared/single-output.ts +17 -0
  47. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  48. package/src/shared/formatters.ts +6 -0
  49. package/src/shared/launch-contract.ts +4 -0
  50. package/src/shared/types.ts +42 -1
  51. package/src/shared/utils.ts +6 -29
  52. package/src/slash/subagents-admin.ts +3 -0
  53. package/src/tui/render.ts +14 -6
  54. package/src/workflows/scripted-workflow.ts +52 -5
  55. package/src/workflows/workflow-receipt.ts +39 -4
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { findConfiguredProjectRoot } from "../../agents/agents.ts";
5
6
  import { getAgentDir, getProjectConfigDir } from "../../shared/utils.ts";
7
+ import { loadAgentPluginMcpServers, loadPackageMcpServers, type McpServerDefinition } from "./mcp-config-sources.ts";
6
8
 
7
9
  const CACHE_VERSION = 1;
8
10
  const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
@@ -24,36 +26,39 @@ const IMPORT_PATHS = {
24
26
  type ToolPrefix = "server" | "none" | "short";
25
27
  type ImportKind = keyof typeof IMPORT_PATHS;
26
28
 
27
- interface ServerEntry {
28
- command?: string;
29
- args?: string[];
30
- socket?: string;
31
- env?: Record<string, string>;
32
- cwd?: string;
33
- url?: string;
34
- headers?: Record<string, string>;
35
- requestHeadersCommand?: {
36
- command: string;
37
- args?: string[];
38
- env?: Record<string, string>;
39
- timeoutMs?: number;
40
- };
41
- auth?: "oauth" | "bearer" | false;
42
- bearerToken?: string;
43
- bearerTokenEnv?: string;
44
- exposeResources?: boolean;
45
- includeTools?: string[];
46
- excludeTools?: string[];
47
- protocolVersion?: string;
48
- directTools?: boolean | string[];
49
- }
29
+ type ServerEntry = McpServerDefinition;
50
30
 
51
- interface McpConfig {
31
+ export interface McpConfig {
52
32
  mcpServers: Record<string, ServerEntry>;
53
33
  imports?: ImportKind[];
54
34
  settings?: {
55
35
  toolPrefix?: ToolPrefix;
56
36
  directTools?: boolean;
37
+ agentPluginPaths?: unknown;
38
+ };
39
+ }
40
+
41
+ export const MCP_RUNTIME_SNAPSHOT_EVENT = "pi-mcp-adapter:runtime-snapshot:v1" as const;
42
+ export const MCP_RUNTIME_SNAPSHOT_VERSION = 1 as const;
43
+
44
+ export interface McpRuntimeServerSnapshot {
45
+ readonly name: string;
46
+ readonly definition: ServerEntry;
47
+ readonly runtime: true;
48
+ readonly persisted: false;
49
+ }
50
+
51
+ interface McpRuntimeSnapshotRequest {
52
+ version: typeof MCP_RUNTIME_SNAPSHOT_VERSION;
53
+ name: string;
54
+ result?:
55
+ | { ok: true; snapshot: McpRuntimeServerSnapshot }
56
+ | { ok: false; error: Error };
57
+ }
58
+
59
+ export interface McpRuntimeSnapshotHost {
60
+ events: {
61
+ emit(event: string, request: McpRuntimeSnapshotRequest): void;
57
62
  };
58
63
  }
59
64
 
@@ -80,17 +85,62 @@ interface MetadataCache {
80
85
 
81
86
  export interface ResolvedMcpDirectToolSelection { name: string; selector: string }
82
87
 
83
- export function resolveMcpDirectToolSelections(mcpDirectTools: string[] | undefined, cwd = process.cwd()): ResolvedMcpDirectToolSelection[] {
84
- if (!mcpDirectTools?.length) return [];
88
+ export interface McpDirectToolResolution {
89
+ selections: ResolvedMcpDirectToolSelection[];
90
+ unresolvedSelectors: string[];
91
+ /** Normal loaded config plus the selected runtime server definitions. */
92
+ mcpConfig?: McpConfig;
93
+ runtimeServerNames?: string[];
94
+ }
95
+
96
+ function normalizeSelectors(selectors: string[] | undefined): string[] {
97
+ return [...new Set((selectors ?? []).map((selector) => selector.replace(/\/+$/, "")).filter(Boolean))];
98
+ }
99
+
100
+ export function resolveMcpDirectToolResolution(
101
+ mcpDirectTools: string[] | undefined,
102
+ cwd = process.cwd(),
103
+ runtimeSnapshotHost?: McpRuntimeSnapshotHost,
104
+ configOverride?: McpConfig,
105
+ ): McpDirectToolResolution {
106
+ const selectors = normalizeSelectors(mcpDirectTools);
107
+ if (selectors.length === 0) return { selections: [], unresolvedSelectors: [] };
108
+
109
+ const config = configOverride ?? loadMcpConfig(cwd);
110
+ const { servers: selectedServers, tools: selectedTools } = parseSelections(selectors);
111
+ const runtimeSelectionServers = new Set([...selectedServers, ...selectedTools.keys()]);
112
+ const runtimeServers = resolveRuntimeMcpServers(config, runtimeSelectionServers, runtimeSnapshotHost);
113
+ const resolvedConfig = Object.keys(runtimeServers).length > 0
114
+ ? mergeConfigs(config, { mcpServers: runtimeServers })
115
+ : config;
116
+ validateSelectedServerDefinitions(resolvedConfig, selectors);
117
+ const cache = loadMetadataCache();
118
+ if (!cache) return { selections: [], unresolvedSelectors: selectors };
119
+ const selections = resolveDirectToolSelections(resolvedConfig, cache, getToolPrefix(resolvedConfig.settings?.toolPrefix), selectors);
120
+ const unresolvedSelectors = selectors.filter((selector) =>
121
+ selector.includes("/")
122
+ ? !selections.some((selection) => selection.selector === selector)
123
+ : !selections.some((selection) => selection.selector.startsWith(`${selector}/`)),
124
+ );
125
+ return {
126
+ selections,
127
+ unresolvedSelectors,
128
+ ...(Object.keys(runtimeServers).length > 0
129
+ ? { mcpConfig: resolvedConfig, runtimeServerNames: Object.keys(runtimeServers) }
130
+ : {}),
131
+ };
132
+ }
85
133
 
86
- try {
87
- const config = loadMcpConfig(cwd);
88
- const cache = loadMetadataCache();
89
- if (!cache) return [];
90
- return resolveDirectToolSelections(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools);
91
- } catch {
92
- return [];
93
- }
134
+ export function resolveMcpDirectToolSelections(
135
+ mcpDirectTools: string[] | undefined,
136
+ cwd = process.cwd(),
137
+ runtimeSnapshotHost?: McpRuntimeSnapshotHost,
138
+ ): ResolvedMcpDirectToolSelection[] {
139
+ return resolveMcpDirectToolResolution(mcpDirectTools, cwd, runtimeSnapshotHost).selections;
140
+ }
141
+
142
+ export function formatUnresolvedMcpDirectToolSelectors(selectors: readonly string[]): string {
143
+ return `Unresolved MCP direct-tool selectors: ${selectors.join(", ")}. Direct MCP tools require a matching configured server and fresh metadata cache; runtime-registered servers require a host/pi-mcp-adapter handoff before child launch.`;
94
144
  }
95
145
 
96
146
  function loadMetadataCache(): MetadataCache | null {
@@ -110,20 +160,76 @@ function loadMetadataCache(): MetadataCache | null {
110
160
  return raw as unknown as MetadataCache;
111
161
  }
112
162
 
163
+ function resolveRuntimeMcpServers(
164
+ config: McpConfig,
165
+ selectedServers: ReadonlySet<string>,
166
+ runtimeSnapshotHost: McpRuntimeSnapshotHost | undefined,
167
+ ): Record<string, ServerEntry> {
168
+ if (!runtimeSnapshotHost) return {};
169
+ const runtimeServers: Record<string, ServerEntry> = {};
170
+ for (const serverName of selectedServers) {
171
+ if (Object.hasOwn(config.mcpServers, serverName)) continue;
172
+ try {
173
+ const snapshot = getRuntimeMcpServerSnapshot(runtimeSnapshotHost, serverName);
174
+ runtimeServers[serverName] = snapshot.definition;
175
+ } catch {
176
+ // Missing, disposed, or shadowed runtime servers remain unresolved below.
177
+ }
178
+ }
179
+ return runtimeServers;
180
+ }
181
+
182
+ function getRuntimeMcpServerSnapshot(
183
+ host: McpRuntimeSnapshotHost,
184
+ name: string,
185
+ ): McpRuntimeServerSnapshot {
186
+ const request: McpRuntimeSnapshotRequest = {
187
+ version: MCP_RUNTIME_SNAPSHOT_VERSION,
188
+ name,
189
+ };
190
+ host.events.emit(MCP_RUNTIME_SNAPSHOT_EVENT, request);
191
+ if (!request.result) throw new Error("pi-mcp-adapter is not installed for this Pi instance");
192
+ if (!request.result.ok) throw request.result.error;
193
+ const snapshot = request.result.snapshot;
194
+ if (
195
+ !snapshot ||
196
+ snapshot.name !== name ||
197
+ snapshot.runtime !== true ||
198
+ snapshot.persisted !== false ||
199
+ !snapshot.definition ||
200
+ typeof snapshot.definition !== "object" ||
201
+ Array.isArray(snapshot.definition)
202
+ ) {
203
+ throw new Error(`Invalid MCP runtime snapshot for server "${name}"`);
204
+ }
205
+ return snapshot;
206
+ }
207
+
113
208
  function loadMcpConfig(cwd: string): McpConfig {
209
+ const resolvedCwd = path.resolve(cwd);
210
+ const projectRoot = findConfiguredProjectRoot(resolvedCwd) ?? resolvedCwd;
114
211
  let config: McpConfig = { mcpServers: {} };
115
- for (const sourcePath of getConfigPaths(cwd)) {
212
+ for (const sourcePath of getConfigPaths(projectRoot)) {
116
213
  const loaded = readConfig(sourcePath);
117
214
  if (!loaded) continue;
118
- config = mergeConfigs(config, expandImports(loaded, cwd));
215
+ config = mergeConfigs(config, expandImports(loaded, projectRoot));
119
216
  }
120
- return config;
217
+
218
+ const packageServers = loadPackageMcpServers(projectRoot);
219
+ const pluginServers = loadAgentPluginMcpServers(config.settings?.agentPluginPaths, projectRoot);
220
+ const packageOnlyServers = Object.fromEntries(
221
+ Object.entries(packageServers).filter(([name]) => !Object.hasOwn(pluginServers, name)),
222
+ );
223
+ return mergeConfigs(
224
+ { mcpServers: packageOnlyServers },
225
+ mergeConfigs({ mcpServers: pluginServers }, config),
226
+ );
121
227
  }
122
228
 
123
- function getConfigPaths(cwd: string): string[] {
229
+ function getConfigPaths(projectRoot: string): string[] {
124
230
  const piGlobalPath = path.join(getAgentDir(), "mcp.json");
125
- const projectPath = path.resolve(cwd, ".mcp.json");
126
- const projectPiPath = path.resolve(getProjectConfigDir(cwd), "mcp.json");
231
+ const projectPath = path.resolve(projectRoot, ".mcp.json");
232
+ const projectPiPath = path.resolve(getProjectConfigDir(projectRoot), "mcp.json");
127
233
  const sources: string[] = [];
128
234
  if (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath) sources.push(GENERIC_GLOBAL_CONFIG_PATH);
129
235
  sources.push(piGlobalPath);
@@ -253,8 +359,7 @@ export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined,
253
359
  function parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {
254
360
  const servers = new Set<string>();
255
361
  const tools = new Map<string, Set<string>>();
256
- for (let item of selections) {
257
- item = item.replace(/\/+$/, "");
362
+ for (const item of selections) {
258
363
  if (item.includes("/")) {
259
364
  const [server, tool] = item.split("/", 2);
260
365
  if (server && tool) {
@@ -270,6 +375,14 @@ function parseSelections(selections: string[]): { servers: Set<string>; tools: M
270
375
  return { servers, tools };
271
376
  }
272
377
 
378
+ function validateSelectedServerDefinitions(config: McpConfig, selectors: string[]): void {
379
+ const { servers, tools } = parseSelections(selectors);
380
+ for (const serverName of new Set([...servers, ...tools.keys()])) {
381
+ const definition = config.mcpServers[serverName];
382
+ if (definition) computeMcpServerHash(definition);
383
+ }
384
+ }
385
+
273
386
  function isServerCacheValid(entry: ServerCacheEntry | undefined, definition: ServerEntry): entry is ServerCacheEntry {
274
387
  if (!entry || entry.configHash !== computeMcpServerHash(definition)) return false;
275
388
  if (!entry.cachedAt || typeof entry.cachedAt !== "number") return false;
@@ -205,6 +205,18 @@ export function isExcluded(modelId: string, provider: string): boolean {
205
205
  return exclusions.some((entry) => entryMatches(entry, modelId, provider, Date.now()));
206
206
  }
207
207
 
208
+ /**
209
+ * Return the active exclusion matching a full model id, if any.
210
+ *
211
+ * The caller uses this for hard-fail diagnostics; fallback filtering should
212
+ * continue to use {@link filterFallbackCandidates}.
213
+ */
214
+ export function findModelExclusion(fullId: string, now = Date.now()): Readonly<ModelExclusion> | undefined {
215
+ ensureLoaded();
216
+ const { provider, modelId } = parseModelKey(fullId);
217
+ return exclusions.find((entry) => entryMatches(entry, modelId, provider, now));
218
+ }
219
+
208
220
  /**
209
221
  * Number of live (non-expired) exclusions.
210
222
  */
@@ -1,6 +1,6 @@
1
1
  import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
2
2
  import type { Usage } from "../../shared/types.ts";
3
- import { filterFallbackCandidates, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
3
+ import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
4
4
  import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
5
5
  import { redactSecretValues } from "./permissions.ts";
6
6
 
@@ -279,6 +279,14 @@ function enforceModelScopes(
279
279
  for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
280
280
  }
281
281
 
282
+ function throwForExplicitModelExclusion(model: string): void {
283
+ const exclusion = findModelExclusion(model);
284
+ if (!exclusion) return;
285
+ const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
286
+ const expiry = Number.isFinite(exclusion.expiresAt) ? `; expires: ${new Date(exclusion.expiresAt).toISOString()}` : "";
287
+ throw new Error(`Requested subagent model '${model}' is excluded and cannot be replaced by a fallback (reason: ${reason}${expiry}).`);
288
+ }
289
+
282
290
  /**
283
291
  * Resolve the `--model` override passed to a spawned subagent.
284
292
  *
@@ -313,6 +321,9 @@ export function resolveSubagentModelOverride(
313
321
  } else {
314
322
  resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
315
323
  }
324
+ if (resolved && explicit !== undefined && options?.source === "explicit") {
325
+ throwForExplicitModelExclusion(resolved);
326
+ }
316
327
  if (resolved && options?.scope) {
317
328
  const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
318
329
  enforceModelScopes(resolved, options.scope, source, options.onWarn);
@@ -326,14 +337,15 @@ export function resolveEffectiveSubagentModel(
326
337
  parentModel: ParentModel | undefined,
327
338
  availableModels: AvailableModelInfo[] | undefined,
328
339
  preferredProvider?: string,
329
- options?: Omit<ResolveSubagentModelOverrideOptions, "source">,
340
+ options?: ResolveSubagentModelOverrideOptions,
330
341
  ): string | undefined {
342
+ const source = options?.source ?? (explicitModel !== undefined ? "explicit" : "inherited");
331
343
  const resolved = resolveSubagentModelOverride(
332
344
  explicitModel ?? agentModel,
333
345
  parentModel,
334
346
  availableModels,
335
347
  preferredProvider,
336
- { ...options, source: explicitModel !== undefined ? "explicit" : "inherited" },
348
+ { ...options, source },
337
349
  );
338
350
  if (resolved || explicitModel === undefined) return resolved;
339
351
  return resolveSubagentModelOverride(
@@ -341,7 +353,7 @@ export function resolveEffectiveSubagentModel(
341
353
  parentModel,
342
354
  availableModels,
343
355
  preferredProvider,
344
- { ...options, source: "inherited" },
356
+ { ...options, source: options?.source ?? "inherited" },
345
357
  );
346
358
  }
347
359
 
@@ -28,22 +28,29 @@ export interface RunnerSubagentStep {
28
28
  outputName?: string;
29
29
  structured?: boolean;
30
30
  cwd?: string;
31
+ /** Original cwd input retained for launch diagnostics. */
32
+ requestedCwd?: string;
31
33
  model?: string;
34
+ contextLimit?: number;
32
35
  fast?: boolean;
33
36
  thinking?: string;
34
37
  thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
35
38
  modelCandidates?: string[];
36
39
  /** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
37
40
  skipPrimaryModelVerification?: boolean;
38
- modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
41
+ modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
39
42
  tools?: string[];
40
43
  extensions?: string[];
41
44
  subagentOnlyExtensions?: string[];
42
45
  mcpDirectTools?: string[];
46
+ mcpConfig?: import("./mcp-direct-tool-allowlist.ts").McpConfig;
47
+ runtimeServerNames?: string[];
48
+ mutationTools?: string[];
43
49
  completionGuard?: boolean;
44
50
  systemPrompt?: string | null;
45
51
  systemPromptMode?: "append" | "replace";
46
52
  inheritProjectContext: boolean;
53
+ inheritGlobalContext: boolean;
47
54
  inheritSkills: boolean;
48
55
  skills?: string[];
49
56
  outputPath?: string;
@@ -9,7 +9,10 @@ import {
9
9
  type NestedPathEntry,
10
10
  } from "./nested-path.ts";
11
11
  import {
12
- resolveMcpDirectToolSelections,
12
+ formatUnresolvedMcpDirectToolSelectors,
13
+ resolveMcpDirectToolResolution,
14
+ type McpConfig,
15
+ type McpRuntimeSnapshotHost,
13
16
  type ResolvedMcpDirectToolSelection,
14
17
  } from "./mcp-direct-tool-allowlist.ts";
15
18
  import { resolvePiPackageRoot } from "./pi-spawn.ts";
@@ -141,6 +144,7 @@ export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
141
144
  export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
142
145
  export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
143
146
  export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
147
+ export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
144
148
 
145
149
  export interface BuildPiArgsInput {
146
150
  parentSessionId?: string;
@@ -153,6 +157,7 @@ export interface BuildPiArgsInput {
153
157
  thinking?: string | false;
154
158
  systemPromptMode?: "append" | "replace";
155
159
  inheritProjectContext: boolean;
160
+ inheritGlobalContext: boolean;
156
161
  inheritSkills: boolean;
157
162
  requireReadTool?: boolean;
158
163
  tools?: string[];
@@ -160,6 +165,8 @@ export interface BuildPiArgsInput {
160
165
  subagentOnlyExtensions?: string[];
161
166
  systemPrompt?: string | null;
162
167
  mcpDirectTools?: string[];
168
+ mcpConfig?: McpConfig;
169
+ runtimeServerNames?: string[];
163
170
  cwd?: string;
164
171
  promptFileStem?: string;
165
172
  intercomSessionName?: string;
@@ -202,6 +209,7 @@ export interface BuildPiArgsInput {
202
209
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
203
210
  thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
204
211
  extensionBindings?: ExtensionBindings;
212
+ runtimeSnapshotHost?: McpRuntimeSnapshotHost;
205
213
  }
206
214
 
207
215
  export interface BuildPiArgsResult {
@@ -278,6 +286,8 @@ export interface ResolvePiLaunchToolPlanInput {
278
286
  extensions?: string[];
279
287
  subagentOnlyExtensions?: string[];
280
288
  mcpDirectTools?: string[];
289
+ mcpConfig?: McpConfig;
290
+ runtimeServerNames?: string[];
281
291
  cwd?: string;
282
292
  requireReadTool?: boolean;
283
293
  structuredOutput?:
@@ -294,6 +304,7 @@ export interface ResolvePiLaunchToolPlanInput {
294
304
  inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
295
305
  agentName?: string;
296
306
  permissionRules?: PermissionRules;
307
+ runtimeSnapshotHost?: McpRuntimeSnapshotHost;
297
308
  }
298
309
 
299
310
  export interface PiLaunchToolPlan {
@@ -316,6 +327,8 @@ export interface PiLaunchToolPlan {
316
327
  capabilityAudit?: SubagentCapabilityAudit;
317
328
  /** Non-fatal launch warnings; they do not change behavior. */
318
329
  warnings: string[];
330
+ mcpConfig?: McpConfig;
331
+ runtimeServerNames?: string[];
319
332
  }
320
333
 
321
334
  function extensionIdentifier(value: string): string {
@@ -337,6 +350,21 @@ function hasPermissionRules(rules: PermissionRules | undefined): boolean {
337
350
  return rules !== undefined && Object.keys(rules).length > 0;
338
351
  }
339
352
 
353
+ function filterRuntimeMcpConfig(
354
+ config: McpConfig,
355
+ runtimeServerNames: readonly string[],
356
+ effectiveRuntimeServerNames: readonly string[],
357
+ ): McpConfig {
358
+ const runtimeNames = new Set(runtimeServerNames);
359
+ const effectiveNames = new Set(effectiveRuntimeServerNames);
360
+ return {
361
+ ...config,
362
+ mcpServers: Object.fromEntries(
363
+ Object.entries(config.mcpServers).filter(([name]) => !runtimeNames.has(name) || effectiveNames.has(name)),
364
+ ),
365
+ };
366
+ }
367
+
340
368
  export function projectLaunchResolvedChildExtensions(
341
369
  toolPlan: Pick<
342
370
  PiLaunchToolPlan,
@@ -461,15 +489,27 @@ export function resolvePiLaunchToolPlan(
461
489
  !requestedBuiltinTools.includes(tool) &&
462
490
  (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
463
491
  );
464
- const resolvedMcpSelections = capabilityCeiling?.denyExtensions
465
- ? []
466
- : resolveMcpDirectToolSelections(input.mcpDirectTools, input.cwd);
492
+ const mcpResolution = capabilityCeiling?.denyExtensions
493
+ ? { selections: [], unresolvedSelectors: [] }
494
+ : resolveMcpDirectToolResolution(input.mcpDirectTools, input.cwd, input.runtimeSnapshotHost, input.mcpConfig);
495
+ if (mcpResolution.unresolvedSelectors.length > 0) {
496
+ throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
497
+ }
498
+ const resolvedMcpSelections = mcpResolution.selections;
467
499
  const effectiveMcpSelections = resolvedMcpSelections.filter(
468
500
  (selection) => !allowedToolSet || allowedToolSet.has(selection.name),
469
501
  );
470
502
  const effectiveMcpTools = effectiveMcpSelections.map(
471
503
  (selection) => selection.name,
472
504
  );
505
+ const runtimeServerNames = mcpResolution.runtimeServerNames ?? input.runtimeServerNames ?? [];
506
+ const effectiveRuntimeServerNames = runtimeServerNames.filter((serverName) =>
507
+ effectiveMcpSelections.some((selection) => selection.selector.startsWith(`${serverName}/`)),
508
+ );
509
+ const effectiveMcpConfig = mcpResolution.mcpConfig ?? input.mcpConfig;
510
+ const filteredMcpConfig = effectiveMcpConfig
511
+ ? filterRuntimeMcpConfig(effectiveMcpConfig, runtimeServerNames, effectiveRuntimeServerNames)
512
+ : undefined;
473
513
  const explicitToolAllowlist =
474
514
  input.tools !== undefined ||
475
515
  (input.mcpDirectTools?.length ?? 0) > 0 ||
@@ -605,6 +645,9 @@ export function resolvePiLaunchToolPlan(
605
645
  extensionArgs,
606
646
  disableAmbientExtensions,
607
647
  warnings,
648
+ ...(effectiveRuntimeServerNames.length > 0 && filteredMcpConfig
649
+ ? { mcpConfig: filteredMcpConfig, runtimeServerNames: effectiveRuntimeServerNames }
650
+ : {}),
608
651
  ...(capabilityAudit ? { capabilityAudit } : {}),
609
652
  };
610
653
  }
@@ -644,6 +687,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
644
687
  extensions: input.extensions,
645
688
  subagentOnlyExtensions: input.subagentOnlyExtensions,
646
689
  mcpDirectTools: input.mcpDirectTools,
690
+ mcpConfig: input.mcpConfig,
691
+ runtimeServerNames: input.runtimeServerNames,
647
692
  cwd: input.cwd,
648
693
  requireReadTool: input.requireReadTool,
649
694
  structuredOutput: input.structuredOutput,
@@ -656,6 +701,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
656
701
  ),
657
702
  agentName: input.childAgentName,
658
703
  permissionRules: input.permissionRules,
704
+ runtimeSnapshotHost: input.runtimeSnapshotHost,
659
705
  });
660
706
  if (toolPlan.explicitToolAllowlist) {
661
707
  args.push(
@@ -669,6 +715,14 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
669
715
  }
670
716
  for (const extPath of toolPlan.extensionArgs)
671
717
  args.push("--extension", extPath);
718
+ let tempDir: string | undefined;
719
+ if (toolPlan.mcpConfig && toolPlan.runtimeServerNames?.length) {
720
+ if (!tempDir)
721
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
722
+ const mcpConfigPath = path.join(tempDir, "mcp-config.json");
723
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(toolPlan.mcpConfig, null, 2), { mode: 0o600 });
724
+ args.push("--mcp-config", mcpConfigPath);
725
+ }
672
726
 
673
727
  if (!input.inheritProjectContext) {
674
728
  args.push("--no-context-files");
@@ -677,9 +731,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
677
731
  args.push("--no-skills");
678
732
  }
679
733
 
680
- let tempDir: string | undefined;
681
734
  if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
682
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
735
+ if (!tempDir)
736
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
683
737
  const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
684
738
  const promptPath = path.join(tempDir, `${stem}.md`);
685
739
  // Inject <active_agent> tag so @gotgenes/pi-permission-system can
@@ -817,6 +871,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
817
871
  env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext
818
872
  ? "1"
819
873
  : "0";
874
+ env[SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV] = input.inheritGlobalContext ? "1" : "0";
820
875
  env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
821
876
  env[PI_INTERCOM_STABLE_ID_ENV] = input.intercomSessionName || undefined;
822
877
  env[PI_INTERCOM_SESSION_ID_ENV] = undefined;
@@ -96,6 +96,23 @@ function formatOutputPathInstruction(outputPath: string, capabilities?: OutputIn
96
96
  ].join("\n");
97
97
  }
98
98
 
99
+ export function requestedOutputPathFromTask(task: string): string | undefined {
100
+ for (const line of task.split(/\r?\n/).reverse()) {
101
+ const match = line.match(/^\s*(?:Write your findings to(?: exactly this path)?:|The runtime will persist it to exactly this path:)\s*(.+?)\s*$/i);
102
+ if (!match?.[1]) continue;
103
+ const requested = match[1].trim();
104
+ return requested.startsWith("`") && requested.endsWith("`") ? requested.slice(1, -1) : requested;
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ export function outputPathMappingFromTask(task: string, savedPath: string | undefined): { requestedPath: string; savedPath: string } | undefined {
110
+ const requestedPath = requestedOutputPathFromTask(task);
111
+ if (!requestedPath || !savedPath) return undefined;
112
+ if (path.isAbsolute(requestedPath) && path.normalize(requestedPath) === path.normalize(savedPath)) return undefined;
113
+ return { requestedPath, savedPath };
114
+ }
115
+
99
116
  export function injectSingleOutputInstruction(task: string, outputPath: string | undefined, capabilities?: OutputInstructionCapabilities): string {
100
117
  if (!outputPath) return task;
101
118
  return `${task}\n\n---\n**Output:**\n${formatOutputPathInstruction(outputPath, capabilities)}`;