import { randomUUID } from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@selesai/code"; import { Type } from "typebox"; import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV, SUBAGENT_ORCHESTRATOR_TARGET_ENV, SUBAGENT_RUN_ID_ENV, SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV, } from "../runs/shared/pi-args.ts"; import { POLL_INTERVAL_MS, TEMP_ROOT_DIR, type SubagentState } from "../shared/types.ts"; import { writeAtomicJson } from "../shared/atomic-json.ts"; const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels"); const REQUESTS_DIR = "requests"; const REPLIES_DIR = "replies"; export const NATIVE_SUPERVISOR_TOOL_NAME = "subagent_supervisor"; const MAX_MESSAGE_BYTES = 64 * 1024; const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000; const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500); type SupervisorReason = "need_decision" | "interview_request" | "progress_update"; interface SupervisorRequest { type: "subagent.supervisor.request"; id: string; createdAt: number; reason: SupervisorReason; message: string; expectsReply: boolean; orchestratorTarget?: string; orchestratorSessionId?: string; runId: string; agent: string; childIndex: number; childTarget?: string; interview?: unknown; } interface PendingSupervisorRequest extends SupervisorRequest { channelDir: string; requestFile: string; } interface SupervisorReply { type: "subagent.supervisor.reply"; requestId: string; createdAt: number; message: string; } interface ContactSupervisorParams { reason: SupervisorReason; message?: string; interview?: unknown; } interface IntercomParams { action: "list" | "send" | "ask" | "reply" | "pending" | "status"; to?: string; message?: string; replyTo?: string; } const ContactSupervisorParamsSchema = Type.Object({ reason: Type.String({ enum: ["need_decision", "interview_request", "progress_update"] }), message: Type.Optional(Type.String()), interview: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: true })), }, { additionalProperties: false }); const IntercomParamsSchema = Type.Object({ action: Type.String({ enum: ["list", "send", "ask", "reply", "pending", "status"] }), to: Type.Optional(Type.String()), message: Type.Optional(Type.String()), replyTo: Type.Optional(Type.String()), }, { additionalProperties: false }); function safeSegment(value: string): string { return value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown"; } export function resolveSupervisorChannelDir(runId: string, agent: string, childIndex: number): string { return path.join(SUPERVISOR_CHANNEL_ROOT, `${safeSegment(runId)}-${safeSegment(agent)}-${childIndex}`); } export function ensureSupervisorChannelDir(channelDir: string): void { fs.mkdirSync(path.join(channelDir, REQUESTS_DIR), { recursive: true, mode: 0o700 }); fs.mkdirSync(path.join(channelDir, REPLIES_DIR), { recursive: true, mode: 0o700 }); } function requestPath(channelDir: string, requestId: string): string { return path.join(channelDir, REQUESTS_DIR, `${safeSegment(requestId)}.json`); } function replyPath(channelDir: string, requestId: string): string { return path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`); } function readTextEnv(name: string): string | undefined { const value = process.env[name]?.trim(); return value ? value : undefined; } function readChildMetadata(): { channelDir: string; runId: string; agent: string; childIndex: number; orchestratorTarget?: string; orchestratorSessionId?: string; childTarget?: string; } | undefined { const channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV); const runId = readTextEnv(SUBAGENT_RUN_ID_ENV); const agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV); const rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV); const orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV); if (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\d+$/.test(rawIndex)) return undefined; return { channelDir, runId, agent, childIndex: Number(rawIndex), orchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV), orchestratorSessionId, childTarget: readTextEnv("SELESAI_SUBAGENT_INTERCOM_SESSION_NAME"), }; } function reasonHeading(reason: SupervisorReason): string { if (reason === "interview_request") return "Subagent requests a structured supervisor interview."; if (reason === "progress_update") return "Subagent progress update."; return "Subagent needs a supervisor decision."; } function formatChildMessage(input: { reason: SupervisorReason; message?: string; interview?: unknown; runId: string; agent: string; childIndex: number; childTarget?: string; }): string { const lines = [ reasonHeading(input.reason), `Run: ${input.runId}`, `Agent: ${input.agent}`, `Child index: ${input.childIndex}`, ]; if (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`); lines.push(""); if (input.message?.trim()) lines.push(input.message.trim()); if (input.reason === "interview_request") { lines.push( "", "Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.", ); if (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, "\t")); } return lines.join("\n").trimEnd(); } function parseStructuredReply(message: string): { value?: unknown; error?: string } { const trimmed = message.trim(); const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim(); try { return { value: JSON.parse(fenced ?? trimmed) }; } catch (error) { return { error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) }; } } function askTimeoutMs(): number { const parsed = Number(process.env.PI_INTERCOM_ASK_TIMEOUT_MS); return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ASK_TIMEOUT_MS; } function delay(ms: number, signal?: AbortSignal): Promise { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new Error("Supervisor request cancelled.")); return; } let timer: ReturnType | undefined; const cleanup = () => { if (timer) clearTimeout(timer); signal?.removeEventListener("abort", onAbort); }; const onAbort = () => { cleanup(); reject(new Error("Supervisor request cancelled.")); }; timer = setTimeout(() => { cleanup(); resolve(); }, ms); signal?.addEventListener("abort", onAbort, { once: true }); }); } async function waitForReply(channelDir: string, requestId: string, signal?: AbortSignal): Promise { const file = replyPath(channelDir, requestId); const deadline = Date.now() + askTimeoutMs(); while (Date.now() <= deadline) { if (signal?.aborted) throw new Error("Supervisor request cancelled."); if (fs.existsSync(file)) { const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as Partial; if (parsed.type === "subagent.supervisor.reply" && parsed.requestId === requestId && typeof parsed.message === "string") { return parsed as SupervisorReply; } } await delay(250, signal); } throw new Error("Timed out waiting for supervisor reply."); } async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise>> { const metadata = readChildMetadata(); if (!metadata) throw new Error("Native supervisor channel is not available for this subagent."); if (params.reason !== "progress_update" && !params.message?.trim() && params.reason !== "interview_request") { throw new Error("message is required for supervisor decisions."); } ensureSupervisorChannelDir(metadata.channelDir); const requestId = randomUUID(); const expectsReply = params.reason !== "progress_update"; const message = formatChildMessage({ ...metadata, reason: params.reason, message: params.message, interview: params.interview }); const request: SupervisorRequest = { type: "subagent.supervisor.request", id: requestId, createdAt: Date.now(), reason: params.reason, message, expectsReply, ...(metadata.orchestratorTarget ? { orchestratorTarget: metadata.orchestratorTarget } : {}), ...(metadata.orchestratorSessionId ? { orchestratorSessionId: metadata.orchestratorSessionId } : {}), runId: metadata.runId, agent: metadata.agent, childIndex: metadata.childIndex, ...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}), ...(params.interview !== undefined ? { interview: params.interview } : {}), }; const serialized = JSON.stringify(request, null, "\t"); if (Buffer.byteLength(serialized, "utf-8") > MAX_MESSAGE_BYTES) throw new Error("Supervisor request is too large."); writeAtomicJson(requestPath(metadata.channelDir, requestId), request); if (!expectsReply) { return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId, reason: params.reason }, }; } const reply = await waitForReply(metadata.channelDir, requestId, signal); const details: Record = { requestId, reason: params.reason }; if (params.reason === "interview_request") { const structured = parseStructuredReply(reply.message); if (structured.error) details.structuredReplyParseError = structured.error; else details.structuredReply = structured.value; } return { content: [{ type: "text", text: `**Reply from supervisor:**\n${reply.message}` }], details, }; } function hasTool(pi: ExtensionAPI, name: string): boolean { try { return pi.getAllTools?.().some((tool: { name?: unknown }) => tool.name === name) === true; } catch { return false; } } export function registerNativeSupervisorClient(pi: ExtensionAPI, options: { includeIntercomFallback?: boolean } = {}): void { if (!readChildMetadata()) return; const includeIntercomFallback = options.includeIntercomFallback !== false; if (!hasTool(pi, "contact_supervisor")) { const tool: ToolDefinition> = { name: "contact_supervisor", label: "Contact Supervisor", description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.", parameters: ContactSupervisorParamsSchema, execute(_id, params, signal) { return sendSupervisorRequest(params as ContactSupervisorParams, signal); }, }; pi.registerTool(tool); } if (includeIntercomFallback && !hasTool(pi, "intercom")) { const tool: ToolDefinition> = { name: "intercom", label: "Intercom", description: "Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.", parameters: IntercomParamsSchema, async execute(_id, params, signal) { const action = (params as IntercomParams).action; if (action === "status") return { content: [{ type: "text", text: "Native supervisor channel is active." }], details: { active: true } }; if (action === "list") return { content: [{ type: "text", text: "Supervisor session available through contact_supervisor." }], details: { sessions: [] } }; if (action === "send") return sendSupervisorRequest({ reason: "progress_update", message: (params as IntercomParams).message ?? "" }, signal); if (action === "ask") return sendSupervisorRequest({ reason: "need_decision", message: (params as IntercomParams).message ?? "" }, signal); throw new Error("Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session."); }, }; pi.registerTool(tool); } } function parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined { try { const parsed = JSON.parse(fs.readFileSync(file, "utf-8")) as Partial; if (parsed.type !== "subagent.supervisor.request") return undefined; if (typeof parsed.id !== "string" || !parsed.id) return undefined; if (parsed.reason !== "need_decision" && parsed.reason !== "interview_request" && parsed.reason !== "progress_update") return undefined; if (typeof parsed.message !== "string" || !parsed.message) return undefined; if (typeof parsed.runId !== "string" || typeof parsed.agent !== "string" || typeof parsed.childIndex !== "number") return undefined; return { ...parsed as SupervisorRequest, channelDir, requestFile: file }; } catch { return undefined; } } function listRequestFiles(): Array<{ channelDir: string; file: string }> { let channelEntries: fs.Dirent[]; try { channelEntries = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; } const files: Array<{ channelDir: string; file: string }> = []; for (const entry of channelEntries) { if (!entry.isDirectory()) continue; const channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name); const requestsDir = path.join(channelDir, REQUESTS_DIR); let requestEntries: fs.Dirent[]; try { requestEntries = fs.readdirSync(requestsDir, { withFileTypes: true }); } catch { continue; } for (const requestEntry of requestEntries) { if (requestEntry.isFile() && requestEntry.name.endsWith(".json")) files.push({ channelDir, file: path.join(requestsDir, requestEntry.name) }); } } return files; } function currentContextSessionId(state: Pick, ctx: ExtensionContext): string | undefined { try { const sessionId = ctx.sessionManager.getSessionId(); if (sessionId) return sessionId; } catch { // Fall through to the last known identity. } return state.currentSessionId ?? undefined; } function requestMatchesContext(request: SupervisorRequest, state: Pick, ctx: ExtensionContext): boolean { const currentSessionId = currentContextSessionId(state, ctx); return Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId); } function formatPendingLine(request: PendingSupervisorRequest): string { const replyHint = request.expectsReply ? ` Reply: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: "reply", replyTo: "${request.id}", message: "..." })` : ""; return `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`; } function requestVisibleText(request: PendingSupervisorRequest): string { const lines = [request.message]; if (request.expectsReply) { lines.push("", `Reply with: ${NATIVE_SUPERVISOR_TOOL_NAME}({ action: "reply", replyTo: "${request.id}", message: "..." })`); } return lines.join("\n"); } function writeReply(request: PendingSupervisorRequest, message: string): void { if (!message.trim()) throw new Error("message is required for supervisor replies."); const reply: SupervisorReply = { type: "subagent.supervisor.reply", requestId: request.id, createdAt: Date.now(), message: message.trim(), }; writeAtomicJson(replyPath(request.channelDir, request.id), reply); try { fs.rmSync(request.requestFile, { force: true }); } catch { // Best effort: the reply file is authoritative for the child. } } function resolvePendingRequest(pending: Map, params: IntercomParams): PendingSupervisorRequest { if (params.replyTo) { const request = pending.get(params.replyTo); if (!request) throw new Error(`No pending supervisor request found for replyTo '${params.replyTo}'.`); return request; } const requests = [...pending.values()].filter((request) => request.expectsReply); if (params.to) { const normalizedTo = params.to.toLowerCase(); const matches = requests.filter((request) => request.id.toLowerCase().startsWith(normalizedTo) || request.agent.toLowerCase() === normalizedTo || request.childTarget?.toLowerCase() === normalizedTo, ); if (matches.length === 1) return matches[0]!; if (matches.length > 1) throw new Error(`Multiple pending supervisor requests match '${params.to}'. Use replyTo.`); } if (requests.length === 1) return requests[0]!; if (requests.length === 0) throw new Error("No pending supervisor requests need a reply."); throw new Error("Multiple pending supervisor requests need replies. Use replyTo."); } function publicPendingRequests(pending: Map): Array> { return [...pending.values()].map((request) => ({ id: request.id, runId: request.runId, agent: request.agent, childIndex: request.childIndex, reason: request.reason, expectsReply: request.expectsReply, })); } function buildParentIntercomTool(pending: Map, name = "intercom"): ToolDefinition> { return { name, label: name === "intercom" ? "Intercom" : "Subagent Supervisor", description: name === "intercom" ? "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests." : "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.", parameters: IntercomParamsSchema, async execute(_id, params) { const input = params as IntercomParams; if (input.action === "status") { return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } }; } if (input.action === "pending" || input.action === "list") { const lines = [...pending.values()].filter((request) => request.expectsReply).map(formatPendingLine); return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No pending supervisor requests." }], details: { pending: publicPendingRequests(pending) } }; } if (input.action === "reply") { const request = resolvePendingRequest(pending, input); writeReply(request, input.message ?? ""); pending.delete(request.id); return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } }; } if (input.action === "send" || input.action === "ask") { throw new Error("Native pi-subagents intercom currently handles supervisor replies. Child agents initiate asks with contact_supervisor."); } throw new Error(`Unsupported intercom action: ${input.action}`); }, }; } export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState): { start: () => void; dispose: () => void; pending: Map } { const pending = new Map(); const seenFiles = new Set(); let poller: ReturnType | undefined; const registerParentTools = (): void => { if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentIntercomTool(pending, NATIVE_SUPERVISOR_TOOL_NAME)); if (!hasTool(pi, "intercom")) pi.registerTool(buildParentIntercomTool(pending)); }; const poll = (): void => { const ctx = state.lastUiContext; if (!ctx) return; for (const { channelDir, file } of listRequestFiles()) { if (seenFiles.has(file)) continue; const request = parseRequestFile(file, channelDir); if (!request || !requestMatchesContext(request, state, ctx)) continue; seenFiles.add(file); if (request.expectsReply) pending.set(request.id, request); else { try { fs.rmSync(request.requestFile, { force: true }); } catch { // Non-blocking progress updates are already delivered to this session. } } pi.sendMessage({ customType: "subagent_supervisor_request", content: requestVisibleText(request), display: true, details: { id: request.id, reason: request.reason, expectsReply: request.expectsReply, runId: request.runId, agent: request.agent, childIndex: request.childIndex, }, }); } }; return { start: () => { if (poller) return; registerParentTools(); poll(); poller = setInterval(poll, CHANNEL_POLL_MS); poller.unref?.(); }, dispose: () => { if (poller) clearInterval(poller); poller = undefined; pending.clear(); seenFiles.clear(); }, pending, }; }