import { randomUUID } from "node:crypto"; import { fileURLToPath } from "node:url"; import type { AgentConfig } from "../agents/agents.ts"; import type { ReadonlyInput } from "../shared/types/inputs.ts"; import { isRecord } from "../shared/unknown.ts"; import { SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT, SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT, type IntercomEventBus, } from "../shared/types.ts"; const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat"; // Resolve the bundled intercom extension next to this module in both layouts: // TypeScript sources (tests, jiti) and compiled dist output. const BUNDLED_INTERCOM_EXTENSION_PATH = fileURLToPath( new URL( import.meta.url.endsWith(".ts") ? "../pi-intercom/index.ts" : "../pi-intercom/index.js", import.meta.url, ), ); export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:"; const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the supervisor in normal assistant text. Use contact_supervisor first. It resolves the supervisor session "{orchestratorTarget}" and run metadata automatically. - If you cannot safely continue without one decision, approval, or product/API/scope clarification: contact_supervisor({ reason: "need_decision", message: "" }). It steers the supervisor and keeps this ephemeral child alive for the reply. - If you cannot safely continue until the supervisor provides multiple structured answers: contact_supervisor({ reason: "interview_request", message: "", interview: { questions: [{ id: "", type: "text", question: "" }] } }). It also steers and keeps this child alive. - After blocking contact_supervisor decisions or interviews, continue only after the reply arrives. Do not finish your final response with a choose-one question. - Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions. Review-only/no-edit wins; leave files unchanged and mention the conflict in your final result only if it matters. - A discovery or change the supervisor needs while working: contact_supervisor({ reason: "progress_update", message: "UPDATE: " }). This steers at the next tool boundary without waiting for a reply. Skip starts, redundant status, and routine completion; retain material findings in your final result. - Generic intercom is lower-level fallback only; call load_intercom({}) first if intercom is inactive. Use blocking intercom({ action: "ask", to: "{orchestratorTarget}", delivery: "steer", message: "" }) only when this child must remain alive for the answer. - Treat a supervisor nudge as supplemental coordination within the active task: incorporate relevant context and continue. Replace the task only when the nudge explicitly says so. If it requests an answer, respond with intercom({ action: "send", to: "{orchestratorTarget}", delivery: "steer", message: "" }). Messages explicitly marked “Direct user message to this agent” come from the human through the owning parent's Agents view. Treat them as user direction, not peer advice, and answer in your own conversation; the human sees it without a parent relay. Regular peer messages retain their existing authority boundaries. Do not use contact_supervisor or intercom for routine completion handoffs. If no coordination is needed, return a focused task result.`; export interface IntercomBridgeState { orchestratorTarget: string; instruction: string; } export function resolveIntercomSessionTarget( sessionName: string | undefined, sessionId: string, ): string { const trimmedName = sessionName?.trim(); if (trimmedName !== undefined && trimmedName.length > 0) { return trimmedName; } const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId; return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`; } export function resolveOrchestratorIntercomTarget( events: ReadonlyInput, fallback: string, ): string { if (typeof events.on !== "function" || typeof events.emit !== "function") { return fallback; } const requestId = randomUUID(); let exactTarget: string | undefined; let unsubscribe: (() => void) | undefined; let failed = false; try { unsubscribe = events.on(SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT, (payload) => { if (!isRecord(payload)) { return; } const response = payload; if ( response.requestId === requestId && typeof response.sessionId === "string" && response.sessionId.trim().length > 0 ) { exactTarget = response.sessionId; } }); events.emit(SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT, { requestId }); } catch { failed = true; } finally { try { unsubscribe?.(); } catch { failed = true; } } return failed ? fallback : (exactTarget ?? fallback); } function sanitizeIntercomTargetPart(value: string): string { const normalized = value .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "-") .replace(/^-+|-+$/g, ""); return normalized.length > 0 ? normalized : "agent"; } export function resolveSubagentIntercomTarget( runId: string, agent: string, index?: number, ): string { const stepSuffix = index !== undefined ? `-${index + 1}` : ""; return `subagent-${sanitizeIntercomTargetPart(agent)}-${sanitizeIntercomTargetPart(runId)}${stepSuffix}`; } function isIntercomExtensionEntry(entry: string): boolean { return entry.trim().replaceAll("\\", "/").toLowerCase().split("/").includes("pi-intercom"); } function extensionSandboxAllowsIntercom(extensions: readonly string[] | undefined): boolean { return extensions === undefined || extensions.some(isIntercomExtensionEntry); } function resolveBundledIntercomExtensions( extensions: readonly string[] | undefined, ): readonly string[] | undefined { if (extensions === undefined) { return undefined; } const resolved = [ ...new Set( extensions.map((entry) => isIntercomExtensionEntry(entry) ? BUNDLED_INTERCOM_EXTENSION_PATH : entry, ), ), ]; return resolved.length === extensions.length && resolved.every((entry, index) => entry === extensions[index]) ? extensions : resolved; } function buildIntercomBridgeInstruction(orchestratorTarget: string): string { return `${INTERCOM_BRIDGE_MARKER} ${DEFAULT_INTERCOM_BRIDGE_TEMPLATE.replaceAll("{orchestratorTarget}", orchestratorTarget).trim()}`; } export function resolveIntercomBridge(orchestratorTarget: string): IntercomBridgeState { const target = orchestratorTarget.trim(); return { orchestratorTarget: target, instruction: buildIntercomBridgeInstruction(target), }; } function appendBridgeInstruction(prompt: string, instruction: string): string { const trimmed = prompt.trim(); if (trimmed.includes(INTERCOM_BRIDGE_MARKER)) { return trimmed; } return trimmed.length > 0 ? `${trimmed}\n\n${instruction}` : instruction; } export function applyIntercomBridgeToAgent< T extends ReadonlyInput>, >(agent: T, bridge: Readonly): T { if (!extensionSandboxAllowsIntercom(agent.extensions)) { return agent; } const bridgeTools = ["intercom", "contact_supervisor"]; const missingTools = agent.tools ? bridgeTools.filter((tool) => agent.tools?.includes(tool) !== true) : []; const tools = agent.tools && missingTools.length > 0 ? [...agent.tools, ...missingTools] : agent.tools; const extensions = resolveBundledIntercomExtensions(agent.extensions); const instruction = bridge.instruction; const systemPrompt = appendBridgeInstruction(agent.systemPrompt, instruction); if ( tools === agent.tools && systemPrompt === agent.systemPrompt && extensions === agent.extensions ) { return agent; } return { ...agent, tools, systemPrompt, extensions, }; }