@oai404iao/pi-subagent 0.2.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.
@@ -0,0 +1,189 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
3
+ import type {
4
+ AgentScope,
5
+ AgentSnapshot,
6
+ AgentSource,
7
+ ReportDelivery,
8
+ SubagentDescriptor,
9
+ SubagentMode,
10
+ SubagentProviderName,
11
+ } from "./types.ts";
12
+
13
+ export const DESCRIPTOR_CUSTOM_TYPE = "pi-subagent/descriptor";
14
+ export const DESCRIPTOR_VERSION = 1;
15
+
16
+ const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
17
+ const AGENT_SOURCES = new Set<AgentSource>(["bundled", "user", "project"]);
18
+ const MODES = new Set<SubagentMode>(["one-shot", "continuable"]);
19
+ const PROVIDERS = new Set<SubagentProviderName>(["spawn", "fork"]);
20
+ const REPORT_DELIVERIES = new Set<ReportDelivery>(["wakeup", "quiet"]);
21
+ const AGENT_SCOPES = new Set<AgentScope>(["user", "project", "both"]);
22
+ const AGENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
23
+
24
+ type UnknownRecord = Record<string, unknown>;
25
+
26
+ export type DescriptorFold =
27
+ | { kind: "none" }
28
+ | { kind: "valid"; descriptor: SubagentDescriptor }
29
+ | { kind: "corrupt"; message: string };
30
+
31
+ function record(value: unknown, field: string): UnknownRecord {
32
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
33
+ throw new Error(`${field} must be an object`);
34
+ }
35
+ return value as UnknownRecord;
36
+ }
37
+
38
+ function string(value: unknown, field: string, allowEmpty = false): string {
39
+ if (typeof value !== "string" || (!allowEmpty && value.trim().length === 0)) {
40
+ throw new Error(`${field} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
41
+ }
42
+ return value;
43
+ }
44
+
45
+ function optionalString(value: unknown, field: string): string | undefined {
46
+ return value === undefined ? undefined : string(value, field);
47
+ }
48
+
49
+ function limitedString(value: unknown, field: string, maxLength: number): string {
50
+ const parsed = string(value, field);
51
+ if (parsed.length > maxLength) throw new Error(`${field} exceeds ${maxLength} characters`);
52
+ return parsed;
53
+ }
54
+
55
+ function safeNatural(value: unknown, field: string): number {
56
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
57
+ throw new Error(`${field} must be a non-negative safe integer`);
58
+ }
59
+ return value;
60
+ }
61
+
62
+ function boundedInteger(value: unknown, field: string, minimum: number, maximum: number): number {
63
+ const parsed = safeNatural(value, field);
64
+ if (parsed < minimum || parsed > maximum) {
65
+ throw new Error(`${field} must be between ${minimum} and ${maximum}`);
66
+ }
67
+ return parsed;
68
+ }
69
+
70
+ function boolean(value: unknown, field: string): boolean {
71
+ if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
72
+ return value;
73
+ }
74
+
75
+ function stringArray(value: unknown, field: string): string[] | undefined {
76
+ if (value === undefined) return undefined;
77
+ if (
78
+ !Array.isArray(value) ||
79
+ value.some(
80
+ (item) => typeof item !== "string" || item.trim().length === 0 || item.length > 128,
81
+ )
82
+ ) {
83
+ throw new Error(`${field} must be an array of non-empty strings`);
84
+ }
85
+ return [...new Set(value)];
86
+ }
87
+
88
+ function parseAgent(value: unknown): AgentSnapshot {
89
+ const input = record(value, "agent");
90
+ const source = string(input.source, "agent.source") as AgentSource;
91
+ if (!AGENT_SOURCES.has(source)) throw new Error(`agent.source is unsupported: ${source}`);
92
+ const thinking = optionalString(input.thinking, "agent.thinking") as ThinkingLevel | undefined;
93
+ if (thinking && !THINKING_LEVELS.has(thinking)) throw new Error(`agent.thinking is unsupported: ${thinking}`);
94
+ const tools = stringArray(input.tools, "agent.tools");
95
+ const model = optionalString(input.model, "agent.model");
96
+ const name = limitedString(input.name, "agent.name", 64);
97
+ if (!AGENT_NAME_PATTERN.test(name)) throw new Error("agent.name has an invalid format");
98
+ return {
99
+ name,
100
+ description: limitedString(input.description, "agent.description", 1000),
101
+ ...(tools !== undefined ? { tools } : {}),
102
+ ...(model !== undefined ? { model } : {}),
103
+ ...(thinking !== undefined ? { thinking } : {}),
104
+ systemPrompt: limitedString(input.systemPrompt, "agent.systemPrompt", 256 * 1024),
105
+ source,
106
+ };
107
+ }
108
+
109
+ export function parseDescriptor(value: unknown): SubagentDescriptor {
110
+ const input = record(value, "descriptor");
111
+ if (input.version !== DESCRIPTOR_VERSION) {
112
+ throw new Error(`unsupported descriptor version: ${String(input.version)}`);
113
+ }
114
+ const mode = string(input.mode, "mode") as SubagentMode;
115
+ if (!MODES.has(mode)) throw new Error(`unsupported descriptor mode: ${mode}`);
116
+ const provider = string(input.provider, "provider") as SubagentProviderName;
117
+ if (!PROVIDERS.has(provider)) throw new Error(`unsupported descriptor provider: ${provider}`);
118
+ const thinkingLevel = string(input.thinkingLevel, "thinkingLevel") as ThinkingLevel;
119
+ if (!THINKING_LEVELS.has(thinkingLevel)) throw new Error(`unsupported thinkingLevel: ${thinkingLevel}`);
120
+
121
+ const model = record(input.model, "model");
122
+ const runtime = record(input.runtime, "runtime");
123
+ const agentScope = string(runtime.agentScope, "runtime.agentScope") as AgentScope;
124
+ if (!AGENT_SCOPES.has(agentScope)) throw new Error(`unsupported runtime.agentScope: ${agentScope}`);
125
+ const reportDelivery = string(runtime.reportDelivery, "runtime.reportDelivery") as ReportDelivery;
126
+ if (!REPORT_DELIVERIES.has(reportDelivery)) {
127
+ throw new Error(`unsupported runtime.reportDelivery: ${reportDelivery}`);
128
+ }
129
+
130
+ const createdAt = string(input.createdAt, "createdAt");
131
+ if (Number.isNaN(Date.parse(createdAt))) throw new Error("createdAt must be an ISO date string");
132
+
133
+ return {
134
+ version: DESCRIPTOR_VERSION,
135
+ mode,
136
+ provider,
137
+ label: limitedString(input.label, "label", 200),
138
+ parentSessionId: string(input.parentSessionId, "parentSessionId"),
139
+ ...(optionalString(input.parentSessionFile, "parentSessionFile")
140
+ ? { parentSessionFile: input.parentSessionFile as string }
141
+ : {}),
142
+ depth: safeNatural(input.depth, "depth"),
143
+ cwd: string(input.cwd, "cwd"),
144
+ createdAt,
145
+ agent: parseAgent(input.agent),
146
+ model: {
147
+ provider: string(model.provider, "model.provider"),
148
+ id: string(model.id, "model.id"),
149
+ },
150
+ thinkingLevel,
151
+ runtime: {
152
+ agentScope,
153
+ syncBundledAgents:
154
+ runtime.syncBundledAgents === undefined
155
+ ? true
156
+ : boolean(runtime.syncBundledAgents, "runtime.syncBundledAgents"),
157
+ maxDepth: safeNatural(runtime.maxDepth, "runtime.maxDepth"),
158
+ enableRunInBackground:
159
+ runtime.enableRunInBackground === undefined
160
+ ? true
161
+ : boolean(runtime.enableRunInBackground, "runtime.enableRunInBackground"),
162
+ defaultBackground: boolean(runtime.defaultBackground, "runtime.defaultBackground"),
163
+ reportDelivery,
164
+ inheritExtensions: boolean(runtime.inheritExtensions, "runtime.inheritExtensions"),
165
+ maxOutputBytes: boundedInteger(
166
+ runtime.maxOutputBytes,
167
+ "runtime.maxOutputBytes",
168
+ 1024,
169
+ 1024 * 1024,
170
+ ),
171
+ },
172
+ };
173
+ }
174
+
175
+ export function foldDescriptor(entries: readonly SessionEntry[]): DescriptorFold {
176
+ let found = false;
177
+ let value: unknown;
178
+ for (const entry of entries) {
179
+ if (entry.type !== "custom" || entry.customType !== DESCRIPTOR_CUSTOM_TYPE) continue;
180
+ found = true;
181
+ value = entry.data;
182
+ }
183
+ if (!found) return { kind: "none" };
184
+ try {
185
+ return { kind: "valid", descriptor: parseDescriptor(value) };
186
+ } catch (error) {
187
+ return { kind: "corrupt", message: error instanceof Error ? error.message : String(error) };
188
+ }
189
+ }
package/src/index.ts ADDED
@@ -0,0 +1,386 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import { DEFAULT_SETTINGS, loadSettings } from "./config.ts";
5
+ import {
6
+ REPORT_CUSTOM_TYPE,
7
+ SETTLED_CUSTOM_TYPE,
8
+ SubagentCoordinator,
9
+ type DelegationInput,
10
+ } from "./coordinator.ts";
11
+ import {
12
+ formatAgentCatalog,
13
+ type AgentDiscoveryResult,
14
+ } from "./agents.ts";
15
+ import type { AgentSyncResult } from "./agent-sync.ts";
16
+ import {
17
+ InterruptParameters,
18
+ ListAgentsParameters,
19
+ SendMessageParameters,
20
+ delegationParameters,
21
+ forkDelegationParameters,
22
+ } from "./schemas.ts";
23
+ import { renderDelegationCall, renderDelegationResult, renderParentMessage } from "./render.ts";
24
+ import type {
25
+ ControlDetails,
26
+ DelegationDetails,
27
+ ParentMessageDetails,
28
+ SubagentSettings,
29
+ } from "./types.ts";
30
+
31
+ const SOURCE_DIR = dirname(fileURLToPath(import.meta.url));
32
+ const PACKAGE_ROOT = dirname(SOURCE_DIR);
33
+ const BUNDLED_AGENTS_DIR = join(PACKAGE_ROOT, "agents");
34
+
35
+ function textContent(content: Array<{ type: string; text?: string }>): string {
36
+ return content.find((item) => item.type === "text")?.text ?? "";
37
+ }
38
+
39
+ function disableOwnedTools(
40
+ pi: ExtensionAPI,
41
+ registeredParameters: ReadonlyMap<string, unknown>,
42
+ ): void {
43
+ const ownedNames = new Set(
44
+ pi
45
+ .getAllTools()
46
+ .filter(
47
+ (tool) =>
48
+ registeredParameters.has(tool.name) &&
49
+ tool.parameters === registeredParameters.get(tool.name) &&
50
+ tool.sourceInfo.source !== "sdk",
51
+ )
52
+ .map((tool) => tool.name),
53
+ );
54
+ if (ownedNames.size === 0) return;
55
+ const activeTools = pi.getActiveTools();
56
+ const nextActiveTools = activeTools.filter((name) => !ownedNames.has(name));
57
+ if (nextActiveTools.length !== activeTools.length) pi.setActiveTools(nextActiveTools);
58
+ }
59
+
60
+ function assertBackgroundControlsEnabled(settings: SubagentSettings, toolName: string): void {
61
+ if (!settings.enableRunInBackground) {
62
+ throw new Error(
63
+ `tool "${toolName}" is unavailable in foreground-only mode (enableRunInBackground: false)`,
64
+ );
65
+ }
66
+ }
67
+
68
+ function registerDelegationTool(
69
+ pi: ExtensionAPI,
70
+ coordinator: SubagentCoordinator,
71
+ settings: SubagentSettings,
72
+ agentDiscovery?: AgentDiscoveryResult,
73
+ ): unknown {
74
+ const { enableRunInBackground, defaultBackground } = settings;
75
+ 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 child 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 child id.";
86
+ const promptGuidelines = !enableRunInBackground
87
+ ? [
88
+ "Use subagent for focused independent work and give it a complete standalone prompt.",
89
+ "This subagent tool is foreground-only: every call waits for and returns the child's final answer.",
90
+ "Independent subagent calls can still be issued together in one assistant message and execute in parallel.",
91
+ ]
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);
104
+ pi.registerTool({
105
+ name: "subagent",
106
+ 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",
111
+ promptGuidelines,
112
+ executionMode: "parallel",
113
+ parameters,
114
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
115
+ const parent = await coordinator.parentFromContext(ctx);
116
+ const outcome = await coordinator.delegate(
117
+ parent,
118
+ "spawn",
119
+ params,
120
+ settings,
121
+ signal,
122
+ (details) =>
123
+ onUpdate?.({
124
+ content: [{ type: "text", text: details.trace.at(-1)?.text ?? `${details.agent}: ${details.status}` }],
125
+ details,
126
+ }),
127
+ agentDiscovery,
128
+ );
129
+ return coordinator.outcomeToolResult(outcome);
130
+ },
131
+ renderCall(args, theme) {
132
+ return renderDelegationCall(args, theme, "spawn");
133
+ },
134
+ renderResult(result, options, theme) {
135
+ return renderDelegationResult(
136
+ result.details as DelegationDetails | undefined,
137
+ textContent(result.content),
138
+ options,
139
+ theme,
140
+ );
141
+ },
142
+ });
143
+ return parameters;
144
+ }
145
+
146
+ function registerForkDelegationTool(
147
+ pi: ExtensionAPI,
148
+ coordinator: SubagentCoordinator,
149
+ settings: SubagentSettings,
150
+ agentDiscovery?: AgentDiscoveryResult,
151
+ ): unknown {
152
+ const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
153
+ const parameters = forkDelegationParameters(agentNames);
154
+ pi.registerTool({
155
+ name: "subagent_fork",
156
+ label: "Subagent Fork",
157
+ 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.",
160
+ promptSnippet: "Delegate context-dependent work to a child seeded with completed turns",
161
+ promptGuidelines: [
162
+ "Use subagent_fork only when completed conversation history materially helps the delegated task.",
163
+ ],
164
+ executionMode: "parallel",
165
+ parameters,
166
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
167
+ const parent = await coordinator.parentFromContext(ctx);
168
+ const outcome = await coordinator.delegate(
169
+ parent,
170
+ "fork",
171
+ params satisfies DelegationInput,
172
+ settings,
173
+ signal,
174
+ (details) =>
175
+ onUpdate?.({
176
+ content: [{ type: "text", text: details.trace.at(-1)?.text ?? `${details.agent}: ${details.status}` }],
177
+ details,
178
+ }),
179
+ agentDiscovery,
180
+ );
181
+ return coordinator.outcomeToolResult(outcome);
182
+ },
183
+ renderCall(args, theme) {
184
+ return renderDelegationCall(args, theme, "fork");
185
+ },
186
+ renderResult(result, options, theme) {
187
+ return renderDelegationResult(
188
+ result.details as DelegationDetails | undefined,
189
+ textContent(result.content),
190
+ options,
191
+ theme,
192
+ );
193
+ },
194
+ });
195
+ return parameters;
196
+ }
197
+
198
+ export default function subagentExtension(pi: ExtensionAPI): void {
199
+ const coordinator = new SubagentCoordinator(pi, BUNDLED_AGENTS_DIR, PACKAGE_ROOT);
200
+ let agentSyncNotified = false;
201
+ let agentSync: AgentSyncResult | undefined;
202
+ let sessionSettings: SubagentSettings = DEFAULT_SETTINGS;
203
+ let sessionDiscovery: AgentDiscoveryResult | undefined;
204
+
205
+ registerDelegationTool(pi, coordinator, DEFAULT_SETTINGS);
206
+ registerForkDelegationTool(pi, coordinator, DEFAULT_SETTINGS);
207
+ const backgroundControlParameters = new Map<string, unknown>([
208
+ ["send_message", SendMessageParameters],
209
+ ["interrupt_agent", InterruptParameters],
210
+ ["list_agents", ListAgentsParameters],
211
+ ]);
212
+
213
+ pi.registerTool({
214
+ name: "send_message",
215
+ label: "Send Message",
216
+ description:
217
+ "Queue a message as a direct continuable child's next FIFO turn. If it is inactive, its persisted session is cold-resumed. " +
218
+ "This call returns acceptance only, never the child's answer.",
219
+ promptSnippet: "Send a later FIFO turn to a direct continuable subagent",
220
+ executionMode: "parallel",
221
+ parameters: SendMessageParameters,
222
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
223
+ assertBackgroundControlsEnabled(sessionSettings, "send_message");
224
+ const parent = await coordinator.parentFromContext(ctx);
225
+ await coordinator.sendMessage(parent, params.subagent_id, params.message, signal);
226
+ return {
227
+ content: [
228
+ {
229
+ type: "text",
230
+ text: `message queued as the next turn for subagent ${params.subagent_id}`,
231
+ },
232
+ ],
233
+ details: { kind: "control", action: "send", id: params.subagent_id } satisfies ControlDetails,
234
+ };
235
+ },
236
+ });
237
+
238
+ pi.registerTool({
239
+ name: "interrupt_agent",
240
+ label: "Interrupt Agent",
241
+ description:
242
+ "Request cancellation of a live child or descendant's current turn. The child session remains available for later messages. " +
243
+ "An inactive or already-settled target is an accepted no-op.",
244
+ promptSnippet: "Interrupt a live descendant's current turn without deleting its session",
245
+ executionMode: "parallel",
246
+ parameters: InterruptParameters,
247
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
248
+ assertBackgroundControlsEnabled(sessionSettings, "interrupt_agent");
249
+ const parent = await coordinator.parentFromContext(ctx);
250
+ await coordinator.interrupt(parent, params.agent_id);
251
+ return {
252
+ content: [{ type: "text", text: `interrupt requested for agent ${params.agent_id}` }],
253
+ details: { kind: "control", action: "interrupt", id: params.agent_id } satisfies ControlDetails,
254
+ };
255
+ },
256
+ });
257
+
258
+ pi.registerTool({
259
+ name: "list_agents",
260
+ label: "List Agents",
261
+ description:
262
+ "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.",
264
+ promptSnippet: "List continuable child agents and their lifecycle status",
265
+ parameters: ListAgentsParameters,
266
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
267
+ assertBackgroundControlsEnabled(sessionSettings, "list_agents");
268
+ const parent = await coordinator.parentFromContext(ctx);
269
+ const scope = params.scope ?? "children";
270
+ const entries = await coordinator.list(parent, scope);
271
+ return {
272
+ content: [{ type: "text", text: coordinator.formatCatalog(entries, scope) }],
273
+ details: { kind: "control", action: "list" } satisfies ControlDetails,
274
+ };
275
+ },
276
+ });
277
+
278
+ for (const customType of [REPORT_CUSTOM_TYPE, SETTLED_CUSTOM_TYPE]) {
279
+ pi.registerMessageRenderer<ParentMessageDetails>(customType, (message, options, theme) => {
280
+ const content =
281
+ typeof message.content === "string"
282
+ ? message.content
283
+ : message.content
284
+ .filter((item): item is Extract<(typeof message.content)[number], { type: "text" }> => item.type === "text")
285
+ .map((item) => item.text)
286
+ .join("");
287
+ return renderParentMessage(content, message.details, options.expanded, options.outputPad, theme);
288
+ });
289
+ }
290
+
291
+ pi.registerCommand("subagents", {
292
+ description: "Show available agent definitions and continuable descendants",
293
+ handler: async (_args, ctx) => {
294
+ const discovery =
295
+ sessionDiscovery ??
296
+ coordinator.discoverAvailableAgents(
297
+ ctx.cwd,
298
+ sessionSettings,
299
+ ctx.isProjectTrusted(),
300
+ );
301
+ const parent = await coordinator.parentFromContext(ctx);
302
+ 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
+ const sections = [
309
+ `Mode: ${schedulingMode}`,
310
+ sessionSettings.syncBundledAgents
311
+ ? `Bundled presets: synchronized to ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`
312
+ : "Bundled presets: package defaults (no filesystem sync)",
313
+ `User agent dir: ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`,
314
+ `Agents:\n${formatAgentCatalog(discovery.agents)}`,
315
+ `Children:\n${coordinator.formatCatalog(entries, "descendants")}`,
316
+ ];
317
+ if (discovery.diagnostics.length > 0) {
318
+ sections.push(`Diagnostics:\n${discovery.diagnostics.join("\n")}`);
319
+ }
320
+ ctx.ui.notify(sections.join("\n\n"), "info");
321
+ },
322
+ });
323
+
324
+ pi.on("session_start", async (_event, ctx) => {
325
+ const loaded = loadSettings({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
326
+ sessionSettings = loaded.settings;
327
+ agentSync = sessionSettings.syncBundledAgents
328
+ ? coordinator.synchronizeBundledAgents()
329
+ : undefined;
330
+ sessionDiscovery = coordinator.discoverAvailableAgents(
331
+ ctx.cwd,
332
+ sessionSettings,
333
+ ctx.isProjectTrusted(),
334
+ );
335
+ const registeredParameters = new Map<string, unknown>([
336
+ [
337
+ "subagent",
338
+ registerDelegationTool(pi, coordinator, sessionSettings, sessionDiscovery),
339
+ ],
340
+ [
341
+ "subagent_fork",
342
+ registerForkDelegationTool(pi, coordinator, sessionSettings, sessionDiscovery),
343
+ ],
344
+ ]);
345
+ if (sessionDiscovery.agents.length === 0) {
346
+ disableOwnedTools(pi, registeredParameters);
347
+ }
348
+ if (!sessionSettings.enableRunInBackground) {
349
+ disableOwnedTools(pi, backgroundControlParameters);
350
+ }
351
+ if (!agentSyncNotified && agentSync) {
352
+ agentSyncNotified = true;
353
+ const lines = [`pi-subagent agent config: ${agentSync.userAgentsDir}`];
354
+ if (agentSync.installed.length > 0) {
355
+ lines.push(`installed: ${agentSync.installed.join(", ")}`);
356
+ }
357
+ if (agentSync.updated.length > 0) {
358
+ lines.push(`updated: ${agentSync.updated.join(", ")}`);
359
+ }
360
+ if (agentSync.removed.length > 0) {
361
+ lines.push(`retired: ${agentSync.removed.join(", ")}`);
362
+ }
363
+ if (agentSync.backups.length > 0) {
364
+ lines.push("backups:", ...agentSync.backups.map((backup) => `- ${backup.name}: ${backup.path}`));
365
+ }
366
+ if (
367
+ agentSync.installed.length > 0 ||
368
+ agentSync.updated.length > 0 ||
369
+ agentSync.removed.length > 0 ||
370
+ agentSync.backups.length > 0
371
+ ) {
372
+ ctx.ui.notify(lines.join("\n"), "info");
373
+ }
374
+ if (agentSync.diagnostics.length > 0) {
375
+ ctx.ui.notify(agentSync.diagnostics.join("\n"), "warning");
376
+ }
377
+ }
378
+ });
379
+
380
+ pi.on("session_shutdown", async () => {
381
+ await coordinator.shutdown();
382
+ });
383
+ }
384
+
385
+ export { SubagentCoordinator } from "./coordinator.ts";
386
+ export type { ChildProvider } from "./providers.ts";