@gotgenes/pi-subagents 6.3.1 → 6.5.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.
@@ -11,6 +11,7 @@ import type { Model } from "@earendil-works/pi-ai";
11
11
  import type { AgentSession, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { AgentRecord } from "./agent-record.js";
13
13
  import type { AgentRunner } from "./agent-runner.js";
14
+ import { AgentTypeRegistry } from "./agent-types.js";
14
15
  import { debugLog } from "./debug.js";
15
16
  import { buildParentSnapshot } from "./parent-snapshot.js";
16
17
  import { subscribeRecordObserver } from "./record-observer.js";
@@ -30,7 +31,9 @@ export interface AgentManagerOptions {
30
31
  runner: AgentRunner;
31
32
  worktrees: WorktreeManager;
32
33
  exec: ShellExec;
33
- maxConcurrent?: number;
34
+ registry: AgentTypeRegistry;
35
+ /** Injected getter for the concurrency limit — owned by SettingsManager. */
36
+ getMaxConcurrent?: () => number;
34
37
  getRunConfig?: () => RunConfig;
35
38
  onStart?: OnAgentStart;
36
39
  onComplete?: OnAgentComplete;
@@ -81,7 +84,8 @@ export class AgentManager {
81
84
  private readonly runner: AgentRunner;
82
85
  private readonly worktrees: WorktreeManager;
83
86
  private readonly exec: ShellExec;
84
- private maxConcurrent: number;
87
+ private readonly registry: AgentTypeRegistry;
88
+ private readonly _getMaxConcurrent: () => number;
85
89
  private getRunConfig?: () => RunConfig;
86
90
 
87
91
  /** Queue of background agents waiting to start. */
@@ -93,27 +97,25 @@ export class AgentManager {
93
97
  this.runner = options.runner;
94
98
  this.worktrees = options.worktrees;
95
99
  this.exec = options.exec;
100
+ this.registry = options.registry;
96
101
  this.onComplete = options.onComplete;
97
102
  this.onStart = options.onStart;
98
103
  this.onCompact = options.onCompact;
99
104
  this.getRunConfig = options.getRunConfig;
100
- this.maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
105
+ this._getMaxConcurrent = options.getMaxConcurrent ?? (() => DEFAULT_MAX_CONCURRENT);
101
106
  // Cleanup completed agents after 10 minutes (but keep sessions for resume)
102
107
  this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
103
108
  this.cleanupInterval.unref();
104
109
  }
105
110
 
106
- /** Update the max concurrent background agents limit. */
107
- setMaxConcurrent(n: number) {
108
- this.maxConcurrent = Math.max(1, n);
109
- // Start queued agents if the new limit allows
111
+ /**
112
+ * Drain the concurrency queue after SettingsManager has updated maxConcurrent.
113
+ * Call this whenever the concurrency limit increases so queued agents can start.
114
+ */
115
+ notifyConcurrencyChanged(): void {
110
116
  this.drainQueue();
111
117
  }
112
118
 
113
- getMaxConcurrent(): number {
114
- return this.maxConcurrent;
115
- }
116
-
117
119
  /**
118
120
  * Spawn an agent and return its ID immediately (for background use).
119
121
  * If the concurrency limit is reached, the agent is queued.
@@ -140,7 +142,7 @@ export class AgentManager {
140
142
  const snapshot = buildParentSnapshot(ctx, options.inheritContext);
141
143
  const args: SpawnArgs = { snapshot, type, prompt, options };
142
144
 
143
- if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
145
+ if (options.isBackground && !options.bypassQueue && this.runningBackground >= this._getMaxConcurrent()) {
144
146
  // Queue it — will be started when a running agent completes
145
147
  this.queue.push({ id, args });
146
148
  return id;
@@ -203,6 +205,7 @@ export class AgentManager {
203
205
  parentSessionFile: options.parentSessionFile,
204
206
  parentSessionId: options.parentSessionId,
205
207
  signal: record.abortController!.signal,
208
+ registry: this.registry,
206
209
  onSessionCreated: (session) => {
207
210
  record.session = session;
208
211
  // Capture the session file path early so it's available for display
@@ -279,7 +282,7 @@ export class AgentManager {
279
282
 
280
283
  /** Start queued agents up to the concurrency limit. */
281
284
  private drainQueue() {
282
- while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
285
+ while (this.queue.length > 0 && this.runningBackground < this._getMaxConcurrent()) {
283
286
  const next = this.queue.shift()!;
284
287
  const record = this.agents.get(next.id);
285
288
  if (!record || record.status !== "queued") continue;
@@ -12,6 +12,7 @@ import {
12
12
  SessionManager,
13
13
  SettingsManager,
14
14
  } from "@earendil-works/pi-coding-agent";
15
+ import type { AgentConfigLookup } from "./agent-types.js";
15
16
  import { extractText } from "./context.js";
16
17
  import { detectEnv } from "./env.js";
17
18
  import { assembleSessionConfig } from "./session-config.js";
@@ -91,6 +92,8 @@ export interface RunOptions {
91
92
  * module-scope `graceTurns` during migration.
92
93
  */
93
94
  graceTurns?: number;
95
+ /** Agent config lookup — provides resolveAgentConfig and getToolNamesForType. */
96
+ registry: AgentConfigLookup;
94
97
  }
95
98
 
96
99
  export interface RunResult {
@@ -189,6 +192,7 @@ export async function runAgent(
189
192
  thinkingLevel: options.thinkingLevel,
190
193
  },
191
194
  env,
195
+ options.registry,
192
196
  );
193
197
 
194
198
  const agentDir = getAgentDir();
@@ -8,79 +8,132 @@
8
8
  import { DEFAULT_AGENTS } from "./default-agents.js";
9
9
  import type { AgentConfig } from "./types.js";
10
10
 
11
- /** All known built-in tool names. */
12
- export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
11
+ // ── AgentConfigLookup interface ──────────────────────────────────────────────
13
12
 
14
- /** Unified runtime registry of all agents (defaults + user-defined). */
15
- const agents = new Map<string, AgentConfig>();
13
+ /**
14
+ * Narrow registry interface for consumers that only need config resolution.
15
+ * Prefer this over the full `AgentTypeRegistry` in function signatures (ISP).
16
+ */
17
+ export interface AgentConfigLookup {
18
+ resolveAgentConfig(type: string): AgentConfig;
19
+ getToolNamesForType(type: string): string[];
20
+ }
21
+
22
+ // ── AgentTypeRegistry class ──────────────────────────────────────────────────
16
23
 
17
24
  /**
18
- * Register agents into the unified registry.
19
- * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
20
- * Disabled agents (enabled === false) are kept in the registry but excluded from spawning.
25
+ * Injectable registry of all agent configurations (defaults + user-defined).
26
+ *
27
+ * Replaces the module-scoped `agents` Map and its companion free functions.
28
+ * The constructor accepts a `loadUserAgents` callback to defer disk I/O to the
29
+ * call site, keeping this class side-effect-free and easy to test.
21
30
  */
22
- export function registerAgents(userAgents: Map<string, AgentConfig>): void {
23
- agents.clear();
31
+ export class AgentTypeRegistry implements AgentConfigLookup {
32
+ private agents = new Map<string, AgentConfig>();
24
33
 
25
- // Start with defaults
26
- for (const [name, config] of DEFAULT_AGENTS) {
27
- agents.set(name, config);
34
+ /** The three embedded default agent names. */
35
+ static readonly DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
36
+
37
+ constructor(private loadUserAgents: () => Map<string, AgentConfig>) {
38
+ this.reload();
28
39
  }
29
40
 
30
- // Overlay user agents (overrides defaults with same name)
31
- for (const [name, config] of userAgents) {
32
- agents.set(name, config);
41
+ /**
42
+ * Re-scan user agents and rebuild the registry.
43
+ * Starts with DEFAULT_AGENTS, then overlays whatever `loadUserAgents()` returns.
44
+ */
45
+ reload(): void {
46
+ this.agents.clear();
47
+ for (const [name, config] of DEFAULT_AGENTS) {
48
+ this.agents.set(name, config);
49
+ }
50
+ for (const [name, config] of this.loadUserAgents()) {
51
+ this.agents.set(name, config);
52
+ }
33
53
  }
34
- }
35
54
 
36
- /** Case-insensitive key resolution. */
37
- function resolveKey(name: string): string | undefined {
38
- if (agents.has(name)) return name;
39
- const lower = name.toLowerCase();
40
- for (const key of agents.keys()) {
41
- if (key.toLowerCase() === lower) return key;
55
+ /** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
56
+ resolveType(name: string): string | undefined {
57
+ return this.resolveKey(name);
42
58
  }
43
- return undefined;
44
- }
45
59
 
46
- /** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
47
- export function resolveType(name: string): string | undefined {
48
- return resolveKey(name);
49
- }
60
+ /** Get all enabled type names (for spawning and tool descriptions). */
61
+ getAvailableTypes(): string[] {
62
+ return [...this.agents.entries()]
63
+ .filter(([_, config]) => config.enabled !== false)
64
+ .map(([name]) => name);
65
+ }
50
66
 
51
- /** Get all enabled type names (for spawning and tool descriptions). */
52
- export function getAvailableTypes(): string[] {
53
- return [...agents.entries()]
54
- .filter(([_, config]) => config.enabled !== false)
55
- .map(([name]) => name);
56
- }
67
+ /** Get all type names including disabled (for UI listing). */
68
+ getAllTypes(): string[] {
69
+ return [...this.agents.keys()];
70
+ }
57
71
 
58
- /** Get all type names including disabled (for UI listing). */
59
- export function getAllTypes(): string[] {
60
- return [...agents.keys()];
61
- }
72
+ /** Get names of default agents currently in the registry. */
73
+ getDefaultAgentNames(): string[] {
74
+ return [...this.agents.entries()]
75
+ .filter(([_, config]) => config.isDefault === true)
76
+ .map(([name]) => name);
77
+ }
62
78
 
63
- /** Get names of default agents currently in the registry. */
64
- export function getDefaultAgentNames(): string[] {
65
- return [...agents.entries()]
66
- .filter(([_, config]) => config.isDefault === true)
67
- .map(([name]) => name);
68
- }
79
+ /** Get names of user-defined agents (non-defaults) currently in the registry. */
80
+ getUserAgentNames(): string[] {
81
+ return [...this.agents.entries()]
82
+ .filter(([_, config]) => config.isDefault !== true)
83
+ .map(([name]) => name);
84
+ }
69
85
 
70
- /** Get names of user-defined agents (non-defaults) currently in the registry. */
71
- export function getUserAgentNames(): string[] {
72
- return [...agents.entries()]
73
- .filter(([_, config]) => config.isDefault !== true)
74
- .map(([name]) => name);
75
- }
86
+ /** Check if a type is valid and enabled (case-insensitive). */
87
+ isValidType(type: string): boolean {
88
+ const key = this.resolveKey(type);
89
+ if (!key) return false;
90
+ return this.agents.get(key)?.enabled !== false;
91
+ }
76
92
 
77
- /** Check if a type is valid and enabled (case-insensitive). */
78
- export function isValidType(type: string): boolean {
79
- const key = resolveKey(type);
80
- if (!key) return false;
81
- return agents.get(key)?.enabled !== false;
93
+ /** Get built-in tool names for a type (case-insensitive). */
94
+ getToolNamesForType(type: string): string[] {
95
+ const key = this.resolveKey(type);
96
+ const raw = key ? this.agents.get(key) : undefined;
97
+ const config = raw?.enabled !== false ? raw : undefined;
98
+ const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
99
+ return names;
100
+ }
101
+
102
+ /** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
103
+ resolveAgentConfig(type: string): AgentConfig {
104
+ const key = this.resolveKey(type);
105
+ const config = key ? this.agents.get(key) : undefined;
106
+ if (config) return config;
107
+
108
+ const gp = this.agents.get("general-purpose");
109
+ if (gp) return gp;
110
+
111
+ // Absolute fallback (should never happen in practice)
112
+ return {
113
+ name: type,
114
+ displayName: "Agent",
115
+ description: "General-purpose agent for complex, multi-step tasks",
116
+ builtinToolNames: BUILTIN_TOOL_NAMES,
117
+ extensions: true,
118
+ skills: true,
119
+ systemPrompt: "",
120
+ promptMode: "append",
121
+ };
122
+ }
123
+
124
+ private resolveKey(name: string): string | undefined {
125
+ if (this.agents.has(name)) return name;
126
+ const lower = name.toLowerCase();
127
+ for (const key of this.agents.keys()) {
128
+ if (key.toLowerCase() === lower) return key;
129
+ }
130
+ return undefined;
131
+ }
82
132
  }
83
133
 
134
+ /** All known built-in tool names. */
135
+ export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
136
+
84
137
  /** Tool names required for memory management. */
85
138
  const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
86
139
 
@@ -100,39 +153,3 @@ const READONLY_MEMORY_TOOL_NAMES = ["read"];
100
153
  export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
101
154
  return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
102
155
  }
103
-
104
- /** Get built-in tool names for a type (case-insensitive). */
105
- export function getToolNamesForType(type: string): string[] {
106
- const key = resolveKey(type);
107
- const raw = key ? agents.get(key) : undefined;
108
- const config = raw?.enabled !== false ? raw : undefined;
109
- const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
110
- return names;
111
- }
112
-
113
- /** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
114
- export function resolveAgentConfig(type: string): AgentConfig {
115
- const key = resolveKey(type);
116
- const config = key ? agents.get(key) : undefined;
117
- if (config) {
118
- return config;
119
- }
120
-
121
- // Fallback to general-purpose for unknown types
122
- const gp = agents.get("general-purpose");
123
- if (gp) {
124
- return gp;
125
- }
126
-
127
- // Absolute fallback (should never happen in practice)
128
- return {
129
- name: type,
130
- displayName: "Agent",
131
- description: "General-purpose agent for complex, multi-step tasks",
132
- builtinToolNames: BUILTIN_TOOL_NAMES,
133
- extensions: true,
134
- skills: true,
135
- systemPrompt: "",
136
- promptMode: "append",
137
- };
138
- }
package/src/index.ts CHANGED
@@ -13,8 +13,8 @@
13
13
  import { join } from "node:path";
14
14
  import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
15
15
  import { AgentManager } from "./agent-manager.js";
16
- import { getAgentConversation, normalizeMaxTurns, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
17
- import { getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveAgentConfig, } from "./agent-types.js";
16
+ import { getAgentConversation, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
17
+ import { AgentTypeRegistry } from "./agent-types.js";
18
18
  import { loadCustomAgents } from "./custom-agents.js";
19
19
  import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
20
20
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
@@ -23,7 +23,7 @@ import { createNotificationRenderer } from "./renderer.js";
23
23
  import { createSubagentRuntime } from "./runtime.js";
24
24
  import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
25
25
  import { createSubagentsService } from "./service-adapter.js";
26
- import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
26
+ import { SettingsManager } from "./settings.js";
27
27
  import { createAgentTool } from "./tools/agent-tool.js";
28
28
  import { createGetResultTool } from "./tools/get-result-tool.js";
29
29
  import { getModelLabelFromConfig } from "./tools/helpers.js";
@@ -40,14 +40,7 @@ export default function (pi: ExtensionAPI) {
40
40
  // ---- Register custom notification renderer ----
41
41
  pi.registerMessageRenderer<NotificationDetails>("subagent-notification", createNotificationRenderer());
42
42
 
43
- /** Reload agents from .pi/agents/*.md and merge with defaults (called on init and each Agent invocation). */
44
- const reloadCustomAgents = () => {
45
- const userAgents = loadCustomAgents(process.cwd());
46
- registerAgents(userAgents);
47
- };
48
-
49
- // Initial load
50
- reloadCustomAgents();
43
+ const registry = new AgentTypeRegistry(() => loadCustomAgents(process.cwd()));
51
44
 
52
45
  // ---- Runtime: all mutable extension state in one place ----
53
46
  const runtime = createSubagentRuntime();
@@ -62,11 +55,19 @@ export default function (pi: ExtensionAPI) {
62
55
  updateWidget: () => runtime.updateWidget(),
63
56
  });
64
57
 
58
+ // Settings: owns all three in-memory values and handles load/save/emit.
59
+ const settings = new SettingsManager({
60
+ emit: (event, payload) => pi.events.emit(event, payload),
61
+ cwd: process.cwd(),
62
+ });
63
+ settings.load();
64
+
65
65
  // Background completion: emit lifecycle event and delegate to notification system
66
66
  const manager = new AgentManager({
67
67
  runner: { run: runAgent, resume: resumeAgent },
68
68
  worktrees: new GitWorktreeManager(process.cwd()),
69
69
  exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
70
+ registry,
70
71
  onComplete: (record) => {
71
72
  // Emit lifecycle event based on terminal status
72
73
  const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
@@ -111,7 +112,8 @@ export default function (pi: ExtensionAPI) {
111
112
  compactionCount: record.compactionCount,
112
113
  });
113
114
  },
114
- getRunConfig: () => ({ defaultMaxTurns: runtime.defaultMaxTurns, graceTurns: runtime.graceTurns }),
115
+ getMaxConcurrent: () => settings.maxConcurrent,
116
+ getRunConfig: () => settings,
115
117
  });
116
118
 
117
119
  // Typed service published via Symbol.for() for cross-extension access.
@@ -137,7 +139,7 @@ export default function (pi: ExtensionAPI) {
137
139
  pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
138
140
 
139
141
  // Live widget: show running agents above editor
140
- runtime.widget = new AgentWidget(manager, runtime.agentActivity);
142
+ runtime.widget = new AgentWidget(manager, runtime.agentActivity, registry);
141
143
 
142
144
  // Grab UI context from first tool execution + clear lingering widget on new turn
143
145
  const toolStart = new ToolStartHandler(runtime);
@@ -145,17 +147,17 @@ export default function (pi: ExtensionAPI) {
145
147
 
146
148
  /** Build the full type list text dynamically from the unified registry. */
147
149
  const buildTypeListText = () => {
148
- const defaultNames = getDefaultAgentNames();
149
- const userNames = getUserAgentNames();
150
+ const defaultNames = registry.getDefaultAgentNames();
151
+ const userNames = registry.getUserAgentNames();
150
152
 
151
153
  const defaultDescs = defaultNames.map((name) => {
152
- const cfg = resolveAgentConfig(name);
154
+ const cfg = registry.resolveAgentConfig(name);
153
155
  const modelSuffix = cfg.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
154
156
  return `- ${name}: ${cfg.description}${modelSuffix}`;
155
157
  });
156
158
 
157
159
  const customDescs = userNames.map((name) => {
158
- const cfg = resolveAgentConfig(name);
160
+ const cfg = registry.resolveAgentConfig(name);
159
161
  return `- ${name}: ${cfg.description}`;
160
162
  });
161
163
 
@@ -170,18 +172,6 @@ export default function (pi: ExtensionAPI) {
170
172
 
171
173
  const typeListText = buildTypeListText();
172
174
 
173
- // Apply persisted settings on startup and emit `subagents:settings_loaded`.
174
- // Global + project merged; missing → defaults; corrupt file emits a warning
175
- // to stderr and falls back to defaults.
176
- applyAndEmitLoaded(
177
- {
178
- setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
179
- setDefaultMaxTurns: (n) => { runtime.defaultMaxTurns = normalizeMaxTurns(n); },
180
- setGraceTurns: (n) => { runtime.graceTurns = Math.max(1, n); },
181
- },
182
- (event, payload) => pi.events.emit(event, payload),
183
- );
184
-
185
175
  // ---- Agent tool ----
186
176
 
187
177
  pi.registerTool(defineTool(createAgentTool({
@@ -190,7 +180,7 @@ export default function (pi: ExtensionAPI) {
190
180
  spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
191
181
  resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
192
182
  getRecord: (id) => manager.getRecord(id),
193
- getMaxConcurrent: () => manager.getMaxConcurrent(),
183
+ getMaxConcurrent: () => settings.maxConcurrent,
194
184
  listAgents: () => manager.listAgents(),
195
185
  },
196
186
  widget: {
@@ -201,11 +191,11 @@ export default function (pi: ExtensionAPI) {
201
191
  },
202
192
  agentActivity: runtime.agentActivity,
203
193
  emitEvent: (name, data) => pi.events.emit(name, data),
204
- reloadCustomAgents,
194
+ registry,
205
195
  typeListText,
206
- availableTypesText: getAvailableTypes().join(", "),
196
+ availableTypesText: registry.getAvailableTypes().join(", "),
207
197
  agentDir: getAgentDir(),
208
- getDefaultMaxTurns: () => runtime.defaultMaxTurns,
198
+ settings,
209
199
  })));
210
200
 
211
201
  // ---- get_subagent_result tool ----
@@ -214,6 +204,7 @@ export default function (pi: ExtensionAPI) {
214
204
  getRecord: (id) => manager.getRecord(id),
215
205
  cancelNudge: (key) => notifications.cancelNudge(key),
216
206
  getConversation: (session) => getAgentConversation(session),
207
+ registry,
217
208
  })));
218
209
 
219
210
  // ---- steer_subagent tool ----
@@ -231,38 +222,20 @@ export default function (pi: ExtensionAPI) {
231
222
  listAgents: () => manager.listAgents(),
232
223
  getRecord: (id) => manager.getRecord(id),
233
224
  spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
234
- getMaxConcurrent: () => manager.getMaxConcurrent(),
235
- setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
225
+ notifyConcurrencyChanged: () => manager.notifyConcurrencyChanged(),
236
226
  },
237
- reloadCustomAgents,
227
+ registry,
238
228
  agentActivity: runtime.agentActivity,
239
- getModelLabel: (type, registry) => {
240
- const cfg = resolveAgentConfig(type);
229
+ getModelLabel: (type, modelRegistry) => {
230
+ const cfg = registry.resolveAgentConfig(type);
241
231
  if (!cfg.model) return 'inherit';
242
- if (registry) {
243
- const resolved = resolveModel(cfg.model, registry);
232
+ if (modelRegistry) {
233
+ const resolved = resolveModel(cfg.model, modelRegistry);
244
234
  if (typeof resolved === 'string') return 'inherit';
245
235
  }
246
236
  return getModelLabelFromConfig(cfg.model);
247
237
  },
248
- snapshotSettings: () => ({
249
- maxConcurrent: manager.getMaxConcurrent(),
250
- defaultMaxTurns: runtime.defaultMaxTurns ?? 0,
251
- graceTurns: runtime.graceTurns,
252
- }),
253
- getDefaultMaxTurns: () => runtime.defaultMaxTurns,
254
- getGraceTurns: () => runtime.graceTurns,
255
- setDefaultMaxTurns: (n) => {
256
- runtime.defaultMaxTurns = normalizeMaxTurns(n);
257
- },
258
- setGraceTurns: (n) => {
259
- runtime.graceTurns = Math.max(1, n);
260
- },
261
- saveSettings: (settings, successMsg) => saveAndEmitChanged(
262
- settings,
263
- successMsg,
264
- (event, payload) => pi.events.emit(event, payload),
265
- ),
238
+ settings,
266
239
  emitEvent: (name, data) => pi.events.emit(name, data),
267
240
  personalAgentsDir: join(getAgentDir(), 'agents'),
268
241
  projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
package/src/runtime.ts CHANGED
@@ -24,12 +24,6 @@ export interface RunConfig {
24
24
  * Tests construct a fresh runtime per test for full isolation.
25
25
  */
26
26
  export class SubagentRuntime {
27
- // ── Execution config (was module-scope in agent-runner.ts) ──────────────
28
- /** Default max turns for all agents. undefined = unlimited. */
29
- defaultMaxTurns: number | undefined = undefined;
30
- /** Additional turns allowed after the soft-limit steer message. */
31
- graceTurns: number = 5;
32
-
33
27
  // ── Session state (was closure-scoped in index.ts) ───────────────────────
34
28
  /** Active Pi session context — set on session_start, cleared on session_shutdown. */
35
29
  currentCtx: { pi: unknown; ctx: unknown } | undefined = undefined;
@@ -11,10 +11,9 @@
11
11
  */
12
12
 
13
13
  import {
14
+ type AgentConfigLookup,
14
15
  getMemoryToolNames,
15
16
  getReadOnlyMemoryToolNames,
16
- getToolNamesForType,
17
- resolveAgentConfig,
18
17
  } from "./agent-types.js";
19
18
  import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
20
19
  import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
@@ -140,14 +139,16 @@ function resolveDefaultModel(
140
139
  * @param ctx Narrow context from the parent session.
141
140
  * @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
142
141
  * @param env Pre-resolved environment info from `detectEnv()`.
142
+ * @param registry Agent config lookup — provides resolveAgentConfig and getToolNamesForType.
143
143
  */
144
144
  export function assembleSessionConfig(
145
145
  type: SubagentType,
146
146
  ctx: AssemblerContext,
147
147
  options: AssemblerOptions,
148
148
  env: EnvInfo,
149
+ registry: AgentConfigLookup,
149
150
  ): SessionConfig {
150
- const agentConfig = resolveAgentConfig(type);
151
+ const agentConfig = registry.resolveAgentConfig(type);
151
152
 
152
153
  const effectiveCwd = options.cwd ?? ctx.cwd;
153
154
 
@@ -166,7 +167,7 @@ export function assembleSessionConfig(
166
167
  }
167
168
  }
168
169
 
169
- let toolNames = getToolNamesForType(type);
170
+ let toolNames = registry.getToolNamesForType(type);
170
171
 
171
172
  // Persistent memory: detect write capability and branch accordingly.
172
173
  // Account for disallowedTools — a tool in the base set but on the denylist