pi-subagents 0.32.0 → 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 (52) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/README.md +147 -58
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +70 -14
  6. package/src/agents/agent-management.ts +177 -5
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +142 -12
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/doctor.ts +1 -9
  12. package/src/extension/fanout-child.ts +2 -2
  13. package/src/extension/index.ts +65 -90
  14. package/src/extension/rpc.ts +369 -0
  15. package/src/extension/schemas.ts +52 -8
  16. package/src/extension/tool-description.ts +200 -0
  17. package/src/intercom/intercom-bridge.ts +21 -253
  18. package/src/intercom/native-supervisor-channel.ts +510 -0
  19. package/src/runs/background/async-execution.ts +51 -7
  20. package/src/runs/background/async-job-tracker.ts +12 -2
  21. package/src/runs/background/async-status.ts +27 -2
  22. package/src/runs/background/completion-batcher.ts +166 -0
  23. package/src/runs/background/control-channel.ts +106 -1
  24. package/src/runs/background/fleet-view.ts +515 -0
  25. package/src/runs/background/notify.ts +161 -44
  26. package/src/runs/background/result-watcher.ts +1 -2
  27. package/src/runs/background/run-id-resolver.ts +3 -2
  28. package/src/runs/background/run-status.ts +166 -6
  29. package/src/runs/background/scheduled-runs.ts +514 -0
  30. package/src/runs/background/subagent-runner.ts +409 -35
  31. package/src/runs/background/wait.ts +353 -0
  32. package/src/runs/foreground/chain-execution.ts +95 -21
  33. package/src/runs/foreground/execution.ts +150 -21
  34. package/src/runs/foreground/subagent-executor.ts +378 -64
  35. package/src/runs/shared/dynamic-fanout.ts +1 -1
  36. package/src/runs/shared/model-fallback.ts +167 -20
  37. package/src/runs/shared/model-scope.ts +128 -0
  38. package/src/runs/shared/nested-events.ts +31 -0
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/pi-args.ts +30 -1
  41. package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
  42. package/src/runs/shared/tool-budget.ts +74 -0
  43. package/src/runs/shared/turn-budget.ts +52 -0
  44. package/src/shared/artifacts.ts +1 -0
  45. package/src/shared/atomic-json.ts +15 -2
  46. package/src/shared/child-transcript.ts +212 -0
  47. package/src/shared/settings.ts +3 -1
  48. package/src/shared/types.ts +134 -19
  49. package/src/slash/prompt-workflows.ts +330 -0
  50. package/src/slash/slash-commands.ts +16 -2
  51. package/src/tui/render.ts +16 -8
  52. package/src/extension/companion-suggestions.ts +0 -359
