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.
- package/CHANGELOG.md +39 -0
- package/docs/agents.md +13 -6
- package/docs/extension-api.md +39 -0
- package/docs/models.md +3 -3
- package/docs/tool-reference.md +3 -2
- package/docs/workflows.md +24 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +23 -0
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +38 -3
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +10 -2
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +26 -1
- package/src/extension/index.ts +15 -4
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +2 -2
- package/src/extension/tool-description.ts +8 -4
- package/src/integrations/herdr-status.ts +5 -0
- package/src/runs/background/async-execution.ts +29 -12
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-watcher.ts +27 -10
- package/src/runs/background/subagent-runner.ts +71 -21
- package/src/runs/foreground/execution.ts +20 -1
- package/src/runs/foreground/subagent-executor.ts +57 -26
- package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +16 -4
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/formatters.ts +6 -0
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/types.ts +42 -1
- package/src/shared/utils.ts +6 -29
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/render.ts +14 -6
- package/src/workflows/scripted-workflow.ts +52 -5
- 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
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
212
|
+
for (const sourcePath of getConfigPaths(projectRoot)) {
|
|
116
213
|
const loaded = readConfig(sourcePath);
|
|
117
214
|
if (!loaded) continue;
|
|
118
|
-
config = mergeConfigs(config, expandImports(loaded,
|
|
215
|
+
config = mergeConfigs(config, expandImports(loaded, projectRoot));
|
|
119
216
|
}
|
|
120
|
-
|
|
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(
|
|
229
|
+
function getConfigPaths(projectRoot: string): string[] {
|
|
124
230
|
const piGlobalPath = path.join(getAgentDir(), "mcp.json");
|
|
125
|
-
const projectPath = path.resolve(
|
|
126
|
-
const projectPiPath = path.resolve(getProjectConfigDir(
|
|
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 (
|
|
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?:
|
|
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
|
|
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
|
-
|
|
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
|
|
465
|
-
? []
|
|
466
|
-
:
|
|
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
|
-
|
|
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)}`;
|