pi-subagents 0.58.0 → 0.60.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 +102 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +93 -5
- package/docs/workflows.md +94 -3
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +52 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +159 -42
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +16 -2
- package/src/extension/schemas.ts +36 -10
- package/src/extension/tool-description.ts +24 -5
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +52 -37
- package/src/runs/background/async-job-tracker.ts +17 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +110 -8
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +30 -9
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +351 -358
- package/src/runs/background/subagent-wait.ts +68 -12
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +25 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +125 -122
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +576 -240
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +597 -0
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +67 -3
- package/src/runs/shared/single-output.ts +72 -22
- package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +232 -30
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +134 -75
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +895 -121
- package/src/watchdog/change-signature.ts +40 -1
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +235 -0
- package/src/workflows/scripted-workflow.ts +503 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -39,6 +39,44 @@ export interface McpServerDefinition {
|
|
|
39
39
|
literalEnv?: boolean;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export function isMcpServerDefinition(value: unknown): value is McpServerDefinition {
|
|
43
|
+
if (!isRecord(value)) return false;
|
|
44
|
+
for (const field of ["command", "socket", "cwd", "url", "bearerToken", "bearerTokenEnv", "protocolVersion", "httpTransport", "pluginDataDir"] as const) {
|
|
45
|
+
if (value[field] !== undefined && typeof value[field] !== "string") return false;
|
|
46
|
+
}
|
|
47
|
+
for (const field of ["args", "includeTools", "excludeTools"] as const) {
|
|
48
|
+
if (value[field] !== undefined && !isStringArray(value[field])) return false;
|
|
49
|
+
}
|
|
50
|
+
for (const field of ["env", "headers"] as const) {
|
|
51
|
+
if (value[field] !== undefined && !isStringRecord(value[field])) return false;
|
|
52
|
+
}
|
|
53
|
+
for (const field of ["exposeResources", "literalEnv"] as const) {
|
|
54
|
+
if (value[field] !== undefined && typeof value[field] !== "boolean") return false;
|
|
55
|
+
}
|
|
56
|
+
if (value.requestHeadersCommand !== undefined && !isRequestHeadersCommand(value.requestHeadersCommand)) return false;
|
|
57
|
+
if (value.auth !== undefined && value.auth !== "oauth" && value.auth !== "bearer" && value.auth !== false) return false;
|
|
58
|
+
return value.directTools === undefined || typeof value.directTools === "boolean" || isStringArray(value.directTools);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
62
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isStringArray(value: unknown): value is string[] {
|
|
66
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
70
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isRequestHeadersCommand(value: unknown): value is NonNullable<McpServerDefinition["requestHeadersCommand"]> {
|
|
74
|
+
if (!isRecord(value) || typeof value.command !== "string") return false;
|
|
75
|
+
if (value.args !== undefined && !isStringArray(value.args)) return false;
|
|
76
|
+
if (value.env !== undefined && !isStringRecord(value.env)) return false;
|
|
77
|
+
return value.timeoutMs === undefined || (typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs));
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
export function loadPackageMcpServers(cwd: string): Record<string, McpServerDefinition> {
|
|
43
81
|
const servers: Record<string, McpServerDefinition> = {};
|
|
44
82
|
const seen = new Set<string>();
|
|
@@ -64,11 +102,11 @@ export function loadPackageMcpServers(cwd: string): Record<string, McpServerDefi
|
|
|
64
102
|
const rawServers = (config as { mcpServers?: unknown }).mcpServers;
|
|
65
103
|
if (!rawServers || typeof rawServers !== "object" || Array.isArray(rawServers)) continue;
|
|
66
104
|
for (const [serverName, definition] of Object.entries(rawServers)) {
|
|
67
|
-
if (!
|
|
105
|
+
if (!isMcpServerDefinition(definition)) continue;
|
|
68
106
|
const normalizedName = `${packagePrefix}__${formatName(serverName, "server")}`;
|
|
69
107
|
if (seen.has(normalizedName)) continue;
|
|
70
108
|
seen.add(normalizedName);
|
|
71
|
-
servers[normalizedName] = definition
|
|
109
|
+
servers[normalizedName] = definition;
|
|
72
110
|
}
|
|
73
111
|
}
|
|
74
112
|
}
|
|
@@ -90,7 +128,7 @@ export function loadAgentPluginMcpServers(paths: unknown, cwd: string): Record<s
|
|
|
90
128
|
const rawServers = (config as { mcpServers: Record<string, unknown> }).mcpServers;
|
|
91
129
|
|
|
92
130
|
for (const [serverName, rawDefinition] of Object.entries(rawServers)) {
|
|
93
|
-
const definition = translatePluginServer(manifest.name, pluginRoot,
|
|
131
|
+
const definition = translatePluginServer(manifest.name, pluginRoot, rawDefinition);
|
|
94
132
|
if (!definition) continue;
|
|
95
133
|
const normalizedName = `${formatName(manifest.name, "plugin")}__${formatName(serverName, "server")}`;
|
|
96
134
|
if (Object.hasOwn(servers, normalizedName)) continue;
|
|
@@ -254,12 +292,11 @@ function isValidPluginConfig(value: unknown): value is { mcpServers: Record<stri
|
|
|
254
292
|
function translatePluginServer(
|
|
255
293
|
pluginName: string,
|
|
256
294
|
pluginRoot: string,
|
|
257
|
-
serverName: string,
|
|
258
295
|
value: unknown,
|
|
259
296
|
): McpServerDefinition | undefined {
|
|
260
297
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
261
298
|
const raw = value as Record<string, unknown>;
|
|
262
|
-
if (raw.type === "stdio") return translatePluginStdioServer(pluginName, pluginRoot,
|
|
299
|
+
if (raw.type === "stdio") return translatePluginStdioServer(pluginName, pluginRoot, raw);
|
|
263
300
|
if (raw.type === "streamable-http" || raw.type === "sse") return translatePluginHttpServer(raw);
|
|
264
301
|
return undefined;
|
|
265
302
|
}
|
|
@@ -267,7 +304,6 @@ function translatePluginServer(
|
|
|
267
304
|
function translatePluginStdioServer(
|
|
268
305
|
pluginName: string,
|
|
269
306
|
pluginRoot: string,
|
|
270
|
-
serverName: string,
|
|
271
307
|
raw: Record<string, unknown>,
|
|
272
308
|
): McpServerDefinition | undefined {
|
|
273
309
|
if ([...Object.keys(raw)].some((key) => !PLUGIN_STDIO_FIELDS.has(key))) return undefined;
|
|
@@ -4,11 +4,19 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { findConfiguredProjectRoot } from "../../agents/agents.ts";
|
|
6
6
|
import { getAgentDir, getProjectConfigDir } from "../../shared/utils.ts";
|
|
7
|
-
import { loadAgentPluginMcpServers, loadPackageMcpServers, type McpServerDefinition } from "./mcp-config-sources.ts";
|
|
7
|
+
import { isMcpServerDefinition, loadAgentPluginMcpServers, loadPackageMcpServers, type McpServerDefinition } from "./mcp-config-sources.ts";
|
|
8
|
+
import {
|
|
9
|
+
normalizeMcpDirectToolSelectors,
|
|
10
|
+
parseMcpDirectToolSelectors,
|
|
11
|
+
planMcpDirectToolGrant,
|
|
12
|
+
} from "./mcp-direct-tool-grant.ts";
|
|
13
|
+
import type { McpToolPrefix, ResolvedMcpDirectToolSelection } from "./mcp-direct-tool-grant.ts";
|
|
14
|
+
|
|
15
|
+
export { formatUnresolvedMcpDirectToolSelectors } from "./mcp-direct-tool-grant.ts";
|
|
16
|
+
export type { ResolvedMcpDirectToolSelection } from "./mcp-direct-tool-grant.ts";
|
|
8
17
|
|
|
9
18
|
const CACHE_VERSION = 1;
|
|
10
19
|
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
-
const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
12
20
|
const GENERIC_GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".config", "mcp", "mcp.json");
|
|
13
21
|
const IMPORT_PATHS = {
|
|
14
22
|
cursor: [path.join(os.homedir(), ".cursor", "mcp.json")],
|
|
@@ -23,7 +31,6 @@ const IMPORT_PATHS = {
|
|
|
23
31
|
vscode: [".vscode/mcp.json"],
|
|
24
32
|
} as const;
|
|
25
33
|
|
|
26
|
-
type ToolPrefix = "server" | "none" | "short";
|
|
27
34
|
type ImportKind = keyof typeof IMPORT_PATHS;
|
|
28
35
|
|
|
29
36
|
type ServerEntry = McpServerDefinition;
|
|
@@ -32,7 +39,7 @@ export interface McpConfig {
|
|
|
32
39
|
mcpServers: Record<string, ServerEntry>;
|
|
33
40
|
imports?: ImportKind[];
|
|
34
41
|
settings?: {
|
|
35
|
-
toolPrefix?:
|
|
42
|
+
toolPrefix?: McpToolPrefix;
|
|
36
43
|
directTools?: boolean;
|
|
37
44
|
agentPluginPaths?: unknown;
|
|
38
45
|
};
|
|
@@ -83,8 +90,6 @@ interface MetadataCache {
|
|
|
83
90
|
servers: Record<string, ServerCacheEntry>;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
export interface ResolvedMcpDirectToolSelection { name: string; selector: string }
|
|
87
|
-
|
|
88
93
|
export interface McpDirectToolResolution {
|
|
89
94
|
selections: ResolvedMcpDirectToolSelection[];
|
|
90
95
|
unresolvedSelectors: string[];
|
|
@@ -93,21 +98,17 @@ export interface McpDirectToolResolution {
|
|
|
93
98
|
runtimeServerNames?: string[];
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
function normalizeSelectors(selectors: string[] | undefined): string[] {
|
|
97
|
-
return [...new Set((selectors ?? []).map((selector) => selector.replace(/\/+$/, "")).filter(Boolean))];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
101
|
export function resolveMcpDirectToolResolution(
|
|
101
102
|
mcpDirectTools: string[] | undefined,
|
|
102
103
|
cwd = process.cwd(),
|
|
103
104
|
runtimeSnapshotHost?: McpRuntimeSnapshotHost,
|
|
104
105
|
configOverride?: McpConfig,
|
|
105
106
|
): McpDirectToolResolution {
|
|
106
|
-
const selectors =
|
|
107
|
+
const selectors = normalizeMcpDirectToolSelectors(mcpDirectTools);
|
|
107
108
|
if (selectors.length === 0) return { selections: [], unresolvedSelectors: [] };
|
|
108
109
|
|
|
109
110
|
const config = configOverride ?? loadMcpConfig(cwd);
|
|
110
|
-
const { servers: selectedServers, tools: selectedTools } =
|
|
111
|
+
const { servers: selectedServers, tools: selectedTools } = parseMcpDirectToolSelectors(selectors);
|
|
111
112
|
const runtimeSelectionServers = new Set([...selectedServers, ...selectedTools.keys()]);
|
|
112
113
|
const runtimeServers = resolveRuntimeMcpServers(config, runtimeSelectionServers, runtimeSnapshotHost);
|
|
113
114
|
const resolvedConfig = Object.keys(runtimeServers).length > 0
|
|
@@ -116,15 +117,20 @@ export function resolveMcpDirectToolResolution(
|
|
|
116
117
|
validateSelectedServerDefinitions(resolvedConfig, selectors);
|
|
117
118
|
const cache = loadMetadataCache();
|
|
118
119
|
if (!cache) return { selections: [], unresolvedSelectors: selectors };
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
const validMetadata: Record<string, ServerCacheEntry> = {};
|
|
121
|
+
for (const [serverName, definition] of Object.entries(resolvedConfig.mcpServers)) {
|
|
122
|
+
const serverCache = cache.servers[serverName];
|
|
123
|
+
if (isServerCacheValid(serverCache, definition)) validMetadata[serverName] = serverCache;
|
|
124
|
+
}
|
|
125
|
+
const grant = planMcpDirectToolGrant({
|
|
126
|
+
selectors,
|
|
127
|
+
servers: resolvedConfig.mcpServers,
|
|
128
|
+
metadata: validMetadata,
|
|
129
|
+
toolPrefix: resolvedConfig.settings?.toolPrefix,
|
|
130
|
+
});
|
|
125
131
|
return {
|
|
126
|
-
selections,
|
|
127
|
-
unresolvedSelectors,
|
|
132
|
+
selections: grant.selections,
|
|
133
|
+
unresolvedSelectors: grant.unresolvedSelectors,
|
|
128
134
|
...(Object.keys(runtimeServers).length > 0
|
|
129
135
|
? { mcpConfig: resolvedConfig, runtimeServerNames: Object.keys(runtimeServers) }
|
|
130
136
|
: {}),
|
|
@@ -139,10 +145,6 @@ export function resolveMcpDirectToolSelections(
|
|
|
139
145
|
return resolveMcpDirectToolResolution(mcpDirectTools, cwd, runtimeSnapshotHost).selections;
|
|
140
146
|
}
|
|
141
147
|
|
|
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.`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
148
|
function loadMetadataCache(): MetadataCache | null {
|
|
147
149
|
const cachePath = path.join(getAgentDir(), "mcp-cache.json");
|
|
148
150
|
let parsed: unknown;
|
|
@@ -152,12 +154,46 @@ function loadMetadataCache(): MetadataCache | null {
|
|
|
152
154
|
return null;
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
if (!parsed ||
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
if (!isRecord(parsed) || parsed.version !== CACHE_VERSION || !isRecord(parsed.servers)) return null;
|
|
158
|
+
const servers: Record<string, ServerCacheEntry> = {};
|
|
159
|
+
for (const [name, value] of Object.entries(parsed.servers)) {
|
|
160
|
+
const entry = parseServerCacheEntry(value);
|
|
161
|
+
if (entry) servers[name] = entry;
|
|
159
162
|
}
|
|
160
|
-
return
|
|
163
|
+
return { version: CACHE_VERSION, servers };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseServerCacheEntry(value: unknown): ServerCacheEntry | undefined {
|
|
167
|
+
if (!isRecord(value)) return undefined;
|
|
168
|
+
if (value.configHash !== undefined && typeof value.configHash !== "string") return undefined;
|
|
169
|
+
if (value.cachedAt !== undefined && (typeof value.cachedAt !== "number" || !Number.isFinite(value.cachedAt))) return undefined;
|
|
170
|
+
if (value.tools !== undefined && !Array.isArray(value.tools)) return undefined;
|
|
171
|
+
if (value.resources !== undefined && !Array.isArray(value.resources)) return undefined;
|
|
172
|
+
const tools = value.tools?.map(parseCachedTool);
|
|
173
|
+
const resources = value.resources?.map(parseCachedResource);
|
|
174
|
+
if (tools && !tools.every((entry): entry is CachedTool => entry !== undefined)) return undefined;
|
|
175
|
+
if (resources && !resources.every((entry): entry is CachedResource => entry !== undefined)) return undefined;
|
|
176
|
+
return {
|
|
177
|
+
...(value.configHash !== undefined ? { configHash: value.configHash } : {}),
|
|
178
|
+
...(value.cachedAt !== undefined ? { cachedAt: value.cachedAt } : {}),
|
|
179
|
+
...(tools ? { tools } : {}),
|
|
180
|
+
...(resources ? { resources } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseCachedTool(value: unknown): CachedTool | undefined {
|
|
185
|
+
if (!isRecord(value) || (value.name !== undefined && typeof value.name !== "string")) return undefined;
|
|
186
|
+
return value.name === undefined ? {} : { name: value.name };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseCachedResource(value: unknown): CachedResource | undefined {
|
|
190
|
+
if (!isRecord(value)) return undefined;
|
|
191
|
+
if (value.uri !== undefined && typeof value.uri !== "string") return undefined;
|
|
192
|
+
if (value.name !== undefined && typeof value.name !== "string") return undefined;
|
|
193
|
+
return {
|
|
194
|
+
...(value.uri !== undefined ? { uri: value.uri } : {}),
|
|
195
|
+
...(value.name !== undefined ? { name: value.name } : {}),
|
|
196
|
+
};
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
function resolveRuntimeMcpServers(
|
|
@@ -196,9 +232,7 @@ function getRuntimeMcpServerSnapshot(
|
|
|
196
232
|
snapshot.name !== name ||
|
|
197
233
|
snapshot.runtime !== true ||
|
|
198
234
|
snapshot.persisted !== false ||
|
|
199
|
-
!snapshot.definition
|
|
200
|
-
typeof snapshot.definition !== "object" ||
|
|
201
|
-
Array.isArray(snapshot.definition)
|
|
235
|
+
!isMcpServerDefinition(snapshot.definition)
|
|
202
236
|
) {
|
|
203
237
|
throw new Error(`Invalid MCP runtime snapshot for server "${name}"`);
|
|
204
238
|
}
|
|
@@ -249,18 +283,33 @@ function readConfig(configPath: string): McpConfig | null {
|
|
|
249
283
|
}
|
|
250
284
|
|
|
251
285
|
function validateConfig(raw: unknown): McpConfig {
|
|
252
|
-
if (!
|
|
253
|
-
const obj = raw
|
|
286
|
+
if (!isRecord(raw)) return { mcpServers: {} };
|
|
287
|
+
const obj = raw;
|
|
254
288
|
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
255
289
|
return {
|
|
256
|
-
mcpServers:
|
|
290
|
+
mcpServers: parseServerEntries(servers),
|
|
257
291
|
imports: Array.isArray(obj.imports) ? obj.imports.filter((value): value is ImportKind => isImportKind(value)) : undefined,
|
|
258
|
-
settings:
|
|
259
|
-
? obj.settings as McpConfig["settings"]
|
|
260
|
-
: undefined,
|
|
292
|
+
settings: parseSettings(obj.settings),
|
|
261
293
|
};
|
|
262
294
|
}
|
|
263
295
|
|
|
296
|
+
function parseServerEntries(value: unknown): Record<string, ServerEntry> {
|
|
297
|
+
if (!isRecord(value)) return {};
|
|
298
|
+
return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, ServerEntry] => isMcpServerDefinition(entry[1])));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseSettings(value: unknown): McpConfig["settings"] | undefined {
|
|
302
|
+
if (!isRecord(value)) return undefined;
|
|
303
|
+
const toolPrefix = value.toolPrefix === "server" || value.toolPrefix === "short" || value.toolPrefix === "none" ? value.toolPrefix : undefined;
|
|
304
|
+
const directTools = typeof value.directTools === "boolean" ? value.directTools : undefined;
|
|
305
|
+
const settings: NonNullable<McpConfig["settings"]> = {
|
|
306
|
+
...(toolPrefix ? { toolPrefix } : {}),
|
|
307
|
+
...(directTools !== undefined ? { directTools } : {}),
|
|
308
|
+
...(value.agentPluginPaths !== undefined ? { agentPluginPaths: value.agentPluginPaths } : {}),
|
|
309
|
+
};
|
|
310
|
+
return Object.keys(settings).length ? settings : undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
264
313
|
function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
265
314
|
const imports = [...(base.imports ?? []), ...(next.imports ?? [])];
|
|
266
315
|
return {
|
|
@@ -304,79 +353,20 @@ function resolveImportPath(importKind: ImportKind, cwd: string): string | null {
|
|
|
304
353
|
}
|
|
305
354
|
|
|
306
355
|
function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
|
|
307
|
-
if (!
|
|
308
|
-
const obj = config
|
|
356
|
+
if (!isRecord(config)) return {};
|
|
357
|
+
const obj = config;
|
|
309
358
|
const servers = kind === "cursor" || kind === "windsurf" || kind === "vscode"
|
|
310
359
|
? obj.mcpServers ?? obj["mcp-servers"]
|
|
311
360
|
: obj.mcpServers;
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function resolveDirectToolSelections(config: McpConfig, cache: MetadataCache, prefix: ToolPrefix, envOverride: string[]): ResolvedMcpDirectToolSelection[] {
|
|
316
|
-
const names: ResolvedMcpDirectToolSelection[] = [];
|
|
317
|
-
const seenNames = new Set<string>();
|
|
318
|
-
const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
|
|
319
|
-
|
|
320
|
-
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
321
|
-
const serverCache = cache.servers[serverName];
|
|
322
|
-
if (!isServerCacheValid(serverCache, definition)) continue;
|
|
323
|
-
|
|
324
|
-
const toolFilter = selectedServers.has(serverName)
|
|
325
|
-
? true
|
|
326
|
-
: selectedTools.get(serverName);
|
|
327
|
-
if (!toolFilter) continue;
|
|
328
|
-
|
|
329
|
-
for (const tool of Array.isArray(serverCache.tools) ? serverCache.tools : []) {
|
|
330
|
-
if (typeof tool?.name !== "string" || !tool.name) continue;
|
|
331
|
-
if (toolFilter !== true && !toolFilter.has(tool.name)) continue;
|
|
332
|
-
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
|
|
333
|
-
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
334
|
-
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
335
|
-
seenNames.add(prefixedName);
|
|
336
|
-
names.push({ name: prefixedName, selector: `${serverName}/${tool.name}` });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (definition.exposeResources === false) continue;
|
|
340
|
-
for (const resource of Array.isArray(serverCache.resources) ? serverCache.resources : []) {
|
|
341
|
-
if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri) continue;
|
|
342
|
-
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
343
|
-
if (toolFilter !== true && !toolFilter.has(baseName)) continue;
|
|
344
|
-
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
|
|
345
|
-
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
346
|
-
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
347
|
-
seenNames.add(prefixedName);
|
|
348
|
-
names.push({ name: prefixedName, selector: `${serverName}/${baseName}` });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return names;
|
|
361
|
+
return parseServerEntries(servers);
|
|
353
362
|
}
|
|
354
363
|
|
|
355
364
|
export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
|
|
356
365
|
return resolveMcpDirectToolSelections(mcpDirectTools, cwd).map((selection) => selection.name);
|
|
357
366
|
}
|
|
358
367
|
|
|
359
|
-
function parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {
|
|
360
|
-
const servers = new Set<string>();
|
|
361
|
-
const tools = new Map<string, Set<string>>();
|
|
362
|
-
for (const item of selections) {
|
|
363
|
-
if (item.includes("/")) {
|
|
364
|
-
const [server, tool] = item.split("/", 2);
|
|
365
|
-
if (server && tool) {
|
|
366
|
-
if (!tools.has(server)) tools.set(server, new Set());
|
|
367
|
-
tools.get(server)!.add(tool);
|
|
368
|
-
} else if (server) {
|
|
369
|
-
servers.add(server);
|
|
370
|
-
}
|
|
371
|
-
} else if (item) {
|
|
372
|
-
servers.add(item);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return { servers, tools };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
368
|
function validateSelectedServerDefinitions(config: McpConfig, selectors: string[]): void {
|
|
379
|
-
const { servers, tools } =
|
|
369
|
+
const { servers, tools } = parseMcpDirectToolSelectors(selectors);
|
|
380
370
|
for (const serverName of new Set([...servers, ...tools.keys()])) {
|
|
381
371
|
const definition = config.mcpServers[serverName];
|
|
382
372
|
if (definition) computeMcpServerHash(definition);
|
|
@@ -417,52 +407,12 @@ export function computeMcpServerHash(definition: ServerEntry): string {
|
|
|
417
407
|
return createHash("sha256").update(stableStringify(identity)).digest("hex");
|
|
418
408
|
}
|
|
419
409
|
|
|
420
|
-
function getToolPrefix(value: unknown): ToolPrefix {
|
|
421
|
-
return value === "none" || value === "short" || value === "server" ? value : "server";
|
|
422
|
-
}
|
|
423
|
-
|
|
424
410
|
function isImportKind(value: unknown): value is ImportKind {
|
|
425
411
|
return typeof value === "string" && Object.hasOwn(IMPORT_PATHS, value);
|
|
426
412
|
}
|
|
427
413
|
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
if (mode === "short") {
|
|
431
|
-
const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
|
|
432
|
-
return short || "mcp";
|
|
433
|
-
}
|
|
434
|
-
return serverName.replace(/-/g, "_");
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string {
|
|
438
|
-
const serverPrefix = getServerPrefix(serverName, prefix);
|
|
439
|
-
return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function isToolExcluded(toolName: string, serverName: string, prefix: ToolPrefix, excludeTools: unknown): boolean {
|
|
443
|
-
if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
|
|
444
|
-
const candidates = new Set([
|
|
445
|
-
normalizeToolName(toolName),
|
|
446
|
-
normalizeToolName(formatToolName(toolName, serverName, prefix)),
|
|
447
|
-
normalizeToolName(formatToolName(toolName, serverName, "server")),
|
|
448
|
-
normalizeToolName(formatToolName(toolName, serverName, "short")),
|
|
449
|
-
]);
|
|
450
|
-
return excludeTools.some((excluded) => typeof excluded === "string" && candidates.has(normalizeToolName(excluded)));
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function normalizeToolName(value: string): string {
|
|
454
|
-
return value.replace(/-/g, "_");
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function resourceNameToToolName(name: string): string {
|
|
458
|
-
let result = name
|
|
459
|
-
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
460
|
-
.replace(/_+/g, "_")
|
|
461
|
-
.replace(/^_+/, "")
|
|
462
|
-
.replace(/_+$/, "")
|
|
463
|
-
.toLowerCase();
|
|
464
|
-
if (!result || /^\d/.test(result)) result = `resource${result ? `_${result}` : ""}`;
|
|
465
|
-
return result;
|
|
414
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
415
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
466
416
|
}
|
|
467
417
|
|
|
468
418
|
function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
2
|
+
|
|
3
|
+
export type McpToolPrefix = "server" | "none" | "short";
|
|
4
|
+
|
|
5
|
+
export interface McpGrantServerFacts {
|
|
6
|
+
readonly exposeResources?: boolean;
|
|
7
|
+
readonly includeTools?: readonly unknown[];
|
|
8
|
+
readonly excludeTools?: readonly unknown[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface McpGrantToolMetadata {
|
|
12
|
+
readonly name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface McpGrantResourceMetadata {
|
|
16
|
+
readonly name?: string;
|
|
17
|
+
readonly uri?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface McpGrantServerMetadata {
|
|
21
|
+
readonly tools?: readonly McpGrantToolMetadata[];
|
|
22
|
+
readonly resources?: readonly McpGrantResourceMetadata[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface McpDirectToolGrantInput {
|
|
26
|
+
readonly selectors?: readonly string[];
|
|
27
|
+
readonly servers: Readonly<Record<string, McpGrantServerFacts>>;
|
|
28
|
+
/** Cache entries already validated by the source-loading adapter. */
|
|
29
|
+
readonly metadata: Readonly<Record<string, McpGrantServerMetadata>>;
|
|
30
|
+
readonly toolPrefix?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResolvedMcpDirectToolSelection {
|
|
34
|
+
name: string;
|
|
35
|
+
selector: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface McpDirectToolGrant {
|
|
39
|
+
selections: ResolvedMcpDirectToolSelection[];
|
|
40
|
+
unresolvedSelectors: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeMcpDirectToolSelectors(selectors: readonly string[] | undefined): string[] {
|
|
44
|
+
return [...new Set((selectors ?? []).map((selector) => selector.replace(/\/+$/, "")).filter(Boolean))];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseMcpDirectToolSelectors(selectors: readonly string[]): {
|
|
48
|
+
servers: Set<string>;
|
|
49
|
+
tools: Map<string, Set<string>>;
|
|
50
|
+
} {
|
|
51
|
+
const servers = new Set<string>();
|
|
52
|
+
const tools = new Map<string, Set<string>>();
|
|
53
|
+
for (const item of selectors) {
|
|
54
|
+
if (item.includes("/")) {
|
|
55
|
+
const [server, tool] = item.split("/", 2);
|
|
56
|
+
if (server && tool) {
|
|
57
|
+
if (!tools.has(server)) tools.set(server, new Set());
|
|
58
|
+
tools.get(server)!.add(tool);
|
|
59
|
+
} else if (server) {
|
|
60
|
+
servers.add(server);
|
|
61
|
+
}
|
|
62
|
+
} else if (item) {
|
|
63
|
+
servers.add(item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { servers, tools };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function planMcpDirectToolGrant(input: McpDirectToolGrantInput): McpDirectToolGrant {
|
|
70
|
+
const selectors = normalizeMcpDirectToolSelectors(input.selectors);
|
|
71
|
+
if (selectors.length === 0) return { selections: [], unresolvedSelectors: [] };
|
|
72
|
+
|
|
73
|
+
const { servers: selectedServers, tools: selectedTools } = parseMcpDirectToolSelectors(selectors);
|
|
74
|
+
const selections: ResolvedMcpDirectToolSelection[] = [];
|
|
75
|
+
const seenNames = new Set<string>();
|
|
76
|
+
const prefix = normalizeMcpToolPrefix(input.toolPrefix);
|
|
77
|
+
|
|
78
|
+
for (const [serverName, server] of Object.entries(input.servers)) {
|
|
79
|
+
const metadata = input.metadata[serverName];
|
|
80
|
+
if (!metadata) continue;
|
|
81
|
+
|
|
82
|
+
const toolFilter: true | ReadonlySet<string> | undefined = selectedServers.has(serverName)
|
|
83
|
+
? true
|
|
84
|
+
: selectedTools.get(serverName);
|
|
85
|
+
if (!toolFilter) continue;
|
|
86
|
+
|
|
87
|
+
for (const tool of Array.isArray(metadata.tools) ? metadata.tools : []) {
|
|
88
|
+
if (typeof tool?.name !== "string" || !tool.name) continue;
|
|
89
|
+
if (toolFilter !== true && !toolFilter.has(tool.name)) continue;
|
|
90
|
+
if (!isToolAllowed(tool.name, serverName, prefix, server.includeTools, server.excludeTools)) continue;
|
|
91
|
+
const name = formatToolName(tool.name, serverName, prefix);
|
|
92
|
+
if (BUILTIN_TOOL_NAMES.has(name) || seenNames.has(name)) continue;
|
|
93
|
+
seenNames.add(name);
|
|
94
|
+
selections.push({ name, selector: `${serverName}/${tool.name}` });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (server.exposeResources === false) continue;
|
|
98
|
+
for (const resource of Array.isArray(metadata.resources) ? metadata.resources : []) {
|
|
99
|
+
if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri) continue;
|
|
100
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
101
|
+
if (toolFilter !== true && !toolFilter.has(baseName)) continue;
|
|
102
|
+
if (!isToolAllowed(baseName, serverName, prefix, server.includeTools, server.excludeTools)) continue;
|
|
103
|
+
const name = formatToolName(baseName, serverName, prefix);
|
|
104
|
+
if (BUILTIN_TOOL_NAMES.has(name) || seenNames.has(name)) continue;
|
|
105
|
+
seenNames.add(name);
|
|
106
|
+
selections.push({ name, selector: `${serverName}/${baseName}` });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const unresolvedSelectors = selectors.filter((selector) =>
|
|
111
|
+
selector.includes("/")
|
|
112
|
+
? !selections.some((selection) => selection.selector === selector)
|
|
113
|
+
: !selections.some((selection) => selection.selector.startsWith(`${selector}/`)),
|
|
114
|
+
);
|
|
115
|
+
return { selections, unresolvedSelectors };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function normalizeMcpToolPrefix(value: unknown): McpToolPrefix {
|
|
119
|
+
return value === "none" || value === "short" || value === "server" ? value : "server";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatUnresolvedMcpDirectToolSelectors(selectors: readonly string[]): string {
|
|
123
|
+
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.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getServerPrefix(serverName: string, mode: McpToolPrefix): string {
|
|
127
|
+
if (mode === "none") return "";
|
|
128
|
+
if (mode === "short") return serverName.replace(/-?mcp$/i, "") || "mcp";
|
|
129
|
+
return serverName;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatToolName(toolName: string, serverName: string, prefix: McpToolPrefix): string {
|
|
133
|
+
const serverPrefix = getServerPrefix(serverName, prefix);
|
|
134
|
+
return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isToolAllowed(
|
|
138
|
+
toolName: string,
|
|
139
|
+
serverName: string,
|
|
140
|
+
prefix: McpToolPrefix,
|
|
141
|
+
includeTools: readonly unknown[] | undefined,
|
|
142
|
+
excludeTools: readonly unknown[] | undefined,
|
|
143
|
+
): boolean {
|
|
144
|
+
const candidates = toolNameCandidates(toolName, serverName, prefix);
|
|
145
|
+
return (!Array.isArray(includeTools) || includeTools.length === 0 || matchesToolPatterns(candidates, includeTools))
|
|
146
|
+
&& !matchesToolPatterns(candidates, excludeTools);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toolNameCandidates(toolName: string, serverName: string, prefix: McpToolPrefix): Set<string> {
|
|
150
|
+
return new Set([
|
|
151
|
+
toolName,
|
|
152
|
+
`mcp_${toolName}`,
|
|
153
|
+
formatToolName(toolName, serverName, prefix),
|
|
154
|
+
formatToolName(toolName, serverName, "server"),
|
|
155
|
+
formatToolName(toolName, serverName, "short"),
|
|
156
|
+
formatToolName(toolName, serverName, "none"),
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function matchesToolPatterns(candidates: ReadonlySet<string>, patterns: readonly unknown[] | undefined): boolean {
|
|
161
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
162
|
+
for (const pattern of patterns) {
|
|
163
|
+
if (typeof pattern !== "string") continue;
|
|
164
|
+
const normalizedPattern = normalizeToolName(pattern);
|
|
165
|
+
const matcher = normalizedPattern.includes("*") || normalizedPattern.includes("?")
|
|
166
|
+
? globToRegExp(normalizedPattern)
|
|
167
|
+
: undefined;
|
|
168
|
+
for (const candidate of candidates) {
|
|
169
|
+
const normalizedCandidate = normalizeToolName(candidate);
|
|
170
|
+
if (matcher ? matcher.test(normalizedCandidate) : normalizedCandidate === normalizedPattern) return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function globToRegExp(pattern: string): RegExp {
|
|
177
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
178
|
+
return new RegExp(`^${escaped}$`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeToolName(value: string): string {
|
|
182
|
+
return value.replace(/-/g, "_");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resourceNameToToolName(name: string): string {
|
|
186
|
+
let result = name
|
|
187
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
188
|
+
.replace(/_+/g, "_")
|
|
189
|
+
.replace(/^_+/, "")
|
|
190
|
+
.replace(/_+$/, "")
|
|
191
|
+
.toLowerCase();
|
|
192
|
+
if (!result || /^\d/.test(result)) result = `resource${result ? `_${result}` : ""}`;
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
@@ -24,7 +24,12 @@ export function formatSubagentModelVerificationError(expectedModel: string, obse
|
|
|
24
24
|
const observedBase = splitThinkingSuffix(observedModel).baseModel;
|
|
25
25
|
if (expectedBase === observedBase) return undefined;
|
|
26
26
|
const expectedEntry = availableModels.find((entry) => entry.fullId === expectedBase);
|
|
27
|
-
if (expectedEntry
|
|
27
|
+
if (expectedEntry) {
|
|
28
|
+
if (expectedEntry.id === observedBase) return undefined;
|
|
29
|
+
const expectedIdLeaf = expectedEntry.id.slice(expectedEntry.id.lastIndexOf("/") + 1);
|
|
30
|
+
const expectedFullIdLeaf = expectedEntry.fullId.slice(expectedEntry.fullId.lastIndexOf("/") + 1);
|
|
31
|
+
if (expectedIdLeaf === observedBase || expectedFullIdLeaf === observedBase) return undefined;
|
|
32
|
+
}
|
|
28
33
|
return `model_verification_failed: child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'.`;
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -446,9 +451,11 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
446
451
|
/upstream/i,
|
|
447
452
|
/timed? out/i,
|
|
448
453
|
/timeout/i,
|
|
454
|
+
/\b500\b/,
|
|
449
455
|
/\b502\b/,
|
|
450
456
|
/\b503\b/,
|
|
451
457
|
/\b504\b/,
|
|
458
|
+
/internal server error/i,
|
|
452
459
|
/cold.?start/i,
|
|
453
460
|
/empty response/i,
|
|
454
461
|
/no output/i,
|
|
@@ -479,7 +486,7 @@ function messageError(message: unknown): string | undefined {
|
|
|
479
486
|
export function isRetryableModelFailureAttempt(input: { error: string | undefined; messages?: readonly unknown[]; toolCount?: number }): boolean {
|
|
480
487
|
if (!isRetryableModelFailure(input.error)) return false;
|
|
481
488
|
if ((input.toolCount ?? 0) > 0) return false;
|
|
482
|
-
if (input.error === "Subagent produced no output (possible model cold-start or empty response).") return true;
|
|
489
|
+
if (input.error === "Subagent produced no output (possible model cold-start or empty response)." || /^Subagent produced no output after terminal assistant stopReason "[^"]+"\.$/.test(input.error ?? "")) return true;
|
|
483
490
|
if ((input.toolCount ?? 0) === 0 && (input.messages?.length ?? 0) === 0) return true;
|
|
484
491
|
const error = input.error?.trim();
|
|
485
492
|
return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
|