@oai404iao/pi-subagent 0.3.0 → 0.4.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/src/descriptor.ts CHANGED
@@ -4,24 +4,33 @@ import type {
4
4
  AgentScope,
5
5
  AgentSnapshot,
6
6
  AgentSource,
7
- ReportDelivery,
7
+ ContextInheritance,
8
+ RuntimeMode,
8
9
  SubagentDescriptor,
9
10
  SubagentMode,
10
11
  SubagentProviderName,
12
+ SubagentRuntimeSnapshot,
11
13
  } from "./types.ts";
14
+ import { DESCRIPTOR_VERSION } from "./types.ts";
15
+ import { validateDescriptorTask } from "./task-path.ts";
12
16
 
13
17
  export const DESCRIPTOR_CUSTOM_TYPE = "pi-subagent/descriptor";
14
- export const DESCRIPTOR_VERSION = 2;
18
+ export { DESCRIPTOR_VERSION };
15
19
 
16
20
  const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
17
- const AGENT_SOURCES = new Set<AgentSource>(["bundled", "user", "project"]);
21
+ const AGENT_SOURCES = new Set<AgentSource>(["user", "project"]);
18
22
  const MODES = new Set<SubagentMode>(["one-shot", "continuable"]);
19
23
  const PROVIDERS = new Set<SubagentProviderName>(["spawn", "fork"]);
20
- const REPORT_DELIVERIES = new Set<ReportDelivery>(["wakeup", "quiet"]);
24
+ const RUNTIME_MODES = new Set<RuntimeMode>(["foreground", "background"]);
21
25
  const AGENT_SCOPES = new Set<AgentScope>(["user", "project", "both"]);
22
26
  const AGENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
23
27
  /** UUIDv7 with the standard RFC 9562 variant bits (version 7, variant 10xx). */
24
28
  const AGENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
29
+ const CONTEXT_MODES = new Set<ContextInheritance["mode"]>([
30
+ "fresh",
31
+ "all_completed",
32
+ "last_n_completed",
33
+ ]);
25
34
 
26
35
  type UnknownRecord = Record<string, unknown>;
27
36
 
@@ -116,10 +125,49 @@ function parseAgent(value: unknown): AgentSnapshot {
116
125
  };
117
126
  }
118
127
 
