pi-subagents 0.24.2 → 0.24.4
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 +26 -0
- package/README.md +13 -5
- package/package.json +4 -8
- package/prompts/review-loop.md +41 -0
- package/skills/pi-subagents/SKILL.md +51 -21
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/index.ts +8 -24
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/runs/background/async-execution.ts +16 -5
- package/src/runs/background/async-job-tracker.ts +16 -8
- package/src/runs/background/async-status.ts +5 -2
- package/src/runs/background/run-status.ts +4 -1
- package/src/runs/background/subagent-runner.ts +34 -7
- package/src/runs/foreground/execution.ts +17 -5
- package/src/runs/foreground/subagent-executor.ts +6 -7
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +5 -0
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/formatters.ts +13 -0
- package/src/shared/model-info.ts +10 -0
- package/src/shared/types.ts +1 -0
- package/src/shared/utils.ts +11 -1
- package/src/tui/render.ts +160 -147
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
6
|
+
|
|
7
|
+
const CACHE_VERSION = 1;
|
|
8
|
+
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
10
|
+
const GENERIC_GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".config", "mcp", "mcp.json");
|
|
11
|
+
const IMPORT_PATHS = {
|
|
12
|
+
cursor: [path.join(os.homedir(), ".cursor", "mcp.json")],
|
|
13
|
+
"claude-code": [
|
|
14
|
+
path.join(os.homedir(), ".claude", "mcp.json"),
|
|
15
|
+
path.join(os.homedir(), ".claude.json"),
|
|
16
|
+
path.join(os.homedir(), ".claude", "claude_desktop_config.json"),
|
|
17
|
+
],
|
|
18
|
+
"claude-desktop": [path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")],
|
|
19
|
+
codex: [path.join(os.homedir(), ".codex", "config.json")],
|
|
20
|
+
windsurf: [path.join(os.homedir(), ".windsurf", "mcp.json")],
|
|
21
|
+
vscode: [".vscode/mcp.json"],
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
type ToolPrefix = "server" | "none" | "short";
|
|
25
|
+
type ImportKind = keyof typeof IMPORT_PATHS;
|
|
26
|
+
|
|
27
|
+
interface ServerEntry {
|
|
28
|
+
command?: string;
|
|
29
|
+
args?: string[];
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
auth?: "oauth" | "bearer" | false;
|
|
35
|
+
bearerToken?: string;
|
|
36
|
+
bearerTokenEnv?: string;
|
|
37
|
+
exposeResources?: boolean;
|
|
38
|
+
excludeTools?: string[];
|
|
39
|
+
directTools?: boolean | string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface McpConfig {
|
|
43
|
+
mcpServers: Record<string, ServerEntry>;
|
|
44
|
+
imports?: ImportKind[];
|
|
45
|
+
settings?: {
|
|
46
|
+
toolPrefix?: ToolPrefix;
|
|
47
|
+
directTools?: boolean;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface CachedTool {
|
|
52
|
+
name?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CachedResource {
|
|
56
|
+
uri?: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ServerCacheEntry {
|
|
61
|
+
configHash?: string;
|
|
62
|
+
tools?: CachedTool[];
|
|
63
|
+
resources?: CachedResource[];
|
|
64
|
+
cachedAt?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface MetadataCache {
|
|
68
|
+
version: number;
|
|
69
|
+
servers: Record<string, ServerCacheEntry>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
|
|
73
|
+
if (!mcpDirectTools?.length) return [];
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const config = loadMcpConfig(cwd);
|
|
77
|
+
const cache = loadMetadataCache();
|
|
78
|
+
if (!cache) return [];
|
|
79
|
+
return resolveDirectToolNames(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function loadMetadataCache(): MetadataCache | null {
|
|
86
|
+
const cachePath = path.join(getAgentDir(), "mcp-cache.json");
|
|
87
|
+
let parsed: unknown;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
95
|
+
const raw = parsed as Record<string, unknown>;
|
|
96
|
+
if (raw.version !== CACHE_VERSION || !raw.servers || typeof raw.servers !== "object" || Array.isArray(raw.servers)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return raw as unknown as MetadataCache;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadMcpConfig(cwd: string): McpConfig {
|
|
103
|
+
let config: McpConfig = { mcpServers: {} };
|
|
104
|
+
for (const sourcePath of getConfigPaths(cwd)) {
|
|
105
|
+
const loaded = readConfig(sourcePath);
|
|
106
|
+
if (!loaded) continue;
|
|
107
|
+
config = mergeConfigs(config, expandImports(loaded, cwd));
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getConfigPaths(cwd: string): string[] {
|
|
113
|
+
const piGlobalPath = path.join(getAgentDir(), "mcp.json");
|
|
114
|
+
const projectPath = path.resolve(cwd, ".mcp.json");
|
|
115
|
+
const projectPiPath = path.resolve(cwd, ".pi", "mcp.json");
|
|
116
|
+
const sources: string[] = [];
|
|
117
|
+
if (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath) sources.push(GENERIC_GLOBAL_CONFIG_PATH);
|
|
118
|
+
sources.push(piGlobalPath);
|
|
119
|
+
if (projectPath !== piGlobalPath) sources.push(projectPath);
|
|
120
|
+
if (projectPiPath !== piGlobalPath && projectPiPath !== projectPath) sources.push(projectPiPath);
|
|
121
|
+
return sources;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readConfig(configPath: string): McpConfig | null {
|
|
125
|
+
let parsed: unknown;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return validateConfig(parsed);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateConfig(raw: unknown): McpConfig {
|
|
135
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { mcpServers: {} };
|
|
136
|
+
const obj = raw as Record<string, unknown>;
|
|
137
|
+
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
138
|
+
return {
|
|
139
|
+
mcpServers: servers && typeof servers === "object" && !Array.isArray(servers) ? servers as Record<string, ServerEntry> : {},
|
|
140
|
+
imports: Array.isArray(obj.imports) ? obj.imports.filter((value): value is ImportKind => isImportKind(value)) : undefined,
|
|
141
|
+
settings: obj.settings && typeof obj.settings === "object" && !Array.isArray(obj.settings)
|
|
142
|
+
? obj.settings as McpConfig["settings"]
|
|
143
|
+
: undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
148
|
+
const imports = [...(base.imports ?? []), ...(next.imports ?? [])];
|
|
149
|
+
return {
|
|
150
|
+
mcpServers: { ...base.mcpServers, ...next.mcpServers },
|
|
151
|
+
imports: imports.length ? [...new Set(imports)] : undefined,
|
|
152
|
+
settings: next.settings ? { ...base.settings, ...next.settings } : base.settings,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function expandImports(config: McpConfig, cwd: string): McpConfig {
|
|
157
|
+
if (!config.imports?.length) return config;
|
|
158
|
+
|
|
159
|
+
const importedServers: Record<string, ServerEntry> = {};
|
|
160
|
+
for (const importKind of config.imports) {
|
|
161
|
+
const importPath = resolveImportPath(importKind, cwd);
|
|
162
|
+
if (!importPath) continue;
|
|
163
|
+
let imported: unknown;
|
|
164
|
+
try {
|
|
165
|
+
imported = JSON.parse(fs.readFileSync(importPath, "utf-8"));
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const [name, definition] of Object.entries(extractServers(imported, importKind))) {
|
|
170
|
+
if (!importedServers[name]) importedServers[name] = definition;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
imports: config.imports,
|
|
176
|
+
settings: config.settings,
|
|
177
|
+
mcpServers: { ...importedServers, ...config.mcpServers },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolveImportPath(importKind: ImportKind, cwd: string): string | null {
|
|
182
|
+
for (const candidate of IMPORT_PATHS[importKind]) {
|
|
183
|
+
const fullPath = candidate.startsWith(".") ? path.resolve(cwd, candidate) : candidate;
|
|
184
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
|
|
190
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return {};
|
|
191
|
+
const obj = config as Record<string, unknown>;
|
|
192
|
+
const servers = kind === "cursor" || kind === "windsurf" || kind === "vscode"
|
|
193
|
+
? obj.mcpServers ?? obj["mcp-servers"]
|
|
194
|
+
: obj.mcpServers;
|
|
195
|
+
return servers && typeof servers === "object" && !Array.isArray(servers) ? servers as Record<string, ServerEntry> : {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, prefix: ToolPrefix, envOverride: string[]): string[] {
|
|
199
|
+
const names: string[] = [];
|
|
200
|
+
const seenNames = new Set<string>();
|
|
201
|
+
const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
|
|
202
|
+
|
|
203
|
+
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
204
|
+
const serverCache = cache.servers[serverName];
|
|
205
|
+
if (!isServerCacheValid(serverCache, definition)) continue;
|
|
206
|
+
|
|
207
|
+
const toolFilter = selectedServers.has(serverName)
|
|
208
|
+
? true
|
|
209
|
+
: selectedTools.get(serverName);
|
|
210
|
+
if (!toolFilter) continue;
|
|
211
|
+
|
|
212
|
+
for (const tool of Array.isArray(serverCache.tools) ? serverCache.tools : []) {
|
|
213
|
+
if (typeof tool?.name !== "string" || !tool.name) continue;
|
|
214
|
+
if (toolFilter !== true && !toolFilter.has(tool.name)) continue;
|
|
215
|
+
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
|
|
216
|
+
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
217
|
+
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
218
|
+
seenNames.add(prefixedName);
|
|
219
|
+
names.push(prefixedName);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (definition.exposeResources === false) continue;
|
|
223
|
+
for (const resource of Array.isArray(serverCache.resources) ? serverCache.resources : []) {
|
|
224
|
+
if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri) continue;
|
|
225
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
226
|
+
if (toolFilter !== true && !toolFilter.has(baseName)) continue;
|
|
227
|
+
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
|
|
228
|
+
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
229
|
+
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
230
|
+
seenNames.add(prefixedName);
|
|
231
|
+
names.push(prefixedName);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return names;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {
|
|
239
|
+
const servers = new Set<string>();
|
|
240
|
+
const tools = new Map<string, Set<string>>();
|
|
241
|
+
for (let item of selections) {
|
|
242
|
+
item = item.replace(/\/+$/, "");
|
|
243
|
+
if (item.includes("/")) {
|
|
244
|
+
const [server, tool] = item.split("/", 2);
|
|
245
|
+
if (server && tool) {
|
|
246
|
+
if (!tools.has(server)) tools.set(server, new Set());
|
|
247
|
+
tools.get(server)!.add(tool);
|
|
248
|
+
} else if (server) {
|
|
249
|
+
servers.add(server);
|
|
250
|
+
}
|
|
251
|
+
} else if (item) {
|
|
252
|
+
servers.add(item);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { servers, tools };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isServerCacheValid(entry: ServerCacheEntry | undefined, definition: ServerEntry): entry is ServerCacheEntry {
|
|
259
|
+
if (!entry || entry.configHash !== computeMcpServerHash(definition)) return false;
|
|
260
|
+
if (!entry.cachedAt || typeof entry.cachedAt !== "number") return false;
|
|
261
|
+
return Date.now() - entry.cachedAt <= CACHE_MAX_AGE_MS;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function computeMcpServerHash(definition: ServerEntry): string {
|
|
265
|
+
const identity: Record<string, unknown> = {
|
|
266
|
+
command: definition.command,
|
|
267
|
+
args: definition.args,
|
|
268
|
+
env: interpolateEnvRecord(definition.env),
|
|
269
|
+
cwd: resolveConfigPath(definition.cwd),
|
|
270
|
+
url: definition.url,
|
|
271
|
+
headers: interpolateEnvRecord(definition.headers),
|
|
272
|
+
auth: definition.auth,
|
|
273
|
+
bearerToken: resolveBearerToken(definition),
|
|
274
|
+
bearerTokenEnv: definition.bearerTokenEnv,
|
|
275
|
+
exposeResources: definition.exposeResources,
|
|
276
|
+
excludeTools: definition.excludeTools,
|
|
277
|
+
};
|
|
278
|
+
return createHash("sha256").update(stableStringify(identity)).digest("hex");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getToolPrefix(value: unknown): ToolPrefix {
|
|
282
|
+
return value === "none" || value === "short" || value === "server" ? value : "server";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isImportKind(value: unknown): value is ImportKind {
|
|
286
|
+
return typeof value === "string" && Object.hasOwn(IMPORT_PATHS, value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getServerPrefix(serverName: string, mode: ToolPrefix): string {
|
|
290
|
+
if (mode === "none") return "";
|
|
291
|
+
if (mode === "short") {
|
|
292
|
+
const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
|
|
293
|
+
return short || "mcp";
|
|
294
|
+
}
|
|
295
|
+
return serverName.replace(/-/g, "_");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string {
|
|
299
|
+
const serverPrefix = getServerPrefix(serverName, prefix);
|
|
300
|
+
return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isToolExcluded(toolName: string, serverName: string, prefix: ToolPrefix, excludeTools: unknown): boolean {
|
|
304
|
+
if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
|
|
305
|
+
const candidates = new Set([
|
|
306
|
+
normalizeToolName(toolName),
|
|
307
|
+
normalizeToolName(formatToolName(toolName, serverName, prefix)),
|
|
308
|
+
normalizeToolName(formatToolName(toolName, serverName, "server")),
|
|
309
|
+
normalizeToolName(formatToolName(toolName, serverName, "short")),
|
|
310
|
+
]);
|
|
311
|
+
return excludeTools.some((excluded) => typeof excluded === "string" && candidates.has(normalizeToolName(excluded)));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeToolName(value: string): string {
|
|
315
|
+
return value.replace(/-/g, "_");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resourceNameToToolName(name: string): string {
|
|
319
|
+
let result = name
|
|
320
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
321
|
+
.replace(/_+/g, "_")
|
|
322
|
+
.replace(/^_+/, "")
|
|
323
|
+
.replace(/_+$/, "")
|
|
324
|
+
.toLowerCase();
|
|
325
|
+
if (!result || /^\d/.test(result)) result = `resource${result ? `_${result}` : ""}`;
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
330
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) return undefined;
|
|
331
|
+
const resolved: Record<string, string> = {};
|
|
332
|
+
for (const [key, value] of Object.entries(values)) {
|
|
333
|
+
if (typeof value === "string") resolved[key] = interpolateEnvVars(value);
|
|
334
|
+
}
|
|
335
|
+
return resolved;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function interpolateEnvVars(value: string): string {
|
|
339
|
+
return value
|
|
340
|
+
.replace(/\$\{(\w+)\}/g, (_, name: string) => process.env[name] ?? "")
|
|
341
|
+
.replace(/\$env:(\w+)/g, (_, name: string) => process.env[name] ?? "");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveConfigPath(value: string | undefined): string | undefined {
|
|
345
|
+
if (typeof value !== "string") return undefined;
|
|
346
|
+
const resolved = interpolateEnvVars(value);
|
|
347
|
+
if (resolved === "~") return os.homedir();
|
|
348
|
+
if (resolved.startsWith("~/") || resolved.startsWith("~\\")) return path.join(os.homedir(), resolved.slice(2));
|
|
349
|
+
return resolved;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function resolveBearerToken(definition: Pick<ServerEntry, "bearerToken" | "bearerTokenEnv">): string | undefined {
|
|
353
|
+
if (typeof definition.bearerToken === "string") return interpolateEnvVars(definition.bearerToken);
|
|
354
|
+
return typeof definition.bearerTokenEnv === "string" ? process.env[definition.bearerTokenEnv] : undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function stableStringify(value: unknown): string {
|
|
358
|
+
if (value === null || value === undefined || typeof value !== "object") {
|
|
359
|
+
const serialized = JSON.stringify(value);
|
|
360
|
+
return serialized === undefined ? "undefined" : serialized;
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
363
|
+
const obj = value as Record<string, unknown>;
|
|
364
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
365
|
+
}
|
|
@@ -3,10 +3,12 @@ export interface RunnerSubagentStep {
|
|
|
3
3
|
task: string;
|
|
4
4
|
cwd?: string;
|
|
5
5
|
model?: string;
|
|
6
|
+
thinking?: string;
|
|
6
7
|
modelCandidates?: string[];
|
|
7
8
|
tools?: string[];
|
|
8
9
|
extensions?: string[];
|
|
9
10
|
mcpDirectTools?: string[];
|
|
11
|
+
completionGuard?: boolean;
|
|
10
12
|
systemPrompt?: string | null;
|
|
11
13
|
systemPromptMode?: "append" | "replace";
|
|
12
14
|
inheritProjectContext: boolean;
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts";
|
|
5
6
|
|
|
6
7
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
7
8
|
const TASK_ARG_LIMIT = 8000;
|
|
@@ -27,6 +28,7 @@ interface BuildPiArgsInput {
|
|
|
27
28
|
extensions?: string[];
|
|
28
29
|
systemPrompt?: string | null;
|
|
29
30
|
mcpDirectTools?: string[];
|
|
31
|
+
cwd?: string;
|
|
30
32
|
promptFileStem?: string;
|
|
31
33
|
intercomSessionName?: string;
|
|
32
34
|
orchestratorIntercomTarget?: string;
|
|
@@ -80,6 +82,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
if (builtinTools.length > 0) {
|
|
85
|
+
if (input.mcpDirectTools?.length) {
|
|
86
|
+
builtinTools.push(...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd));
|
|
87
|
+
}
|
|
83
88
|
args.push("--tools", builtinTools.join(","));
|
|
84
89
|
}
|
|
85
90
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
4
4
|
|
|
5
5
|
export interface RunEntry {
|
|
6
6
|
agent: string;
|
|
@@ -11,10 +11,13 @@ export interface RunEntry {
|
|
|
11
11
|
exit?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const HISTORY_PATH = path.join(os.homedir(), ".pi", "agent", "run-history.jsonl");
|
|
15
14
|
const ROTATE_READ_THRESHOLD = 1200;
|
|
16
15
|
const ROTATE_KEEP = 1000;
|
|
17
16
|
|
|
17
|
+
function getHistoryPath(): string {
|
|
18
|
+
return path.join(getAgentDir(), "run-history.jsonl");
|
|
19
|
+
}
|
|
20
|
+
|
|
18
21
|
export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
|
|
19
22
|
try {
|
|
20
23
|
const entry: RunEntry = {
|
|
@@ -25,18 +28,20 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
25
28
|
duration: durationMs,
|
|
26
29
|
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
27
30
|
};
|
|
28
|
-
|
|
29
|
-
fs.
|
|
31
|
+
const historyPath = getHistoryPath();
|
|
32
|
+
fs.mkdirSync(path.dirname(historyPath), { recursive: true });
|
|
33
|
+
fs.appendFileSync(historyPath, `${JSON.stringify(entry)}\n`);
|
|
30
34
|
} catch {
|
|
31
35
|
// Best-effort — never crash the execution flow for history recording
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
36
|
-
|
|
40
|
+
const historyPath = getHistoryPath();
|
|
41
|
+
if (!fs.existsSync(historyPath)) return [];
|
|
37
42
|
let raw: string;
|
|
38
43
|
try {
|
|
39
|
-
raw = fs.readFileSync(
|
|
44
|
+
raw = fs.readFileSync(historyPath, "utf-8");
|
|
40
45
|
} catch {
|
|
41
46
|
return [];
|
|
42
47
|
}
|
|
@@ -45,7 +50,7 @@ export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
|
45
50
|
|
|
46
51
|
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
47
52
|
lines = lines.slice(-ROTATE_KEEP);
|
|
48
|
-
try { fs.writeFileSync(
|
|
53
|
+
try { fs.writeFileSync(historyPath, `${lines.join("\n")}\n`, "utf-8"); } catch {}
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
return lines
|
|
@@ -8,12 +8,22 @@ export interface SingleOutputSnapshot {
|
|
|
8
8
|
size?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function normalizeSingleOutputOverride(
|
|
12
|
+
output: string | boolean | undefined,
|
|
13
|
+
defaultOutput: string | undefined,
|
|
14
|
+
): string | false | undefined {
|
|
15
|
+
if (output === false || output === "false") return false;
|
|
16
|
+
if (output === true || output === "true") return defaultOutput;
|
|
17
|
+
if (typeof output === "string" && output.length > 0) return output;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export function resolveSingleOutputPath(
|
|
12
|
-
output: string |
|
|
22
|
+
output: string | boolean | undefined,
|
|
13
23
|
runtimeCwd: string,
|
|
14
24
|
requestedCwd?: string,
|
|
15
25
|
): string | undefined {
|
|
16
|
-
if (typeof output !== "string" || !output) return undefined;
|
|
26
|
+
if (typeof output !== "string" || !output || output === "false" || output === "true") return undefined;
|
|
17
27
|
if (path.isAbsolute(output)) return output;
|
|
18
28
|
const baseCwd = requestedCwd
|
|
19
29
|
? (path.isAbsolute(requestedCwd) ? requestedCwd : path.resolve(runtimeCwd, requestedCwd))
|
package/src/shared/artifacts.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
|
|
4
|
+
import { getAgentDir } from "./utils.ts";
|
|
5
5
|
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
6
6
|
|
|
7
7
|
export function getArtifactsDir(sessionFile: string | null): string {
|
|
@@ -74,7 +74,7 @@ export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
|
74
74
|
export function cleanupAllArtifactDirs(maxAgeDays: number): void {
|
|
75
75
|
cleanupOldArtifacts(TEMP_ARTIFACTS_DIR, maxAgeDays);
|
|
76
76
|
|
|
77
|
-
const sessionsBase = path.join(
|
|
77
|
+
const sessionsBase = path.join(getAgentDir(), "sessions");
|
|
78
78
|
if (!fs.existsSync(sessionsBase)) return;
|
|
79
79
|
|
|
80
80
|
let dirs: string[];
|
package/src/shared/formatters.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import type { Usage, SingleResult } from "./types.ts";
|
|
8
8
|
import type { ChainStep } from "./settings.ts";
|
|
9
9
|
import { isParallelStep } from "./settings.ts";
|
|
10
|
+
import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Format token count with k suffix for large numbers
|
|
@@ -15,6 +16,18 @@ export function formatTokens(n: number): string {
|
|
|
15
16
|
return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export function formatModelThinking(model?: string, thinking?: string): string {
|
|
20
|
+
const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
|
|
21
|
+
let displayModel = parsed?.baseModel ?? model;
|
|
22
|
+
const explicitThinking = THINKING_LEVELS.find((level) => level === thinking?.trim());
|
|
23
|
+
const displayThinking = parsed?.thinkingSuffix ? parsed.thinkingSuffix.slice(1) : explicitThinking;
|
|
24
|
+
if (displayModel) {
|
|
25
|
+
const slashIdx = displayModel.lastIndexOf("/");
|
|
26
|
+
if (slashIdx !== -1) displayModel = displayModel.slice(slashIdx + 1);
|
|
27
|
+
}
|
|
28
|
+
return [displayModel, displayThinking ? `thinking ${displayThinking}` : undefined].filter(Boolean).join(" · ");
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
/**
|
|
19
32
|
* Format usage statistics into a compact string
|
|
20
33
|
*/
|
package/src/shared/model-info.ts
CHANGED
|
@@ -27,6 +27,16 @@ export function toModelInfo(model: RegistryModelLike): ModelInfo {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Resolve the effective thinking level from a model string (which may contain a known suffix like `:high`)
|
|
31
|
+
* and an explicit thinking config value. Returns `undefined` when no thinking is applicable
|
|
32
|
+
* (e.g. no model was specified, or the model has no suffix and no config was provided). */
|
|
33
|
+
export function resolveEffectiveThinking(model: string | undefined, configThinking: string | undefined): string | undefined {
|
|
34
|
+
if (!model) return undefined;
|
|
35
|
+
const { thinkingSuffix } = splitKnownThinkingSuffix(model);
|
|
36
|
+
if (thinkingSuffix) return thinkingSuffix.slice(1);
|
|
37
|
+
return THINKING_LEVELS.find((level) => level === configThinking);
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
export function splitKnownThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
31
41
|
const colonIdx = model.lastIndexOf(":");
|
|
32
42
|
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
package/src/shared/types.ts
CHANGED
package/src/shared/utils.ts
CHANGED
|
@@ -13,6 +13,13 @@ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, Singl
|
|
|
13
13
|
// File System Utilities
|
|
14
14
|
// ============================================================================
|
|
15
15
|
|
|
16
|
+
export function getAgentDir(): string {
|
|
17
|
+
const configured = process.env.PI_CODING_AGENT_DIR;
|
|
18
|
+
if (configured === "~") return os.homedir();
|
|
19
|
+
if (configured?.startsWith("~/")) return path.join(os.homedir(), configured.slice(2));
|
|
20
|
+
return configured || path.join(os.homedir(), ".pi", "agent");
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
const statusCache = new Map<string, { mtime: number; status: AsyncStatus }>();
|
|
17
24
|
|
|
18
25
|
function getErrorMessage(error: unknown): string {
|
|
@@ -182,8 +189,11 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
182
189
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
183
190
|
const msg = messages[i];
|
|
184
191
|
if (msg.role === "assistant") {
|
|
192
|
+
const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
|
|
193
|
+
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
194
|
+
if (hasAssistantError) continue;
|
|
185
195
|
for (const part of msg.content) {
|
|
186
|
-
if (part.type === "text") return part.text;
|
|
196
|
+
if (part.type === "text" && part.text.trim().length > 0) return part.text;
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
199
|
}
|