@@ -0,0 +1,510 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
6
+ import { Type } from "typebox";
7
+ import {
8
+ SUBAGENT_CHILD_AGENT_ENV,
9
+ SUBAGENT_CHILD_INDEX_ENV,
10
+ SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,
11
+ SUBAGENT_ORCHESTRATOR_TARGET_ENV,
12
+ SUBAGENT_RUN_ID_ENV,
13
+ SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
14
+ } from "../runs/shared/pi-args.ts";
15
+ import { POLL_INTERVAL_MS, TEMP_ROOT_DIR, type SubagentState } from "../shared/types.ts";
16
+ import { writeAtomicJson } from "../shared/atomic-json.ts";
17
+
18
+ const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
19
+ const REQUESTS_DIR = "requests";
20
+ const REPLIES_DIR = "replies";
21
+ const MAX_MESSAGE_BYTES = 64 * 1024;
22
+ const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;
23
+ const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
24
+
25
+ type SupervisorReason = "need_decision" | "interview_request" | "progress_update";
26
+
27
+ interface SupervisorRequest {
28
+ type: "subagent.supervisor.request";
29
+ id: string;
30
+ createdAt: number;
31
+ reason: SupervisorReason;
32
+ message: string;
33
+ expectsReply: boolean;
34
+ orchestratorTarget?: string;
35
+ orchestratorSessionId?: string;
36
+ runId: string;
37
+ agent: string;
38
+ childIndex: number;
39
+ childTarget?: string;
40
+ interview?: unknown;
41
+ }
42
+
43
+ interface PendingSupervisorRequest extends SupervisorRequest {
44
+ channelDir: string;
45
+ requestFile: string;
46
+ }
47
+
48
+ interface SupervisorReply {
49
+ type: "subagent.supervisor.reply";
50
+ requestId: string;
51
+ createdAt: number;
52
+ message: string;
53
+ }
54
+
55
+ interface ContactSupervisorParams {
56
+ reason: SupervisorReason;
57
+ message?: string;
58
+ interview?: unknown;
59
+ }
60
+
61
+ interface IntercomParams {
62
+ action: "list" | "send" | "ask" | "reply" | "pending" | "status";
63
+ to?: string;
64
+ message?: string;
65
+ replyTo?: string;
66
+ }
67
+
68
+ const ContactSupervisorParamsSchema = Type.Object({
69
+ reason: Type.String({ enum: ["need_decision", "interview_request", "progress_update"] }),
70
+ message: Type.Optional(Type.String()),
71
+ interview: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true })),
72
+ }, { additionalProperties: false });
73
+
74
+ const IntercomParamsSchema = Type.Object({
75
+ action: Type.String({ enum: ["list", "send", "ask", "reply", "pending", "status"] }),
76
+ to: Type.Optional(Type.String()),
77
+ message: Type.Optional(Type.String()),
78
+ replyTo: Type.Optional(Type.String()),
79
+ }, { additionalProperties: false });
80
+
81
+ function safeSegment(value: string): string {
82
+ return value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
83
+ }
84
+
85
+ export function resolveSupervisorChannelDir(runId: string, agent: string, childIndex: number): string {
86
+ return path.join(SUPERVISOR_CHANNEL_ROOT, `${safeSegment(runId)}-${safeSegment(agent)}-${childIndex}`);
87
+ }
88
+
89
+ export function ensureSupervisorChannelDir(channelDir: string): void {
90
+ fs.mkdirSync(path.join(channelDir, REQUESTS_DIR), { recursive: true, mode: 0o700 });
91
+ fs.mkdirSync(path.join(channelDir, REPLIES_DIR), { recursive: true, mode: 0o700 });
92
+ }
93
+
94
+ function requestPath(channelDir: string, requestId: string): string {
95
+ return path.join(channelDir, REQUESTS_DIR, `${safeSegment(requestId)}.json`);
96
+ }
97
+
98
+ function replyPath(channelDir: string, requestId: string): string {
99
+ return path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`);
100
+ }
101
+
102
+ function readTextEnv(name: string): string | undefined {
103
+ const value = process.env[name]?.trim();
104
+ return value ? value : undefined;
105
+ }
106
+
107
+ function readChildMetadata(): {
108
+ channelDir: string;
109
+ runId: string;
110
+ agent: string;
111
+ childIndex: number;
112
+ orchestratorTarget?: string;
113
+ orchestratorSessionId?: string;
114
+ childTarget?: string;
115
+ } | undefined {
116
+ const channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV);
117
+ const runId = readTextEnv(SUBAGENT_RUN_ID_ENV);
118
+ const agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV);
119
+ const rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV);
120
+ const orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV);
121
+ if (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\d+$/.test(rawIndex)) return undefined;
122
+ return {
123
+ channelDir,
124
+ runId,
125
+ agent,
126
+ childIndex: Number(rawIndex),
127
+ orchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV),
128
+ orchestratorSessionId,
129
+ childTarget: readTextEnv("PI_SUBAGENT_INTERCOM_SESSION_NAME"),
130
+ };
131
+ }
132
+
133
+ function reasonHeading(reason: SupervisorReason): string {
134
+ if (reason === "interview_request") return "Subagent requests a structured supervisor interview.";
135
+ if (reason === "progress_update") return "Subagent progress update.";
136
+ return "Subagent needs a supervisor decision.";
137
+ }
138
+
139
+ function formatChildMessage(input: {
140
+ reason: SupervisorReason;
141
+ message?: string;
142
+ interview?: unknown;
143
+ runId: string;
144
+ agent: string;
145
+ childIndex: number;
146
+ childTarget?: string;
147
+ }): string {
148
+ const lines = [
149
+ reasonHeading(input.reason),
150
+ `Run: ${input.runId}`,
151
+ `Agent: ${input.agent}`,
152
+ `Child index: ${input.childIndex}`,
153
+ ];
154
+ if (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`);
155
+ lines.push("");
156
+ if (input.message?.trim()) lines.push(input.message.trim());
157
+ if (input.reason === "interview_request") {
158
+ lines.push(
159
+ "",
160
+ "Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
161
+ );
162
+ if (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, "\t"));
163
+ }
164
+ return lines.join("\n").trimEnd();
165
+ }
166
+
167
+ function parseStructuredReply(message: string): { value?: unknown; error?: string } {
168
+ const trimmed = message.trim();
169
+ const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim();
170
+ try {
171
+ return { value: JSON.parse(fenced ?? trimmed) };
172
+ } catch (error) {
173
+ return { error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };
174
+ }
175
+ }
176
+
177
+ function askTimeoutMs(): number {
178
+ const parsed = Number(process.env.PI_INTERCOM_ASK_TIMEOUT_MS);
179
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ASK_TIMEOUT_MS;
180
+ }
181
+
182
+ function delay(ms: number, signal?: AbortSignal): Promise<void> {
183
+ return new Promise((resolve, reject) => {
184
+ if (signal?.aborted) {
185
+ reject(new Error("Supervisor request cancelled."));
186
+ return;
187
+ }
188
+ let timer: ReturnType<typeof setTimeout> | undefined;
189
+ const cleanup = () => {
190
+ if (timer) clearTimeout(timer);
191
+ signal?.removeEventListener("abort", onAbort);
192
+ };
193
+ const onAbort = () => {
194
+ cleanup();
195
+ reject(new Error("Supervisor request cancelled."));
196
+ };
197
+ timer = setTimeout(() => {
198
+ cleanup();
199
+ resolve();
200
+ }, ms);
201
+ signal?.addEventListener("abort", onAbort, { once: true });
202
+ });
203
+ }
204
+
205
+ async function waitForReply(channelDir: string, requestId: string, signal?: AbortSignal): Promise<SupervisorReply> {
206
+ const file = replyPath(channelDir, requestId);
207
+ const deadline = Date.now() + askTimeoutMs();
208
+ while (Date.now() <= deadline) {
209
+ if (signal?.aborted) throw new Error("Supervisor request cancelled.");
210
+ if (fs.existsSync(file)) {
211
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as Partial<SupervisorReply>;
212
+ if (parsed.type === "subagent.supervisor.reply" && parsed.requestId === requestId && typeof parsed.message === "string") {
213
+ return parsed as SupervisorReply;
214
+ }
215
+ }
216
+ await delay(250, signal);
217
+ }
218
+ throw new Error("Timed out waiting for supervisor reply.");
219
+ }
220
+
221
+ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise<AgentToolResult<Record<string, unknown>>> {
222
+ const metadata = readChildMetadata();
223
+ if (!metadata) throw new Error("Native supervisor channel is not available for this subagent.");
224
+ if (params.reason !== "progress_update" && !params.message?.trim() && params.reason !== "interview_request") {
225
+ throw new Error("message is required for supervisor decisions.");
226
+ }
227
+ ensureSupervisorChannelDir(metadata.channelDir);
228
+ const requestId = randomUUID();
229
+ const expectsReply = params.reason !== "progress_update";
230
+ const message = formatChildMessage({ ...metadata, reason: params.reason, message: params.message, interview: params.interview });
231
+ const request: SupervisorRequest = {
232
+ type: "subagent.supervisor.request",
233
+ id: requestId,
234
+ createdAt: Date.now(),
235
+ reason: params.reason,
236
+ message,
237
+ expectsReply,
238
+ ...(metadata.orchestratorTarget ? { orchestratorTarget: metadata.orchestratorTarget } : {}),
239
+ ...(metadata.orchestratorSessionId ? { orchestratorSessionId: metadata.orchestratorSessionId } : {}),
240
+ runId: metadata.runId,
241
+ agent: metadata.agent,
242
+ childIndex: metadata.childIndex,
243
+ ...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}),
244
+ ...(params.interview !== undefined ? { interview: params.interview } : {}),
245
+ };
246
+ const serialized = JSON.stringify(request, null, "\t");
247
+ if (Buffer.byteLength(serialized, "utf-8") > MAX_MESSAGE_BYTES) throw new Error("Supervisor request is too large.");
248
+ writeAtomicJson(requestPath(metadata.channelDir, requestId), request);
249
+
250
+ if (!expectsReply) {
251
+ return {
252
+ content: [{ type: "text", text: "Supervisor progress update queued." }],
253
+ details: { delivered: true, requestId, reason: params.reason },
254
+ };
255
+ }
256
+
257
+ const reply = await waitForReply(metadata.channelDir, requestId, signal);
258
+ const details: Record<string, unknown> = { requestId, reason: params.reason };
259
+ if (params.reason === "interview_request") {
260
+ const structured = parseStructuredReply(reply.message);
261
+ if (structured.error) details.structuredReplyParseError = structured.error;
262
+ else details.structuredReply = structured.value;
263
+ }
264
+ return {
265
+ content: [{ type: "text", text: `**Reply from supervisor:**\n${reply.message}` }],
266
+ details,
267
+ };
268
+ }
269
+
270
+ function hasTool(pi: ExtensionAPI, name: string): boolean {
271
+ try {
272
+ return pi.getAllTools?.().some((tool: { name?: unknown }) => tool.name === name) === true;
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+
278
+ export function registerNativeSupervisorClient(pi: ExtensionAPI): void {
279
+ if (!readChildMetadata()) return;
280
+ if (!hasTool(pi, "contact_supervisor")) {
281
+ const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
282
+ name: "contact_supervisor",
283
+ label: "Contact Supervisor",
284
+ description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
285
+ parameters: ContactSupervisorParamsSchema,
286
+ execute(_id, params, signal) {
287
+ return sendSupervisorRequest(params as ContactSupervisorParams, signal);
288
+ },
289
+ };
290
+ pi.registerTool(tool);
291
+ }
292
+ if (!hasTool(pi, "intercom")) {
293
+ const tool: ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> = {
294
+ name: "intercom",
295
+ label: "Intercom",
296
+ description: "Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.",
297
+ parameters: IntercomParamsSchema,
298
+ async execute(_id, params, signal) {
299
+ const action = (params as IntercomParams).action;
300
+ if (action === "status") return { content: [{ type: "text", text: "Native supervisor channel is active." }], details: { active: true } };
301
+ if (action === "list") return { content: [{ type: "text", text: "Supervisor session available through contact_supervisor." }], details: { sessions: [] } };
302
+ if (action === "send") return sendSupervisorRequest({ reason: "progress_update", message: (params as IntercomParams).message ?? "" }, signal);
303
+ if (action === "ask") return sendSupervisorRequest({ reason: "need_decision", message: (params as IntercomParams).message ?? "" }, signal);
304
+ throw new Error("Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session.");
305
+ },
306
+ };
307
+ pi.registerTool(tool);
308
+ }
309
+ }
310
+
311
+ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined {
312
+ try {
313
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as Partial<SupervisorRequest>;
314
+ if (parsed.type !== "subagent.supervisor.request") return undefined;
315
+ if (typeof parsed.id !== "string" || !parsed.id) return undefined;
316
+ if (parsed.reason !== "need_decision" && parsed.reason !== "interview_request" && parsed.reason !== "progress_update") return undefined;
317
+ if (typeof parsed.message !== "string" || !parsed.message) return undefined;
318
+ if (typeof parsed.runId !== "string" || typeof parsed.agent !== "string" || typeof parsed.childIndex !== "number") return undefined;
319
+ return { ...parsed as SupervisorRequest, channelDir, requestFile: file };
320
+ } catch {
321
+ return undefined;
322
+ }
323
+ }
324
+
325
+ function listRequestFiles(): Array<{ channelDir: string; file: string }> {
326
+ let channelEntries: fs.Dirent[];
327
+ try {
328
+ channelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true });
329
+ } catch (error) {
330
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
331
+ throw error;
332
+ }
333
+ const files: Array<{ channelDir: string; file: string }> = [];
334
+ for (const entry of channelEntries) {
335
+ if (!entry.isDirectory()) continue;
336
+ const channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);
337
+ const requestsDir = path.join(channelDir, REQUESTS_DIR);
338
+ let requestEntries: fs.Dirent[];
339
+ try {
340
+ requestEntries = fs.readdirSync(requestsDir, { withFileTypes: true });
341
+ } catch {
342
+ continue;
343
+ }
344
+ for (const requestEntry of requestEntries) {
345
+ if (requestEntry.isFile() && requestEntry.name.endsWith(".json")) files.push({ channelDir, file: path.join(requestsDir, requestEntry.name) });
346
+ }
347
+ }
348
+ return files;
349
+ }
350
+
351
+ function currentContextSessionId(state: Pick<SubagentState, "currentSessionId">, ctx: ExtensionContext): string | undefined {
352
+ if (state.currentSessionId) return state.currentSessionId;
353
+ try {
354
+ return ctx.sessionManager.getSessionId();
355
+ } catch {
356
+ return undefined;
357
+ }
358
+ }
359
+
360
+ function requestMatchesContext(request: SupervisorRequest, state: Pick<SubagentState, "currentSessionId">, ctx: ExtensionContext): boolean {
361
+ const currentSessionId = currentContextSessionId(state, ctx);
362
+ return Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId);
363
+ }
364
+
365
+ function formatPendingLine(request: PendingSupervisorRequest): string {
366
+ const replyHint = request.expectsReply ? ` Reply: intercom({ action: "reply", replyTo: "${request.id}", message: "..." })` : "";
367
+ return `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`;
368
+ }
369
+
370
+ function requestVisibleText(request: PendingSupervisorRequest): string {
371
+ const lines = [request.message];
372
+ if (request.expectsReply) {
373
+ lines.push("", `Reply with: intercom({ action: "reply", replyTo: "${request.id}", message: "..." })`);
374
+ }
375
+ return lines.join("\n");
376
+ }
377
+
378
+ function writeReply(request: PendingSupervisorRequest, message: string): void {
379
+ if (!message.trim()) throw new Error("message is required for supervisor replies.");
380
+ const reply: SupervisorReply = {
381
+ type: "subagent.supervisor.reply",
382
+ requestId: request.id,
383
+ createdAt: Date.now(),
384
+ message: message.trim(),
385
+ };
386
+ writeAtomicJson(replyPath(request.channelDir, request.id), reply);
387
+ try {
388
+ fs.rmSync(request.requestFile, { force: true });
389
+ } catch {
390
+ // Best effort: the reply file is authoritative for the child.
391
+ }
392
+ }
393
+
394
+ function resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, params: IntercomParams): PendingSupervisorRequest {
395
+ if (params.replyTo) {
396
+ const request = pending.get(params.replyTo);
397
+ if (!request) throw new Error(`No pending supervisor request found for replyTo '${params.replyTo}'.`);
398
+ return request;
399
+ }
400
+ const requests = [...pending.values()].filter((request) => request.expectsReply);
401
+ if (params.to) {
402
+ const normalizedTo = params.to.toLowerCase();
403
+ const matches = requests.filter((request) =>
404
+ request.id.toLowerCase().startsWith(normalizedTo)
405
+ || request.agent.toLowerCase() === normalizedTo
406
+ || request.childTarget?.toLowerCase() === normalizedTo,
407
+ );
408
+ if (matches.length === 1) return matches[0]!;
409
+ if (matches.length > 1) throw new Error(`Multiple pending supervisor requests match '${params.to}'. Use replyTo.`);
410
+ }
411
+ if (requests.length === 1) return requests[0]!;
412
+ if (requests.length === 0) throw new Error("No pending supervisor requests need a reply.");
413
+ throw new Error("Multiple pending supervisor requests need replies. Use replyTo.");
414
+ }
415
+
416
+ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>): Array<Record<string, unknown>> {
417
+ return [...pending.values()].map((request) => ({
418
+ id: request.id,
419
+ runId: request.runId,
420
+ agent: request.agent,
421
+ childIndex: request.childIndex,
422
+ reason: request.reason,
423
+ expectsReply: request.expectsReply,
424
+ }));
425
+ }
426
+
427
+ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
428
+ return {
429
+ name: "intercom",
430
+ label: "Intercom",
431
+ description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests.",
432
+ parameters: IntercomParamsSchema,
433
+ async execute(_id, params) {
434
+ const input = params as IntercomParams;
435
+ if (input.action === "status") {
436
+ return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };
437
+ }
438
+ if (input.action === "pending" || input.action === "list") {
439
+ const lines = [...pending.values()].filter((request) => request.expectsReply).map(formatPendingLine);
440
+ return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No pending supervisor requests." }], details: { pending: publicPendingRequests(pending) } };
441
+ }
442
+ if (input.action === "reply") {
443
+ const request = resolvePendingRequest(pending, input);
444
+ writeReply(request, input.message ?? "");
445
+ pending.delete(request.id);
446
+ return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
447
+ }
448
+ if (input.action === "send" || input.action === "ask") {
449
+ throw new Error("Native pi-subagents intercom currently handles supervisor replies. Child agents initiate asks with contact_supervisor.");
450
+ }
451
+ throw new Error(`Unsupported intercom action: ${input.action}`);
452
+ },
453
+ };
454
+ }
455
+
456
+ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
457
+ const pending = new Map<string, PendingSupervisorRequest>();
458
+ const seenFiles = new Set<string>();
459
+ let poller: ReturnType<typeof setInterval> | undefined;
460
+
461
+ if (!hasTool(pi, "intercom")) pi.registerTool(buildParentIntercomTool(pending));
462
+
463
+ const poll = (): void => {
464
+ const ctx = state.lastUiContext;
465
+ if (!ctx) return;
466
+ for (const { channelDir, file } of listRequestFiles()) {
467
+ if (seenFiles.has(file)) continue;
468
+ const request = parseRequestFile(file, channelDir);
469
+ if (!request || !requestMatchesContext(request, state, ctx)) continue;
470
+ seenFiles.add(file);
471
+ if (request.expectsReply) pending.set(request.id, request);
472
+ else {
473
+ try {
474
+ fs.rmSync(request.requestFile, { force: true });
475
+ } catch {
476
+ // Non-blocking progress updates are already delivered to this session.
477
+ }
478
+ }
479
+ pi.sendMessage({
480
+ customType: "subagent_supervisor_request",
481
+ content: requestVisibleText(request),
482
+ display: true,
483
+ details: {
484
+ id: request.id,
485
+ reason: request.reason,
486
+ expectsReply: request.expectsReply,
487
+ runId: request.runId,
488
+ agent: request.agent,
489
+ childIndex: request.childIndex,
490
+ },
491
+ });
492
+ }
493
+ };
494
+
495
+ return {
496
+ start: () => {
497
+ if (poller) return;
498
+ poll();
499
+ poller = setInterval(poll, CHANNEL_POLL_MS);
500
+ poller.unref?.();
501
+ },
502
+ dispose: () => {
503
+ if (poller) clearInterval(poller);
504
+ poller = undefined;
505
+ pending.clear();
506
+ seenFiles.clear();
507
+ },
508
+ pending,
509
+ };
510
+ }