128
+ function parseContext(value: unknown): ContextInheritance {
129
+ const input = record(value, "context");
130
+ const mode = string(input.mode, "context.mode") as ContextInheritance["mode"];
131
+ if (!CONTEXT_MODES.has(mode)) {
132
+ throw new Error(`unsupported context.mode: ${mode}`);
133
+ }
134
+ if (mode === "last_n_completed") {
135
+ return {
136
+ mode,
137
+ completedTurns: boundedInteger(
138
+ input.completedTurns,
139
+ "context.completedTurns",
140
+ 1,
141
+ 100,
142
+ ),
143
+ };
144
+ }
145
+ if (input.completedTurns !== undefined) {
146
+ throw new Error(
147
+ "context.completedTurns is available only for last_n_completed",
148
+ );
149
+ }
150
+ return { mode };
151
+ }
152
+
153
+ function parseRuntimeMode(value: unknown): RuntimeMode {
154
+ if (
155
+ typeof value !== "string"
156
+ || !RUNTIME_MODES.has(value as RuntimeMode)
157
+ ) {
158
+ throw new Error(
159
+ 'runtime.runtimeMode must be "foreground" or "background"',
160
+ );
161
+ }
162
+ return value as RuntimeMode;
163
+ }
164
+
119
165
  export function parseDescriptor(value: unknown): SubagentDescriptor {
120
166
  const input = record(value, "descriptor");
121
167
  if (input.version !== DESCRIPTOR_VERSION) {
122
- throw new Error(`unsupported descriptor version: ${String(input.version)}`);
168
+ throw new Error(
169
+ `unsupported descriptor version: ${String(input.version)}; this release persists version ${DESCRIPTOR_VERSION}`,
170
+ );
123
171
  }
124
172
  const mode = string(input.mode, "mode") as SubagentMode;
125
173
  if (!MODES.has(mode)) throw new Error(`unsupported descriptor mode: ${mode}`);
@@ -132,16 +180,36 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
132
180
  const runtime = record(input.runtime, "runtime");
133
181
  const agentScope = string(runtime.agentScope, "runtime.agentScope") as AgentScope;
134
182
  if (!AGENT_SCOPES.has(agentScope)) throw new Error(`unsupported runtime.agentScope: ${agentScope}`);
135
- const reportDelivery = string(runtime.reportDelivery, "runtime.reportDelivery") as ReportDelivery;
136
- if (!REPORT_DELIVERIES.has(reportDelivery)) {
137
- throw new Error(`unsupported runtime.reportDelivery: ${reportDelivery}`);
138
- }
183
+ const parsedRuntime: SubagentRuntimeSnapshot = {
184
+ agentScope,
185
+ maxDepth: safeNatural(runtime.maxDepth, "runtime.maxDepth"),
186
+ runtimeMode: parseRuntimeMode(runtime.runtimeMode),
187
+ maxConcurrentBackgroundRuns: boundedInteger(
188
+ runtime.maxConcurrentBackgroundRuns,
189
+ "runtime.maxConcurrentBackgroundRuns",
190
+ 1,
191
+ Number.MAX_SAFE_INTEGER,
192
+ ),
193
+ maxIdleRuntimes: boundedInteger(
194
+ runtime.maxIdleRuntimes,
195
+ "runtime.maxIdleRuntimes",
196
+ 0,
197
+ Number.MAX_SAFE_INTEGER,
198
+ ),
199
+ inheritExtensions: boolean(runtime.inheritExtensions, "runtime.inheritExtensions"),
200
+ openAIIdentity: boolean(runtime.openAIIdentity, "runtime.openAIIdentity"),
201
+ maxOutputBytes: boundedInteger(
202
+ runtime.maxOutputBytes,
203
+ "runtime.maxOutputBytes",
204
+ 1024,
205
+ 1024 * 1024,
206
+ ),
207
+ };
139
208
 
140
209
  const createdAt = string(input.createdAt, "createdAt");
141
210
  if (Number.isNaN(Date.parse(createdAt))) throw new Error("createdAt must be an ISO date string");
142
211
 
143
- return {
144
- version: DESCRIPTOR_VERSION,
212
+ const base = {
145
213
  mode,
146
214
  provider,
147
215
  label: limitedString(input.label, "label", 200),
@@ -160,28 +228,17 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
160
228
  id: string(model.id, "model.id"),
161
229
  },
162
230
  thinkingLevel,
163
- runtime: {
164
- agentScope,
165
- syncBundledAgents:
166
- runtime.syncBundledAgents === undefined
167
- ? true
168
- : boolean(runtime.syncBundledAgents, "runtime.syncBundledAgents"),
169
- maxDepth: safeNatural(runtime.maxDepth, "runtime.maxDepth"),
170
- enableRunInBackground:
171
- runtime.enableRunInBackground === undefined
172
- ? true
173
- : boolean(runtime.enableRunInBackground, "runtime.enableRunInBackground"),
174
- defaultBackground: boolean(runtime.defaultBackground, "runtime.defaultBackground"),
175
- reportDelivery,
176
- inheritExtensions: boolean(runtime.inheritExtensions, "runtime.inheritExtensions"),
177
- openAIIdentity: boolean(runtime.openAIIdentity, "runtime.openAIIdentity"),
178
- maxOutputBytes: boundedInteger(
179
- runtime.maxOutputBytes,
180
- "runtime.maxOutputBytes",
181
- 1024,
182
- 1024 * 1024,
183
- ),
184
- },
231
+ runtime: parsedRuntime,
232
+ };
233
+ const task = record(input.task, "task");
234
+ return {
235
+ version: DESCRIPTOR_VERSION,
236
+ ...base,
237
+ task: validateDescriptorTask({
238
+ name: string(task.name, "task.name"),
239
+ path: string(task.path, "task.path"),
240
+ }),
241
+ context: parseContext(input.context),
185
242
  };
186
243
  }
