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.
Files changed (60) hide show
  1. package/CHANGELOG.md +67 -4
  2. package/README.md +219 -40
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +71 -10
  6. package/src/agents/agent-management.ts +179 -2
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +193 -19
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/config.ts +27 -4
  12. package/src/extension/doctor.ts +1 -7
  13. package/src/extension/fanout-child.ts +3 -2
  14. package/src/extension/index.ts +70 -41
  15. package/src/extension/rpc.ts +369 -0
  16. package/src/extension/schemas.ts +54 -10
  17. package/src/extension/tool-description.ts +200 -0
  18. package/src/intercom/intercom-bridge.ts +21 -253
  19. package/src/intercom/native-supervisor-channel.ts +510 -0
  20. package/src/runs/background/async-execution.ts +187 -38
  21. package/src/runs/background/async-job-tracker.ts +88 -2
  22. package/src/runs/background/async-status.ts +67 -10
  23. package/src/runs/background/chain-root-attachment.ts +34 -4
  24. package/src/runs/background/completion-batcher.ts +166 -0
  25. package/src/runs/background/control-channel.ts +156 -1
  26. package/src/runs/background/fleet-view.ts +515 -0
  27. package/src/runs/background/notify.ts +161 -44
  28. package/src/runs/background/result-watcher.ts +1 -2
  29. package/src/runs/background/run-id-resolver.ts +3 -2
  30. package/src/runs/background/run-status.ts +167 -6
  31. package/src/runs/background/scheduled-runs.ts +514 -0
  32. package/src/runs/background/stale-run-reconciler.ts +28 -1
  33. package/src/runs/background/subagent-runner.ts +840 -127
  34. package/src/runs/background/wait.ts +353 -0
  35. package/src/runs/foreground/chain-execution.ts +123 -27
  36. package/src/runs/foreground/execution.ts +174 -27
  37. package/src/runs/foreground/subagent-executor.ts +569 -81
  38. package/src/runs/shared/acceptance.ts +45 -22
  39. package/src/runs/shared/dynamic-fanout.ts +2 -2
  40. package/src/runs/shared/model-fallback.ts +171 -20
  41. package/src/runs/shared/model-scope.ts +128 -0
  42. package/src/runs/shared/nested-events.ts +89 -0
  43. package/src/runs/shared/parallel-utils.ts +50 -1
  44. package/src/runs/shared/pi-args.ts +35 -4
  45. package/src/runs/shared/pi-spawn.ts +52 -20
  46. package/src/runs/shared/single-output.ts +2 -0
  47. package/src/runs/shared/subagent-prompt-runtime.ts +110 -4
  48. package/src/runs/shared/tool-budget.ts +74 -0
  49. package/src/runs/shared/turn-budget.ts +52 -0
  50. package/src/runs/shared/worktree.ts +28 -5
  51. package/src/shared/artifacts.ts +16 -1
  52. package/src/shared/atomic-json.ts +15 -2
  53. package/src/shared/child-transcript.ts +212 -0
  54. package/src/shared/fork-context.ts +133 -22
  55. package/src/shared/settings.ts +3 -1
  56. package/src/shared/types.ts +197 -4
  57. package/src/shared/utils.ts +99 -14
  58. package/src/slash/prompt-workflows.ts +330 -0
  59. package/src/slash/slash-commands.ts +133 -2
  60. package/src/tui/render.ts +32 -12
@@ -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 = Array.from(packageMap.values());
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 taskErrors = validateAcceptanceInput((task as Record<string, unknown>).acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
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 templateErrors = validateAcceptanceInput((parallel as Record<string, unknown>).acceptance, `step ${i + 1} dynamic template acceptance`);
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("");
@@ -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 = path.join(getAgentDir(), "extensions", "subagent", "config.json");
32
+ const configPath = getConfigPath();
8
33
  try {
9
- if (fs.existsSync(configPath)) {
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
  }
@@ -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
- `- pi-intercom: ${diagnostic.piIntercomAvailable ? "available" : "unavailable"} at ${diagnostic.extensionDir}`,
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, and delete are blocked in this mode.",
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) {