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.
Files changed (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. 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
@@ -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-cli'.`);
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='external-cli' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
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
- if (parsedPackage.error) continue;
1605
+ packageSpecified = parsedPackage.packageName !== undefined || parsedPackage.error !== undefined;
1606
+ if (parsedPackage.error) throw new Error(parsedPackage.error);
1536
1607
  const packageName = parsedPackage.packageName;
1537
- const runtimeName = buildRuntimeName(localName, packageName);
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(loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"), defaultModel, defaultThinking, defaultExtensions),
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 userAgentsExtra = scope === "project" ? [] : extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user"));
1783
- const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
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([...userAgentsExtra, ...userAgentsOld, ...userAgentsNew], defaultModel, defaultThinking, defaultExtensions),
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(scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project")), defaultModel, defaultThinking, defaultExtensions),
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(packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package")), defaultModel, defaultThinking, defaultExtensions),
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
- return { agents, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}) };
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(loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"), defaultModel, defaultThinking, defaultExtensions),
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
- for (const dir of packageSubagentPaths.agents) {
1861
- for (const agent of loadAgentsFromDir(dir, "package")) {
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
- for (const agent of loadAgentsFromDir(dir, "project")) {
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
+ }