187
244
 
@@ -200,3 +257,9 @@ export function foldDescriptor(entries: readonly SessionEntry[]): DescriptorFold
200
257
  return { kind: "corrupt", message: error instanceof Error ? error.message : String(error) };
201
258
  }
202
259
  }
260
+
261
+ export function descriptorContext(
262
+ descriptor: SubagentDescriptor,
263
+ ): ContextInheritance {
264
+ return structuredClone(descriptor.context);
265
+ }
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { DEFAULT_SETTINGS, loadSettings } from "./config.ts";
5
5
  import {
6
6
  REPORT_CUSTOM_TYPE,
7
- SETTLED_CUSTOM_TYPE,
8
7
  SubagentCoordinator,
9
8
  type DelegationInput,
10
9
  } from "./coordinator.ts";
@@ -14,9 +13,11 @@ import {
14
13
  } from "./agents.ts";
15
14
  import type { AgentSyncResult } from "./agent-sync.ts";
16
15
  import {
16
+ FollowupTaskParameters,
17
17
  InterruptParameters,
18
18
  ListAgentsParameters,
19
19
  SendMessageParameters,
20
+ WaitAgentParameters,
20
21
  delegationParameters,
21
22
  forkDelegationParameters,
22
23
  } from "./schemas.ts";
@@ -58,56 +59,53 @@ function disableOwnedTools(
58
59
  }
59
60
 
