pi-subagents 0.31.1 → 0.33.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 +67 -4
- package/README.md +219 -40
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +71 -10
- package/src/agents/agent-management.ts +179 -2
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +193 -19
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +1 -7
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +70 -41
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +54 -10
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +187 -38
- package/src/runs/background/async-job-tracker.ts +88 -2
- package/src/runs/background/async-status.ts +67 -10
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +156 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +167 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +840 -127
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +123 -27
- package/src/runs/foreground/execution.ts +174 -27
- package/src/runs/foreground/subagent-executor.ts +569 -81
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/model-fallback.ts +171 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +89 -0
- package/src/runs/shared/parallel-utils.ts +50 -1
- package/src/runs/shared/pi-args.ts +35 -4
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +110 -4
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +16 -1
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +197 -4
- package/src/shared/utils.ts +99 -14
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +133 -2
- package/src/tui/render.ts +32 -12
package/src/agents/agents.ts
CHANGED
|
@@ -7,14 +7,16 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
|
|
10
|
+
import type { AcceptanceInput, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
|
|
11
11
|
import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
12
12
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
13
13
|
import { parseChain, parseJsonChain } from "./chain-serializer.ts";
|
|
14
14
|
import { mergeAgentsForScope } from "./agent-selection.ts";
|
|
15
15
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
16
16
|
import { buildRuntimeName, parsePackageName } from "./identity.ts";
|
|
17
|
+
import { parseModelScopeConfig, type ModelScopeConfig } from "../runs/shared/model-scope.ts";
|
|
17
18
|
export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
19
|
+
import { parseMemoryFrontmatter } from "./agent-memory.ts";
|
|
18
20
|
|
|
19
21
|
export type AgentScope = "user" | "project" | "both";
|
|
20
22
|
|
|
@@ -22,6 +24,13 @@ export type AgentSource = "builtin" | "package" | "user" | "project";
|
|
|
22
24
|
type SystemPromptMode = "append" | "replace";
|
|
23
25
|
export type AgentDefaultContext = "fresh" | "fork";
|
|
24
26
|
|
|
27
|
+
export type AgentMemoryScope = "project" | "user";
|
|
28
|
+
|
|
29
|
+
export interface AgentMemoryConfig {
|
|
30
|
+
scope: AgentMemoryScope;
|
|
31
|
+
path: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
export const BUILTIN_AGENT_NAMES = [
|
|
26
35
|
"context-builder",
|
|
27
36
|
"delegate",
|
|
@@ -60,6 +69,7 @@ export interface BuiltinAgentOverrideBase {
|
|
|
60
69
|
mcpDirectTools?: string[];
|
|
61
70
|
subagentOnlyExtensions?: string[];
|
|
62
71
|
completionGuard?: boolean;
|
|
72
|
+
toolBudget?: ToolBudgetConfig;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
interface BuiltinAgentOverrideConfig {
|
|
@@ -76,6 +86,7 @@ interface BuiltinAgentOverrideConfig {
|
|
|
76
86
|
tools?: string[] | false;
|
|
77
87
|
subagentOnlyExtensions?: string[] | false;
|
|
78
88
|
completionGuard?: boolean;
|
|
89
|
+
toolBudget?: ToolBudgetConfig | false;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
interface BuiltinAgentOverrideInfo {
|
|
@@ -84,6 +95,13 @@ interface BuiltinAgentOverrideInfo {
|
|
|
84
95
|
base: BuiltinAgentOverrideBase;
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
export interface AgentModelSourceInfo {
|
|
99
|
+
type: "subagents.defaultModel";
|
|
100
|
+
scope: "user" | "project";
|
|
101
|
+
path: string;
|
|
102
|
+
model: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
export interface AgentConfig {
|
|
88
106
|
name: string;
|
|
89
107
|
localName?: string;
|
|
@@ -110,15 +128,20 @@ export interface AgentConfig {
|
|
|
110
128
|
interactive?: boolean;
|
|
111
129
|
maxSubagentDepth?: number;
|
|
112
130
|
completionGuard?: boolean;
|
|
131
|
+
toolBudget?: ToolBudgetConfig;
|
|
132
|
+
memory?: AgentMemoryConfig;
|
|
113
133
|
disabled?: boolean;
|
|
114
134
|
extraFields?: Record<string, string>;
|
|
115
135
|
override?: BuiltinAgentOverrideInfo;
|
|
136
|
+
modelSource?: AgentModelSourceInfo;
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
interface SubagentSettings {
|
|
119
140
|
overrides: Record<string, BuiltinAgentOverrideConfig>;
|
|
141
|
+
defaultModel?: string;
|
|
120
142
|
disableBuiltins?: boolean;
|
|
121
143
|
disableThinking?: boolean;
|
|
144
|
+
modelScope?: ModelScopeConfig;
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
|
|
@@ -144,6 +167,7 @@ export interface ChainStepConfig {
|
|
|
144
167
|
failFast?: boolean;
|
|
145
168
|
worktree?: boolean;
|
|
146
169
|
acceptance?: AcceptanceInput;
|
|
170
|
+
toolBudget?: ToolBudgetConfig;
|
|
147
171
|
}
|
|
148
172
|
|
|
149
173
|
export interface ChainConfig {
|
|
@@ -166,6 +190,7 @@ export interface ChainDiscoveryDiagnostic {
|
|
|
166
190
|
interface AgentDiscoveryResult {
|
|
167
191
|
agents: AgentConfig[];
|
|
168
192
|
projectAgentsDir: string | null;
|
|
193
|
+
modelScope?: ModelScopeConfig;
|
|
169
194
|
}
|
|
170
195
|
|
|
171
196
|
function getUserChainDir(): string {
|
|
@@ -476,6 +501,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
476
501
|
mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
|
|
477
502
|
subagentOnlyExtensions: agent.subagentOnlyExtensions ? [...agent.subagentOnlyExtensions] : undefined,
|
|
478
503
|
completionGuard: agent.completionGuard,
|
|
504
|
+
toolBudget: agent.toolBudget,
|
|
479
505
|
};
|
|
480
506
|
}
|
|
481
507
|
|
|
@@ -496,10 +522,11 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
496
522
|
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
497
523
|
...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
|
|
498
524
|
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
525
|
+
...(override.toolBudget !== undefined ? { toolBudget: override.toolBudget === false ? false : { ...override.toolBudget, ...(Array.isArray(override.toolBudget.block) ? { block: [...override.toolBudget.block] } : {}) } } : {}),
|
|
499
526
|
};
|
|
500
527
|
}
|
|
501
528
|
|
|
502
|
-
function findNearestProjectRoot(cwd: string): string | null {
|
|
529
|
+
export function findNearestProjectRoot(cwd: string): string | null {
|
|
503
530
|
let currentDir = cwd;
|
|
504
531
|
while (true) {
|
|
505
532
|
if (isDirectory(getProjectConfigDir(currentDir)) || isDirectory(path.join(currentDir, ".agents"))) {
|
|
@@ -640,6 +667,16 @@ function parseBuiltinOverrideEntry(
|
|
|
640
667
|
}
|
|
641
668
|
}
|
|
642
669
|
|
|
670
|
+
if ("toolBudget" in input) {
|
|
671
|
+
if (input.toolBudget === false) {
|
|
672
|
+
override.toolBudget = false;
|
|
673
|
+
} else if (input.toolBudget && typeof input.toolBudget === "object" && !Array.isArray(input.toolBudget)) {
|
|
674
|
+
override.toolBudget = input.toolBudget as ToolBudgetConfig;
|
|
675
|
+
} else {
|
|
676
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolBudget'; expected an object or false.`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
643
680
|
if ("systemPrompt" in input) {
|
|
644
681
|
if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
|
|
645
682
|
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
|
|
@@ -683,17 +720,51 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
|
|
|
683
720
|
throw new Error(`Subagent settings in '${filePath}' have invalid 'disableThinking'; expected a boolean.`);
|
|
684
721
|
}
|
|
685
722
|
}
|
|
723
|
+
let defaultModel: string | undefined;
|
|
724
|
+
if ("defaultModel" in subagentsObject) {
|
|
725
|
+
if (typeof subagentsObject.defaultModel === "string" && subagentsObject.defaultModel.trim()) {
|
|
726
|
+
defaultModel = subagentsObject.defaultModel.trim();
|
|
727
|
+
} else {
|
|
728
|
+
throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultModel'; expected a non-empty string.`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const modelScope = parseModelScopeConfig(subagentsObject.modelScope, { filePath });
|
|
686
732
|
|
|
687
733
|
const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
|
|
688
734
|
const agentOverrides = subagentsObject.agentOverrides;
|
|
689
735
|
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
|
|
690
|
-
return { overrides: parsed, disableBuiltins, disableThinking };
|
|
736
|
+
return { overrides: parsed, defaultModel, disableBuiltins, disableThinking, modelScope };
|
|
691
737
|
}
|
|
692
738
|
for (const [name, value] of Object.entries(agentOverrides)) {
|
|
693
739
|
const override = parseBuiltinOverrideEntry(name, value, filePath);
|
|
694
740
|
if (override) parsed[name] = override;
|
|
695
741
|
}
|
|
696
|
-
return { overrides: parsed, disableBuiltins, disableThinking };
|
|
742
|
+
return { overrides: parsed, defaultModel, disableBuiltins, disableThinking, modelScope };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function resolveSubagentDefaultModel(
|
|
746
|
+
userSettings: SubagentSettings,
|
|
747
|
+
projectSettings: SubagentSettings,
|
|
748
|
+
userSettingsPath: string,
|
|
749
|
+
projectSettingsPath: string | null,
|
|
750
|
+
): AgentModelSourceInfo | undefined {
|
|
751
|
+
if (projectSettingsPath && projectSettings.defaultModel !== undefined) {
|
|
752
|
+
return { type: "subagents.defaultModel", scope: "project", path: projectSettingsPath, model: projectSettings.defaultModel };
|
|
753
|
+
}
|
|
754
|
+
return userSettings.defaultModel !== undefined
|
|
755
|
+
? { type: "subagents.defaultModel", scope: "user", path: userSettingsPath, model: userSettings.defaultModel }
|
|
756
|
+
: undefined;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function applySubagentDefaultModel(agents: AgentConfig[], defaultModel: AgentModelSourceInfo | undefined): AgentConfig[] {
|
|
760
|
+
if (!defaultModel) return agents;
|
|
761
|
+
return agents.map((agent) => {
|
|
762
|
+
if (agent.model !== undefined) return agent;
|
|
763
|
+
const next = { ...agent, model: defaultModel.model, modelSource: defaultModel };
|
|
764
|
+
const frontmatterFields = agentFrontmatterFields.get(agent);
|
|
765
|
+
if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields);
|
|
766
|
+
return next;
|
|
767
|
+
});
|
|
697
768
|
}
|
|
698
769
|
|
|
699
770
|
function applyBuiltinOverride(
|
|
@@ -727,6 +798,7 @@ function applyBuiltinOverride(
|
|
|
727
798
|
next.subagentOnlyExtensions = override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions];
|
|
728
799
|
}
|
|
729
800
|
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
801
|
+
if (override.toolBudget !== undefined) next.toolBudget = override.toolBudget === false ? undefined : override.toolBudget;
|
|
730
802
|
|
|
731
803
|
return next;
|
|
732
804
|
}
|
|
@@ -872,6 +944,9 @@ function applyCustomAgentOverride(
|
|
|
872
944
|
if (override.completionGuard !== undefined) {
|
|
873
945
|
fill("completionGuard", ["completionGuard"], override.completionGuard);
|
|
874
946
|
}
|
|
947
|
+
if (override.toolBudget !== undefined) {
|
|
948
|
+
fill("toolBudget", ["toolBudget"], override.toolBudget === false ? undefined : override.toolBudget);
|
|
949
|
+
}
|
|
875
950
|
|
|
876
951
|
if (!anyFilled || !next) return agent;
|
|
877
952
|
next.override = { ...meta, base: cloneOverrideBase(agent) };
|
|
@@ -902,7 +977,7 @@ function applyCustomAgentOverrides(
|
|
|
902
977
|
|
|
903
978
|
export function buildBuiltinOverrideConfig(
|
|
904
979
|
base: BuiltinAgentOverrideBase,
|
|
905
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "subagentOnlyExtensions" | "completionGuard">,
|
|
980
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget">,
|
|
906
981
|
): BuiltinAgentOverrideConfig | undefined {
|
|
907
982
|
const override: BuiltinAgentOverrideConfig = {};
|
|
908
983
|
|
|
@@ -926,6 +1001,7 @@ export function buildBuiltinOverrideConfig(
|
|
|
926
1001
|
if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
|
|
927
1002
|
override.completionGuard = draft.completionGuard !== false;
|
|
928
1003
|
}
|
|
1004
|
+
if (JSON.stringify(draft.toolBudget) !== JSON.stringify(base.toolBudget)) override.toolBudget = draft.toolBudget ?? false;
|
|
929
1005
|
|
|
930
1006
|
return Object.keys(override).length > 0 ? override : undefined;
|
|
931
1007
|
}
|
|
@@ -954,19 +1030,20 @@ export function saveBuiltinAgentOverride(
|
|
|
954
1030
|
return filePath;
|
|
955
1031
|
}
|
|
956
1032
|
|
|
957
|
-
export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project"): string {
|
|
1033
|
+
export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project"): { path: string; removed: boolean } {
|
|
958
1034
|
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
959
1035
|
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
960
|
-
if (!fs.existsSync(filePath)) return filePath;
|
|
1036
|
+
if (!fs.existsSync(filePath)) return { path: filePath, removed: false };
|
|
961
1037
|
|
|
962
1038
|
const settings = readSettingsFileStrict(filePath);
|
|
963
1039
|
const subagents = settings.subagents;
|
|
964
|
-
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return filePath;
|
|
1040
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return { path: filePath, removed: false };
|
|
965
1041
|
const nextSubagents = { ...(subagents as Record<string, unknown>) };
|
|
966
1042
|
const agentOverrides = nextSubagents.agentOverrides;
|
|
967
|
-
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return filePath;
|
|
1043
|
+
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return { path: filePath, removed: false };
|
|
968
1044
|
|
|
969
1045
|
const nextOverrides = { ...(agentOverrides as Record<string, unknown>) };
|
|
1046
|
+
if (!Object.prototype.hasOwnProperty.call(nextOverrides, name)) return { path: filePath, removed: false };
|
|
970
1047
|
delete nextOverrides[name];
|
|
971
1048
|
if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides;
|
|
972
1049
|
else delete nextSubagents.agentOverrides;
|
|
@@ -974,10 +1051,82 @@ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "us
|
|
|
974
1051
|
if (Object.keys(nextSubagents).length > 0) settings.subagents = nextSubagents;
|
|
975
1052
|
else delete settings.subagents;
|
|
976
1053
|
|
|
1054
|
+
writeSettingsFile(filePath, settings);
|
|
1055
|
+
return { path: filePath, removed: true };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export function mergeBuiltinAgentOverride(
|
|
1059
|
+
cwd: string,
|
|
1060
|
+
name: string,
|
|
1061
|
+
scope: "user" | "project",
|
|
1062
|
+
fields: BuiltinAgentOverrideConfig,
|
|
1063
|
+
): string {
|
|
1064
|
+
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
1065
|
+
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
1066
|
+
|
|
1067
|
+
const settings = readSettingsFileStrict(filePath);
|
|
1068
|
+
const subagents = settings.subagents && typeof settings.subagents === "object" && !Array.isArray(settings.subagents)
|
|
1069
|
+
? { ...(settings.subagents as Record<string, unknown>) }
|
|
1070
|
+
: {};
|
|
1071
|
+
const agentOverrides = subagents.agentOverrides && typeof subagents.agentOverrides === "object" && !Array.isArray(subagents.agentOverrides)
|
|
1072
|
+
? { ...(subagents.agentOverrides as Record<string, unknown>) }
|
|
1073
|
+
: {};
|
|
1074
|
+
|
|
1075
|
+
const existing = agentOverrides[name];
|
|
1076
|
+
const base = existing && typeof existing === "object" && !Array.isArray(existing)
|
|
1077
|
+
? existing as Record<string, unknown>
|
|
1078
|
+
: {};
|
|
1079
|
+
agentOverrides[name] = { ...base, ...cloneOverrideValue(fields) };
|
|
1080
|
+
subagents.agentOverrides = agentOverrides;
|
|
1081
|
+
settings.subagents = subagents;
|
|
977
1082
|
writeSettingsFile(filePath, settings);
|
|
978
1083
|
return filePath;
|
|
979
1084
|
}
|
|
980
1085
|
|
|
1086
|
+
export function removeBuiltinAgentOverrideFields(
|
|
1087
|
+
cwd: string,
|
|
1088
|
+
name: string,
|
|
1089
|
+
scope: "user" | "project",
|
|
1090
|
+
fields: string[],
|
|
1091
|
+
): { path: string; removed: boolean } {
|
|
1092
|
+
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
1093
|
+
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
1094
|
+
if (!fs.existsSync(filePath)) return { path: filePath, removed: false };
|
|
1095
|
+
|
|
1096
|
+
const settings = readSettingsFileStrict(filePath);
|
|
1097
|
+
const subagents = settings.subagents;
|
|
1098
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return { path: filePath, removed: false };
|
|
1099
|
+
const agentOverrides = (subagents as Record<string, unknown>).agentOverrides;
|
|
1100
|
+
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return { path: filePath, removed: false };
|
|
1101
|
+
|
|
1102
|
+
const entry = (agentOverrides as Record<string, unknown>)[name];
|
|
1103
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { path: filePath, removed: false };
|
|
1104
|
+
|
|
1105
|
+
const nextEntry: Record<string, unknown> = { ...(entry as Record<string, unknown>) };
|
|
1106
|
+
let removed = false;
|
|
1107
|
+
for (const field of fields) {
|
|
1108
|
+
if (Object.prototype.hasOwnProperty.call(nextEntry, field)) {
|
|
1109
|
+
delete nextEntry[field];
|
|
1110
|
+
removed = true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (!removed) return { path: filePath, removed: false };
|
|
1114
|
+
|
|
1115
|
+
const nextSubagents = { ...(subagents as Record<string, unknown>) };
|
|
1116
|
+
if (Object.keys(nextEntry).length > 0) {
|
|
1117
|
+
(nextSubagents.agentOverrides as Record<string, unknown>)[name] = nextEntry;
|
|
1118
|
+
} else {
|
|
1119
|
+
const nextOverrides = { ...(agentOverrides as Record<string, unknown>) };
|
|
1120
|
+
delete nextOverrides[name];
|
|
1121
|
+
if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides;
|
|
1122
|
+
else delete nextSubagents.agentOverrides;
|
|
1123
|
+
}
|
|
1124
|
+
if (Object.keys(nextSubagents).length > 0) settings.subagents = nextSubagents;
|
|
1125
|
+
else delete settings.subagents;
|
|
1126
|
+
writeSettingsFile(filePath, settings);
|
|
1127
|
+
return { path: filePath, removed: true };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
981
1130
|
function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
|
|
982
1131
|
const files: string[] = [];
|
|
983
1132
|
if (!fs.existsSync(dir)) return files;
|
|
@@ -1111,6 +1260,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1111
1260
|
}
|
|
1112
1261
|
|
|
1113
1262
|
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
1263
|
+
let toolBudget: ToolBudgetConfig | undefined;
|
|
1264
|
+
if (frontmatter.toolBudget !== undefined && frontmatter.toolBudget.trim()) {
|
|
1265
|
+
const parsed = JSON.parse(frontmatter.toolBudget) as unknown;
|
|
1266
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1267
|
+
throw new Error(`Agent '${localName}' has invalid toolBudget frontmatter; expected a JSON object.`);
|
|
1268
|
+
}
|
|
1269
|
+
toolBudget = parsed as ToolBudgetConfig;
|
|
1270
|
+
}
|
|
1114
1271
|
const completionGuard = frontmatter.completionGuard === "false"
|
|
1115
1272
|
? false
|
|
1116
1273
|
: frontmatter.completionGuard === "true"
|
|
@@ -1146,6 +1303,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1146
1303
|
? parsedMaxSubagentDepth
|
|
1147
1304
|
: undefined,
|
|
1148
1305
|
completionGuard,
|
|
1306
|
+
toolBudget,
|
|
1307
|
+
memory: parseMemoryFrontmatter(frontmatter.memory),
|
|
1149
1308
|
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
1150
1309
|
};
|
|
1151
1310
|
agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
|
|
@@ -1241,13 +1400,15 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1241
1400
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
1242
1401
|
const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
|
|
1243
1402
|
const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
|
|
1403
|
+
const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath);
|
|
1404
|
+
const modelScope = projectSettings.modelScope ?? userSettings.modelScope;
|
|
1244
1405
|
const packageSubagentPaths = collectPackageSubagentPaths(cwd, {
|
|
1245
1406
|
includeUser: scope !== "project",
|
|
1246
1407
|
includeProject: scope !== "user",
|
|
1247
1408
|
});
|
|
1248
1409
|
|
|
1249
1410
|
const builtinAgents = applyBuiltinOverrides(
|
|
1250
|
-
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
1411
|
+
applySubagentDefaultModel(loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"), defaultModel),
|
|
1251
1412
|
userSettings,
|
|
1252
1413
|
projectSettings,
|
|
1253
1414
|
userSettingsPath,
|
|
@@ -1258,7 +1419,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1258
1419
|
const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
|
|
1259
1420
|
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
1260
1421
|
const userAgents = applyCustomAgentOverrides(
|
|
1261
|
-
[...userAgentsExtra, ...userAgentsOld, ...userAgentsNew],
|
|
1422
|
+
applySubagentDefaultModel([...userAgentsExtra, ...userAgentsOld, ...userAgentsNew], defaultModel),
|
|
1262
1423
|
userSettings,
|
|
1263
1424
|
projectSettings,
|
|
1264
1425
|
userSettingsPath,
|
|
@@ -1266,17 +1427,23 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1266
1427
|
);
|
|
1267
1428
|
|
|
1268
1429
|
const projectAgents = applyCustomAgentOverrides(
|
|
1269
|
-
scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project")),
|
|
1430
|
+
applySubagentDefaultModel(scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project")), defaultModel),
|
|
1431
|
+
userSettings,
|
|
1432
|
+
projectSettings,
|
|
1433
|
+
userSettingsPath,
|
|
1434
|
+
projectSettingsPath,
|
|
1435
|
+
);
|
|
1436
|
+
const packageAgents = applyCustomAgentOverrides(
|
|
1437
|
+
applySubagentDefaultModel(packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package")), defaultModel),
|
|
1270
1438
|
userSettings,
|
|
1271
1439
|
projectSettings,
|
|
1272
1440
|
userSettingsPath,
|
|
1273
1441
|
projectSettingsPath,
|
|
1274
1442
|
);
|
|
1275
|
-
const packageAgents = packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package"));
|
|
1276
1443
|
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
|
|
1277
1444
|
.filter((agent) => agent.disabled !== true);
|
|
1278
1445
|
|
|
1279
|
-
return { agents, projectAgentsDir };
|
|
1446
|
+
return { agents, projectAgentsDir, modelScope };
|
|
1280
1447
|
}
|
|
1281
1448
|
|
|
1282
1449
|
export function discoverAgentsAll(cwd: string): {
|
|
@@ -1302,21 +1469,22 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1302
1469
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
1303
1470
|
const userSettings = readSubagentSettings(userSettingsPath);
|
|
1304
1471
|
const projectSettings = readSubagentSettings(projectSettingsPath);
|
|
1472
|
+
const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath);
|
|
1305
1473
|
const packageSubagentPaths = collectPackageSubagentPaths(cwd);
|
|
1306
1474
|
|
|
1307
1475
|
const builtin = applyBuiltinOverrides(
|
|
1308
|
-
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
1476
|
+
applySubagentDefaultModel(loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"), defaultModel),
|
|
1309
1477
|
userSettings,
|
|
1310
1478
|
projectSettings,
|
|
1311
1479
|
userSettingsPath,
|
|
1312
1480
|
projectSettingsPath,
|
|
1313
1481
|
);
|
|
1314
1482
|
const user = applyCustomAgentOverrides(
|
|
1315
|
-
[
|
|
1483
|
+
applySubagentDefaultModel([
|
|
1316
1484
|
...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
1317
1485
|
...loadAgentsFromDir(userDirOld, "user"),
|
|
1318
1486
|
...loadAgentsFromDir(userDirNew, "user"),
|
|
1319
|
-
],
|
|
1487
|
+
], defaultModel),
|
|
1320
1488
|
userSettings,
|
|
1321
1489
|
projectSettings,
|
|
1322
1490
|
userSettingsPath,
|
|
@@ -1328,7 +1496,13 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1328
1496
|
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1329
1497
|
}
|
|
1330
1498
|
}
|
|
1331
|
-
const packageAgents =
|
|
1499
|
+
const packageAgents = applyCustomAgentOverrides(
|
|
1500
|
+
applySubagentDefaultModel(Array.from(packageMap.values()), defaultModel),
|
|
1501
|
+
userSettings,
|
|
1502
|
+
projectSettings,
|
|
1503
|
+
userSettingsPath,
|
|
1504
|
+
projectSettingsPath,
|
|
1505
|
+
);
|
|
1332
1506
|
const projectMap = new Map<string, AgentConfig>();
|
|
1333
1507
|
for (const dir of projectDirs) {
|
|
1334
1508
|
for (const agent of loadAgentsFromDir(dir, "project")) {
|
|
@@ -1336,7 +1510,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1336
1510
|
}
|
|
1337
1511
|
}
|
|
1338
1512
|
const project = applyCustomAgentOverrides(
|
|
1339
|
-
Array.from(projectMap.values()),
|
|
1513
|
+
applySubagentDefaultModel(Array.from(projectMap.values()), defaultModel),
|
|
1340
1514
|
userSettings,
|
|
1341
1515
|
projectSettings,
|
|
1342
1516
|
userSettingsPath,
|
|
@@ -3,6 +3,7 @@ import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./
|
|
|
3
3
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
4
4
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
|
+
import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
|
|
6
7
|
import type { ChainStep } from "../shared/settings.ts";
|
|
7
8
|
import type { AgentSource } from "./agents.ts";
|
|
8
9
|
|
|
@@ -78,6 +79,19 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
78
79
|
if (key === "progress") {
|
|
79
80
|
if (rawValue === "true") step.progress = true;
|
|
80
81
|
else if (rawValue === "false") step.progress = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (key === "toolbudget") {
|
|
85
|
+
let parsed: unknown;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(rawValue);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
throw new Error(`Invalid toolBudget in .chain.md step '${agent}': ${message}`);
|
|
91
|
+
}
|
|
92
|
+
const validation = validateToolBudgetConfig(parsed, `toolBudget for step '${agent}'`);
|
|
93
|
+
if (validation.error) throw new Error(validation.error);
|
|
94
|
+
step.toolBudget = parsed as ChainStepConfig["toolBudget"];
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
97
|
|
|
@@ -125,6 +139,11 @@ export function parseChain(content: string, source: AgentSource, filePath: strin
|
|
|
125
139
|
};
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
function validateJsonChainToolBudget(value: unknown, label: string): void {
|
|
143
|
+
const validation = validateToolBudgetConfig(value, label);
|
|
144
|
+
if (validation.error) throw new Error(validation.error);
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
export function parseJsonChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
129
148
|
let parsed: unknown;
|
|
130
149
|
try {
|
|
@@ -152,6 +171,7 @@ export function parseJsonChain(content: string, source: AgentSource, filePath: s
|
|
|
152
171
|
throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
|
|
153
172
|
}
|
|
154
173
|
const stepRecord = step as Record<string, unknown>;
|
|
174
|
+
if (stepRecord.toolBudget !== undefined) validateJsonChainToolBudget(stepRecord.toolBudget, `step ${i + 1} toolBudget`);
|
|
155
175
|
const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
|
|
156
176
|
if (acceptanceErrors.length > 0) {
|
|
157
177
|
throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
|
|
@@ -161,13 +181,17 @@ export function parseJsonChain(content: string, source: AgentSource, filePath: s
|
|
|
161
181
|
for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
|
|
162
182
|
const task = parallel[taskIndex];
|
|
163
183
|
if (!task || typeof task !== "object" || Array.isArray(task)) continue;
|
|
164
|
-
const
|
|
184
|
+
const taskRecord = task as Record<string, unknown>;
|
|
185
|
+
if (taskRecord.toolBudget !== undefined) validateJsonChainToolBudget(taskRecord.toolBudget, `step ${i + 1} parallel task ${taskIndex + 1} toolBudget`);
|
|
186
|
+
const taskErrors = validateAcceptanceInput(taskRecord.acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
|
|
165
187
|
if (taskErrors.length > 0) {
|
|
166
188
|
throw new Error(`Invalid JSON chain '${filePath}': ${taskErrors.join(" ")}`);
|
|
167
189
|
}
|
|
168
190
|
}
|
|
169
191
|
} else if (parallel && typeof parallel === "object") {
|
|
170
|
-
const
|
|
192
|
+
const parallelRecord = parallel as Record<string, unknown>;
|
|
193
|
+
if (parallelRecord.toolBudget !== undefined) validateJsonChainToolBudget(parallelRecord.toolBudget, `step ${i + 1} dynamic template toolBudget`);
|
|
194
|
+
const templateErrors = validateAcceptanceInput(parallelRecord.acceptance, `step ${i + 1} dynamic template acceptance`);
|
|
171
195
|
if (templateErrors.length > 0) {
|
|
172
196
|
throw new Error(`Invalid JSON chain '${filePath}': ${templateErrors.join(" ")}`);
|
|
173
197
|
}
|
|
@@ -243,6 +267,7 @@ export function serializeChain(config: ChainConfig): string {
|
|
|
243
267
|
if (step.skills === false) lines.push("skills: false");
|
|
244
268
|
else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(`skills: ${step.skills.join(", ")}`);
|
|
245
269
|
if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
|
|
270
|
+
if (step.toolBudget !== undefined) lines.push(`toolBudget: ${JSON.stringify(step.toolBudget)}`);
|
|
246
271
|
lines.push("");
|
|
247
272
|
lines.push(step.task ?? "");
|
|
248
273
|
if (i < config.steps.length - 1) lines.push("");
|
package/src/extension/config.ts
CHANGED
|
@@ -3,12 +3,35 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ExtensionConfig } from "../shared/types.ts";
|
|
4
4
|
import { getAgentDir } from "../shared/utils.ts";
|
|
5
5
|
|
|
6
|
+
export function getConfigPath(): string {
|
|
7
|
+
return path.join(getAgentDir(), "extensions", "subagent", "config.json");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readConfigForUpdate(configPath = getConfigPath()): ExtensionConfig {
|
|
11
|
+
if (!fs.existsSync(configPath)) return {};
|
|
12
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
|
|
13
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14
|
+
throw new Error(`Subagent config at '${configPath}' must be a JSON object`);
|
|
15
|
+
}
|
|
16
|
+
return parsed as ExtensionConfig;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function saveConfig(config: ExtensionConfig, configPath = getConfigPath()): void {
|
|
20
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
21
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConfig): ExtensionConfig {
|
|
25
|
+
const configPath = getConfigPath();
|
|
26
|
+
const next = updater(readConfigForUpdate(configPath));
|
|
27
|
+
saveConfig(next, configPath);
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
export function loadConfig(): ExtensionConfig {
|
|
7
|
-
const configPath =
|
|
32
|
+
const configPath = getConfigPath();
|
|
8
33
|
try {
|
|
9
|
-
|
|
10
|
-
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
11
|
-
}
|
|
34
|
+
return readConfigForUpdate(configPath);
|
|
12
35
|
} catch (error) {
|
|
13
36
|
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
14
37
|
}
|
package/src/extension/doctor.ts
CHANGED
|
@@ -157,14 +157,8 @@ function formatIntercomDiagnostic(diagnostic: IntercomBridgeDiagnostic, context:
|
|
|
157
157
|
`- bridge: ${diagnostic.active ? "active" : "inactive"}${diagnostic.reason ? ` (${diagnostic.reason})` : ""}`,
|
|
158
158
|
`- mode: ${diagnostic.mode}; context: ${context ?? "unspecified"}`,
|
|
159
159
|
`- orchestrator target: ${diagnostic.orchestratorTarget ?? "not available"}`,
|
|
160
|
-
`-
|
|
160
|
+
`- supervisor channel: ${diagnostic.supervisorChannelAvailable ? "available" : "unavailable"} (${diagnostic.extensionDir})`,
|
|
161
161
|
];
|
|
162
|
-
if (diagnostic.configPath && diagnostic.intercomConfigEnabled !== undefined) {
|
|
163
|
-
lines.push(`- intercom config: ${diagnostic.intercomConfigEnabled === false ? "disabled" : "enabled or absent"} (${diagnostic.configPath})`);
|
|
164
|
-
}
|
|
165
|
-
if (diagnostic.intercomConfigError) {
|
|
166
|
-
lines.push(`- intercom config warning: ${diagnostic.intercomConfigError}; runtime assumes enabled`);
|
|
167
|
-
}
|
|
168
162
|
return lines;
|
|
169
163
|
}
|
|
170
164
|
|
|
@@ -31,6 +31,7 @@ function createChildSafeState(): SubagentState {
|
|
|
31
31
|
baseCwd: "",
|
|
32
32
|
currentSessionId: null,
|
|
33
33
|
subagentInProgress: false,
|
|
34
|
+
subagentSpawns: { sessionId: null, count: 0 },
|
|
34
35
|
asyncJobs: new Map(),
|
|
35
36
|
foregroundRuns: new Map(),
|
|
36
37
|
foregroundControls: new Map(),
|
|
@@ -157,8 +158,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
157
158
|
label: "Subagent",
|
|
158
159
|
description: [
|
|
159
160
|
"Delegate to subagents from child-safe fanout mode.",
|
|
160
|
-
"Allowed management/control actions: list, get, status, interrupt, resume, append-step, doctor.",
|
|
161
|
-
"Agent config mutation actions create, update,
|
|
161
|
+
"Allowed management/control actions: list, get, status, interrupt, resume, steer, append-step, doctor.",
|
|
162
|
+
"Agent config mutation actions (create, update, delete, eject, disable, enable, reset) are blocked in this mode.",
|
|
162
163
|
].join("\n"),
|
|
163
164
|
parameters: SubagentParams,
|
|
164
165
|
execute(id, params, signal, onUpdate, ctx) {
|