pi-subagents 0.52.1 → 0.54.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 +69 -0
- package/README.md +4 -0
- package/docs/configuration.md +12 -2
- package/docs/extension-api.md +3 -1
- package/docs/models.md +17 -3
- package/docs/workflows.md +2 -0
- package/index.ts +10 -1
- package/package.json +2 -1
- package/prompts/council.md +51 -0
- package/skills/council-mode/SKILL.md +231 -0
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
- package/skills/pi-subagents/references/execution-controls.md +13 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
- package/src/agents/agent-management.ts +107 -8
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +96 -37
- package/src/agents/builtin-names.ts +9 -0
- package/src/agents/runtime-agent-registry.ts +418 -0
- package/src/api/agents.ts +7 -0
- package/src/api/preflight.ts +8 -3
- package/src/extension/config.ts +3 -0
- package/src/extension/doctor.ts +11 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +20 -4
- package/src/extension/public-execution.ts +1 -1
- package/src/extension/rpc.ts +41 -1
- package/src/extension/schemas.ts +6 -3
- package/src/extension/tool-description.ts +2 -2
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/runs/background/async-execution.ts +12 -6
- package/src/runs/background/async-job-tracker.ts +4 -3
- package/src/runs/background/async-resume.ts +2 -1
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status-snapshot.ts +14 -5
- package/src/runs/background/auto-drain.ts +1 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/result-watcher.ts +9 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +32 -5
- package/src/runs/background/subagent-wait.ts +9 -5
- package/src/runs/background/terminal-run-index.ts +15 -6
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/execution.ts +34 -2
- package/src/runs/foreground/subagent-executor.ts +196 -52
- package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
- package/src/runs/shared/acceptance.ts +44 -1
- package/src/runs/shared/model-exclusions.ts +242 -0
- package/src/runs/shared/model-fallback.ts +72 -16
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/pi-args.ts +34 -1
- package/src/runs/shared/subagent-control.ts +25 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
- package/src/shared/fork-context.ts +17 -1
- package/src/shared/model-info.ts +20 -0
- package/src/shared/settings.ts +2 -2
- package/src/shared/types.ts +47 -2
- package/src/slash/slash-commands.ts +20 -6
- package/src/slash/slash-live-state.ts +3 -3
- package/src/tui/fleet-status.ts +86 -1
- package/src/tui/fleet.ts +55 -2
- package/src/tui/render.ts +73 -3
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +100 -12
- package/src/workflows/workflow-receipt.ts +140 -0
|
@@ -36,10 +36,12 @@ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
|
36
36
|
import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
|
|
37
37
|
import { getProjectConfigDir } from "../shared/utils.ts";
|
|
38
38
|
import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
39
|
+
import { listRuntimeAgentConfigs, mergeRuntimeAgents, type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
|
|
40
|
+
import { listExternalJobProviders } from "../api/external-job-provider.ts";
|
|
39
41
|
|
|
40
42
|
type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
|
|
41
43
|
type ManagementScope = "user" | "project";
|
|
42
|
-
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string };
|
|
44
|
+
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner };
|
|
43
45
|
|
|
44
46
|
interface ManagementParams {
|
|
45
47
|
action?: string;
|
|
@@ -142,7 +144,7 @@ function diagnosticsForScope(diagnostics: AgentDiscoveryDiagnostic[] | undefined
|
|
|
142
144
|
return diagnostics?.filter((diagnostic) => diagnostic.source !== excludedSource);
|
|
143
145
|
}
|
|
144
146
|
|
|
145
|
-
const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
|
|
147
|
+
const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3, runtime: 4 };
|
|
146
148
|
|
|
147
149
|
// Returns the highest-precedence definition for a resolved canonical name (project > user > package > builtin),
|
|
148
150
|
// matching mergeAgentsForScope for "both", including disabled agents so disable/enable can locate hidden targets.
|
|
@@ -213,6 +215,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
|
|
|
213
215
|
const base = agent.override?.base;
|
|
214
216
|
const {
|
|
215
217
|
override: _override,
|
|
218
|
+
outputMode: _outputMode,
|
|
216
219
|
model: _model,
|
|
217
220
|
fallbackModels: _fallbackModels,
|
|
218
221
|
thinking: _thinking,
|
|
@@ -240,6 +243,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
|
|
|
240
243
|
|
|
241
244
|
return withDeclaredExtensionPaths({
|
|
242
245
|
...editable,
|
|
246
|
+
...(base.outputMode !== undefined ? { outputMode: base.outputMode } : {}),
|
|
243
247
|
...(base.model !== undefined ? { model: base.model } : {}),
|
|
244
248
|
...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
|
|
245
249
|
...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
|
|
@@ -311,6 +315,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
|
|
|
311
315
|
if (hasKey(cfg, "acceptance")) changed("acceptance");
|
|
312
316
|
if (hasKey(cfg, "acceptanceRole")) changed("acceptanceRole");
|
|
313
317
|
if (hasKey(cfg, "output")) changed("output");
|
|
318
|
+
if (hasKey(cfg, "outputMode")) changed("outputMode");
|
|
314
319
|
if (hasKey(cfg, "reads")) changed("defaultReads");
|
|
315
320
|
if (hasKey(cfg, "progress")) changed("defaultProgress");
|
|
316
321
|
if (hasKey(cfg, "maxSubagentDepth")) changed("maxSubagentDepth");
|
|
@@ -501,6 +506,10 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
501
506
|
else if (typeof cfg.output === "string") target.output = cfg.output;
|
|
502
507
|
else return "config.output must be a string or false when provided.";
|
|
503
508
|
}
|
|
509
|
+
if (hasKey(cfg, "outputMode")) {
|
|
510
|
+
if (cfg.outputMode === "inline" || cfg.outputMode === "file-only") target.outputMode = cfg.outputMode;
|
|
511
|
+
else return "config.outputMode must be 'inline' or 'file-only' when provided.";
|
|
512
|
+
}
|
|
504
513
|
if (hasKey(cfg, "reads")) {
|
|
505
514
|
if (cfg.reads === false || cfg.reads === "") delete target.defaultReads;
|
|
506
515
|
else if (typeof cfg.reads === "string") {
|
|
@@ -582,9 +591,83 @@ function renamePath(currentPath: string, newName: string, scope: ManagementScope
|
|
|
582
591
|
return { filePath };
|
|
583
592
|
}
|
|
584
593
|
|
|
594
|
+
function packageSourceLabel(agent: AgentConfig): string {
|
|
595
|
+
if (!agent.packageSourceName) return agent.source;
|
|
596
|
+
return agent.packageSourceVersion ? `${agent.packageSourceName}@${agent.packageSourceVersion}` : agent.packageSourceName;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
type ExternalJobProviderStatus =
|
|
600
|
+
| { ok: true; names: Set<string> }
|
|
601
|
+
| { ok: false; error: string };
|
|
602
|
+
|
|
603
|
+
function errorMessage(error: unknown): string {
|
|
604
|
+
return error instanceof Error ? error.message : String(error);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function registeredExternalJobProviderStatus(): ExternalJobProviderStatus {
|
|
608
|
+
try {
|
|
609
|
+
return { ok: true, names: new Set(listExternalJobProviders().map((provider) => provider.name)) };
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return { ok: false, error: errorMessage(error) };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function externalJobProviderSuffix(provider: string, names: Set<string> | undefined): string {
|
|
616
|
+
if (!names) return "?";
|
|
617
|
+
return names.has(provider) ? "✓" : "missing";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
|
|
621
|
+
if (agent.runner?.type === "external-job") return `external-job:${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
|
|
622
|
+
if (agent.runner?.type === "external-cli") return "external-cli";
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
|
|
627
|
+
const source = agent.source === "package" ? packageSourceLabel(agent) : agent.source;
|
|
628
|
+
const parts = [
|
|
629
|
+
source,
|
|
630
|
+
runnerListBadge(agent, providerNames),
|
|
631
|
+
agent.defaultContext ? `context: ${agent.defaultContext}` : undefined,
|
|
632
|
+
agent.aliases?.length ? `aliases: ${agent.aliases.join(", ")}` : undefined,
|
|
633
|
+
].filter((part): part is string => Boolean(part));
|
|
634
|
+
return `- ${agent.name} (${parts.join(", ")}): ${agent.description}`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function formatAgentListSections(agents: AgentConfig[], providerNames: Set<string> | undefined): string[] {
|
|
638
|
+
if (agents.length === 0) return ["- (none)"];
|
|
639
|
+
const sections: Array<[AgentSource, string]> = [
|
|
640
|
+
["package", "Package agents"],
|
|
641
|
+
["user", "User agents"],
|
|
642
|
+
["project", "Project agents"],
|
|
643
|
+
["runtime", "Runtime agents"],
|
|
644
|
+
["builtin", "Builtin agents"],
|
|
645
|
+
];
|
|
646
|
+
const lines: string[] = [];
|
|
647
|
+
for (const [source, label] of sections) {
|
|
648
|
+
const matches = agents.filter((agent) => agent.source === source);
|
|
649
|
+
if (matches.length === 0) continue;
|
|
650
|
+
if (lines.length > 0) lines.push("");
|
|
651
|
+
lines.push(label, ...matches.map((agent) => formatAgentListLine(agent, providerNames)));
|
|
652
|
+
}
|
|
653
|
+
return lines;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatRunnerDetail(agent: AgentConfig, providerNames: Set<string> | undefined): string | undefined {
|
|
657
|
+
if (!agent.runner) return undefined;
|
|
658
|
+
if (agent.runner.type === "external-job") return `Runner: external-job via ${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
|
|
659
|
+
if (agent.runner.type === "external-cli") return `Runner: external-cli ${agent.runner.command}`;
|
|
660
|
+
return `Runner: ${JSON.stringify(agent.runner)}`;
|
|
661
|
+
}
|
|
662
|
+
|
|
585
663
|
function formatAgentDetail(agent: AgentConfig): string {
|
|
586
664
|
const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
|
|
665
|
+
const providerStatus = registeredExternalJobProviderStatus();
|
|
587
666
|
const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
|
|
667
|
+
if (agent.source === "package" && agent.packageSourceName) {
|
|
668
|
+
lines.push(`Source package: ${packageSourceLabel(agent)}`);
|
|
669
|
+
if (agent.packageSourceRoot) lines.push(`Package root: ${agent.packageSourceRoot}`);
|
|
670
|
+
}
|
|
588
671
|
if (agent.packageName) {
|
|
589
672
|
lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
|
|
590
673
|
lines.push(`Package: ${agent.packageName}`);
|
|
@@ -596,7 +679,12 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
596
679
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
597
680
|
if (agent.skillPath?.length) lines.push(`Skill paths: ${agent.skillPath.join(", ")}`);
|
|
598
681
|
lines.push(`System prompt mode: ${agent.systemPromptMode}`);
|
|
599
|
-
|
|
682
|
+
const runnerDetail = formatRunnerDetail(agent, providerStatus.ok ? providerStatus.names : undefined);
|
|
683
|
+
if (runnerDetail) {
|
|
684
|
+
lines.push(runnerDetail);
|
|
685
|
+
if (agent.runner?.type === "external-job" && !providerStatus.ok) lines.push(`External-job provider registry unavailable: ${providerStatus.error}`);
|
|
686
|
+
if (agent.runner?.type === "external-job" && agent.runner.options) lines.push(`Runner options: ${JSON.stringify(agent.runner.options)}`);
|
|
687
|
+
}
|
|
600
688
|
lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
|
|
601
689
|
lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
|
|
602
690
|
if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
|
|
@@ -610,6 +698,7 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
610
698
|
if (agent.subagentOnlyExtensions !== undefined) lines.push(`Subagent-only extensions: ${agent.subagentOnlyExtensions.length ? agent.subagentOnlyExtensions.join(", ") : "(none)"}`);
|
|
611
699
|
if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
|
|
612
700
|
if (agent.output) lines.push(`Output: ${agent.output}`);
|
|
701
|
+
if (agent.outputMode) lines.push(`Output mode: ${agent.outputMode}`);
|
|
613
702
|
if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
|
|
614
703
|
if (agent.defaultProgress) lines.push("Progress: true");
|
|
615
704
|
if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
|
|
@@ -623,7 +712,17 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
623
712
|
export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
624
713
|
const scope = normalizeListScope(params.agentScope) ?? "both";
|
|
625
714
|
const d = discoverAgentsAll(ctx.cwd);
|
|
626
|
-
|
|
715
|
+
let scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
|
|
716
|
+
if (ctx.runtimeAgentOwner && listRuntimeAgentConfigs(ctx.runtimeAgentOwner).length > 0) {
|
|
717
|
+
const configuredAgents: AgentConfig[] = [
|
|
718
|
+
...d.builtin,
|
|
719
|
+
...d.package,
|
|
720
|
+
...d.user,
|
|
721
|
+
...d.project,
|
|
722
|
+
];
|
|
723
|
+
scopedAgents = mergeRuntimeAgents(ctx.runtimeAgentOwner, { agents: scopedAgents }, configuredAgents).agents;
|
|
724
|
+
}
|
|
725
|
+
scopedAgents = scopedAgents
|
|
627
726
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
628
727
|
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
|
|
629
728
|
const visibleAgents = scopedAgents.filter((a) => !a.disabled);
|
|
@@ -635,16 +734,16 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
635
734
|
...(ctx.config?.proactiveSkillSubagents !== undefined ? { config: ctx.config.proactiveSkillSubagents } : {}),
|
|
636
735
|
discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
|
|
637
736
|
});
|
|
737
|
+
const providerStatus = registeredExternalJobProviderStatus();
|
|
638
738
|
const lines = [
|
|
639
739
|
"Executable agents:",
|
|
640
|
-
...(agents.
|
|
641
|
-
? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
|
|
642
|
-
: ["- (none)"]),
|
|
740
|
+
...formatAgentListSections(agents, providerStatus.ok ? providerStatus.names : undefined),
|
|
643
741
|
...(restrictedAgents.length ? [
|
|
644
742
|
"",
|
|
645
743
|
`Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
|
|
646
|
-
...restrictedAgents.map((a) =>
|
|
744
|
+
...restrictedAgents.map((a) => formatAgentListLine(a, providerStatus.ok ? providerStatus.names : undefined)),
|
|
647
745
|
] : []),
|
|
746
|
+
...(!providerStatus.ok && [...agents, ...restrictedAgents].some((agent) => agent.runner?.type === "external-job") ? ["", `External-job provider registry unavailable: ${providerStatus.error}`] : []),
|
|
648
747
|
...(d.agentDiagnostics?.length ? [
|
|
649
748
|
"",
|
|
650
749
|
"Invalid agent definitions:",
|
|
@@ -28,6 +28,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
28
28
|
"extensions",
|
|
29
29
|
"subagentOnlyExtensions",
|
|
30
30
|
"output",
|
|
31
|
+
"outputMode",
|
|
31
32
|
"defaultReads",
|
|
32
33
|
"defaultProgress",
|
|
33
34
|
"interactive",
|
|
@@ -113,6 +114,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
if (config.output) lines.push(`output: ${config.output}`);
|
|
117
|
+
if (config.outputMode || preserve("outputMode")) lines.push(`outputMode: ${config.outputMode ?? ""}`);
|
|
116
118
|
|
|
117
119
|
const readsValue = joinComma(config.defaultReads);
|
|
118
120
|
if (readsValue) lines.push(`defaultReads: ${readsValue}`);
|
package/src/agents/agents.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { mergeAgentsForScope } from "./agent-selection.ts";
|
|
|
16
16
|
import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.ts";
|
|
17
17
|
import { buildRuntimeName, parsePackageName } from "./identity.ts";
|
|
18
18
|
import { parseModelScopeConfig, type ModelScopeConfig } from "../runs/shared/model-scope.ts";
|
|
19
|
+
export { BUILTIN_AGENT_NAMES } from "./builtin-names.ts";
|
|
19
20
|
export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
20
21
|
import { parseMemoryFrontmatter } from "./agent-memory.ts";
|
|
21
22
|
import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
|
|
@@ -24,7 +25,7 @@ import { validatePermissionRules, type PermissionRules } from "../runs/shared/pe
|
|
|
24
25
|
|
|
25
26
|
export type AgentScope = "user" | "project" | "both";
|
|
26
27
|
|
|
27
|
-
export type AgentSource = "builtin" | "package" | "user" | "project";
|
|
28
|
+
export type AgentSource = "builtin" | "package" | "user" | "project" | "runtime";
|
|
28
29
|
type SystemPromptMode = "append" | "replace";
|
|
29
30
|
export type AgentDefaultContext = "fresh" | "fork";
|
|
30
31
|
|
|
@@ -35,16 +36,6 @@ export interface AgentMemoryConfig {
|
|
|
35
36
|
path: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export const BUILTIN_AGENT_NAMES = [
|
|
39
|
-
"advisor",
|
|
40
|
-
"delegate",
|
|
41
|
-
"oracle",
|
|
42
|
-
"researcher",
|
|
43
|
-
"reviewer",
|
|
44
|
-
"scout",
|
|
45
|
-
"worker",
|
|
46
|
-
] as const;
|
|
47
|
-
|
|
48
39
|
export function defaultSystemPromptMode(name: string): SystemPromptMode {
|
|
49
40
|
return name === "delegate" ? "append" : "replace";
|
|
50
41
|
}
|
|
@@ -59,6 +50,7 @@ export function defaultInheritSkills(): boolean {
|
|
|
59
50
|
|
|
60
51
|
export interface BuiltinAgentOverrideBase {
|
|
61
52
|
description?: string;
|
|
53
|
+
outputMode?: OutputMode;
|
|
62
54
|
model?: string;
|
|
63
55
|
fallbackModels?: string[];
|
|
64
56
|
thinking?: string | false;
|
|
@@ -81,6 +73,7 @@ export interface BuiltinAgentOverrideBase {
|
|
|
81
73
|
|
|
82
74
|
interface BuiltinAgentOverrideConfig {
|
|
83
75
|
description?: string;
|
|
76
|
+
outputMode?: OutputMode;
|
|
84
77
|
model?: string | false;
|
|
85
78
|
fallbackModels?: string[] | false;
|
|
86
79
|
thinking?: string | false;
|
|
@@ -117,6 +110,9 @@ export interface AgentConfig {
|
|
|
117
110
|
runner?: AgentRunnerConfig;
|
|
118
111
|
localName?: string;
|
|
119
112
|
packageName?: string;
|
|
113
|
+
packageSourceName?: string;
|
|
114
|
+
packageSourceVersion?: string;
|
|
115
|
+
packageSourceRoot?: string;
|
|
120
116
|
description: string;
|
|
121
117
|
aliases?: string[];
|
|
122
118
|
tools?: string[];
|
|
@@ -144,6 +140,7 @@ export interface AgentConfig {
|
|
|
144
140
|
extensionsFromDefault?: boolean;
|
|
145
141
|
subagentOnlyExtensions?: string[];
|
|
146
142
|
output?: string;
|
|
143
|
+
outputMode?: OutputMode;
|
|
147
144
|
defaultReads?: string[];
|
|
148
145
|
defaultProgress?: boolean;
|
|
149
146
|
interactive?: boolean;
|
|
@@ -225,6 +222,7 @@ const AGENT_SOURCE_PRIORITY: Record<AgentSource, number> = {
|
|
|
225
222
|
package: 1,
|
|
226
223
|
user: 2,
|
|
227
224
|
project: 3,
|
|
225
|
+
runtime: 4,
|
|
228
226
|
};
|
|
229
227
|
|
|
230
228
|
function agentDefinitionPriority(definition: Pick<AgentConfig | AgentDiscoveryDiagnostic, "source" | "discoveryPriority">): number {
|
|
@@ -260,8 +258,15 @@ function getUserChainDir(): string {
|
|
|
260
258
|
return path.join(getAgentDir(), "chains");
|
|
261
259
|
}
|
|
262
260
|
|
|
261
|
+
interface PackageSubagentPath {
|
|
262
|
+
dir: string;
|
|
263
|
+
packageName?: string;
|
|
264
|
+
packageVersion?: string;
|
|
265
|
+
packageRoot: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
263
268
|
interface PackageSubagentPaths {
|
|
264
|
-
agents:
|
|
269
|
+
agents: PackageSubagentPath[];
|
|
265
270
|
chains: string[];
|
|
266
271
|
}
|
|
267
272
|
|
|
@@ -396,18 +401,30 @@ function stringArray(value: unknown): string[] {
|
|
|
396
401
|
return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
|
|
397
402
|
}
|
|
398
403
|
|
|
404
|
+
function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omit<PackageSubagentPath, "dir"> {
|
|
405
|
+
const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : undefined;
|
|
406
|
+
const version = typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : undefined;
|
|
407
|
+
return {
|
|
408
|
+
packageRoot,
|
|
409
|
+
...(name ? { packageName: name } : {}),
|
|
410
|
+
...(version ? { packageVersion: version } : {}),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
399
414
|
function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
|
|
400
415
|
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
401
416
|
const pkg = readJsonFileBestEffort(packageJsonPath);
|
|
402
417
|
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
|
|
418
|
+
const pkgRecord = pkg as Record<string, unknown>;
|
|
419
|
+
const metadata = packageMetadata(pkgRecord, packageRoot);
|
|
403
420
|
|
|
404
421
|
const roots: Record<string, unknown>[] = [];
|
|
405
|
-
const piSubagents =
|
|
422
|
+
const piSubagents = pkgRecord["pi-subagents"];
|
|
406
423
|
if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) {
|
|
407
424
|
roots.push(piSubagents as Record<string, unknown>);
|
|
408
425
|
}
|
|
409
426
|
|
|
410
|
-
const pi =
|
|
427
|
+
const pi = pkgRecord.pi;
|
|
411
428
|
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
412
429
|
const subagents = (pi as { subagents?: unknown }).subagents;
|
|
413
430
|
if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) {
|
|
@@ -415,10 +432,10 @@ function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubage
|
|
|
415
432
|
}
|
|
416
433
|
}
|
|
417
434
|
|
|
418
|
-
const agents:
|
|
435
|
+
const agents: PackageSubagentPath[] = [];
|
|
419
436
|
const chains: string[] = [];
|
|
420
437
|
for (const root of roots) {
|
|
421
|
-
for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry));
|
|
438
|
+
for (const entry of stringArray(root.agents)) agents.push({ dir: path.resolve(packageRoot, entry), ...metadata });
|
|
422
439
|
for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
|
|
423
440
|
}
|
|
424
441
|
return { agents, chains };
|
|
@@ -510,17 +527,17 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
|
|
|
510
527
|
const seenRoots = new Set<string>();
|
|
511
528
|
const seenAgents = new Set<string>();
|
|
512
529
|
const seenChains = new Set<string>();
|
|
513
|
-
const agents:
|
|
530
|
+
const agents: PackageSubagentPath[] = [];
|
|
514
531
|
const chains: string[] = [];
|
|
515
532
|
for (const packageRoot of packageRoots) {
|
|
516
533
|
const resolvedRoot = path.resolve(packageRoot);
|
|
517
534
|
if (seenRoots.has(resolvedRoot)) continue;
|
|
518
535
|
seenRoots.add(resolvedRoot);
|
|
519
536
|
const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
|
|
520
|
-
for (const
|
|
521
|
-
if (seenAgents.has(
|
|
522
|
-
seenAgents.add(
|
|
523
|
-
agents.push(
|
|
537
|
+
for (const agentPath of paths.agents) {
|
|
538
|
+
if (seenAgents.has(agentPath.dir)) continue;
|
|
539
|
+
seenAgents.add(agentPath.dir);
|
|
540
|
+
agents.push(agentPath);
|
|
524
541
|
}
|
|
525
542
|
for (const chainDir of paths.chains) {
|
|
526
543
|
if (seenChains.has(chainDir)) continue;
|
|
@@ -540,7 +557,7 @@ function normalizeAgentAliases(rawAliases: string[] | undefined, agentName: stri
|
|
|
540
557
|
function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; error?: string } {
|
|
541
558
|
const distinctNames = [...new Set(matches.map((agent) => agent.name))];
|
|
542
559
|
if (distinctNames.length === 1) {
|
|
543
|
-
const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3]]);
|
|
560
|
+
const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3], ["runtime", 4]]);
|
|
544
561
|
const agent = [...matches].sort((a, b) => (sourceRank.get(b.source) ?? 0) - (sourceRank.get(a.source) ?? 0))[0];
|
|
545
562
|
return agent ? { agent } : {};
|
|
546
563
|
}
|
|
@@ -604,6 +621,7 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
|
|
|
604
621
|
function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
605
622
|
return {
|
|
606
623
|
description: agent.description,
|
|
624
|
+
...(agent.outputMode !== undefined ? { outputMode: agent.outputMode } : {}),
|
|
607
625
|
...(agent.model !== undefined ? { model: agent.model } : {}),
|
|
608
626
|
...(agent.fallbackModels ? { fallbackModels: [...agent.fallbackModels] } : {}),
|
|
609
627
|
...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
|
|
@@ -628,6 +646,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
628
646
|
function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
|
|
629
647
|
return {
|
|
630
648
|
...(override.description !== undefined ? { description: override.description } : {}),
|
|
649
|
+
...(override.outputMode !== undefined ? { outputMode: override.outputMode } : {}),
|
|
631
650
|
...(override.model !== undefined ? { model: override.model } : {}),
|
|
632
651
|
...(override.fallbackModels !== undefined
|
|
633
652
|
? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
|
|
@@ -808,6 +827,14 @@ function parseBuiltinOverrideEntry(
|
|
|
808
827
|
}
|
|
809
828
|
}
|
|
810
829
|
|
|
830
|
+
if ("outputMode" in input) {
|
|
831
|
+
if (input.outputMode === "inline" || input.outputMode === "file-only") {
|
|
832
|
+
override.outputMode = input.outputMode;
|
|
833
|
+
} else {
|
|
834
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'outputMode'; expected 'inline' or 'file-only'.`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
811
838
|
if ("model" in input) {
|
|
812
839
|
if (typeof input.model === "string" || input.model === false) override.model = input.model;
|
|
813
840
|
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
|
|
@@ -1076,6 +1103,7 @@ function applyBuiltinOverride(
|
|
|
1076
1103
|
};
|
|
1077
1104
|
|
|
1078
1105
|
if (override.description !== undefined) next.description = override.description;
|
|
1106
|
+
if (override.outputMode !== undefined) next.outputMode = override.outputMode;
|
|
1079
1107
|
if (override.model !== undefined) {
|
|
1080
1108
|
if (override.model === false) delete next.model; else next.model = override.model;
|
|
1081
1109
|
delete next.modelSource;
|
|
@@ -1193,6 +1221,9 @@ function applyCustomAgentOverride(
|
|
|
1193
1221
|
mutable().description = override.description;
|
|
1194
1222
|
anyFilled = true;
|
|
1195
1223
|
}
|
|
1224
|
+
if (override.outputMode !== undefined) {
|
|
1225
|
+
fill("outputMode", ["outputMode"], override.outputMode);
|
|
1226
|
+
}
|
|
1196
1227
|
if (override.model !== undefined && !agentHasFrontmatterField(agent, "model")) {
|
|
1197
1228
|
const target = mutable();
|
|
1198
1229
|
if (override.model === false) delete target.model; else target.model = override.model;
|
|
@@ -1224,7 +1255,18 @@ function applyCustomAgentOverride(
|
|
|
1224
1255
|
if (override.acceptanceRole !== undefined) {
|
|
1225
1256
|
fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole);
|
|
1226
1257
|
}
|
|
1227
|
-
if (override.disabled !== undefined
|
|
1258
|
+
if (override.disabled !== undefined) {
|
|
1259
|
+
// Unconditional, matching applyBuiltinOverride: custom agents have no
|
|
1260
|
+
// frontmatter concept of `disabled` to protect (unlike model/thinking/
|
|
1261
|
+
// etc., which use `fill()` + agentHasFrontmatterField to defer to the
|
|
1262
|
+
// agent's own file). A guard here previously read `agent.disabled ===
|
|
1263
|
+
// undefined`, which was a no-op before user+project layering existed
|
|
1264
|
+
// (this function only ever ran once, against the pristine base agent,
|
|
1265
|
+
// whose `.disabled` is always undefined for custom agents) but became a
|
|
1266
|
+
// real bug once a user-scope override could run first: the guard then
|
|
1267
|
+
// silently blocked a later project-scope override from ever changing
|
|
1268
|
+
// `disabled`, breaking this PR's own "project wins" precedence for this
|
|
1269
|
+
// one field.
|
|
1228
1270
|
mutable().disabled = override.disabled;
|
|
1229
1271
|
anyFilled = true;
|
|
1230
1272
|
}
|
|
@@ -1266,24 +1308,31 @@ function applyCustomAgentOverrides(
|
|
|
1266
1308
|
userSettingsPath: string,
|
|
1267
1309
|
projectSettingsPath: string | null,
|
|
1268
1310
|
): AgentConfig[] {
|
|
1311
|
+
// Both scopes are applied, user first then project, so a project override that
|
|
1312
|
+
// only sets a subset of fields (e.g. just `extensions`) layers on top of the
|
|
1313
|
+
// user's override instead of silently discarding it (#1341 / issue writeup:
|
|
1314
|
+
// "agentOverrides project override drops user-only fields for custom agents").
|
|
1315
|
+
// Per-field precedence still favors project over user: applyCustomAgentOverride
|
|
1316
|
+
// only fills fields present in the override it's given, and project is applied
|
|
1317
|
+
// last, so any field set at both scopes ends up with the project's value.
|
|
1269
1318
|
return agents.map((agent) => {
|
|
1319
|
+
const userOverride = userSettings.overrides[agent.name];
|
|
1320
|
+
const withUserOverride = userOverride
|
|
1321
|
+
? applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath })
|
|
1322
|
+
: agent;
|
|
1323
|
+
|
|
1270
1324
|
const projectOverride = projectSettings.overrides[agent.name];
|
|
1271
1325
|
if (projectOverride && projectSettingsPath) {
|
|
1272
|
-
return applyCustomAgentOverride(
|
|
1326
|
+
return applyCustomAgentOverride(withUserOverride, projectOverride, { scope: "project", path: projectSettingsPath });
|
|
1273
1327
|
}
|
|
1274
1328
|
|
|
1275
|
-
|
|
1276
|
-
if (userOverride) {
|
|
1277
|
-
return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
return agent;
|
|
1329
|
+
return withUserOverride;
|
|
1281
1330
|
});
|
|
1282
1331
|
}
|
|
1283
1332
|
|
|
1284
1333
|
export function buildBuiltinOverrideConfig(
|
|
1285
1334
|
base: BuiltinAgentOverrideBase,
|
|
1286
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description">>,
|
|
1335
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "outputMode">>,
|
|
1287
1336
|
): BuiltinAgentOverrideConfig | undefined {
|
|
1288
1337
|
const override: BuiltinAgentOverrideConfig = {};
|
|
1289
1338
|
|
|
@@ -1291,6 +1340,7 @@ export function buildBuiltinOverrideConfig(
|
|
|
1291
1340
|
const description = draft.description.trim();
|
|
1292
1341
|
if (description && description !== base.description) override.description = description;
|
|
1293
1342
|
}
|
|
1343
|
+
if (draft.outputMode !== undefined && draft.outputMode !== base.outputMode) override.outputMode = draft.outputMode;
|
|
1294
1344
|
if (draft.model !== base.model) override.model = draft.model ?? false;
|
|
1295
1345
|
if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
|
|
1296
1346
|
if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
|
|
@@ -1605,7 +1655,7 @@ function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFi
|
|
|
1605
1655
|
});
|
|
1606
1656
|
}
|
|
1607
1657
|
|
|
1608
|
-
function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1658
|
+
function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1609
1659
|
const agents: AgentConfig[] = [];
|
|
1610
1660
|
const diagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1611
1661
|
|
|
@@ -1690,6 +1740,11 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1690
1740
|
defaultTurnBudget = resolved.turnBudget;
|
|
1691
1741
|
}
|
|
1692
1742
|
const defaultAcceptance = parseAgentAcceptanceFrontmatter(frontmatter.acceptance, localName);
|
|
1743
|
+
let outputMode: OutputMode | undefined;
|
|
1744
|
+
if (frontmatter.outputMode !== undefined) {
|
|
1745
|
+
if (frontmatter.outputMode === "inline" || frontmatter.outputMode === "file-only") outputMode = frontmatter.outputMode;
|
|
1746
|
+
else throw new Error(`Agent '${localName}' has invalid outputMode frontmatter; expected 'inline' or 'file-only'.`);
|
|
1747
|
+
}
|
|
1693
1748
|
let acceptanceRole: AcceptanceRole | undefined;
|
|
1694
1749
|
if (frontmatter.acceptanceRole !== undefined && frontmatter.acceptanceRole.trim()) {
|
|
1695
1750
|
if (frontmatter.acceptanceRole === "read-only" || frontmatter.acceptanceRole === "writer") acceptanceRole = frontmatter.acceptanceRole;
|
|
@@ -1735,6 +1790,9 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1735
1790
|
...(runner !== undefined ? { runner } : {}),
|
|
1736
1791
|
localName,
|
|
1737
1792
|
...(packageName !== undefined ? { packageName } : {}),
|
|
1793
|
+
...(packageSource?.packageName ? { packageSourceName: packageSource.packageName } : {}),
|
|
1794
|
+
...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
|
|
1795
|
+
...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
|
|
1738
1796
|
description: frontmatter.description,
|
|
1739
1797
|
...(aliases !== undefined ? { aliases } : {}),
|
|
1740
1798
|
...(rawTools !== undefined ? { tools } : {}),
|
|
@@ -1761,6 +1819,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1761
1819
|
...(extensions !== undefined ? { extensions } : {}),
|
|
1762
1820
|
...(subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions } : {}),
|
|
1763
1821
|
...(frontmatter.output !== undefined ? { output: frontmatter.output } : {}),
|
|
1822
|
+
...(outputMode !== undefined ? { outputMode } : {}),
|
|
1764
1823
|
...(defaultReads?.length ? { defaultReads } : {}),
|
|
1765
1824
|
defaultProgress: frontmatter.defaultProgress === "true",
|
|
1766
1825
|
interactive: frontmatter.interactive === "true",
|
|
@@ -1781,8 +1840,8 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1781
1840
|
return { agents, diagnostics };
|
|
1782
1841
|
}
|
|
1783
1842
|
|
|
1784
|
-
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1785
|
-
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority);
|
|
1843
|
+
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1844
|
+
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority, packageSource);
|
|
1786
1845
|
}
|
|
1787
1846
|
|
|
1788
1847
|
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
@@ -1908,7 +1967,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1908
1967
|
userSettingsPath,
|
|
1909
1968
|
projectSettingsPath,
|
|
1910
1969
|
);
|
|
1911
|
-
const packageLoaded = packageSubagentPaths.agents.map((
|
|
1970
|
+
const packageLoaded = packageSubagentPaths.agents.map((entry, index) => loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry));
|
|
1912
1971
|
const packageMap = new Map<string, AgentConfig>();
|
|
1913
1972
|
for (const loaded of packageLoaded) {
|
|
1914
1973
|
for (const agent of loaded.agents) {
|
|
@@ -1982,8 +2041,8 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1982
2041
|
);
|
|
1983
2042
|
const packageMap = new Map<string, AgentConfig>();
|
|
1984
2043
|
const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1985
|
-
for (const [index,
|
|
1986
|
-
const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
|
|
2044
|
+
for (const [index, entry] of packageSubagentPaths.agents.entries()) {
|
|
2045
|
+
const loaded = loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry);
|
|
1987
2046
|
packageAgentDiagnostics.push(...loaded.diagnostics);
|
|
1988
2047
|
for (const agent of loaded.agents) {
|
|
1989
2048
|
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|