60
61
  function assertBackgroundControlsEnabled(settings: SubagentSettings, toolName: string): void {
61
- if (!settings.enableRunInBackground) {
62
+ if (settings.runtimeMode === "foreground") {
62
63
  throw new Error(
63
- `tool "${toolName}" is unavailable in foreground-only mode (enableRunInBackground: false)`,
64
+ `tool "${toolName}" is unavailable in foreground-only mode (runtimeMode: "foreground")`,
64
65
  );
65
66
  }
66
67
  }
67
68
 
69
+ function modeDescription(settings: SubagentSettings): string {
70
+ return settings.runtimeMode === "foreground"
71
+ ? "Delegate a complete task to a named child path with selectable completed-turn context. " +
72
+ "This foreground-only tool waits for the child and returns its final answer. " +
73
+ "Independent sibling calls may still execute in parallel."
74
+ : "Delegate a complete task to a named child path with selectable completed-turn context. " +
75
+ "Background mode is continuable and returns a readable path plus durable id; use send_message to enqueue, followup_task to start, and wait_agent for quiet completions. " +
76
+ "Start independent children together in one assistant message.";
77
+ }
78
+
68
79
  function registerDelegationTool(
69
80
  pi: ExtensionAPI,
70
81
  coordinator: SubagentCoordinator,
71
82
  settings: SubagentSettings,
72
83
  agentDiscovery?: AgentDiscoveryResult,
73
84
  ): unknown {
74
- const { enableRunInBackground, defaultBackground } = settings;
85
+ const foregroundOnly = settings.runtimeMode === "foreground";
75
86
  const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
76
- const description = !enableRunInBackground
77
- ? "Delegate a complete standalone task to a fresh child with its own Pi session and context. " +
78
- "This foreground-only tool waits for the child and returns its final answer. " +
79
- "Independent sibling calls may still execute in parallel."
80
- : defaultBackground
81
- ? "Delegate a complete standalone task to a fresh child with its own Pi session and context. " +
82
- "Background mode is continuable and returns a durable agent id; use send_message for later FIFO turns. " +
83
- "Start independent children together in one assistant message."
84
- : "Delegate a complete standalone task to a fresh child with its own Pi session and context. " +
85
- "This tool waits for the result by default; set run_in_background to true to return a durable agent id.";
86
- const promptGuidelines = !enableRunInBackground
87
+ const promptGuidelines = (foregroundOnly
87
88
  ? [
88
89
  "Use subagent for focused independent work and give it a complete standalone prompt.",
89
90
  "This subagent tool is foreground-only: every call waits for and returns the child's final answer.",
90
91
  "Independent subagent calls can still be issued together in one assistant message and execute in parallel.",
91
92
  ]
92
- : defaultBackground
93
- ? [
94
- "Use subagent for focused independent work and give it a complete standalone prompt.",
95
- "Call subagent multiple times in one assistant message when delegations are independent.",
96
- "Keep useful parent work moving after a background subagent starts; use foreground only when the next action needs its result.",
97
- ]
98
- : [
99
- "Use subagent for focused independent work and give it a complete standalone prompt.",
100
- "Subagent calls wait for the result by default; request background mode only when work can continue independently.",
101
- "Independent subagent calls can still be issued together in one assistant message and execute in parallel.",
102
- ];
103
- const parameters = delegationParameters(enableRunInBackground, agentNames);
93
+ : [
94
+ "Use subagent for focused independent work and give it a complete standalone prompt.",
95
+ "Call subagent multiple times in one assistant message when delegations are independent.",
96
+ "Every child is durable: continue parent work, then use send_message plus followup_task to give it more work and wait_agent for its completion updates.",
97
+ ]).concat([
98
+ "Set task_name when a stable readable path will help later control calls; otherwise a name is generated from description.",
99
+ "Use fresh context by default and request all_completed or last_n_completed only when parent history materially helps.",
100
+ ]);
101
+ const parameters = delegationParameters(agentNames);
104
102
  pi.registerTool({
105
103
  name: "subagent",
106
104
  label: "Subagent",
107
- description,
108
- promptSnippet: enableRunInBackground
109
- ? "Delegate focused independent work to fresh child agents"
110
- : "Run focused independent work in foreground child agents",
105
+ description: modeDescription(settings),
106
+ promptSnippet: foregroundOnly
107
+ ? "Run focused independent work in foreground child agents"
108
+ : "Delegate focused work to named child agents",
111
109
  promptGuidelines,
112
110
  executionMode: "parallel",
113
111
  parameters,
@@ -129,7 +127,7 @@ function registerDelegationTool(
129
127
  return coordinator.outcomeToolResult(outcome);
130
128
  },
131
129
  renderCall(args, theme) {
132
- return renderDelegationCall(args, theme, "spawn");
130
+ return renderDelegationCall(args, theme, "spawn", settings.runtimeMode);
133
131
  },
134
132
  renderResult(result, options, theme) {
135
133
  return renderDelegationResult(
@@ -155,8 +153,11 @@ function registerForkDelegationTool(
155
153
  name: "subagent_fork",
156
154
  label: "Subagent Fork",
157
155
  description:
158
- "Delegate a one-shot task to a child seeded with all completed turns in this conversation. " +
159
- "The current in-flight tool-calling turn is excluded. Use this when the child needs parent history.",
156
+ "Delegate a task to a child seeded with all completed turns in this conversation. " +
157
+ "The current in-flight tool-calling turn is excluded. " +
158
+ (settings.runtimeMode === "foreground"
159
+ ? "This foreground-only tool waits for the child's final answer."
160
+ : "The fork is continuable and returns a readable path plus durable id."),
160
161
  promptSnippet: "Delegate context-dependent work to a child seeded with completed turns",
161
162
  promptGuidelines: [
162
163
  "Use subagent_fork only when completed conversation history materially helps the delegated task.",
@@ -181,7 +182,7 @@ function registerForkDelegationTool(
181
182
  return coordinator.outcomeToolResult(outcome);
182
183
  },
183
184
  renderCall(args, theme) {
184
- return renderDelegationCall(args, theme, "fork");
185
+ return renderDelegationCall(args, theme, "fork", settings.runtimeMode);
185
186
  },
186
187
  renderResult(result, options, theme) {
187
188
  return renderDelegationResult(
@@ -206,6 +207,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
206
207
  registerForkDelegationTool(pi, coordinator, DEFAULT_SETTINGS);
207
208
  const backgroundControlParameters = new Map<string, unknown>([
208
209
  ["send_message", SendMessageParameters],
210
+ ["followup_task", FollowupTaskParameters],
211
+ ["wait_agent", WaitAgentParameters],
209
212
  ["interrupt_agent", InterruptParameters],
210
213
  ["list_agents", ListAgentsParameters],
211
214
  ]);
@@ -214,23 +217,107 @@ export default function subagentExtension(pi: ExtensionAPI): void {
214
217
  name: "send_message",
215
218
  label: "Send Message",
216
219
  description:
217
- "Queue a message as a direct continuable child's next FIFO turn. If it is inactive, its persisted session is cold-resumed by agent id. " +
220
+ "Send a message to a direct continuable child. It is durably appended to the child's FIFO mailbox and requires followup_task to start a turn. " +
218
221
  "This call returns acceptance only, never the child's answer.",
219
- promptSnippet: "Send a later FIFO turn to a direct continuable subagent",
222
+ promptSnippet: "Send or enqueue a message for a direct continuable subagent",
220
223
  executionMode: "parallel",
221
224
  parameters: SendMessageParameters,
222
225
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
223
226
  assertBackgroundControlsEnabled(sessionSettings, "send_message");
224
227
  const parent = await coordinator.parentFromContext(ctx);
225
- await coordinator.sendMessage(parent, params.subagent_id, params.message, signal);
228
+ const delivery = await coordinator.sendMessageWithOutcome(
229
+ parent,
230
+ params.subagent_id,
231
+ params.message,
232
+ signal,
233
+ );
234
+ return {
235
+ content: [
236
+ {
237
+ type: "text",
238
+ text: `message ${delivery.messageId} durably enqueued for ${delivery.taskPath}; ${delivery.pendingMessages} pending`,
239
+ },
240
+ ],
241
+ details: {
242
+ kind: "control",
243
+ action: "send",
244
+ agentId: delivery.agentId,
245
+ taskPath: delivery.taskPath,
246
+ messageId: delivery.messageId,
247
+ pendingMessages: delivery.pendingMessages,
248
+ } satisfies ControlDetails,
249
+ };
250
+ },
251
+ });
252
+
253
+ pi.registerTool({
254
+ name: "followup_task",
255
+ label: "Follow-up Task",
256
+ description:
257
+ "For a direct continuable child, atomically claim its current pending FIFO mailbox and start exactly one scheduled turn. " +
258
+ "This call returns turn acceptance, not the child's answer.",
259
+ promptSnippet: "Start one child turn from queued mailbox messages",
260
+ executionMode: "parallel",
261
+ parameters: FollowupTaskParameters,
262
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
263
+ assertBackgroundControlsEnabled(sessionSettings, "followup_task");
264
+ const parent = await coordinator.parentFromContext(ctx);
265
+ const outcome = await coordinator.followupTask(
266
+ parent,
267
+ params.subagent_id,
268
+ signal,
269
+ );
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: `started turn ${outcome.turnId} for ${outcome.taskPath}, claiming ${outcome.claimedMessages} mailbox message${outcome.claimedMessages === 1 ? "" : "s"}`,
275
+ },
276
+ ],
277
+ details: {
278
+ kind: "control",
279
+ action: "followup",
280
+ agentId: outcome.agentId,
281
+ taskPath: outcome.taskPath,
282
+ turnId: outcome.turnId,
283
+ claimedMessages: outcome.claimedMessages,
284
+ } satisfies ControlDetails,
285
+ };
286
+ },
287
+ });
288
+
289
+ pi.registerTool({
290
+ name: "wait_agent",
291
+ label: "Wait Agent",
292
+ description:
293
+ "Wait event-driven for unread completion updates from direct children. Existing updates return immediately; timeout does not consume later updates.",
294
+ promptSnippet: "Wait for quiet child completion updates",
295
+ parameters: WaitAgentParameters,
296
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
297
+ assertBackgroundControlsEnabled(sessionSettings, "wait_agent");
298
+ const parent = await coordinator.parentFromContext(ctx);
299
+ const outcome = await coordinator.waitAgent(
300
+ parent,
301
+ toolCallId,
302
+ params.timeout_ms,
303
+ signal,
304
+ );
226
305
  return {
227
306
  content: [
228
307
  {
229
308
  type: "text",
230
- text: `message queued as the next turn for subagent ${params.subagent_id}`,
309
+ text: coordinator.formatWaitAgentOutcome(outcome),
231
310
  },
232
311
  ],
233
- details: { kind: "control", action: "send", agentId: params.subagent_id } satisfies ControlDetails,
312
+ details: {
313
+ kind: "control",
314
+ action: "wait",
315
+ timedOut: outcome.timedOut,
316
+ completionIds: outcome.updates.map(
317
+ (update) => update.completionId,
318
+ ),
319
+ unreadUpdates: outcome.unreadUpdates,
320
+ } satisfies ControlDetails,
234
321
  };
235
322
  },
236
323
  });
@@ -247,10 +334,18 @@ export default function subagentExtension(pi: ExtensionAPI): void {
247
334
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
248
335
  assertBackgroundControlsEnabled(sessionSettings, "interrupt_agent");
249
336
  const parent = await coordinator.parentFromContext(ctx);
250
- await coordinator.interrupt(parent, params.agent_id);
337
+ const outcome = await coordinator.interruptWithOutcome(
338
+ parent,
339
+ params.agent_id,
340
+ );
251
341
  return {
252
- content: [{ type: "text", text: `interrupt requested for agent ${params.agent_id}` }],
253
- details: { kind: "control", action: "interrupt", agentId: params.agent_id } satisfies ControlDetails,
342
+ content: [{ type: "text", text: `interrupt requested for ${outcome.taskPath}` }],
343
+ details: {
344
+ kind: "control",
345
+ action: "interrupt",
346
+ agentId: outcome.agentId,
347
+ taskPath: outcome.taskPath,
348
+ } satisfies ControlDetails,
254
349
  };
255
350
  },
256
351
  });
@@ -260,7 +355,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
260
355
  label: "List Agents",
261
356
  description:
262
357
  "List direct continuable children or all descendants. running means an active turn, idle means resident between turns, " +
263
- "and ready means persisted and cold-resumable.",
358
+ "ready means persisted and cold-resumable, and children show pending task messages separately from unread completion updates.",
264
359
  promptSnippet: "List continuable child agents and their lifecycle status",
265
360
  parameters: ListAgentsParameters,
266
361
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -275,7 +370,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
275
370
  },
