pi-subagents 0.56.0 → 0.57.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 +53 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +111 -15
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +11 -1
- package/docs/workflows.md +20 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/src/agents/agent-management.ts +13 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agents.ts +219 -48
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-registry.ts +8 -2
- package/src/api/preflight.ts +2 -2
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +18 -4
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +20 -5
- package/src/extension/schemas.ts +4 -3
- package/src/extension/tool-description.ts +11 -9
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +46 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +100 -9
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +5 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +9 -5
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +177 -44
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +72 -10
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +270 -81
- package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/model-exclusions.ts +57 -7
- package/src/runs/shared/model-fallback.ts +23 -1
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +7 -1
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +118 -9
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +47 -21
- package/src/workflows/scripted-workflow.ts +248 -27
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +119 -4
package/src/agents/agents.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as os from "node:os";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
|
|
12
|
+
import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
|
|
12
13
|
import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
13
14
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
14
15
|
import { parseChain, parseJsonChain } from "./chain-serializer.ts";
|
|
@@ -262,14 +263,72 @@ export function findBlockingAgentDiagnostic(name: string, agent: AgentConfig | r
|
|
|
262
263
|
return !agents.length || (match && agentDefinitionPriority(match) > highestPriority) ? match : undefined;
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
+
export type AgentDefinitionDirectoryState = "absent" | "empty" | "candidates" | "unreadable" | "not-directory";
|
|
267
|
+
|
|
268
|
+
/** A definition directory actually inspected during one agent-discovery operation. */
|
|
269
|
+
export interface AgentDefinitionDirectoryReport {
|
|
270
|
+
source: AgentSource;
|
|
271
|
+
path: string;
|
|
272
|
+
state: AgentDefinitionDirectoryState;
|
|
273
|
+
candidateCount?: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Filesystem provenance for a failed agent resolution. Context is created from
|
|
278
|
+
* the discovery result that supplied the effective agents; callers must not
|
|
279
|
+
* pair arbitrary agent arrays with these directory reports.
|
|
280
|
+
*/
|
|
281
|
+
export interface UnknownAgentDiagnosticContext {
|
|
282
|
+
cwd: string;
|
|
283
|
+
scope: AgentScope;
|
|
284
|
+
directories: readonly AgentDefinitionDirectoryReport[];
|
|
285
|
+
agents: readonly AgentConfig[];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface AgentDiscoveryResult {
|
|
266
289
|
agents: AgentConfig[];
|
|
267
290
|
agentDiagnostics?: AgentDiscoveryDiagnostic[];
|
|
268
291
|
projectAgentsDir: string | null;
|
|
292
|
+
cwd: string;
|
|
293
|
+
scope: AgentScope;
|
|
294
|
+
directories: readonly AgentDefinitionDirectoryReport[];
|
|
269
295
|
modelScope?: ModelScopeConfig;
|
|
270
296
|
maxThinking?: ThinkingLevel;
|
|
271
297
|
}
|
|
272
298
|
|
|
299
|
+
/** Create formatter input from the exact discovery operation used for resolution. */
|
|
300
|
+
export function unknownAgentDiagnosticContext(discovered: Pick<AgentDiscoveryResult, "cwd" | "scope" | "directories" | "agents">): UnknownAgentDiagnosticContext {
|
|
301
|
+
return {
|
|
302
|
+
cwd: discovered.cwd,
|
|
303
|
+
scope: discovered.scope,
|
|
304
|
+
directories: discovered.directories,
|
|
305
|
+
agents: discovered.agents,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Render local discovery evidence without exposing filesystem error details. */
|
|
310
|
+
export function formatUnknownAgentError(name: string, context: UnknownAgentDiagnosticContext, prefix = "Unknown agent"): string {
|
|
311
|
+
const directories = context.directories.map((directory) => {
|
|
312
|
+
const state = directory.state === "candidates"
|
|
313
|
+
? `${directory.candidateCount ?? 0} candidate${directory.candidateCount === 1 ? "" : "s"}`
|
|
314
|
+
: directory.state === "empty" ? "present but empty"
|
|
315
|
+
: directory.state === "not-directory" ? "not a directory"
|
|
316
|
+
: directory.state;
|
|
317
|
+
return `- ${directory.source}: ${directory.path} (${state})`;
|
|
318
|
+
});
|
|
319
|
+
const agents = [...context.agents]
|
|
320
|
+
.sort((left, right) => left.name.localeCompare(right.name) || left.source.localeCompare(right.source))
|
|
321
|
+
.map((agent) => `- ${agent.name} (${agent.source})`);
|
|
322
|
+
return [
|
|
323
|
+
`${prefix}: ${name}`,
|
|
324
|
+
`Effective cwd: ${path.resolve(context.cwd)}`,
|
|
325
|
+
"Consulted agent-definition directories:",
|
|
326
|
+
...(directories.length ? directories : ["- (none)"]),
|
|
327
|
+
"Discovered agents:",
|
|
328
|
+
...(agents.length ? agents : ["- (none)"]),
|
|
329
|
+
].join("\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
273
332
|
function getUserChainDir(): string {
|
|
274
333
|
return path.join(getAgentDir(), "chains");
|
|
275
334
|
}
|
|
@@ -381,6 +440,10 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
|
|
|
381
440
|
if (normalized === "." || normalized === ".." || normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
382
441
|
return path.resolve(baseDir, normalized);
|
|
383
442
|
}
|
|
443
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
444
|
+
const parsed = parseGitPackagePath(`git:${trimmed}`);
|
|
445
|
+
return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
|
|
446
|
+
}
|
|
384
447
|
return undefined;
|
|
385
448
|
}
|
|
386
449
|
|
|
@@ -1598,9 +1661,22 @@ function shouldPruneDiscoveryDir(rootDir: string, dir: string, dirName: string):
|
|
|
1598
1661
|
return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir);
|
|
1599
1662
|
}
|
|
1600
1663
|
|
|
1601
|
-
function listFilesRecursive(
|
|
1664
|
+
function listFilesRecursive(
|
|
1665
|
+
dir: string,
|
|
1666
|
+
predicate: (fileName: string) => boolean,
|
|
1667
|
+
rootDir = dir,
|
|
1668
|
+
visitedDirectories = new Set<string>(),
|
|
1669
|
+
): string[] {
|
|
1602
1670
|
const files: string[] = [];
|
|
1603
1671
|
if (!fs.existsSync(dir)) return files;
|
|
1672
|
+
let realDir: string;
|
|
1673
|
+
try {
|
|
1674
|
+
realDir = fs.realpathSync(dir);
|
|
1675
|
+
} catch {
|
|
1676
|
+
return files;
|
|
1677
|
+
}
|
|
1678
|
+
if (visitedDirectories.has(realDir)) return files;
|
|
1679
|
+
visitedDirectories.add(realDir);
|
|
1604
1680
|
|
|
1605
1681
|
let entries: fs.Dirent[];
|
|
1606
1682
|
try {
|
|
@@ -1611,9 +1687,17 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
|
|
|
1611
1687
|
|
|
1612
1688
|
for (const entry of entries) {
|
|
1613
1689
|
const filePath = path.join(dir, entry.name);
|
|
1614
|
-
|
|
1690
|
+
let isDirectory = entry.isDirectory();
|
|
1691
|
+
if (entry.isSymbolicLink()) {
|
|
1692
|
+
try {
|
|
1693
|
+
isDirectory = fs.statSync(filePath).isDirectory();
|
|
1694
|
+
} catch {
|
|
1695
|
+
isDirectory = false;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (isDirectory) {
|
|
1615
1699
|
if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) {
|
|
1616
|
-
files.push(...listFilesRecursive(filePath, predicate, rootDir));
|
|
1700
|
+
files.push(...listFilesRecursive(filePath, predicate, rootDir, visitedDirectories));
|
|
1617
1701
|
}
|
|
1618
1702
|
continue;
|
|
1619
1703
|
}
|
|
@@ -1624,6 +1708,81 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
|
|
|
1624
1708
|
return files;
|
|
1625
1709
|
}
|
|
1626
1710
|
|
|
1711
|
+
export interface AgentDefinitionInspection {
|
|
1712
|
+
files: string[];
|
|
1713
|
+
state: AgentDefinitionDirectoryState;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** Narrow filesystem seam so unavailable paths can be tested without permissions. */
|
|
1717
|
+
export interface AgentDefinitionInspectionFs {
|
|
1718
|
+
existsSync(filePath: string): boolean;
|
|
1719
|
+
realpathSync?(filePath: string): string;
|
|
1720
|
+
statSync(filePath: string): { isDirectory(): boolean };
|
|
1721
|
+
readdirSync(dir: string): fs.Dirent[];
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const DEFAULT_AGENT_DEFINITION_INSPECTION_FS: AgentDefinitionInspectionFs = {
|
|
1725
|
+
existsSync: fs.existsSync,
|
|
1726
|
+
realpathSync: fs.realpathSync,
|
|
1727
|
+
statSync: fs.statSync,
|
|
1728
|
+
readdirSync: (dir) => fs.readdirSync(dir, { withFileTypes: true }),
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Inspect one agent-definition directory while retaining traversal failure
|
|
1733
|
+
* state. A nested unreadable directory makes the whole inspection unavailable,
|
|
1734
|
+
* preventing a partial traversal from being reported as empty or complete.
|
|
1735
|
+
*/
|
|
1736
|
+
export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDefinitionInspectionFs = DEFAULT_AGENT_DEFINITION_INSPECTION_FS): AgentDefinitionInspection {
|
|
1737
|
+
const root = path.resolve(dir);
|
|
1738
|
+
try {
|
|
1739
|
+
if (!operations.existsSync(root)) return { files: [], state: "absent" };
|
|
1740
|
+
if (!operations.statSync(root).isDirectory()) return { files: [], state: "not-directory" };
|
|
1741
|
+
} catch {
|
|
1742
|
+
return { files: [], state: "unreadable" };
|
|
1743
|
+
}
|
|
1744
|
+
const files: string[] = [];
|
|
1745
|
+
let unreadable = false;
|
|
1746
|
+
const visitedDirectories = new Set<string>();
|
|
1747
|
+
const visit = (current: string): void => {
|
|
1748
|
+
let realDir: string;
|
|
1749
|
+
try {
|
|
1750
|
+
realDir = operations.realpathSync?.(current) ?? path.resolve(current);
|
|
1751
|
+
} catch {
|
|
1752
|
+
unreadable = true;
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (visitedDirectories.has(realDir)) return;
|
|
1756
|
+
visitedDirectories.add(realDir);
|
|
1757
|
+
|
|
1758
|
+
let entries: fs.Dirent[];
|
|
1759
|
+
try {
|
|
1760
|
+
entries = operations.readdirSync(current).sort((a, b) => a.name.localeCompare(b.name));
|
|
1761
|
+
} catch {
|
|
1762
|
+
unreadable = true;
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
for (const entry of entries) {
|
|
1766
|
+
const filePath = path.join(current, entry.name);
|
|
1767
|
+
let isDirectory = entry.isDirectory();
|
|
1768
|
+
if (entry.isSymbolicLink()) {
|
|
1769
|
+
try {
|
|
1770
|
+
isDirectory = operations.statSync(filePath).isDirectory();
|
|
1771
|
+
} catch {
|
|
1772
|
+
isDirectory = false;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
if (isDirectory) {
|
|
1776
|
+
if (!shouldPruneDiscoveryDir(root, filePath, entry.name)) visit(filePath);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md") && !entry.name.endsWith(".chain.md") && !isLegacyAgentSkillPath(root, filePath)) files.push(filePath);
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
visit(root);
|
|
1783
|
+
return { files, state: unreadable ? "unreadable" : files.length ? "candidates" : "empty" };
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1627
1786
|
function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
|
|
1628
1787
|
const relative = path.relative(rootDir, filePath);
|
|
1629
1788
|
const parts = relative.split(path.sep).map((part) => part.toLowerCase());
|
|
@@ -1682,18 +1841,23 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
|
|
|
1682
1841
|
if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) {
|
|
1683
1842
|
throw new Error(`Agent '${agentName}' external-cli runner args must be an array of strings.`);
|
|
1684
1843
|
}
|
|
1844
|
+
if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Agent '${agentName}' external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
|
|
1845
|
+
if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Agent '${agentName}' ${runner.adapter} adapter owns its argv; runner args are not supported.`);
|
|
1685
1846
|
if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") {
|
|
1686
1847
|
throw new Error(`Agent '${agentName}' external-cli runner promptDelivery must be 'stdin'.`);
|
|
1687
1848
|
}
|
|
1688
|
-
const
|
|
1849
|
+
const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, `Agent '${agentName}' external-cli runner capabilities`);
|
|
1850
|
+
const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
|
|
1689
1851
|
const unknown = Object.keys(runner).filter((key) => !supported.has(key));
|
|
1690
1852
|
if (unknown.length > 0) throw new Error(`Agent '${agentName}' external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
|
|
1691
1853
|
const runnerArgs = Array.isArray(runner.args) ? runner.args.filter((arg): arg is string => typeof arg === "string") : undefined;
|
|
1692
1854
|
return {
|
|
1693
1855
|
type: "external-cli",
|
|
1856
|
+
...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}),
|
|
1694
1857
|
command: runner.command.trim(),
|
|
1695
1858
|
...(runnerArgs?.length ? { args: runnerArgs } : {}),
|
|
1696
1859
|
...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}),
|
|
1860
|
+
...(capabilities ? { capabilities } : {}),
|
|
1697
1861
|
};
|
|
1698
1862
|
}
|
|
1699
1863
|
|
|
@@ -1724,9 +1888,9 @@ interface AgentDefinitionFile {
|
|
|
1724
1888
|
content: string;
|
|
1725
1889
|
}
|
|
1726
1890
|
|
|
1727
|
-
function readAgentDefinitionFiles(dir: string): AgentDefinitionFile[] {
|
|
1891
|
+
function readAgentDefinitionFiles(dir: string, inspection = inspectAgentDefinitionDirectory(dir)): AgentDefinitionFile[] {
|
|
1728
1892
|
const files: AgentDefinitionFile[] = [];
|
|
1729
|
-
for (const filePath of
|
|
1893
|
+
for (const filePath of inspection.files) {
|
|
1730
1894
|
if (isLegacyAgentSkillPath(dir, filePath)) {
|
|
1731
1895
|
continue;
|
|
1732
1896
|
}
|
|
@@ -1783,6 +1947,8 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1783
1947
|
const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
|
|
1784
1948
|
const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
|
|
1785
1949
|
const aliases = normalizeAgentAliases(parseFrontmatterList(frontmatter.aliases ?? frontmatter.alias), runtimeName);
|
|
1950
|
+
const profileError = validateCodeOwnedProfileRunner({ name: runtimeName, localName, aliases, runner });
|
|
1951
|
+
if (profileError) throw new Error(profileError);
|
|
1786
1952
|
const skillStr = frontmatter.skill || frontmatter.skills;
|
|
1787
1953
|
const skills = parseFrontmatterList(skillStr);
|
|
1788
1954
|
const skillPath = parseFrontmatterList(frontmatter.skillPath);
|
|
@@ -1944,8 +2110,17 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1944
2110
|
return { agents, diagnostics };
|
|
1945
2111
|
}
|
|
1946
2112
|
|
|
1947
|
-
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir"
|
|
1948
|
-
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority, packageSource);
|
|
2113
|
+
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">, inspection = inspectAgentDefinitionDirectory(dir)): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
2114
|
+
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir, inspection), source, discoveryPriority, packageSource);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function reportAgentDefinitionDirectory(source: AgentSource, dir: string, inspection: AgentDefinitionInspection): AgentDefinitionDirectoryReport {
|
|
2118
|
+
return {
|
|
2119
|
+
source,
|
|
2120
|
+
path: path.resolve(dir),
|
|
2121
|
+
state: inspection.state,
|
|
2122
|
+
...(inspection.state === "candidates" ? { candidateCount: inspection.files.length } : {}),
|
|
2123
|
+
};
|
|
1949
2124
|
}
|
|
1950
2125
|
|
|
1951
2126
|
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
@@ -1982,20 +2157,18 @@ function isDirectory(p: string): boolean {
|
|
|
1982
2157
|
}
|
|
1983
2158
|
}
|
|
1984
2159
|
|
|
1985
|
-
function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
2160
|
+
function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; candidateDirs: string[]; preferredDir: string | null } {
|
|
1986
2161
|
const projectRoot = findConfiguredProjectRoot(cwd);
|
|
1987
|
-
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
2162
|
+
if (!projectRoot) return { readDirs: [], candidateDirs: [], preferredDir: null };
|
|
1988
2163
|
|
|
1989
2164
|
const legacyDir = path.join(projectRoot, ".agents");
|
|
1990
2165
|
const preferredDir = path.join(getProjectConfigDir(projectRoot), "agents");
|
|
2166
|
+
const candidateDirs = [legacyDir, preferredDir];
|
|
1991
2167
|
const readDirs: string[] = [];
|
|
1992
2168
|
if (isDirectory(legacyDir)) readDirs.push(legacyDir);
|
|
1993
2169
|
if (isDirectory(preferredDir)) readDirs.push(preferredDir);
|
|
1994
2170
|
|
|
1995
|
-
return {
|
|
1996
|
-
readDirs,
|
|
1997
|
-
preferredDir,
|
|
1998
|
-
};
|
|
2171
|
+
return { readDirs, candidateDirs, preferredDir };
|
|
1999
2172
|
}
|
|
2000
2173
|
|
|
2001
2174
|
function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
@@ -2009,7 +2182,9 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
|
|
|
2009
2182
|
};
|
|
2010
2183
|
}
|
|
2011
2184
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
2012
|
-
|
|
2185
|
+
// Candidate files and inspection state must describe the same cached builtin scan.
|
|
2186
|
+
const BUILTIN_AGENT_DEFINITION_INSPECTION = inspectAgentDefinitionDirectory(BUILTIN_AGENTS_DIR);
|
|
2187
|
+
const BUILTIN_AGENT_DEFINITION_FILES = readAgentDefinitionFiles(BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION);
|
|
2013
2188
|
|
|
2014
2189
|
export const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
|
|
2015
2190
|
|
|
@@ -2028,11 +2203,12 @@ function extraUserAgentDirs(): string[] {
|
|
|
2028
2203
|
}
|
|
2029
2204
|
|
|
2030
2205
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
2206
|
+
const effectiveCwd = path.resolve(cwd);
|
|
2031
2207
|
const userDirOld = path.join(getAgentDir(), "agents");
|
|
2032
2208
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
2033
|
-
const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(
|
|
2209
|
+
const { readDirs: projectAgentDirs, candidateDirs: projectCandidateDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(effectiveCwd);
|
|
2034
2210
|
const userSettingsPath = getUserAgentSettingsPath();
|
|
2035
|
-
const projectSettingsPath = getProjectAgentSettingsPath(
|
|
2211
|
+
const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
|
|
2036
2212
|
const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
|
|
2037
2213
|
const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
|
|
2038
2214
|
const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
|
|
@@ -2041,65 +2217,60 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
2041
2217
|
const maxThinking = resolveSubagentMaxThinking(userSettings, projectSettings, projectSettingsPath);
|
|
2042
2218
|
const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
|
|
2043
2219
|
const modelScope = projectSettings.modelScope ?? userSettings.modelScope;
|
|
2044
|
-
const packageSubagentPaths = collectPackageSubagentPaths(
|
|
2220
|
+
const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd, {
|
|
2045
2221
|
includeUser: scope !== "project",
|
|
2046
2222
|
includeProject: scope !== "user",
|
|
2047
2223
|
});
|
|
2224
|
+
const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
|
|
2048
2225
|
|
|
2049
2226
|
const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
|
|
2050
2227
|
const builtinAgents = applyBuiltinOverrides(
|
|
2051
2228
|
applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions),
|
|
2052
|
-
userSettings,
|
|
2053
|
-
projectSettings,
|
|
2054
|
-
userSettingsPath,
|
|
2055
|
-
projectSettingsPath,
|
|
2229
|
+
userSettings, projectSettings, userSettingsPath, projectSettingsPath,
|
|
2056
2230
|
);
|
|
2057
2231
|
|
|
2058
|
-
const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew]
|
|
2059
|
-
|
|
2232
|
+
const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority) => {
|
|
2233
|
+
const inspection = inspectAgentDefinitionDirectory(dir);
|
|
2234
|
+
directories.push(reportAgentDefinitionDirectory("user", dir, inspection));
|
|
2235
|
+
return loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection);
|
|
2236
|
+
});
|
|
2060
2237
|
const userAgents = applyCustomAgentOverrides(
|
|
2061
2238
|
applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
|
|
2062
|
-
userSettings,
|
|
2063
|
-
projectSettings,
|
|
2064
|
-
userSettingsPath,
|
|
2065
|
-
projectSettingsPath,
|
|
2239
|
+
userSettings, projectSettings, userSettingsPath, projectSettingsPath,
|
|
2066
2240
|
);
|
|
2067
2241
|
|
|
2068
|
-
|
|
2242
|
+
// Report both legacy and preferred project definition paths only after the
|
|
2243
|
+
// existing configured-root resolver found a root; never synthesize cwd paths.
|
|
2244
|
+
const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
|
|
2245
|
+
if (scope !== "user") for (const dir of projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, projectInspections.get(dir)!));
|
|
2246
|
+
const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, projectInspections.get(dir)!));
|
|
2069
2247
|
const projectAgents = applyCustomAgentOverrides(
|
|
2070
2248
|
applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
|
|
2071
|
-
userSettings,
|
|
2072
|
-
projectSettings,
|
|
2073
|
-
userSettingsPath,
|
|
2074
|
-
projectSettingsPath,
|
|
2249
|
+
userSettings, projectSettings, userSettingsPath, projectSettingsPath,
|
|
2075
2250
|
);
|
|
2076
|
-
|
|
2251
|
+
|
|
2252
|
+
const packageLoaded = packageSubagentPaths.agents.map((entry, index) => {
|
|
2253
|
+
const inspection = inspectAgentDefinitionDirectory(entry.dir);
|
|
2254
|
+
directories.push(reportAgentDefinitionDirectory("package", entry.dir, inspection));
|
|
2255
|
+
return loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection);
|
|
2256
|
+
});
|
|
2077
2257
|
const packageMap = new Map<string, AgentConfig>();
|
|
2078
|
-
for (const loaded of packageLoaded)
|
|
2079
|
-
for (const agent of loaded.agents) {
|
|
2080
|
-
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2258
|
+
for (const loaded of packageLoaded) for (const agent of loaded.agents) if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
2083
2259
|
const packageAgents = applyCustomAgentOverrides(
|
|
2084
2260
|
applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
|
|
2085
|
-
userSettings,
|
|
2086
|
-
projectSettings,
|
|
2087
|
-
userSettingsPath,
|
|
2088
|
-
projectSettingsPath,
|
|
2261
|
+
userSettings, projectSettings, userSettingsPath, projectSettingsPath,
|
|
2089
2262
|
);
|
|
2090
2263
|
const agents = applySubagentMaxThinking(
|
|
2091
|
-
mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
|
|
2092
|
-
.filter((agent) => agent.disabled !== true),
|
|
2264
|
+
mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents).filter((agent) => agent.disabled !== true),
|
|
2093
2265
|
maxThinking,
|
|
2094
2266
|
);
|
|
2095
|
-
|
|
2096
2267
|
const agentDiagnostics = [
|
|
2097
2268
|
...builtinLoaded.diagnostics,
|
|
2098
2269
|
...userLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
2099
2270
|
...projectLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
2100
2271
|
...packageLoaded.flatMap((loaded) => loaded.diagnostics),
|
|
2101
2272
|
];
|
|
2102
|
-
return { agents, agentDiagnostics, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
|
|
2273
|
+
return { agents, agentDiagnostics, projectAgentsDir, cwd: effectiveCwd, scope, directories, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
|
|
2103
2274
|
}
|
|
2104
2275
|
|
|
2105
2276
|
export function discoverAgentsAll(cwd: string): {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
|
|
3
|
+
import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
|
|
3
4
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
4
5
|
import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
|
|
5
6
|
import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
|
|
@@ -169,12 +170,15 @@ function validateRunner(value: unknown): AgentRunnerConfig | undefined {
|
|
|
169
170
|
if (runner.type !== "external-cli") throw new Error("Runtime agent definition runner.type must be 'pi', 'external-cli', or 'external-job'.");
|
|
170
171
|
if (typeof runner.command !== "string" || !runner.command.trim()) throw new Error("Runtime agent definition external-cli runner requires a non-empty command string.");
|
|
171
172
|
if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) throw new Error("Runtime agent definition external-cli runner args must be an array of strings.");
|
|
173
|
+
if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Runtime agent definition external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
|
|
174
|
+
if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Runtime agent definition ${runner.adapter} adapter owns its argv; runner args are not supported.`);
|
|
172
175
|
if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") throw new Error("Runtime agent definition external-cli runner promptDelivery must be 'stdin'.");
|
|
173
|
-
const
|
|
176
|
+
const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, "Runtime agent definition external-cli runner capabilities");
|
|
177
|
+
const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
|
|
174
178
|
const unknown = Object.keys(runner).filter((key) => !supported.has(key));
|
|
175
179
|
if (unknown.length > 0) throw new Error(`Runtime agent definition external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
|
|
176
180
|
const args = runner.args as string[] | undefined;
|
|
177
|
-
return { type: "external-cli", command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}) };
|
|
181
|
+
return { type: "external-cli", ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}), command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}), ...(capabilities ? { capabilities } : {}) };
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
function validateTurnBudget(value: unknown): TurnBudgetConfig | undefined {
|
|
@@ -364,6 +368,8 @@ export function registerRuntimeAgent(input: RegisterRuntimeAgentInput): RuntimeA
|
|
|
364
368
|
const name = validateString(input.name, "Runtime agent name", MAX_AGENT_NAME_LENGTH);
|
|
365
369
|
const definition = validateDefinition(input.definition);
|
|
366
370
|
const agent = toAgentConfig(name, definition);
|
|
371
|
+
const profileError = validateCodeOwnedProfileRunner(agent);
|
|
372
|
+
if (profileError) throw new Error(profileError);
|
|
367
373
|
assertNoBuiltinCollision(agent);
|
|
368
374
|
const current = registry();
|
|
369
375
|
const records = current.byPi.get(pi) ?? [];
|
package/src/api/preflight.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
4
|
+
import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
5
5
|
import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
|
|
6
6
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
|
|
7
7
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
@@ -251,7 +251,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
251
251
|
return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
|
|
252
252
|
}
|
|
253
253
|
if (!resolvedAgent.agent) {
|
|
254
|
-
return { ok: false, code: "missing_agent", message:
|
|
254
|
+
return { ok: false, code: "missing_agent", message: formatUnknownAgentError(input.agent, unknownAgentDiagnosticContext(discovered)), diagnostics };
|
|
255
255
|
}
|
|
256
256
|
const agent = resolvedAgent.agent;
|
|
257
257
|
let extensionBindings: ExtensionBindings | undefined;
|
package/src/extension/config.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
|
|
|
6
6
|
import { validateMissionStoreConfig } from "../missions/store.ts";
|
|
7
7
|
import { validateAuthorityPolicy } from "../policy/authority.ts";
|
|
8
8
|
import { getAgentDir } from "../shared/utils.ts";
|
|
9
|
+
import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
|
|
9
10
|
import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
11
|
+
import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
|
|
10
12
|
|
|
11
13
|
const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
|
|
12
14
|
const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
|
|
@@ -16,6 +18,23 @@ const BASE_KEY_IDS = new Set([
|
|
|
16
18
|
...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : []),
|
|
17
19
|
]);
|
|
18
20
|
|
|
21
|
+
class PrunedForkConfigError extends Error {}
|
|
22
|
+
|
|
23
|
+
function validateForkContextConfig(value: unknown): void {
|
|
24
|
+
if (value === undefined) return;
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.forkContext must be a JSON object");
|
|
26
|
+
const config = value as Record<string, unknown>;
|
|
27
|
+
if (config.mode !== undefined && config.mode !== "full" && config.mode !== "pruned") {
|
|
28
|
+
throw new Error('config.forkContext.mode must be "full" or "pruned"');
|
|
29
|
+
}
|
|
30
|
+
if (config.model !== undefined && (typeof config.model !== "string" || !config.model.trim())) {
|
|
31
|
+
throw new PrunedForkConfigError("config.forkContext.model must be a non-empty string");
|
|
32
|
+
}
|
|
33
|
+
if (config.mode === "pruned" && config.model === undefined) {
|
|
34
|
+
throw new PrunedForkConfigError('config.forkContext.model is required when config.forkContext.mode is "pruned"');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
function isValidKeyId(value: string): boolean {
|
|
20
39
|
if (value !== value.trim()) return false;
|
|
21
40
|
const parts = value.toLowerCase().split("+");
|
|
@@ -62,6 +81,31 @@ function validateArtifactConfig(value: unknown): void {
|
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
function validateCapacityConfig(value: unknown): void {
|
|
85
|
+
if (value === undefined) return;
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.capacity must be a JSON object");
|
|
87
|
+
const abandonedSlotReleaseAfterMs = (value as Record<string, unknown>).abandonedSlotReleaseAfterMs;
|
|
88
|
+
if (abandonedSlotReleaseAfterMs !== undefined
|
|
89
|
+
&& abandonedSlotReleaseAfterMs !== false
|
|
90
|
+
&& (typeof abandonedSlotReleaseAfterMs !== "number"
|
|
91
|
+
|| !Number.isInteger(abandonedSlotReleaseAfterMs)
|
|
92
|
+
|| abandonedSlotReleaseAfterMs < MIN_ABANDONED_SLOT_RELEASE_AFTER_MS
|
|
93
|
+
|| abandonedSlotReleaseAfterMs > MAX_ABANDONED_SLOT_RELEASE_AFTER_MS)) {
|
|
94
|
+
throw new Error(`config.capacity.abandonedSlotReleaseAfterMs must be false or an integer from ${MIN_ABANDONED_SLOT_RELEASE_AFTER_MS} to ${MAX_ABANDONED_SLOT_RELEASE_AFTER_MS}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validate the user-controlled TTL policy before it reaches the exclusion store. */
|
|
99
|
+
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
100
|
+
function validateModelExclusionsConfig(value: unknown): void {
|
|
101
|
+
if (value === undefined) return;
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
|
|
103
|
+
const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
|
|
104
|
+
if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
|
|
105
|
+
throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
function validateOrcaProgressTabsConfig(value: unknown): void {
|
|
66
110
|
if (value === undefined) return;
|
|
67
111
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
|
|
@@ -97,6 +141,7 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
97
141
|
if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
|
|
98
142
|
throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
|
|
99
143
|
}
|
|
144
|
+
validateForkContextConfig(config.forkContext);
|
|
100
145
|
if (config.foregroundDetachShortcut !== undefined
|
|
101
146
|
&& (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
|
|
102
147
|
throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
|
|
@@ -119,6 +164,8 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
119
164
|
validateScheduledRunsConfig(config.scheduledRuns);
|
|
120
165
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
121
166
|
validateArtifactConfig(config.artifactConfig);
|
|
167
|
+
validateCapacityConfig(config.capacity);
|
|
168
|
+
validateModelExclusionsConfig(config.modelExclusions);
|
|
122
169
|
validateMainWindowRendererConfig(config.mainWindowRenderer);
|
|
123
170
|
validateOrcaProgressTabsConfig(config.orcaProgressTabs);
|
|
124
171
|
}
|
|
@@ -150,6 +197,28 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
|
|
|
150
197
|
return next;
|
|
151
198
|
}
|
|
152
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the default TTL that the process-wide exclusion store should use.
|
|
202
|
+
*
|
|
203
|
+
* @param config Extension configuration after validation.
|
|
204
|
+
* @returns The configured TTL in milliseconds, or the built-in 24-hour default.
|
|
205
|
+
*/
|
|
206
|
+
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
207
|
+
export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
|
|
208
|
+
return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Apply the configured exclusion policy to the process-wide model store.
|
|
213
|
+
*
|
|
214
|
+
* @param config Extension configuration after validation.
|
|
215
|
+
* @returns Nothing.
|
|
216
|
+
*/
|
|
217
|
+
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
218
|
+
export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
|
|
219
|
+
setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
|
|
220
|
+
}
|
|
221
|
+
|
|
153
222
|
export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
|
|
154
223
|
return config.asyncByDefault !== false;
|
|
155
224
|
}
|
|
@@ -159,6 +228,7 @@ export function loadConfig(): ExtensionConfig {
|
|
|
159
228
|
try {
|
|
160
229
|
return readConfigForUpdate(configPath);
|
|
161
230
|
} catch (error) {
|
|
231
|
+
if (error instanceof PrunedForkConfigError) throw error;
|
|
162
232
|
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
163
233
|
}
|
|
164
234
|
return {};
|
package/src/extension/doctor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
|
|
4
4
|
import { isAsyncAvailable } from "../runs/background/async-execution.ts";
|
|
5
5
|
import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
|
|
6
|
-
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
6
|
+
import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
7
7
|
import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
|
|
8
8
|
import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
|
|
9
9
|
import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
|
|
@@ -197,13 +197,13 @@ function formatActiveAsyncCapacitySection(input: DoctorReportInput): string[] {
|
|
|
197
197
|
const limit = resolveMaxActiveAsyncRunsPerSession(input.config.maxActiveAsyncRunsPerSession);
|
|
198
198
|
const sessionId = input.currentSessionId ?? input.state.currentSessionId;
|
|
199
199
|
const snapshot = sessionId
|
|
200
|
-
? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []) })
|
|
200
|
+
? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.config.capacity?.abandonedSlotReleaseAfterMs) })
|
|
201
201
|
: { used: 0, limit: limit ?? 0 };
|
|
202
202
|
input.state.activeAsyncCapacity = snapshot;
|
|
203
203
|
return [
|
|
204
204
|
`- usage: ${snapshot.used}/${snapshot.limit || "unlimited"} used`,
|
|
205
205
|
"- scope: top-level async runs in the current parent session; foreground and nested workflow children are not charged again",
|
|
206
|
-
"- release: terminal
|
|
206
|
+
"- release: terminal state plus matching observed process-terminal proof, or abandoned-timeout for failed runs with a dead runner PID and stale activity when enabled; false keeps unknown-proof slots",
|
|
207
207
|
];
|
|
208
208
|
}
|
|
209
209
|
|