pi-subagents 0.28.0 → 0.30.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 +31 -0
- package/README.md +26 -62
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -35
- package/src/agents/agent-management.ts +29 -22
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +5 -10
- package/src/agents/agents.ts +339 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +1 -3
- package/src/extension/index.ts +6 -9
- package/src/extension/schemas.ts +63 -26
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +186 -74
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/run-status.ts +2 -7
- package/src/runs/background/subagent-runner.ts +160 -219
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +39 -343
- package/src/runs/foreground/subagent-executor.ts +316 -111
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +13 -10
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +20 -49
- package/src/shared/utils.ts +2 -8
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- package/src/runs/shared/acceptance-reports.ts +0 -127
package/src/agents/agents.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Agent discovery and configuration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
5
6
|
import * as fs from "node:fs";
|
|
6
7
|
import * as os from "node:os";
|
|
7
8
|
import * as path from "node:path";
|
|
@@ -17,7 +18,7 @@ export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./
|
|
|
17
18
|
|
|
18
19
|
export type AgentScope = "user" | "project" | "both";
|
|
19
20
|
|
|
20
|
-
export type AgentSource = "builtin" | "user" | "project";
|
|
21
|
+
export type AgentSource = "builtin" | "package" | "user" | "project";
|
|
21
22
|
type SystemPromptMode = "append" | "replace";
|
|
22
23
|
export type AgentDefaultContext = "fresh" | "fork";
|
|
23
24
|
|
|
@@ -46,8 +47,7 @@ export interface BuiltinAgentOverrideBase {
|
|
|
46
47
|
skills?: string[];
|
|
47
48
|
tools?: string[];
|
|
48
49
|
mcpDirectTools?: string[];
|
|
49
|
-
|
|
50
|
-
maxTokens?: number;
|
|
50
|
+
subagentOnlyExtensions?: string[];
|
|
51
51
|
completionGuard?: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -63,8 +63,7 @@ interface BuiltinAgentOverrideConfig {
|
|
|
63
63
|
systemPrompt?: string;
|
|
64
64
|
skills?: string[] | false;
|
|
65
65
|
tools?: string[] | false;
|
|
66
|
-
|
|
67
|
-
maxTokens?: number | false;
|
|
66
|
+
subagentOnlyExtensions?: string[] | false;
|
|
68
67
|
completionGuard?: boolean;
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -93,13 +92,12 @@ export interface AgentConfig {
|
|
|
93
92
|
filePath: string;
|
|
94
93
|
skills?: string[];
|
|
95
94
|
extensions?: string[];
|
|
95
|
+
subagentOnlyExtensions?: string[];
|
|
96
96
|
output?: string;
|
|
97
97
|
defaultReads?: string[];
|
|
98
98
|
defaultProgress?: boolean;
|
|
99
99
|
interactive?: boolean;
|
|
100
100
|
maxSubagentDepth?: number;
|
|
101
|
-
maxExecutionTimeMs?: number;
|
|
102
|
-
maxTokens?: number;
|
|
103
101
|
completionGuard?: boolean;
|
|
104
102
|
disabled?: boolean;
|
|
105
103
|
extraFields?: Record<string, string>;
|
|
@@ -147,7 +145,7 @@ export interface ChainConfig {
|
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
export interface ChainDiscoveryDiagnostic {
|
|
150
|
-
source:
|
|
148
|
+
source: AgentSource;
|
|
151
149
|
filePath: string;
|
|
152
150
|
error: string;
|
|
153
151
|
}
|
|
@@ -161,6 +159,259 @@ function getUserChainDir(): string {
|
|
|
161
159
|
return path.join(getAgentDir(), "chains");
|
|
162
160
|
}
|
|
163
161
|
|
|
162
|
+
interface PackageSubagentPaths {
|
|
163
|
+
agents: string[];
|
|
164
|
+
chains: string[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let cachedGlobalNpmRoot: string | null = null;
|
|
168
|
+
|
|
169
|
+
function readJsonFileBestEffort(filePath: string): unknown {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
172
|
+
} catch {
|
|
173
|
+
// Installed package scans are opportunistic; bad third-party manifests
|
|
174
|
+
// should not break local agent discovery.
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readOptionalJsonFile(filePath: string): unknown {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
184
|
+
? (error as { code?: unknown }).code
|
|
185
|
+
: undefined;
|
|
186
|
+
if (code === "ENOENT") return null;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isSafePackagePath(value: string): boolean {
|
|
192
|
+
return value.length > 0
|
|
193
|
+
&& !path.isAbsolute(value)
|
|
194
|
+
&& value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseNpmPackageName(source: string): string | undefined {
|
|
198
|
+
const spec = source.slice(4).trim();
|
|
199
|
+
if (!spec) return undefined;
|
|
200
|
+
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
|
|
201
|
+
const packageName = match?.[1] ?? spec;
|
|
202
|
+
return isSafePackagePath(packageName) ? packageName : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function stripGitRef(repoPath: string): string {
|
|
206
|
+
const atIndex = repoPath.indexOf("@");
|
|
207
|
+
const hashIndex = repoPath.indexOf("#");
|
|
208
|
+
const refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
209
|
+
return refIndex === undefined ? repoPath : repoPath.slice(0, refIndex);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined {
|
|
213
|
+
const spec = source.slice(4).trim();
|
|
214
|
+
if (!spec) return undefined;
|
|
215
|
+
|
|
216
|
+
let host = "";
|
|
217
|
+
let repoPath = "";
|
|
218
|
+
const scpLike = spec.match(/^git@([^:]+):(.+)$/);
|
|
219
|
+
if (scpLike) {
|
|
220
|
+
host = scpLike[1] ?? "";
|
|
221
|
+
repoPath = scpLike[2] ?? "";
|
|
222
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(spec)) {
|
|
223
|
+
try {
|
|
224
|
+
const url = new URL(spec);
|
|
225
|
+
host = url.hostname;
|
|
226
|
+
repoPath = url.pathname.replace(/^\/+/, "");
|
|
227
|
+
} catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
const slashIndex = spec.indexOf("/");
|
|
232
|
+
if (slashIndex < 0) return undefined;
|
|
233
|
+
host = spec.slice(0, slashIndex);
|
|
234
|
+
repoPath = spec.slice(slashIndex + 1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const normalizedPath = stripGitRef(repoPath).replace(/\.git$/, "").replace(/^\/+/, "");
|
|
238
|
+
if (!host || !isSafePackagePath(host) || !isSafePackagePath(normalizedPath) || normalizedPath.split(/[\\/]/).length < 2) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return { host, repoPath: normalizedPath };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {
|
|
245
|
+
const trimmed = source.trim();
|
|
246
|
+
if (!trimmed) return undefined;
|
|
247
|
+
if (trimmed.startsWith("git:")) {
|
|
248
|
+
const parsed = parseGitPackagePath(trimmed);
|
|
249
|
+
return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
|
|
250
|
+
}
|
|
251
|
+
if (trimmed.startsWith("npm:")) {
|
|
252
|
+
const packageName = parseNpmPackageName(trimmed);
|
|
253
|
+
return packageName ? path.join(baseDir, "npm", "node_modules", packageName) : undefined;
|
|
254
|
+
}
|
|
255
|
+
const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
|
|
256
|
+
if (normalized === "~") return os.homedir();
|
|
257
|
+
if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2));
|
|
258
|
+
if (path.isAbsolute(normalized)) return normalized;
|
|
259
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
260
|
+
return path.resolve(baseDir, normalized);
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getGlobalNpmRoot(): string | null {
|
|
266
|
+
if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;
|
|
267
|
+
try {
|
|
268
|
+
cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
|
|
269
|
+
return cachedGlobalNpmRoot;
|
|
270
|
+
} catch {
|
|
271
|
+
cachedGlobalNpmRoot = "";
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function stringArray(value: unknown): string[] {
|
|
277
|
+
if (!Array.isArray(value)) return [];
|
|
278
|
+
return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
|
|
282
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
283
|
+
const pkg = readJsonFileBestEffort(packageJsonPath);
|
|
284
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
|
|
285
|
+
|
|
286
|
+
const roots: Record<string, unknown>[] = [];
|
|
287
|
+
const piSubagents = (pkg as { "pi-subagents"?: unknown })["pi-subagents"];
|
|
288
|
+
if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) {
|
|
289
|
+
roots.push(piSubagents as Record<string, unknown>);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const pi = (pkg as { pi?: unknown }).pi;
|
|
293
|
+
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
294
|
+
const subagents = (pi as { subagents?: unknown }).subagents;
|
|
295
|
+
if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) {
|
|
296
|
+
roots.push(subagents as Record<string, unknown>);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const agents: string[] = [];
|
|
301
|
+
const chains: string[] = [];
|
|
302
|
+
for (const root of roots) {
|
|
303
|
+
for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry));
|
|
304
|
+
for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
|
|
305
|
+
}
|
|
306
|
+
return { agents, chains };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function collectPackageRootsFromNodeModules(nodeModulesDir: string): string[] {
|
|
310
|
+
const roots: string[] = [];
|
|
311
|
+
if (!fs.existsSync(nodeModulesDir)) return roots;
|
|
312
|
+
|
|
313
|
+
let entries: fs.Dirent[];
|
|
314
|
+
try {
|
|
315
|
+
entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
316
|
+
} catch {
|
|
317
|
+
return roots;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
if (entry.name.startsWith(".")) continue;
|
|
322
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
323
|
+
|
|
324
|
+
if (entry.name.startsWith("@")) {
|
|
325
|
+
const scopeDir = path.join(nodeModulesDir, entry.name);
|
|
326
|
+
let scopeEntries: fs.Dirent[];
|
|
327
|
+
try {
|
|
328
|
+
scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
329
|
+
} catch {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
for (const scopeEntry of scopeEntries) {
|
|
333
|
+
if (scopeEntry.name.startsWith(".")) continue;
|
|
334
|
+
if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue;
|
|
335
|
+
roots.push(path.join(scopeDir, scopeEntry.name));
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
roots.push(path.join(nodeModulesDir, entry.name));
|
|
341
|
+
}
|
|
342
|
+
return roots;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function collectSettingsPackageRoots(settingsFile: string, baseDir: string): string[] {
|
|
346
|
+
const settings = readOptionalJsonFile(settingsFile);
|
|
347
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return [];
|
|
348
|
+
const packages = (settings as { packages?: unknown }).packages;
|
|
349
|
+
if (!Array.isArray(packages)) return [];
|
|
350
|
+
|
|
351
|
+
const roots: string[] = [];
|
|
352
|
+
for (const entry of packages) {
|
|
353
|
+
const packageSource = typeof entry === "string"
|
|
354
|
+
? entry
|
|
355
|
+
: typeof entry === "object" && entry !== null && typeof (entry as { source?: unknown }).source === "string"
|
|
356
|
+
? (entry as { source: string }).source
|
|
357
|
+
: undefined;
|
|
358
|
+
if (!packageSource) continue;
|
|
359
|
+
const packageRoot = resolveSettingsPackageRoot(packageSource, baseDir);
|
|
360
|
+
if (packageRoot) roots.push(packageRoot);
|
|
361
|
+
}
|
|
362
|
+
return roots;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolean; includeProject: boolean } = { includeUser: true, includeProject: true }): PackageSubagentPaths {
|
|
366
|
+
const agentDir = getAgentDir();
|
|
367
|
+
const projectRoot = findNearestProjectRoot(cwd) ?? cwd;
|
|
368
|
+
const packageRoots = [
|
|
369
|
+
projectRoot,
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
if (options.includeProject) {
|
|
373
|
+
packageRoots.push(
|
|
374
|
+
...collectPackageRootsFromNodeModules(path.join(projectRoot, ".pi", "npm", "node_modules")),
|
|
375
|
+
...collectSettingsPackageRoots(path.join(projectRoot, ".pi", "settings.json"), path.join(projectRoot, ".pi")),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (options.includeUser) {
|
|
380
|
+
packageRoots.push(
|
|
381
|
+
...collectPackageRootsFromNodeModules(path.join(agentDir, "npm", "node_modules")),
|
|
382
|
+
...collectSettingsPackageRoots(path.join(agentDir, "settings.json"), agentDir),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (options.includeUser) {
|
|
387
|
+
const globalRoot = getGlobalNpmRoot();
|
|
388
|
+
if (globalRoot) packageRoots.push(...collectPackageRootsFromNodeModules(globalRoot));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const seenRoots = new Set<string>();
|
|
392
|
+
const seenAgents = new Set<string>();
|
|
393
|
+
const seenChains = new Set<string>();
|
|
394
|
+
const agents: string[] = [];
|
|
395
|
+
const chains: string[] = [];
|
|
396
|
+
for (const packageRoot of packageRoots) {
|
|
397
|
+
const resolvedRoot = path.resolve(packageRoot);
|
|
398
|
+
if (seenRoots.has(resolvedRoot)) continue;
|
|
399
|
+
seenRoots.add(resolvedRoot);
|
|
400
|
+
const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
|
|
401
|
+
for (const agentDir of paths.agents) {
|
|
402
|
+
if (seenAgents.has(agentDir)) continue;
|
|
403
|
+
seenAgents.add(agentDir);
|
|
404
|
+
agents.push(agentDir);
|
|
405
|
+
}
|
|
406
|
+
for (const chainDir of paths.chains) {
|
|
407
|
+
if (seenChains.has(chainDir)) continue;
|
|
408
|
+
seenChains.add(chainDir);
|
|
409
|
+
chains.push(chainDir);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { agents, chains };
|
|
413
|
+
}
|
|
414
|
+
|
|
164
415
|
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
165
416
|
const mcpDirectTools: string[] = [];
|
|
166
417
|
const tools: string[] = [];
|
|
@@ -209,8 +460,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
209
460
|
skills: agent.skills ? [...agent.skills] : undefined,
|
|
210
461
|
tools: agent.tools ? [...agent.tools] : undefined,
|
|
211
462
|
mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
|
|
212
|
-
|
|
213
|
-
maxTokens: agent.maxTokens,
|
|
463
|
+
subagentOnlyExtensions: agent.subagentOnlyExtensions ? [...agent.subagentOnlyExtensions] : undefined,
|
|
214
464
|
completionGuard: agent.completionGuard,
|
|
215
465
|
};
|
|
216
466
|
}
|
|
@@ -230,8 +480,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
230
480
|
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
231
481
|
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
232
482
|
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
233
|
-
...(override.
|
|
234
|
-
...(override.maxTokens !== undefined ? { maxTokens: override.maxTokens } : {}),
|
|
483
|
+
...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
|
|
235
484
|
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
236
485
|
};
|
|
237
486
|
}
|
|
@@ -369,22 +618,6 @@ function parseBuiltinOverrideEntry(
|
|
|
369
618
|
}
|
|
370
619
|
}
|
|
371
620
|
|
|
372
|
-
if ("maxExecutionTimeMs" in input) {
|
|
373
|
-
if (input.maxExecutionTimeMs === false || (typeof input.maxExecutionTimeMs === "number" && Number.isInteger(input.maxExecutionTimeMs) && input.maxExecutionTimeMs >= 1)) {
|
|
374
|
-
override.maxExecutionTimeMs = input.maxExecutionTimeMs;
|
|
375
|
-
} else {
|
|
376
|
-
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'maxExecutionTimeMs'; expected an integer >= 1 or false.`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if ("maxTokens" in input) {
|
|
381
|
-
if (input.maxTokens === false || (typeof input.maxTokens === "number" && Number.isInteger(input.maxTokens) && input.maxTokens >= 1)) {
|
|
382
|
-
override.maxTokens = input.maxTokens;
|
|
383
|
-
} else {
|
|
384
|
-
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'maxTokens'; expected an integer >= 1 or false.`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
621
|
if ("completionGuard" in input) {
|
|
389
622
|
if (typeof input.completionGuard === "boolean") {
|
|
390
623
|
override.completionGuard = input.completionGuard;
|
|
@@ -407,6 +640,9 @@ function parseBuiltinOverrideEntry(
|
|
|
407
640
|
const tools = parseOverrideStringArrayOrFalse(input.tools, { filePath, name, field: "tools" });
|
|
408
641
|
if (tools !== undefined) override.tools = tools;
|
|
409
642
|
|
|
643
|
+
const subagentOnlyExtensions = parseOverrideStringArrayOrFalse(input.subagentOnlyExtensions, { filePath, name, field: "subagentOnlyExtensions" });
|
|
644
|
+
if (subagentOnlyExtensions !== undefined) override.subagentOnlyExtensions = subagentOnlyExtensions;
|
|
645
|
+
|
|
410
646
|
return Object.keys(override).length > 0 ? override : undefined;
|
|
411
647
|
}
|
|
412
648
|
|
|
@@ -465,8 +701,9 @@ function applyBuiltinOverride(
|
|
|
465
701
|
next.tools = tools;
|
|
466
702
|
next.mcpDirectTools = mcpDirectTools;
|
|
467
703
|
}
|
|
468
|
-
if (override.
|
|
469
|
-
|
|
704
|
+
if (override.subagentOnlyExtensions !== undefined) {
|
|
705
|
+
next.subagentOnlyExtensions = override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions];
|
|
706
|
+
}
|
|
470
707
|
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
471
708
|
|
|
472
709
|
return next;
|
|
@@ -507,7 +744,7 @@ function applyBuiltinOverrides(
|
|
|
507
744
|
|
|
508
745
|
export function buildBuiltinOverrideConfig(
|
|
509
746
|
base: BuiltinAgentOverrideBase,
|
|
510
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "
|
|
747
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "subagentOnlyExtensions" | "completionGuard">,
|
|
511
748
|
): BuiltinAgentOverrideConfig | undefined {
|
|
512
749
|
const override: BuiltinAgentOverrideConfig = {};
|
|
513
750
|
|
|
@@ -525,8 +762,9 @@ export function buildBuiltinOverrideConfig(
|
|
|
525
762
|
const baseTools = joinToolList(base);
|
|
526
763
|
const draftTools = joinToolList(draft);
|
|
527
764
|
if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
|
|
528
|
-
if (draft.
|
|
529
|
-
|
|
765
|
+
if (!arraysEqual(draft.subagentOnlyExtensions, base.subagentOnlyExtensions)) {
|
|
766
|
+
override.subagentOnlyExtensions = draft.subagentOnlyExtensions ? [...draft.subagentOnlyExtensions] : false;
|
|
767
|
+
}
|
|
530
768
|
if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
|
|
531
769
|
override.completionGuard = draft.completionGuard !== false;
|
|
532
770
|
}
|
|
@@ -606,10 +844,23 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
|
|
|
606
844
|
return files;
|
|
607
845
|
}
|
|
608
846
|
|
|
847
|
+
function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
|
|
848
|
+
const relative = path.relative(rootDir, filePath);
|
|
849
|
+
const parts = relative.split(path.sep).map((part) => part.toLowerCase());
|
|
850
|
+
if (path.basename(rootDir).toLowerCase() === ".agents") {
|
|
851
|
+
parts.unshift(".agents");
|
|
852
|
+
}
|
|
853
|
+
return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
|
|
854
|
+
}
|
|
855
|
+
|
|
609
856
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
610
857
|
const agents: AgentConfig[] = [];
|
|
611
858
|
|
|
612
859
|
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
|
|
860
|
+
if (isLegacyAgentSkillPath(dir, filePath)) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
613
864
|
let content: string;
|
|
614
865
|
try {
|
|
615
866
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -688,6 +939,13 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
688
939
|
.map((e) => e.trim())
|
|
689
940
|
.filter(Boolean);
|
|
690
941
|
}
|
|
942
|
+
let subagentOnlyExtensions: string[] | undefined;
|
|
943
|
+
if (frontmatter.subagentOnlyExtensions !== undefined) {
|
|
944
|
+
subagentOnlyExtensions = frontmatter.subagentOnlyExtensions
|
|
945
|
+
.split(",")
|
|
946
|
+
.map((e) => e.trim())
|
|
947
|
+
.filter(Boolean);
|
|
948
|
+
}
|
|
691
949
|
|
|
692
950
|
const extraFields: Record<string, string> = {};
|
|
693
951
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
@@ -695,8 +953,6 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
695
953
|
}
|
|
696
954
|
|
|
697
955
|
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
698
|
-
const parsedMaxExecutionTimeMs = Number(frontmatter.maxExecutionTimeMs);
|
|
699
|
-
const parsedMaxTokens = Number(frontmatter.maxTokens);
|
|
700
956
|
const completionGuard = frontmatter.completionGuard === "false"
|
|
701
957
|
? false
|
|
702
958
|
: frontmatter.completionGuard === "true"
|
|
@@ -722,6 +978,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
722
978
|
filePath,
|
|
723
979
|
skills: skills && skills.length > 0 ? skills : undefined,
|
|
724
980
|
extensions,
|
|
981
|
+
subagentOnlyExtensions,
|
|
725
982
|
output: frontmatter.output,
|
|
726
983
|
defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
|
|
727
984
|
defaultProgress: frontmatter.defaultProgress === "true",
|
|
@@ -730,14 +987,6 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
730
987
|
Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
|
|
731
988
|
? parsedMaxSubagentDepth
|
|
732
989
|
: undefined,
|
|
733
|
-
maxExecutionTimeMs:
|
|
734
|
-
Number.isInteger(parsedMaxExecutionTimeMs) && parsedMaxExecutionTimeMs >= 1
|
|
735
|
-
? parsedMaxExecutionTimeMs
|
|
736
|
-
: undefined,
|
|
737
|
-
maxTokens:
|
|
738
|
-
Number.isInteger(parsedMaxTokens) && parsedMaxTokens >= 1
|
|
739
|
-
? parsedMaxTokens
|
|
740
|
-
: undefined,
|
|
741
990
|
completionGuard,
|
|
742
991
|
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
743
992
|
});
|
|
@@ -746,7 +995,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
746
995
|
return agents;
|
|
747
996
|
}
|
|
748
997
|
|
|
749
|
-
function loadChainsFromDir(dir: string, source:
|
|
998
|
+
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
750
999
|
const chains = new Map<string, ChainConfig>();
|
|
751
1000
|
const diagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
752
1001
|
|
|
@@ -808,6 +1057,22 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
|
|
|
808
1057
|
}
|
|
809
1058
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
810
1059
|
|
|
1060
|
+
export const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
|
|
1061
|
+
|
|
1062
|
+
// Additional read-only directories to scan for agent definitions, supplied by the
|
|
1063
|
+
// launcher via PI_SUBAGENT_EXTRA_AGENT_DIRS (PATH-style, split on os/path delimiter).
|
|
1064
|
+
// Lets a hermetic wrapper (e.g. a Nix-store install) expose bundled agents without
|
|
1065
|
+
// copying or symlinking them into the writable agent dir. Loaded as "user" source,
|
|
1066
|
+
// at lower precedence than agents the user placed in their own agent dir.
|
|
1067
|
+
function extraUserAgentDirs(): string[] {
|
|
1068
|
+
const raw = process.env[EXTRA_AGENT_DIRS_ENV];
|
|
1069
|
+
if (!raw) return [];
|
|
1070
|
+
return raw
|
|
1071
|
+
.split(path.delimiter)
|
|
1072
|
+
.map((dir) => dir.trim())
|
|
1073
|
+
.filter((dir) => dir.length > 0);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
811
1076
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
812
1077
|
const userDirOld = path.join(getAgentDir(), "agents");
|
|
813
1078
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
@@ -816,6 +1081,10 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
816
1081
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
817
1082
|
const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
|
|
818
1083
|
const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
|
|
1084
|
+
const packageSubagentPaths = collectPackageSubagentPaths(cwd, {
|
|
1085
|
+
includeUser: scope !== "project",
|
|
1086
|
+
includeProject: scope !== "user",
|
|
1087
|
+
});
|
|
819
1088
|
|
|
820
1089
|
const builtinAgents = applyBuiltinOverrides(
|
|
821
1090
|
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
@@ -825,12 +1094,14 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
825
1094
|
projectSettingsPath,
|
|
826
1095
|
);
|
|
827
1096
|
|
|
1097
|
+
const userAgentsExtra = scope === "project" ? [] : extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user"));
|
|
828
1098
|
const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
|
|
829
1099
|
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
830
|
-
const userAgents = [...userAgentsOld, ...userAgentsNew];
|
|
1100
|
+
const userAgents = [...userAgentsExtra, ...userAgentsOld, ...userAgentsNew];
|
|
831
1101
|
|
|
832
1102
|
const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
|
|
833
|
-
const
|
|
1103
|
+
const packageAgents = packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package"));
|
|
1104
|
+
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
|
|
834
1105
|
.filter((agent) => agent.disabled !== true);
|
|
835
1106
|
|
|
836
1107
|
return { agents, projectAgentsDir };
|
|
@@ -838,6 +1109,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
838
1109
|
|
|
839
1110
|
export function discoverAgentsAll(cwd: string): {
|
|
840
1111
|
builtin: AgentConfig[];
|
|
1112
|
+
package: AgentConfig[];
|
|
841
1113
|
user: AgentConfig[];
|
|
842
1114
|
project: AgentConfig[];
|
|
843
1115
|
chains: ChainConfig[];
|
|
@@ -858,6 +1130,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
858
1130
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
859
1131
|
const userSettings = readSubagentSettings(userSettingsPath);
|
|
860
1132
|
const projectSettings = readSubagentSettings(projectSettingsPath);
|
|
1133
|
+
const packageSubagentPaths = collectPackageSubagentPaths(cwd);
|
|
861
1134
|
|
|
862
1135
|
const builtin = applyBuiltinOverrides(
|
|
863
1136
|
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
@@ -867,9 +1140,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
867
1140
|
projectSettingsPath,
|
|
868
1141
|
);
|
|
869
1142
|
const user = [
|
|
1143
|
+
...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
870
1144
|
...loadAgentsFromDir(userDirOld, "user"),
|
|
871
1145
|
...loadAgentsFromDir(userDirNew, "user"),
|
|
872
1146
|
];
|
|
1147
|
+
const packageMap = new Map<string, AgentConfig>();
|
|
1148
|
+
for (const dir of packageSubagentPaths.agents) {
|
|
1149
|
+
for (const agent of loadAgentsFromDir(dir, "package")) {
|
|
1150
|
+
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const packageAgents = Array.from(packageMap.values());
|
|
873
1154
|
const projectMap = new Map<string, AgentConfig>();
|
|
874
1155
|
for (const dir of projectDirs) {
|
|
875
1156
|
for (const agent of loadAgentsFromDir(dir, "project")) {
|
|
@@ -879,6 +1160,15 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
879
1160
|
const project = Array.from(projectMap.values());
|
|
880
1161
|
|
|
881
1162
|
const chainMap = new Map<string, ChainConfig>();
|
|
1163
|
+
const packageChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
1164
|
+
const packageChainMap = new Map<string, ChainConfig>();
|
|
1165
|
+
for (const dir of packageSubagentPaths.chains) {
|
|
1166
|
+
const loaded = loadChainsFromDir(dir, "package");
|
|
1167
|
+
packageChainDiagnostics.push(...loaded.diagnostics);
|
|
1168
|
+
for (const chain of loaded.chains) {
|
|
1169
|
+
if (!packageChainMap.has(chain.name)) packageChainMap.set(chain.name, chain);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
882
1172
|
const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
883
1173
|
for (const dir of projectChainDirs) {
|
|
884
1174
|
const loaded = loadChainsFromDir(dir, "project");
|
|
@@ -889,15 +1179,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
889
1179
|
}
|
|
890
1180
|
const userChains = loadChainsFromDir(userChainDir, "user");
|
|
891
1181
|
const chains = [
|
|
1182
|
+
...Array.from(packageChainMap.values()),
|
|
892
1183
|
...userChains.chains,
|
|
893
1184
|
...Array.from(chainMap.values()),
|
|
894
1185
|
];
|
|
895
1186
|
const chainDiagnostics = [
|
|
1187
|
+
...packageChainDiagnostics,
|
|
896
1188
|
...userChains.diagnostics,
|
|
897
1189
|
...projectChainDiagnostics,
|
|
898
1190
|
];
|
|
899
1191
|
|
|
900
1192
|
const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
901
1193
|
|
|
902
|
-
return { builtin, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
1194
|
+
return { builtin, package: packageAgents, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
903
1195
|
}
|
|
@@ -4,6 +4,7 @@ import { parseFrontmatter } from "./frontmatter.ts";
|
|
|
4
4
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
6
|
import type { ChainStep } from "../shared/settings.ts";
|
|
7
|
+
import type { AgentSource } from "./agents.ts";
|
|
7
8
|
|
|
8
9
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
9
10
|
const lines = sectionBody.split("\n");
|
|
@@ -83,7 +84,7 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
83
84
|
return step;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
export function parseChain(content: string, source:
|
|
87
|
+
export function parseChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
87
88
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
88
89
|
if (!frontmatter.name || !frontmatter.description) {
|
|
89
90
|
throw new Error("Chain frontmatter must include name and description");
|
|
@@ -124,7 +125,7 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
export function parseJsonChain(content: string, source:
|
|
128
|
+
export function parseJsonChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
128
129
|
let parsed: unknown;
|
|
129
130
|
try {
|
|
130
131
|
parsed = JSON.parse(content);
|
|
@@ -151,17 +152,11 @@ export function parseJsonChain(content: string, source: "user" | "project", file
|
|
|
151
152
|
throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
|
|
152
153
|
}
|
|
153
154
|
const stepRecord = step as Record<string, unknown>;
|
|
154
|
-
const parallel = stepRecord.parallel;
|
|
155
|
-
if (Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
156
|
-
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
|
|
157
|
-
}
|
|
158
|
-
if (parallel && typeof parallel === "object" && !Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
159
|
-
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on dynamic fanout groups; set acceptance on the dynamic template.`);
|
|
160
|
-
}
|
|
161
155
|
const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
|
|
162
156
|
if (acceptanceErrors.length > 0) {
|
|
163
157
|
throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
|
|
164
158
|
}
|
|
159
|
+
const parallel = stepRecord.parallel;
|
|
165
160
|
if (Array.isArray(parallel)) {
|
|
166
161
|
for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
|
|
167
162
|
const task = parallel[taskIndex];
|