276
371
  });
277
372
 
278
- for (const customType of [REPORT_CUSTOM_TYPE, SETTLED_CUSTOM_TYPE]) {
373
+ for (const customType of [REPORT_CUSTOM_TYPE]) {
279
374
  pi.registerMessageRenderer<ParentMessageDetails>(customType, (message, options, theme) => {
280
375
  const content =
281
376
  typeof message.content === "string"
@@ -300,18 +395,19 @@ export default function subagentExtension(pi: ExtensionAPI): void {
300
395
  );
301
396
  const parent = await coordinator.parentFromContext(ctx);
302
397
  const entries = await coordinator.list(parent, "descendants");
303
- const schedulingMode = !sessionSettings.enableRunInBackground
304
- ? "foreground-only"
305
- : sessionSettings.defaultBackground
306
- ? "background-first"
307
- : "foreground-first";
308
398
  const sections = [
309
- `Mode: ${schedulingMode}`,
399
+ `Mode: ${sessionSettings.runtimeMode}`,
400
+ `Background concurrency: ${sessionSettings.maxConcurrentBackgroundRuns}`,
401
+ `Idle runtime LRU: ${sessionSettings.maxIdleRuntimes}`,
402
+ "Background protocol: durable mailbox",
310
403
  `OpenAI identity inline: ${sessionSettings.openAIIdentity ? "enabled" : "disabled"}`,
311
- sessionSettings.syncBundledAgents
312
- ? `Bundled presets: synchronized to ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`
313
- : "Bundled presets: package defaults (no filesystem sync)",
404
+ agentSync?.diagnostics.length
405
+ ? "Bundled templates: initialization skipped (see diagnostics)"
406
+ : `Bundled templates: initialization only (${agentSync?.packageVersion ?? "not initialized"})`,
314
407
  `User agent dir: ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`,
408
+ agentSync && agentSync.retired.length > 0
409
+ ? `Deleted presets (never restored): ${agentSync.retired.join(", ")}`
410
+ : null,
315
411
  `Agents:\n${formatAgentCatalog(discovery.agents)}`,
316
412
  `Children:\n${coordinator.formatCatalog(entries, "descendants")}`,
317
413
  ];
@@ -324,10 +420,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
324
420
 
325
421
  pi.on("session_start", async (_event, ctx) => {
326
422
  const loaded = loadSettings({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
423
+ coordinator.configureBackgroundRuns(
424
+ loaded.settings.maxConcurrentBackgroundRuns,
425
+ );
426
+ await coordinator.configureIdleRuntimes(
427
+ loaded.settings.maxIdleRuntimes,
428
+ );
327
429
  sessionSettings = loaded.settings;
328
- agentSync = sessionSettings.syncBundledAgents
329
- ? coordinator.synchronizeBundledAgents()
330
- : undefined;
430
+ agentSync = coordinator.synchronizeBundledAgents();
331
431
  sessionDiscovery = coordinator.discoverAvailableAgents(
332
432
  ctx.cwd,
333
433
  sessionSettings,
@@ -346,7 +446,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
346
446
  if (sessionDiscovery.agents.length === 0) {
347
447
  disableOwnedTools(pi, registeredParameters);
348
448
  }
349
- if (!sessionSettings.enableRunInBackground) {
449
+ if (sessionSettings.runtimeMode === "foreground") {
350
450
  disableOwnedTools(pi, backgroundControlParameters);
351
451
  }
352
452
  if (!agentSyncNotified && agentSync) {
@@ -361,6 +461,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
361
461
  if (agentSync.removed.length > 0) {
362
462
  lines.push(`retired: ${agentSync.removed.join(", ")}`);
363
463
  }
464
+ if (agentSync.retirementChanged && agentSync.retired.length > 0) {
465
+ lines.push(
466
+ `deleted by you (not restored): ${agentSync.retired.join(", ")}`,
467
+ );
468
+ }
469
+ if (agentSync.restored.length > 0) {
470
+ lines.push(`restored as managed presets: ${agentSync.restored.join(", ")}`);
471
+ }
364
472
  if (agentSync.backups.length > 0) {
365
473
  lines.push("backups:", ...agentSync.backups.map((backup) => `- ${backup.name}: ${backup.path}`));
366
474
  }
@@ -368,6 +476,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
368
476
  agentSync.installed.length > 0 ||
369
477
  agentSync.updated.length > 0 ||
370
478
  agentSync.removed.length > 0 ||
479
+ (agentSync.retirementChanged && agentSync.retired.length > 0) ||
480
+ agentSync.restored.length > 0 ||
371
481
  agentSync.backups.length > 0
372
482
  ) {
373
483
  ctx.ui.notify(lines.join("\n"), "info");
@@ -378,6 +488,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
378
488
  }
379
489
  });
380
490
 
491
+ pi.on("agent_end", async (_event, ctx) => {
492
+ const parent = await coordinator.parentFromContext(ctx);
493
+ await coordinator.releaseWaitAgentDeliveries(
494
+ parent,
495
+ "parent agent turn ended without a durable wait_agent result",
496
+ );
497
+ });
498
+
381
499
  pi.on("session_shutdown", async () => {
382
500
  await coordinator.shutdown();
383
501
  });