pi-subagents 0.49.0 → 0.51.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 +97 -0
- package/agents/gpt-pro.md +17 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -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 +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- 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 +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
|
@@ -18,6 +18,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
18
18
|
"defaultContext",
|
|
19
19
|
"async",
|
|
20
20
|
"timeoutMs",
|
|
21
|
+
"toolTimeoutMs",
|
|
21
22
|
"turnBudget",
|
|
22
23
|
"acceptance",
|
|
23
24
|
"acceptanceRole",
|
|
@@ -86,6 +87,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
|
|
|
86
87
|
}
|
|
87
88
|
if (config.defaultAsync !== undefined || preserve("async")) lines.push(`async: ${config.defaultAsync === undefined ? "" : config.defaultAsync ? "true" : "false"}`);
|
|
88
89
|
if (config.defaultTimeoutMs !== undefined || preserve("timeoutMs")) lines.push(`timeoutMs: ${config.defaultTimeoutMs ?? ""}`);
|
|
90
|
+
if (config.defaultToolTimeoutMs !== undefined || preserve("toolTimeoutMs")) lines.push(`toolTimeoutMs: ${config.defaultToolTimeoutMs ?? ""}`);
|
|
89
91
|
if (config.defaultTurnBudget || preserve("turnBudget")) lines.push(`turnBudget: ${config.defaultTurnBudget ? JSON.stringify(config.defaultTurnBudget) : ""}`);
|
|
90
92
|
if (config.defaultAcceptance !== undefined || preserve("acceptance")) {
|
|
91
93
|
lines.push(`acceptance: ${config.defaultAcceptance === undefined
|
package/src/agents/agents.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface AgentMemoryConfig {
|
|
|
38
38
|
export const BUILTIN_AGENT_NAMES = [
|
|
39
39
|
"advisor",
|
|
40
40
|
"delegate",
|
|
41
|
+
"gpt-pro",
|
|
41
42
|
"oracle",
|
|
42
43
|
"researcher",
|
|
43
44
|
"reviewer",
|
|
@@ -130,12 +131,14 @@ export interface AgentConfig {
|
|
|
130
131
|
defaultContext?: AgentDefaultContext;
|
|
131
132
|
defaultAsync?: boolean;
|
|
132
133
|
defaultTimeoutMs?: number;
|
|
134
|
+
defaultToolTimeoutMs?: number;
|
|
133
135
|
defaultTurnBudget?: TurnBudgetConfig;
|
|
134
136
|
defaultAcceptance?: AcceptanceInput;
|
|
135
137
|
acceptanceRole?: AcceptanceRole;
|
|
136
138
|
systemPrompt: string;
|
|
137
139
|
source: AgentSource;
|
|
138
140
|
filePath: string;
|
|
141
|
+
discoveryPriority?: number;
|
|
139
142
|
skills?: string[];
|
|
140
143
|
skillPath?: string[];
|
|
141
144
|
extensions?: string[];
|
|
@@ -211,8 +214,45 @@ export interface ChainDiscoveryDiagnostic {
|
|
|
211
214
|
error: string;
|
|
212
215
|
}
|
|
213
216
|
|
|
217
|
+
export interface AgentDiscoveryDiagnostic extends ChainDiscoveryDiagnostic {
|
|
218
|
+
name?: string;
|
|
219
|
+
runtimeName?: string;
|
|
220
|
+
packageSpecified?: boolean;
|
|
221
|
+
discoveryPriority?: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const AGENT_SOURCE_PRIORITY: Record<AgentSource, number> = {
|
|
225
|
+
builtin: 0,
|
|
226
|
+
package: 1,
|
|
227
|
+
user: 2,
|
|
228
|
+
project: 3,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function agentDefinitionPriority(definition: Pick<AgentConfig | AgentDiscoveryDiagnostic, "source" | "discoveryPriority">): number {
|
|
232
|
+
return AGENT_SOURCE_PRIORITY[definition.source] * 1_000_000
|
|
233
|
+
+ (definition.discoveryPriority ?? 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function findBlockingAgentDiagnostic(name: string, agent: AgentConfig | readonly AgentConfig[] | undefined, diagnostics: AgentDiscoveryDiagnostic[] | undefined): AgentDiscoveryDiagnostic | undefined {
|
|
237
|
+
const normalizedName = name.trim();
|
|
238
|
+
const agents = Array.isArray(agent) ? agent : agent ? [agent] : [];
|
|
239
|
+
let match: AgentDiscoveryDiagnostic | undefined;
|
|
240
|
+
for (const diagnostic of diagnostics ?? []) {
|
|
241
|
+
if ((diagnostic.runtimeName === normalizedName
|
|
242
|
+
|| (diagnostic.name === normalizedName && (!diagnostic.packageSpecified
|
|
243
|
+
|| diagnostic.runtimeName === undefined
|
|
244
|
+
|| agents.some((agent) => agent.name === diagnostic.runtimeName && agent.localName === diagnostic.name))))
|
|
245
|
+
&& (!match || agentDefinitionPriority(diagnostic) > agentDefinitionPriority(match))) {
|
|
246
|
+
match = diagnostic;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const highestPriority = Math.max(...agents.map(agentDefinitionPriority), -Infinity);
|
|
250
|
+
return !agents.length || (match && agentDefinitionPriority(match) > highestPriority) ? match : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
214
253
|
interface AgentDiscoveryResult {
|
|
215
254
|
agents: AgentConfig[];
|
|
255
|
+
agentDiagnostics?: AgentDiscoveryDiagnostic[];
|
|
216
256
|
projectAgentsDir: string | null;
|
|
217
257
|
modelScope?: ModelScopeConfig;
|
|
218
258
|
}
|
|
@@ -1447,6 +1487,14 @@ function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
|
|
|
1447
1487
|
return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
|
|
1448
1488
|
}
|
|
1449
1489
|
|
|
1490
|
+
function isJsonSerializable(value: unknown): boolean {
|
|
1491
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1492
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1493
|
+
if (Array.isArray(value)) return value.every(isJsonSerializable);
|
|
1494
|
+
if (value && typeof value === "object") return Object.values(value).every(isJsonSerializable);
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1450
1498
|
function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string): AgentRunnerConfig | undefined {
|
|
1451
1499
|
if (raw === undefined || !raw.trim()) return undefined;
|
|
1452
1500
|
let parsed: unknown;
|
|
@@ -1463,8 +1511,24 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
|
|
|
1463
1511
|
if (Object.keys(runner).some((key) => key !== "type")) throw new Error(`Agent '${agentName}' has invalid Pi runner frontmatter; only 'type' is supported.`);
|
|
1464
1512
|
return { type: "pi" };
|
|
1465
1513
|
}
|
|
1514
|
+
if (runner.type === "external-job") {
|
|
1515
|
+
if (typeof runner.provider !== "string" || !runner.provider.trim() || runner.provider.trim() !== runner.provider) {
|
|
1516
|
+
throw new Error(`Agent '${agentName}' external-job runner requires a non-empty trimmed provider string.`);
|
|
1517
|
+
}
|
|
1518
|
+
if (runner.options !== undefined && (!runner.options || typeof runner.options !== "object" || Array.isArray(runner.options) || !isJsonSerializable(runner.options))) {
|
|
1519
|
+
throw new Error(`Agent '${agentName}' external-job runner options must be a JSON-serializable object.`);
|
|
1520
|
+
}
|
|
1521
|
+
const supported = new Set(["type", "provider", "options"]);
|
|
1522
|
+
const unknown = Object.keys(runner).filter((key) => !supported.has(key));
|
|
1523
|
+
if (unknown.length > 0) throw new Error(`Agent '${agentName}' external-job runner has unsupported fields: ${unknown.join(", ")}.`);
|
|
1524
|
+
return {
|
|
1525
|
+
type: "external-job",
|
|
1526
|
+
provider: runner.provider,
|
|
1527
|
+
...(runner.options ? { options: runner.options as Record<string, unknown> } : {}),
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1466
1530
|
if (runner.type !== "external-cli") {
|
|
1467
|
-
throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi' or 'external-
|
|
1531
|
+
throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi', 'external-cli', or 'external-job'.`);
|
|
1468
1532
|
}
|
|
1469
1533
|
if (typeof runner.command !== "string" || !runner.command.trim()) {
|
|
1470
1534
|
throw new Error(`Agent '${agentName}' external-cli runner requires a non-empty command string.`);
|
|
@@ -1488,11 +1552,11 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
|
|
|
1488
1552
|
}
|
|
1489
1553
|
|
|
1490
1554
|
function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
|
|
1491
|
-
if (runner?.type !== "external-cli") return;
|
|
1555
|
+
if (runner?.type !== "external-cli" && runner?.type !== "external-job") return;
|
|
1492
1556
|
const unsupported = ["tools", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
|
|
1493
1557
|
.filter((field) => frontmatter[field] !== undefined);
|
|
1494
1558
|
if (unsupported.length > 0) {
|
|
1495
|
-
throw new Error(`Agent '${agentName}' uses runner.type='
|
|
1559
|
+
throw new Error(`Agent '${agentName}' uses runner.type='${runner.type}' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
|
|
1496
1560
|
}
|
|
1497
1561
|
}
|
|
1498
1562
|
|
|
@@ -1509,8 +1573,9 @@ function parseAgentAcceptanceFrontmatter(raw: string | undefined, agentName: str
|
|
|
1509
1573
|
return parsed as AcceptanceInput;
|
|
1510
1574
|
}
|
|
1511
1575
|
|
|
1512
|
-
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
1576
|
+
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1513
1577
|
const agents: AgentConfig[] = [];
|
|
1578
|
+
const diagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1514
1579
|
|
|
1515
1580
|
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
|
|
1516
1581
|
if (isLegacyAgentSkillPath(dir, filePath)) {
|
|
@@ -1524,6 +1589,10 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1524
1589
|
continue;
|
|
1525
1590
|
}
|
|
1526
1591
|
|
|
1592
|
+
let name: string | undefined;
|
|
1593
|
+
let runtimeName: string | undefined;
|
|
1594
|
+
let packageSpecified = false;
|
|
1595
|
+
try {
|
|
1527
1596
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
1528
1597
|
|
|
1529
1598
|
if (!frontmatter.name || !frontmatter.description) {
|
|
@@ -1531,10 +1600,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1531
1600
|
}
|
|
1532
1601
|
|
|
1533
1602
|
const localName = frontmatter.name;
|
|
1603
|
+
name = localName;
|
|
1534
1604
|
const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);
|
|
1535
|
-
|
|
1605
|
+
packageSpecified = parsedPackage.packageName !== undefined || parsedPackage.error !== undefined;
|
|
1606
|
+
if (parsedPackage.error) throw new Error(parsedPackage.error);
|
|
1536
1607
|
const packageName = parsedPackage.packageName;
|
|
1537
|
-
|
|
1608
|
+
runtimeName = buildRuntimeName(localName, packageName);
|
|
1538
1609
|
|
|
1539
1610
|
const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
|
|
1540
1611
|
validateExternalRunnerProfile(frontmatter, localName, runner);
|
|
@@ -1582,6 +1653,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1582
1653
|
}
|
|
1583
1654
|
defaultTimeoutMs = parsed;
|
|
1584
1655
|
}
|
|
1656
|
+
let defaultToolTimeoutMs: number | undefined;
|
|
1657
|
+
if (frontmatter.toolTimeoutMs !== undefined) {
|
|
1658
|
+
const parsed = Number(frontmatter.toolTimeoutMs);
|
|
1659
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 2_147_483_647) {
|
|
1660
|
+
throw new Error(`Agent '${localName}' has invalid toolTimeoutMs frontmatter; expected a positive integer no larger than 2147483647.`);
|
|
1661
|
+
}
|
|
1662
|
+
defaultToolTimeoutMs = parsed;
|
|
1663
|
+
}
|
|
1585
1664
|
let defaultTurnBudget: TurnBudgetConfig | undefined;
|
|
1586
1665
|
if (frontmatter.turnBudget !== undefined && frontmatter.turnBudget.trim()) {
|
|
1587
1666
|
const parsed = JSON.parse(frontmatter.turnBudget) as unknown;
|
|
@@ -1648,12 +1727,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1648
1727
|
...(defaultContext !== undefined ? { defaultContext } : {}),
|
|
1649
1728
|
...(defaultAsync !== undefined ? { defaultAsync } : {}),
|
|
1650
1729
|
...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
|
|
1730
|
+
...(defaultToolTimeoutMs !== undefined ? { defaultToolTimeoutMs } : {}),
|
|
1651
1731
|
...(defaultTurnBudget !== undefined ? { defaultTurnBudget } : {}),
|
|
1652
1732
|
...(defaultAcceptance !== undefined ? { defaultAcceptance } : {}),
|
|
1653
1733
|
...(acceptanceRole !== undefined ? { acceptanceRole } : {}),
|
|
1654
1734
|
systemPrompt: body,
|
|
1655
1735
|
source,
|
|
1656
1736
|
filePath,
|
|
1737
|
+
...(discoveryPriority !== undefined ? { discoveryPriority } : {}),
|
|
1657
1738
|
...(skills?.length ? { skills } : {}),
|
|
1658
1739
|
...(skillPath?.length ? { skillPath } : {}),
|
|
1659
1740
|
...(extensions !== undefined ? { extensions } : {}),
|
|
@@ -1671,9 +1752,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1671
1752
|
};
|
|
1672
1753
|
agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
|
|
1673
1754
|
agents.push(agent);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
diagnostics.push({ source, filePath, ...(name ? { name } : {}), ...(runtimeName && runtimeName !== name ? { runtimeName } : {}), ...(packageSpecified ? { packageSpecified: true } : {}), ...(discoveryPriority !== undefined ? { discoveryPriority } : {}), error: error instanceof Error ? error.message : String(error) });
|
|
1757
|
+
}
|
|
1674
1758
|
}
|
|
1675
1759
|
|
|
1676
|
-
return agents;
|
|
1760
|
+
return { agents, diagnostics };
|
|
1677
1761
|
}
|
|
1678
1762
|
|
|
1679
1763
|
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
@@ -1771,34 +1855,42 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1771
1855
|
includeProject: scope !== "user",
|
|
1772
1856
|
});
|
|
1773
1857
|
|
|
1858
|
+
const builtinLoaded = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
|
|
1774
1859
|
const builtinAgents = applyBuiltinOverrides(
|
|
1775
|
-
applySubagentDefaults(
|
|
1860
|
+
applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultThinking, defaultExtensions),
|
|
1776
1861
|
userSettings,
|
|
1777
1862
|
projectSettings,
|
|
1778
1863
|
userSettingsPath,
|
|
1779
1864
|
projectSettingsPath,
|
|
1780
1865
|
);
|
|
1781
1866
|
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
1867
|
+
const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew]
|
|
1868
|
+
.map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
|
|
1785
1869
|
const userAgents = applyCustomAgentOverrides(
|
|
1786
|
-
applySubagentDefaults(
|
|
1870
|
+
applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
|
|
1787
1871
|
userSettings,
|
|
1788
1872
|
projectSettings,
|
|
1789
1873
|
userSettingsPath,
|
|
1790
1874
|
projectSettingsPath,
|
|
1791
1875
|
);
|
|
1792
1876
|
|
|
1877
|
+
const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0));
|
|
1793
1878
|
const projectAgents = applyCustomAgentOverrides(
|
|
1794
|
-
applySubagentDefaults(
|
|
1879
|
+
applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
|
|
1795
1880
|
userSettings,
|
|
1796
1881
|
projectSettings,
|
|
1797
1882
|
userSettingsPath,
|
|
1798
1883
|
projectSettingsPath,
|
|
1799
1884
|
);
|
|
1885
|
+
const packageLoaded = packageSubagentPaths.agents.map((dir, index) => loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index));
|
|
1886
|
+
const packageMap = new Map<string, AgentConfig>();
|
|
1887
|
+
for (const loaded of packageLoaded) {
|
|
1888
|
+
for (const agent of loaded.agents) {
|
|
1889
|
+
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1800
1892
|
const packageAgents = applyCustomAgentOverrides(
|
|
1801
|
-
applySubagentDefaults(
|
|
1893
|
+
applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultThinking, defaultExtensions),
|
|
1802
1894
|
userSettings,
|
|
1803
1895
|
projectSettings,
|
|
1804
1896
|
userSettingsPath,
|
|
@@ -1807,7 +1899,13 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1807
1899
|
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
|
|
1808
1900
|
.filter((agent) => agent.disabled !== true);
|
|
1809
1901
|
|
|
1810
|
-
|
|
1902
|
+
const agentDiagnostics = [
|
|
1903
|
+
...builtinLoaded.diagnostics,
|
|
1904
|
+
...userLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
1905
|
+
...projectLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
1906
|
+
...packageLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
1907
|
+
];
|
|
1908
|
+
return { agents, agentDiagnostics, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}) };
|
|
1811
1909
|
}
|
|
1812
1910
|
|
|
1813
1911
|
export function discoverAgentsAll(cwd: string): {
|
|
@@ -1815,6 +1913,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1815
1913
|
package: AgentConfig[];
|
|
1816
1914
|
user: AgentConfig[];
|
|
1817
1915
|
project: AgentConfig[];
|
|
1916
|
+
agentDiagnostics?: AgentDiscoveryDiagnostic[];
|
|
1818
1917
|
chains: ChainConfig[];
|
|
1819
1918
|
chainDiagnostics: ChainDiscoveryDiagnostic[];
|
|
1820
1919
|
userDir: string;
|
|
@@ -1838,27 +1937,29 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1838
1937
|
const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
|
|
1839
1938
|
const packageSubagentPaths = collectPackageSubagentPaths(cwd);
|
|
1840
1939
|
|
|
1940
|
+
const builtinLoaded = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
|
|
1841
1941
|
const builtin = applyBuiltinOverrides(
|
|
1842
|
-
applySubagentDefaults(
|
|
1942
|
+
applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultThinking, defaultExtensions),
|
|
1843
1943
|
userSettings,
|
|
1844
1944
|
projectSettings,
|
|
1845
1945
|
userSettingsPath,
|
|
1846
1946
|
projectSettingsPath,
|
|
1847
1947
|
);
|
|
1948
|
+
const userLoaded = [...extraUserAgentDirs(), userDirOld, userDirNew]
|
|
1949
|
+
.map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
|
|
1848
1950
|
const user = applyCustomAgentOverrides(
|
|
1849
|
-
applySubagentDefaults(
|
|
1850
|
-
...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
1851
|
-
...loadAgentsFromDir(userDirOld, "user"),
|
|
1852
|
-
...loadAgentsFromDir(userDirNew, "user"),
|
|
1853
|
-
], defaultModel, defaultThinking, defaultExtensions),
|
|
1951
|
+
applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
|
|
1854
1952
|
userSettings,
|
|
1855
1953
|
projectSettings,
|
|
1856
1954
|
userSettingsPath,
|
|
1857
1955
|
projectSettingsPath,
|
|
1858
1956
|
);
|
|
1859
1957
|
const packageMap = new Map<string, AgentConfig>();
|
|
1860
|
-
|
|
1861
|
-
|
|
1958
|
+
const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1959
|
+
for (const [index, dir] of packageSubagentPaths.agents.entries()) {
|
|
1960
|
+
const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
|
|
1961
|
+
packageAgentDiagnostics.push(...loaded.diagnostics);
|
|
1962
|
+
for (const agent of loaded.agents) {
|
|
1862
1963
|
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1863
1964
|
}
|
|
1864
1965
|
}
|
|
@@ -1870,8 +1971,11 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1870
1971
|
projectSettingsPath,
|
|
1871
1972
|
);
|
|
1872
1973
|
const projectMap = new Map<string, AgentConfig>();
|
|
1974
|
+
const projectAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1873
1975
|
for (const dir of projectDirs) {
|
|
1874
|
-
|
|
1976
|
+
const loaded = loadAgentsFromDir(dir, "project", dir === projectDir ? 1 : 0);
|
|
1977
|
+
projectAgentDiagnostics.push(...loaded.diagnostics);
|
|
1978
|
+
for (const agent of loaded.agents) {
|
|
1875
1979
|
projectMap.set(agent.name, agent);
|
|
1876
1980
|
}
|
|
1877
1981
|
}
|
|
@@ -1912,8 +2016,14 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1912
2016
|
...userChains.diagnostics,
|
|
1913
2017
|
...projectChainDiagnostics,
|
|
1914
2018
|
];
|
|
2019
|
+
const agentDiagnostics = [
|
|
2020
|
+
...builtinLoaded.diagnostics,
|
|
2021
|
+
...userLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
2022
|
+
...packageAgentDiagnostics,
|
|
2023
|
+
...projectAgentDiagnostics,
|
|
2024
|
+
];
|
|
1915
2025
|
|
|
1916
2026
|
const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
1917
2027
|
|
|
1918
|
-
return { builtin, package: packageAgents, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
2028
|
+
return { builtin, package: packageAgents, user, project, agentDiagnostics, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
1919
2029
|
}
|
|
@@ -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
|
+
}
|