pi-subagents 0.50.0 → 0.52.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 +103 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +40 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +41 -5
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +171 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
- package/skills/pi-subagents/references/execution-controls.md +29 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
- package/src/agents/agent-management.ts +124 -351
- package/src/agents/agents.ts +157 -31
- package/src/agents/skills.ts +1 -1
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +36 -8
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +166 -88
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +32 -10
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/missions/workflow-state.ts +2 -2
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +68 -40
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +47 -15
- package/src/runs/background/async-retention.ts +886 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +31 -5
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +116 -20
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +24 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +253 -140
- package/src/runs/background/subagent-wait.ts +8 -8
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +81 -2
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +666 -1579
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +444 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +98 -38
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +14 -2
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +71 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +29 -3
- package/src/tui/fleet.ts +63 -15
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +2 -2
- package/src/workflows/scripted-workflow.ts +220 -67
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1581
package/src/agents/agents.ts
CHANGED
|
@@ -137,6 +137,7 @@ export interface AgentConfig {
|
|
|
137
137
|
systemPrompt: string;
|
|
138
138
|
source: AgentSource;
|
|
139
139
|
filePath: string;
|
|
140
|
+
discoveryPriority?: number;
|
|
140
141
|
skills?: string[];
|
|
141
142
|
skillPath?: string[];
|
|
142
143
|
extensions?: string[];
|
|
@@ -212,8 +213,45 @@ export interface ChainDiscoveryDiagnostic {
|
|
|
212
213
|
error: string;
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
export interface AgentDiscoveryDiagnostic extends ChainDiscoveryDiagnostic {
|
|
217
|
+
name?: string;
|
|
218
|
+
runtimeName?: string;
|
|
219
|
+
packageSpecified?: boolean;
|
|
220
|
+
discoveryPriority?: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const AGENT_SOURCE_PRIORITY: Record<AgentSource, number> = {
|
|
224
|
+
builtin: 0,
|
|
225
|
+
package: 1,
|
|
226
|
+
user: 2,
|
|
227
|
+
project: 3,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function agentDefinitionPriority(definition: Pick<AgentConfig | AgentDiscoveryDiagnostic, "source" | "discoveryPriority">): number {
|
|
231
|
+
return AGENT_SOURCE_PRIORITY[definition.source] * 1_000_000
|
|
232
|
+
+ (definition.discoveryPriority ?? 0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function findBlockingAgentDiagnostic(name: string, agent: AgentConfig | readonly AgentConfig[] | undefined, diagnostics: AgentDiscoveryDiagnostic[] | undefined): AgentDiscoveryDiagnostic | undefined {
|
|
236
|
+
const normalizedName = name.trim();
|
|
237
|
+
const agents = Array.isArray(agent) ? agent : agent ? [agent] : [];
|
|
238
|
+
let match: AgentDiscoveryDiagnostic | undefined;
|
|
239
|
+
for (const diagnostic of diagnostics ?? []) {
|
|
240
|
+
if ((diagnostic.runtimeName === normalizedName
|
|
241
|
+
|| (diagnostic.name === normalizedName && (!diagnostic.packageSpecified
|
|
242
|
+
|| diagnostic.runtimeName === undefined
|
|
243
|
+
|| agents.some((agent) => agent.name === diagnostic.runtimeName && agent.localName === diagnostic.name))))
|
|
244
|
+
&& (!match || agentDefinitionPriority(diagnostic) > agentDefinitionPriority(match))) {
|
|
245
|
+
match = diagnostic;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const highestPriority = Math.max(...agents.map(agentDefinitionPriority), -Infinity);
|
|
249
|
+
return !agents.length || (match && agentDefinitionPriority(match) > highestPriority) ? match : undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
215
252
|
interface AgentDiscoveryResult {
|
|
216
253
|
agents: AgentConfig[];
|
|
254
|
+
agentDiagnostics?: AgentDiscoveryDiagnostic[];
|
|
217
255
|
projectAgentsDir: string | null;
|
|
218
256
|
modelScope?: ModelScopeConfig;
|
|
219
257
|
}
|
|
@@ -345,7 +383,7 @@ function getGlobalNpmRoot(): string | null {
|
|
|
345
383
|
}
|
|
346
384
|
|
|
347
385
|
try {
|
|
348
|
-
cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
|
|
386
|
+
cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000, windowsHide: true }).trim());
|
|
349
387
|
return cachedGlobalNpmRoot;
|
|
350
388
|
} catch {
|
|
351
389
|
cachedGlobalNpmRoot = "";
|
|
@@ -1448,6 +1486,14 @@ function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
|
|
|
1448
1486
|
return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
|
|
1449
1487
|
}
|
|
1450
1488
|
|
|
1489
|
+
function isJsonSerializable(value: unknown): boolean {
|
|
1490
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1491
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1492
|
+
if (Array.isArray(value)) return value.every(isJsonSerializable);
|
|
1493
|
+
if (value && typeof value === "object") return Object.values(value).every(isJsonSerializable);
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1451
1497
|
function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string): AgentRunnerConfig | undefined {
|
|
1452
1498
|
if (raw === undefined || !raw.trim()) return undefined;
|
|
1453
1499
|
let parsed: unknown;
|
|
@@ -1464,8 +1510,24 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
|
|
|
1464
1510
|
if (Object.keys(runner).some((key) => key !== "type")) throw new Error(`Agent '${agentName}' has invalid Pi runner frontmatter; only 'type' is supported.`);
|
|
1465
1511
|
return { type: "pi" };
|
|
1466
1512
|
}
|
|
1513
|
+
if (runner.type === "external-job") {
|
|
1514
|
+
if (typeof runner.provider !== "string" || !runner.provider.trim() || runner.provider.trim() !== runner.provider) {
|
|
1515
|
+
throw new Error(`Agent '${agentName}' external-job runner requires a non-empty trimmed provider string.`);
|
|
1516
|
+
}
|
|
1517
|
+
if (runner.options !== undefined && (!runner.options || typeof runner.options !== "object" || Array.isArray(runner.options) || !isJsonSerializable(runner.options))) {
|
|
1518
|
+
throw new Error(`Agent '${agentName}' external-job runner options must be a JSON-serializable object.`);
|
|
1519
|
+
}
|
|
1520
|
+
const supported = new Set(["type", "provider", "options"]);
|
|
1521
|
+
const unknown = Object.keys(runner).filter((key) => !supported.has(key));
|
|
1522
|
+
if (unknown.length > 0) throw new Error(`Agent '${agentName}' external-job runner has unsupported fields: ${unknown.join(", ")}.`);
|
|
1523
|
+
return {
|
|
1524
|
+
type: "external-job",
|
|
1525
|
+
provider: runner.provider,
|
|
1526
|
+
...(runner.options ? { options: runner.options as Record<string, unknown> } : {}),
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1467
1529
|
if (runner.type !== "external-cli") {
|
|
1468
|
-
throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi' or 'external-
|
|
1530
|
+
throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi', 'external-cli', or 'external-job'.`);
|
|
1469
1531
|
}
|
|
1470
1532
|
if (typeof runner.command !== "string" || !runner.command.trim()) {
|
|
1471
1533
|
throw new Error(`Agent '${agentName}' external-cli runner requires a non-empty command string.`);
|
|
@@ -1489,11 +1551,11 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
|
|
|
1489
1551
|
}
|
|
1490
1552
|
|
|
1491
1553
|
function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
|
|
1492
|
-
if (runner?.type !== "external-cli") return;
|
|
1554
|
+
if (runner?.type !== "external-cli" && runner?.type !== "external-job") return;
|
|
1493
1555
|
const unsupported = ["tools", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
|
|
1494
1556
|
.filter((field) => frontmatter[field] !== undefined);
|
|
1495
1557
|
if (unsupported.length > 0) {
|
|
1496
|
-
throw new Error(`Agent '${agentName}' uses runner.type='
|
|
1558
|
+
throw new Error(`Agent '${agentName}' uses runner.type='${runner.type}' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
|
|
1497
1559
|
}
|
|
1498
1560
|
}
|
|
1499
1561
|
|
|
@@ -1510,21 +1572,48 @@ function parseAgentAcceptanceFrontmatter(raw: string | undefined, agentName: str
|
|
|
1510
1572
|
return parsed as AcceptanceInput;
|
|
1511
1573
|
}
|
|
1512
1574
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1575
|
+
interface AgentDefinitionFile {
|
|
1576
|
+
filePath: string;
|
|
1577
|
+
content: string;
|
|
1578
|
+
}
|
|
1515
1579
|
|
|
1580
|
+
function readAgentDefinitionFiles(dir: string): AgentDefinitionFile[] {
|
|
1581
|
+
const files: AgentDefinitionFile[] = [];
|
|
1516
1582
|
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
|
|
1517
1583
|
if (isLegacyAgentSkillPath(dir, filePath)) {
|
|
1518
1584
|
continue;
|
|
1519
1585
|
}
|
|
1520
1586
|
|
|
1521
|
-
let content: string;
|
|
1522
1587
|
try {
|
|
1523
|
-
content
|
|
1588
|
+
files.push({ filePath, content: fs.readFileSync(filePath, "utf-8") });
|
|
1524
1589
|
} catch {
|
|
1525
1590
|
continue;
|
|
1526
1591
|
}
|
|
1592
|
+
}
|
|
1593
|
+
return files;
|
|
1594
|
+
}
|
|
1527
1595
|
|
|
1596
|
+
function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFilePath: string): string[] | undefined {
|
|
1597
|
+
if (paths === undefined) return undefined;
|
|
1598
|
+
const baseDir = path.dirname(agentFilePath);
|
|
1599
|
+
return paths.map((entry) => {
|
|
1600
|
+
const trimmed = entry.trim();
|
|
1601
|
+
if (trimmed === "." || trimmed === ".." || trimmed.startsWith("./") || trimmed.startsWith("../")) {
|
|
1602
|
+
return path.resolve(baseDir, trimmed);
|
|
1603
|
+
}
|
|
1604
|
+
return entry;
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1609
|
+
const agents: AgentConfig[] = [];
|
|
1610
|
+
const diagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1611
|
+
|
|
1612
|
+
for (const { filePath, content } of files) {
|
|
1613
|
+
let name: string | undefined;
|
|
1614
|
+
let runtimeName: string | undefined;
|
|
1615
|
+
let packageSpecified = false;
|
|
1616
|
+
try {
|
|
1528
1617
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
1529
1618
|
|
|
1530
1619
|
if (!frontmatter.name || !frontmatter.description) {
|
|
@@ -1532,10 +1621,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1532
1621
|
}
|
|
1533
1622
|
|
|
1534
1623
|
const localName = frontmatter.name;
|
|
1624
|
+
name = localName;
|
|
1535
1625
|
const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);
|
|
1536
|
-
|
|
1626
|
+
packageSpecified = parsedPackage.packageName !== undefined || parsedPackage.error !== undefined;
|
|
1627
|
+
if (parsedPackage.error) throw new Error(parsedPackage.error);
|
|
1537
1628
|
const packageName = parsedPackage.packageName;
|
|
1538
|
-
|
|
1629
|
+
runtimeName = buildRuntimeName(localName, packageName);
|
|
1539
1630
|
|
|
1540
1631
|
const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
|
|
1541
1632
|
validateExternalRunnerProfile(frontmatter, localName, runner);
|
|
@@ -1605,8 +1696,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1605
1696
|
else throw new Error(`Agent '${localName}' has invalid acceptanceRole frontmatter; expected 'read-only' or 'writer'.`);
|
|
1606
1697
|
}
|
|
1607
1698
|
|
|
1608
|
-
const extensions = parseFrontmatterList(frontmatter.extensions);
|
|
1609
|
-
const subagentOnlyExtensions = parseFrontmatterList(frontmatter.subagentOnlyExtensions);
|
|
1699
|
+
const extensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.extensions), filePath);
|
|
1700
|
+
const subagentOnlyExtensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.subagentOnlyExtensions), filePath);
|
|
1610
1701
|
|
|
1611
1702
|
const extraFields: Record<string, string> = {};
|
|
1612
1703
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
@@ -1664,6 +1755,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1664
1755
|
systemPrompt: body,
|
|
1665
1756
|
source,
|
|
1666
1757
|
filePath,
|
|
1758
|
+
...(discoveryPriority !== undefined ? { discoveryPriority } : {}),
|
|
1667
1759
|
...(skills?.length ? { skills } : {}),
|
|
1668
1760
|
...(skillPath?.length ? { skillPath } : {}),
|
|
1669
1761
|
...(extensions !== undefined ? { extensions } : {}),
|
|
@@ -1681,9 +1773,16 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1681
1773
|
};
|
|
1682
1774
|
agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
|
|
1683
1775
|
agents.push(agent);
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
diagnostics.push({ source, filePath, ...(name ? { name } : {}), ...(runtimeName && runtimeName !== name ? { runtimeName } : {}), ...(packageSpecified ? { packageSpecified: true } : {}), ...(discoveryPriority !== undefined ? { discoveryPriority } : {}), error: error instanceof Error ? error.message : String(error) });
|
|
1778
|
+
}
|
|
1684
1779
|
}
|
|
1685
1780
|
|
|
1686
|
-
return agents;
|
|
1781
|
+
return { agents, diagnostics };
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1785
|
+
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority);
|
|
1687
1786
|
}
|
|
1688
1787
|
|
|
1689
1788
|
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
@@ -1747,6 +1846,7 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
|
|
|
1747
1846
|
};
|
|
1748
1847
|
}
|
|
1749
1848
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
1849
|
+
const BUILTIN_AGENT_DEFINITION_FILES = readAgentDefinitionFiles(BUILTIN_AGENTS_DIR);
|
|
1750
1850
|
|
|
1751
1851
|
export const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
|
|
1752
1852
|
|
|
@@ -1781,34 +1881,42 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1781
1881
|
includeProject: scope !== "user",
|
|
1782
1882
|
});
|
|
1783
1883
|
|
|
1884
|
+
const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
|
|
1784
1885
|
const builtinAgents = applyBuiltinOverrides(
|
|
1785
|
-
applySubagentDefaults(
|
|
1886
|
+
applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultThinking, defaultExtensions),
|
|
1786
1887
|
userSettings,
|
|
1787
1888
|
projectSettings,
|
|
1788
1889
|
userSettingsPath,
|
|
1789
1890
|
projectSettingsPath,
|
|
1790
1891
|
);
|
|
1791
1892
|
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
1893
|
+
const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew]
|
|
1894
|
+
.map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
|
|
1795
1895
|
const userAgents = applyCustomAgentOverrides(
|
|
1796
|
-
applySubagentDefaults(
|
|
1896
|
+
applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
|
|
1797
1897
|
userSettings,
|
|
1798
1898
|
projectSettings,
|
|
1799
1899
|
userSettingsPath,
|
|
1800
1900
|
projectSettingsPath,
|
|
1801
1901
|
);
|
|
1802
1902
|
|
|
1903
|
+
const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0));
|
|
1803
1904
|
const projectAgents = applyCustomAgentOverrides(
|
|
1804
|
-
applySubagentDefaults(
|
|
1905
|
+
applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
|
|
1805
1906
|
userSettings,
|
|
1806
1907
|
projectSettings,
|
|
1807
1908
|
userSettingsPath,
|
|
1808
1909
|
projectSettingsPath,
|
|
1809
1910
|
);
|
|
1911
|
+
const packageLoaded = packageSubagentPaths.agents.map((dir, index) => loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index));
|
|
1912
|
+
const packageMap = new Map<string, AgentConfig>();
|
|
1913
|
+
for (const loaded of packageLoaded) {
|
|
1914
|
+
for (const agent of loaded.agents) {
|
|
1915
|
+
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1810
1918
|
const packageAgents = applyCustomAgentOverrides(
|
|
1811
|
-
applySubagentDefaults(
|
|
1919
|
+
applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultThinking, defaultExtensions),
|
|
1812
1920
|
userSettings,
|
|
1813
1921
|
projectSettings,
|
|
1814
1922
|
userSettingsPath,
|
|
@@ -1817,7 +1925,13 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1817
1925
|
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
|
|
1818
1926
|
.filter((agent) => agent.disabled !== true);
|
|
1819
1927
|
|
|
1820
|
-
|
|
1928
|
+
const agentDiagnostics = [
|
|
1929
|
+
...builtinLoaded.diagnostics,
|
|
1930
|
+
...userLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
1931
|
+
...projectLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
1932
|
+
...packageLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
1933
|
+
];
|
|
1934
|
+
return { agents, agentDiagnostics, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}) };
|
|
1821
1935
|
}
|
|
1822
1936
|
|
|
1823
1937
|
export function discoverAgentsAll(cwd: string): {
|
|
@@ -1825,6 +1939,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1825
1939
|
package: AgentConfig[];
|
|
1826
1940
|
user: AgentConfig[];
|
|
1827
1941
|
project: AgentConfig[];
|
|
1942
|
+
agentDiagnostics?: AgentDiscoveryDiagnostic[];
|
|
1828
1943
|
chains: ChainConfig[];
|
|
1829
1944
|
chainDiagnostics: ChainDiscoveryDiagnostic[];
|
|
1830
1945
|
userDir: string;
|
|
@@ -1848,27 +1963,29 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1848
1963
|
const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
|
|
1849
1964
|
const packageSubagentPaths = collectPackageSubagentPaths(cwd);
|
|
1850
1965
|
|
|
1966
|
+
const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
|
|
1851
1967
|
const builtin = applyBuiltinOverrides(
|
|
1852
|
-
applySubagentDefaults(
|
|
1968
|
+
applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultThinking, defaultExtensions),
|
|
1853
1969
|
userSettings,
|
|
1854
1970
|
projectSettings,
|
|
1855
1971
|
userSettingsPath,
|
|
1856
1972
|
projectSettingsPath,
|
|
1857
1973
|
);
|
|
1974
|
+
const userLoaded = [...extraUserAgentDirs(), userDirOld, userDirNew]
|
|
1975
|
+
.map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
|
|
1858
1976
|
const user = applyCustomAgentOverrides(
|
|
1859
|
-
applySubagentDefaults(
|
|
1860
|
-
...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
1861
|
-
...loadAgentsFromDir(userDirOld, "user"),
|
|
1862
|
-
...loadAgentsFromDir(userDirNew, "user"),
|
|
1863
|
-
], defaultModel, defaultThinking, defaultExtensions),
|
|
1977
|
+
applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
|
|
1864
1978
|
userSettings,
|
|
1865
1979
|
projectSettings,
|
|
1866
1980
|
userSettingsPath,
|
|
1867
1981
|
projectSettingsPath,
|
|
1868
1982
|
);
|
|
1869
1983
|
const packageMap = new Map<string, AgentConfig>();
|
|
1870
|
-
|
|
1871
|
-
|
|
1984
|
+
const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1985
|
+
for (const [index, dir] of packageSubagentPaths.agents.entries()) {
|
|
1986
|
+
const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
|
|
1987
|
+
packageAgentDiagnostics.push(...loaded.diagnostics);
|
|
1988
|
+
for (const agent of loaded.agents) {
|
|
1872
1989
|
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1873
1990
|
}
|
|
1874
1991
|
}
|
|
@@ -1880,8 +1997,11 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1880
1997
|
projectSettingsPath,
|
|
1881
1998
|
);
|
|
1882
1999
|
const projectMap = new Map<string, AgentConfig>();
|
|
2000
|
+
const projectAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1883
2001
|
for (const dir of projectDirs) {
|
|
1884
|
-
|
|
2002
|
+
const loaded = loadAgentsFromDir(dir, "project", dir === projectDir ? 1 : 0);
|
|
2003
|
+
projectAgentDiagnostics.push(...loaded.diagnostics);
|
|
2004
|
+
for (const agent of loaded.agents) {
|
|
1885
2005
|
projectMap.set(agent.name, agent);
|
|
1886
2006
|
}
|
|
1887
2007
|
}
|
|
@@ -1922,8 +2042,14 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1922
2042
|
...userChains.diagnostics,
|
|
1923
2043
|
...projectChainDiagnostics,
|
|
1924
2044
|
];
|
|
2045
|
+
const agentDiagnostics = [
|
|
2046
|
+
...builtinLoaded.diagnostics,
|
|
2047
|
+
...userLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
2048
|
+
...packageAgentDiagnostics,
|
|
2049
|
+
...projectAgentDiagnostics,
|
|
2050
|
+
];
|
|
1925
2051
|
|
|
1926
2052
|
const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
1927
2053
|
|
|
1928
|
-
return { builtin, package: packageAgents, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
2054
|
+
return { builtin, package: packageAgents, user, project, agentDiagnostics, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
1929
2055
|
}
|
package/src/agents/skills.ts
CHANGED
|
@@ -142,7 +142,7 @@ function getGlobalNpmRoot(): string | null {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
try {
|
|
145
|
-
cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
|
|
145
|
+
cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000, windowsHide: true }).trim());
|
|
146
146
|
return cachedGlobalNpmRoot;
|
|
147
147
|
} catch {
|
|
148
148
|
// Global npm root is optional in constrained environments.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export const EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION = 1;
|
|
2
|
+
export const EXTERNAL_JOB_PROVIDER_REGISTRY_KEY = "pi-subagents.external-job-providers.v1";
|
|
3
|
+
|
|
4
|
+
const MAX_PROVIDER_NAME_LENGTH = 128;
|
|
5
|
+
const MAX_PROVIDERS = 100;
|
|
6
|
+
const MAX_JOB_ID_LENGTH = 256;
|
|
7
|
+
const MAX_FAILURE_CODE_LENGTH = 128;
|
|
8
|
+
const MAX_FAILURE_MESSAGE_LENGTH = 4_096;
|
|
9
|
+
const MAX_URL_LENGTH = 4_096;
|
|
10
|
+
|
|
11
|
+
export type ExternalJobState = "queued" | "running" | "completed" | "failed" | "stopped" | "blocked";
|
|
12
|
+
export type ExternalJobOperation = "start" | "status" | "result" | "reattach";
|
|
13
|
+
|
|
14
|
+
export type ExternalJobOptions = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export interface ExternalJobStartInput {
|
|
17
|
+
prompt: string;
|
|
18
|
+
promptDigest: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
runId: string;
|
|
21
|
+
stepIndex: number;
|
|
22
|
+
agent: string;
|
|
23
|
+
options: ExternalJobOptions;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExternalJobHandle {
|
|
28
|
+
providerJobId: string;
|
|
29
|
+
state: ExternalJobState;
|
|
30
|
+
handleUrl?: string;
|
|
31
|
+
conversationUrl?: string;
|
|
32
|
+
failureCode?: string;
|
|
33
|
+
failureMessage?: string;
|
|
34
|
+
blockingJobId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExternalJobResult extends ExternalJobHandle {
|
|
38
|
+
output?: string;
|
|
39
|
+
artifactPath?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ExternalJobProvider {
|
|
43
|
+
name: string;
|
|
44
|
+
start(input: ExternalJobStartInput): Promise<ExternalJobHandle> | ExternalJobHandle;
|
|
45
|
+
status(providerJobId: string): Promise<ExternalJobHandle> | ExternalJobHandle;
|
|
46
|
+
result(providerJobId: string): Promise<ExternalJobResult> | ExternalJobResult;
|
|
47
|
+
reattach(providerJobId: string): Promise<ExternalJobHandle> | ExternalJobHandle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ExternalJobProviderError extends Error {
|
|
51
|
+
readonly code: string;
|
|
52
|
+
readonly blockingJobId?: string;
|
|
53
|
+
|
|
54
|
+
constructor(message: string, options: { code: string; blockingJobId?: string; cause?: unknown }) {
|
|
55
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
56
|
+
this.name = "ExternalJobProviderError";
|
|
57
|
+
this.code = options.code;
|
|
58
|
+
this.blockingJobId = options.blockingJobId;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ExternalJobProviderRegistry {
|
|
63
|
+
version: typeof EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION;
|
|
64
|
+
providers: Map<string, ExternalJobProvider>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registry(): ExternalJobProviderRegistry {
|
|
68
|
+
const key = Symbol.for(EXTERNAL_JOB_PROVIDER_REGISTRY_KEY);
|
|
69
|
+
const globalObject = globalThis as Record<PropertyKey, unknown>;
|
|
70
|
+
const existing = globalObject[key];
|
|
71
|
+
if (existing === undefined) {
|
|
72
|
+
const created: ExternalJobProviderRegistry = {
|
|
73
|
+
version: EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION,
|
|
74
|
+
providers: new Map(),
|
|
75
|
+
};
|
|
76
|
+
globalObject[key] = created;
|
|
77
|
+
return created;
|
|
78
|
+
}
|
|
79
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
80
|
+
throw new Error(`Malformed external-job provider registry at Symbol.for("${EXTERNAL_JOB_PROVIDER_REGISTRY_KEY}").`);
|
|
81
|
+
}
|
|
82
|
+
const candidate = existing as Partial<ExternalJobProviderRegistry>;
|
|
83
|
+
if (candidate.version !== EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION || !(candidate.providers instanceof Map)) {
|
|
84
|
+
throw new Error(`Unsupported external-job provider registry at Symbol.for("${EXTERNAL_JOB_PROVIDER_REGISTRY_KEY}").`);
|
|
85
|
+
}
|
|
86
|
+
return candidate as ExternalJobProviderRegistry;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validateString(value: unknown, field: string, maxLength: number): string {
|
|
90
|
+
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
|
|
91
|
+
throw new Error(`${field} must be a non-empty string without leading or trailing whitespace.`);
|
|
92
|
+
}
|
|
93
|
+
if (value.length > maxLength) throw new Error(`${field} must be at most ${maxLength} characters.`);
|
|
94
|
+
if (value.includes("\0")) throw new Error(`${field} must not contain NUL characters.`);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validateOptionalString(value: unknown, field: string, maxLength: number): string | undefined {
|
|
99
|
+
if (value === undefined) return undefined;
|
|
100
|
+
return validateString(value, field, maxLength);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateState(value: unknown, field: string): ExternalJobState {
|
|
104
|
+
if (value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "stopped" || value === "blocked") return value;
|
|
105
|
+
throw new Error(`${field} must be queued, running, completed, failed, stopped, or blocked.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateHandle(provider: string, value: unknown, field: string, extraFields: readonly string[] = []): ExternalJobHandle {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} from external-job provider '${provider}' must be an object.`);
|
|
110
|
+
const handle = value as Record<string, unknown>;
|
|
111
|
+
const supported = new Set(["providerJobId", "state", "handleUrl", "conversationUrl", "failureCode", "failureMessage", "blockingJobId", ...extraFields]);
|
|
112
|
+
const unknown = Object.keys(handle).filter((key) => !supported.has(key));
|
|
113
|
+
if (unknown.length > 0) throw new Error(`${field} from external-job provider '${provider}' has unknown fields: ${unknown.join(", ")}.`);
|
|
114
|
+
const handleUrl = validateOptionalString(handle.handleUrl, `${field}.handleUrl`, MAX_URL_LENGTH);
|
|
115
|
+
const conversationUrl = validateOptionalString(handle.conversationUrl, `${field}.conversationUrl`, MAX_URL_LENGTH);
|
|
116
|
+
const failureCode = validateOptionalString(handle.failureCode, `${field}.failureCode`, MAX_FAILURE_CODE_LENGTH);
|
|
117
|
+
const failureMessage = validateOptionalString(handle.failureMessage, `${field}.failureMessage`, MAX_FAILURE_MESSAGE_LENGTH);
|
|
118
|
+
const blockingJobId = validateOptionalString(handle.blockingJobId, `${field}.blockingJobId`, MAX_JOB_ID_LENGTH);
|
|
119
|
+
return {
|
|
120
|
+
providerJobId: validateString(handle.providerJobId, `${field}.providerJobId`, MAX_JOB_ID_LENGTH),
|
|
121
|
+
state: validateState(handle.state, `${field}.state`),
|
|
122
|
+
...(handleUrl ? { handleUrl } : {}),
|
|
123
|
+
...(conversationUrl ? { conversationUrl } : {}),
|
|
124
|
+
...(failureCode ? { failureCode } : {}),
|
|
125
|
+
...(failureMessage ? { failureMessage } : {}),
|
|
126
|
+
...(blockingJobId ? { blockingJobId } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validateExternalJobHandle(provider: string, value: unknown, field = "External-job handle"): ExternalJobHandle {
|
|
131
|
+
return validateHandle(provider, value, field);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function validateExternalJobResult(provider: string, value: unknown, field = "External-job result"): ExternalJobResult {
|
|
135
|
+
const result = validateHandle(provider, value, field, ["output", "artifactPath"]) as ExternalJobResult;
|
|
136
|
+
const record = value as Record<string, unknown>;
|
|
137
|
+
const output = validateOptionalString(record.output, `${field}.output`, 1024 * 1024);
|
|
138
|
+
const artifactPath = validateOptionalString(record.artifactPath, `${field}.artifactPath`, MAX_URL_LENGTH);
|
|
139
|
+
return {
|
|
140
|
+
...result,
|
|
141
|
+
...(output !== undefined ? { output } : {}),
|
|
142
|
+
...(artifactPath !== undefined ? { artifactPath } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateProvider(value: unknown): ExternalJobProvider {
|
|
147
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("External-job provider must be an object.");
|
|
148
|
+
const provider = value as Record<string, unknown>;
|
|
149
|
+
const unknownFields = Object.keys(provider).filter((key) => !["name", "start", "status", "result", "reattach"].includes(key));
|
|
150
|
+
if (unknownFields.length > 0) throw new Error(`External-job provider has unknown fields: ${unknownFields.join(", ")}.`);
|
|
151
|
+
const name = validateString(provider.name, "External-job provider name", MAX_PROVIDER_NAME_LENGTH);
|
|
152
|
+
for (const op of ["start", "status", "result", "reattach"] as const) {
|
|
153
|
+
if (typeof provider[op] !== "function") throw new Error(`External-job provider '${name}' must expose ${op}().`);
|
|
154
|
+
}
|
|
155
|
+
return value as ExternalJobProvider;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function registerExternalJobProvider(provider: ExternalJobProvider): () => void {
|
|
159
|
+
const validated = validateProvider(provider);
|
|
160
|
+
const current = registry();
|
|
161
|
+
if (!current.providers.has(validated.name) && current.providers.size >= MAX_PROVIDERS) {
|
|
162
|
+
throw new Error(`External-job provider registry supports at most ${MAX_PROVIDERS} providers.`);
|
|
163
|
+
}
|
|
164
|
+
current.providers.set(validated.name, validated);
|
|
165
|
+
return () => {
|
|
166
|
+
if (current.providers.get(validated.name) === validated) current.providers.delete(validated.name);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function listExternalJobProviders(): readonly ExternalJobProvider[] {
|
|
171
|
+
const current = registry();
|
|
172
|
+
if (current.providers.size > MAX_PROVIDERS) throw new Error(`External-job provider registry contains more than ${MAX_PROVIDERS} providers.`);
|
|
173
|
+
const providers: ExternalJobProvider[] = [];
|
|
174
|
+
for (const [key, value] of current.providers) {
|
|
175
|
+
const provider = validateProvider(value);
|
|
176
|
+
if (key !== provider.name) throw new Error(`External-job provider registry key '${key}' does not match provider name '${provider.name}'.`);
|
|
177
|
+
providers.push(provider);
|
|
178
|
+
}
|
|
179
|
+
return providers;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getExternalJobProvider(name: string): ExternalJobProvider | undefined {
|
|
183
|
+
const safeName = validateString(name, "External-job provider name", MAX_PROVIDER_NAME_LENGTH);
|
|
184
|
+
return listExternalJobProviders().find((provider) => provider.name === safeName);
|
|
185
|
+
}
|