/** * inter-agent-pi * Pi extension for connecting to the inter-agent message bus * * Provides commands and tools to send, broadcast, list sessions, check status, * inspect local identity, and receive incoming messages as Pi notifications. * * Installation: * ```bash * pi install npm:@arcanemachine/inter-agent-pi * ``` * * Or load directly from the source checkout: * ```bash * pi -e /path/to/inter-agent-pi * ``` */ import type { ContextEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme, } from "@earendil-works/pi-coding-agent"; import type { AutocompleteItem, Component } from "@earendil-works/pi-tui"; import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { spawn, ChildProcess } from "node:child_process"; type PiCommandInfo = ReturnType[number]; type PiSourceInfo = PiCommandInfo["sourceInfo"]; function samePiSource(left: PiSourceInfo, right: PiSourceInfo): boolean { return ( left.source === right.source && left.scope === right.scope && left.origin === right.origin && left.baseDir === right.baseDir ); } // Test seam: production uses Node's `spawn`; behavior tests inject a fake // factory to drive listener stdout without a real bus. Keep the default so the // real runtime is unchanged. let spawnChildProcess: typeof spawn = spawn; /** @internal Replace the child process factory for behavior tests. */ export function _setSpawnForTest(impl: typeof spawn | null): void { spawnChildProcess = impl ?? spawn; } // Test seam: production resolves a process-global one-use carrier; behavior // tests inject a fake carrier so reload lifecycle cases stay hermetic. let reloadCarrierOverride: ReloadHandoffCarrier | null = null; const defaultProcessCarrier = createProcessGlobalHandoffCarrier(); function resolveReloadCarrier(): ReloadHandoffCarrier { return reloadCarrierOverride ?? defaultProcessCarrier; } /** @internal Replace the reload handoff carrier for behavior tests. */ export function _setReloadCarrierForTest( carrier: ReloadHandoffCarrier | null, ): void { reloadCarrierOverride = carrier; } import { accessSync, constants, existsSync, lstatSync, readFileSync, realpathSync, } from "node:fs"; import { homedir } from "node:os"; import { delimiter, dirname, isAbsolute, join, relative, resolve, } from "node:path"; import { MAILBOX_MAX_UNREAD, MAILBOX_NOTICE_DEBOUNCE_MS_DEFAULT, MailboxDispatcher, buildNoticeCompact, buildNoticeExpanded, createProcessGlobalHandoffCarrier, deriveInboundMetadata, describeDestination, effectiveDeliveryMode, effectiveDebounceMs, isValidDebounceMs, isValidDeliveryMode, } from "./mailbox.js"; import type { InboundImmediateMessage, MailboxSnapshot, ReloadHandoffCarrier, } from "./mailbox.js"; import { ControlController, ControlEngine, CONTROL_CUSTOM_TYPE, } from "./control.js"; import type { ControlControllerHost, ControlHost, ControlResponse, ControlToolResult, ControlWireRequest, } from "./control.js"; // ── Configuration ─────────────────────────────────────────────────────────── interface InterAgentConfig { projectPaths?: string[]; projectPathsExplicit?: boolean; projectPathsError?: string; host?: string; port?: number | string; dataDir?: string; secret?: string; tls?: boolean | string; tlsCert?: string; tlsKey?: string; deliveryMode?: string; mailboxNoticeDebounceMs?: number; } interface RawInterAgentConfig extends Omit< InterAgentConfig, "projectPaths" | "projectPathsExplicit" | "projectPathsError" > { projectPaths?: unknown; projectPath?: unknown; } interface Settings { interAgent?: RawInterAgentConfig; } function managedRuntimeVenv(): string { return join(homedir(), ".pi", "agent", "inter-agent", "venv"); } const RUNTIME_SETUP_DOCS = "README.md"; const SETUP_PYTHON_ENV = "INTER_AGENT_PI_SETUP_PYTHON"; const SETUP_SOURCE_ENV = "INTER_AGENT_PI_SETUP_SOURCE"; const SETUP_HELPER_REQUIREMENT = "inter-agent-pi~=0.3.1"; const SETUP_OUTPUT_MAX_BYTES = 8 * 1024; const SETUP_PROCESS_TIMEOUT_MS = 120_000; const PROJECT_PATHS_CONFIG_ERROR = "interAgent.projectPaths must be a non-empty list of non-empty strings"; const LEGACY_PROJECT_PATH_ERROR = "interAgent.projectPath is no longer supported; use interAgent.projectPaths with a list of checkout paths"; function expandHome(path: string): string { if (path === "~") return homedir(); if (path.startsWith("~/")) return join(homedir(), path.slice(2)); return path; } function resolvePathOption(path: string | undefined, baseDir: string) { if (!path) return path; const expanded = expandHome(path); return isAbsolute(expanded) ? expanded : resolve(baseDir, expanded); } function resolveProjectPaths( value: unknown, baseDir: string, ): { paths?: string[]; error?: string } { if ( !Array.isArray(value) || value.length === 0 || !value.every( (candidate): candidate is string => typeof candidate === "string" && candidate.trim().length > 0, ) ) { return { error: PROJECT_PATHS_CONFIG_ERROR }; } return { paths: value.map((candidate) => resolvePathOption(candidate, baseDir)!), }; } function resolveConfigPaths( config: RawInterAgentConfig, settingsPath: string, ): InterAgentConfig { const baseDir = dirname(settingsPath); const { projectPath: _legacyProjectPath, projectPaths: rawProjectPaths, ...rest } = config; const resolved: InterAgentConfig = { ...rest }; if (Object.prototype.hasOwnProperty.call(config, "dataDir")) { resolved.dataDir = resolvePathOption(config.dataDir, baseDir); } if (Object.prototype.hasOwnProperty.call(config, "tlsCert")) { resolved.tlsCert = resolvePathOption(config.tlsCert, baseDir); } if (Object.prototype.hasOwnProperty.call(config, "tlsKey")) { resolved.tlsKey = resolvePathOption(config.tlsKey, baseDir); } if (Object.prototype.hasOwnProperty.call(config, "projectPath")) { return { ...resolved, projectPaths: undefined, projectPathsError: LEGACY_PROJECT_PATH_ERROR, }; } if (Object.prototype.hasOwnProperty.call(config, "projectPaths")) { const normalized = resolveProjectPaths(rawProjectPaths, baseDir); return { ...resolved, projectPaths: normalized.paths, projectPathsError: normalized.error, }; } return resolved; } function mergeConfig( current: InterAgentConfig, next: RawInterAgentConfig, settingsPath: string, ): InterAgentConfig { const resolved = resolveConfigPaths(next, settingsPath); const hasProjectPaths = Object.prototype.hasOwnProperty.call( next, "projectPaths", ); const hasLegacyProjectPath = Object.prototype.hasOwnProperty.call( next, "projectPath", ); return { ...current, ...resolved, projectPathsExplicit: hasProjectPaths || hasLegacyProjectPath || current.projectPathsExplicit === true, projectPathsError: hasProjectPaths || hasLegacyProjectPath ? resolved.projectPathsError : current.projectPathsError, }; } function loadConfig(): InterAgentConfig { const globalSettingsPath = join(homedir(), ".pi", "agent", "settings.json"); const projectSettingsPath = join(process.cwd(), ".pi", "settings.json"); let config: InterAgentConfig = {}; // Load global settings first if (existsSync(globalSettingsPath)) { try { const parsed: Settings = JSON.parse( readFileSync(globalSettingsPath, "utf-8"), ); if (parsed.interAgent) { config = mergeConfig(config, parsed.interAgent, globalSettingsPath); } } catch { // Invalid JSON, ignore } } // Project settings override global if (existsSync(projectSettingsPath)) { try { const parsed: Settings = JSON.parse( readFileSync(projectSettingsPath, "utf-8"), ); if (parsed.interAgent) { config = mergeConfig(config, parsed.interAgent, projectSettingsPath); } } catch { // Invalid JSON, ignore } } return config; } type FailureRemedy = "setup" | "doctor"; interface InterAgentScripts { pi: string; connect: string; server: string; unavailableMessage?: string; unavailableRemedy?: FailureRemedy; } function isExecutable(path: string): boolean { try { accessSync(path, constants.X_OK); return true; } catch { return false; } } function scriptsFromBinDir(binDir: string): InterAgentScripts { return { pi: join(binDir, "inter-agent-pi"), connect: join(binDir, "inter-agent-connect"), server: join(binDir, "inter-agent-server"), }; } function scriptsAvailable(scripts: InterAgentScripts): boolean { return [scripts.pi, scripts.connect, scripts.server].every( (script) => regularExecutable(script) && viableInterpreter(script), ); } function regularExecutable(path: string): boolean { try { return lstatSync(path).isFile() && isExecutable(path); } catch { return false; } } function viableInterpreter(path: string): boolean { try { const firstLine = readFileSync(path, "utf8").split(/\r?\n/, 1)[0]; if (!firstLine.startsWith("#!")) return false; const parts = firstLine.slice(2).trim().split(/\s+/).filter(Boolean); if (parts.length === 0) return false; if (parts[0] === "/usr/bin/env") { const interpreter = parts.slice(1).find((part) => !part.startsWith("-")); return interpreter ? findPathCommand(interpreter) !== null : false; } return isExecutable(parts[0]); } catch { return false; } } function managedHelpersUsable(): boolean { const scripts = scriptsFromBinDir(join(managedRuntimeVenv(), "bin")); return [scripts.pi, scripts.connect, scripts.server].every( (script) => regularExecutable(script) && viableInterpreter(script), ); } type ManagedRuntimeState = | { kind: "missing" } | { kind: "healthy" } | { kind: "broken"; reason: string } | { kind: "unsafe"; reason: string }; type SafeManagedRuntimeState = Exclude; function pathIsWithinHome(target: string): boolean { const home = resolve(homedir()); if (target === resolve("/") || target === home) return false; const relativeTarget = relative(home, target); if ( !relativeTarget || relativeTarget.startsWith(`..${delimiter}`) || relativeTarget === ".." ) { return false; } let existing = target; while (!existsSync(existing)) { const parent = dirname(existing); if (parent === existing) return false; existing = parent; } try { const realHome = realpathSync(home); const realExisting = realpathSync(existing); const realRelative = relative(realHome, realExisting); return ( realRelative === "" || (realRelative !== ".." && !realRelative.startsWith(`..${delimiter}`) && !isAbsolute(realRelative)) ); } catch { return false; } } function classifyManagedRuntime(): ManagedRuntimeState { const target = resolve(managedRuntimeVenv()); if (!pathIsWithinHome(target)) { return { kind: "unsafe", reason: "managed environment path is unsafe" }; } let targetStat; try { targetStat = lstatSync(target); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { return { kind: "missing" }; } return { kind: "unsafe", reason: "managed environment cannot be inspected", }; } if (targetStat.isSymbolicLink()) { return { kind: "unsafe", reason: "managed environment is a symlink" }; } if (!targetStat.isDirectory()) { return { kind: "unsafe", reason: "managed environment is not a directory" }; } const marker = join(target, "pyvenv.cfg"); try { if (!lstatSync(marker).isFile()) { return { kind: "unsafe", reason: "managed environment is not a verified venv", }; } } catch { return { kind: "unsafe", reason: "managed environment is not a verified venv", }; } const python = join(target, "bin", "python"); if (!isExecutable(python) || !managedHelpersUsable()) { return { kind: "broken", reason: "managed environment is incomplete or unusable", }; } return { kind: "healthy" }; } function setupPython(): string { return process.env[SETUP_PYTHON_ENV]?.trim() || "python3"; } function setupSource(): string { return process.env[SETUP_SOURCE_ENV]?.trim() || SETUP_HELPER_REQUIREMENT; } function setupSourceDescription(): string { return process.env[SETUP_SOURCE_ENV]?.trim() ? `an explicit ${SETUP_SOURCE_ENV} override` : SETUP_HELPER_REQUIREMENT; } function setupOverrideWarning(config: InterAgentConfig): string | null { if (process.env.INTER_AGENT_PI_HELPER?.trim()) { return "INTER_AGENT_PI_HELPER remains higher precedence, so the managed environment is inactive while that override is configured; run /inter-agent doctor if this is unintended"; } if (config.projectPathsExplicit) { return "configured interAgent.projectPaths remain higher precedence, so the managed environment is inactive while those paths are configured; run /inter-agent doctor if this is unintended"; } return null; } function setupEnvironment(): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...process.env }; for (const key of Object.keys(env)) { if ( key === "PYTHONPATH" || key === "PYTHONHOME" || key === "VIRTUAL_ENV" || key.startsWith("PIP_") || key === "INTER_AGENT_SECRET" || key === "INTER_AGENT_TLS_KEY" || key === "INTER_AGENT_TLS_CERT" ) { delete env[key]; } } env.PYTHONUNBUFFERED = "1"; return env; } function findPathEntries(command: string): string[] { const entries: string[] = []; for (const dir of (process.env.PATH || "").split(delimiter)) { if (!dir) continue; const candidate = join(dir, command); if (existsSync(candidate)) entries.push(candidate); } return entries; } function findPathCommand(command: string): string | null { return ( findPathEntries(command).find((candidate) => isExecutable(candidate)) ?? null ); } function pathScripts(): { scripts: InterAgentScripts | null; partial: boolean; } { const entries = [ findPathEntries("inter-agent-pi"), findPathEntries("inter-agent-connect"), findPathEntries("inter-agent-server"), ]; const executable = entries.map( (candidates) => candidates.find((candidate) => isExecutable(candidate)) ?? null, ); const present = entries.filter((candidates) => candidates.length > 0).length; if (executable.every(Boolean)) { return { scripts: { pi: executable[0]!, connect: executable[1]!, server: executable[2]!, }, partial: false, }; } return { scripts: null, partial: present > 0 }; } function missingConfiguredProjectPathsMessage(paths: string[]): string { const binDirs = paths.map((path) => join(path, ".venv", "bin")); return `inter-agent runtime was not found in configured projectPaths candidates: ${binDirs.join(", ")}. See ${RUNTIME_SETUP_DOCS}`; } function setupNeededMessage(): string { return `inter-agent managed runtime is not installed. See ${RUNTIME_SETUP_DOCS}`; } function getScripts(config: InterAgentConfig): InterAgentScripts { const helper = process.env.INTER_AGENT_PI_HELPER; if (helper) { const expanded = expandHome(helper); const scripts = scriptsFromBinDir(dirname(expanded)); if (!scriptsAvailable(scripts) || expanded !== scripts.pi) { return { ...scripts, unavailableMessage: `inter-agent helper override is invalid at ${expanded}. See ${RUNTIME_SETUP_DOCS}`, unavailableRemedy: "doctor", }; } return scripts; } if (config.projectPathsExplicit) { const managedScripts = scriptsFromBinDir(join(managedRuntimeVenv(), "bin")); if (config.projectPathsError) { return { ...managedScripts, unavailableMessage: `${config.projectPathsError}. See ${RUNTIME_SETUP_DOCS}`, unavailableRemedy: "doctor", }; } const projectPaths = config.projectPaths ?? []; for (const projectPath of projectPaths) { const scripts = scriptsFromBinDir(join(projectPath, ".venv", "bin")); if (scriptsAvailable(scripts)) return scripts; } return { ...managedScripts, unavailableMessage: missingConfiguredProjectPathsMessage(projectPaths), unavailableRemedy: "doctor", }; } const managedScripts = scriptsFromBinDir(join(managedRuntimeVenv(), "bin")); const managedState = classifyManagedRuntime(); if (managedState.kind === "unsafe") { return { ...managedScripts, unavailableMessage: `${managedState.reason}. See ${RUNTIME_SETUP_DOCS}`, unavailableRemedy: "doctor", }; } if (managedState.kind === "broken") { return { ...managedScripts, unavailableMessage: `${managedState.reason}. See ${RUNTIME_SETUP_DOCS}`, unavailableRemedy: "setup", }; } if (managedState.kind === "healthy") return managedScripts; const fromPath = pathScripts(); if (fromPath.scripts && scriptsAvailable(fromPath.scripts)) return fromPath.scripts; if (fromPath.partial || fromPath.scripts) { return { ...managedScripts, unavailableMessage: "PATH inter-agent runtime is incomplete or unusable", unavailableRemedy: "doctor", }; } return { ...managedScripts, unavailableMessage: setupNeededMessage(), unavailableRemedy: "setup", }; } function interAgentEnv( config: InterAgentConfig = loadConfig(), ): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...process.env, PYTHONUNBUFFERED: "1" }; if (config.host) env.INTER_AGENT_HOST = String(config.host); if (config.port !== undefined && config.port !== null) { env.INTER_AGENT_PORT = String(config.port); } if (config.dataDir) env.INTER_AGENT_DATA_DIR = config.dataDir; if (config.secret !== undefined) env.INTER_AGENT_SECRET = String(config.secret); if (config.tls !== undefined) env.INTER_AGENT_TLS = String(config.tls); if (config.tlsCert) env.INTER_AGENT_TLS_CERT = config.tlsCert; if (config.tlsKey) env.INTER_AGENT_TLS_KEY = config.tlsKey; return env; } // ── Constants ─────────────────────────────────────────────────────────────── const NOTIFY_MAX_LEN = 1000; const DEFAULT_NAME = "pi"; const AUTO_STARTED_SERVER_IDLE_TIMEOUT_S = 300; const SERVER_START_WAIT_ATTEMPTS = 30; const SERVER_START_WAIT_MS = 500; const LISTENER_STOP_SIGTERM_TIMEOUT_MS = 2000; const LISTENER_STOP_SIGKILL_TIMEOUT_MS = 2000; const CONTROL_HELPER_TIMEOUT_MS = 2500; const CONTROL_HELPER_SIGKILL_GRACE_MS = 100; const CONTROL_HELPER_MAX_STDOUT_BYTES = 64 * 1024; const DOCTOR_SKILL_COMMAND = "skill:inter-agent-doctor"; // Test seam: behavior tests shorten the kill races so a hung-child failing stop // exercise stays bounded; production keeps the real millisecond timeouts. let stopTermTimeoutMs = LISTENER_STOP_SIGTERM_TIMEOUT_MS; let stopKillTimeoutMs = LISTENER_STOP_SIGKILL_TIMEOUT_MS; let controlHelperTimeoutMs = CONTROL_HELPER_TIMEOUT_MS; let controlHelperSigkillGraceMs = CONTROL_HELPER_SIGKILL_GRACE_MS; /** @internal Shorten the listener stop kill races for behavior tests. */ export function _setStopTimeoutsForTest( termMs: number | null, killMs: number | null, ): void { stopTermTimeoutMs = termMs ?? LISTENER_STOP_SIGTERM_TIMEOUT_MS; stopKillTimeoutMs = killMs ?? LISTENER_STOP_SIGKILL_TIMEOUT_MS; } /** @internal Shorten controller-helper timeout races for behavior tests. */ export function _setControlHelperTimeoutsForTest( timeoutMs: number | null, graceMs: number | null, ): void { controlHelperTimeoutMs = timeoutMs ?? CONTROL_HELPER_TIMEOUT_MS; controlHelperSigkillGraceMs = graceMs ?? CONTROL_HELPER_SIGKILL_GRACE_MS; } // ── State ─────────────────────────────────────────────────────────────────── interface ConnectionState { name: string; label: string | null; connected: boolean; } interface ScriptResult { stdout: string; stderr: string; code: number | null; } type FailureNotifier = ( title: string, body: string, remedy?: FailureRemedy, ) => void; interface ListenerOptions { notifyOnReady?: boolean; failureNotifier?: FailureNotifier; } let listenerProc: ChildProcess | null = null; let currentCtx: ExtensionContext | null = null; let messageBuffer = ""; let listenerReady = false; let currentConnection: ConnectionState | null = null; let activeStop: Promise | null = null; let mailboxController: MailboxDispatcher | null = null; let controlEngine: ControlEngine | null = null; let controlController: ControlController | null = null; // ── Helpers ───────────────────────────────────────────────────────────────── function truncate(text: string, max: number): string { if (text.length <= max) return text; return text.slice(0, max) + " …"; } interface ListSession { name: string; label?: string | null; } function isListSession(value: unknown): value is ListSession { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const entry = value as Record; if (typeof entry.name !== "string") return false; return ( entry.label === undefined || entry.label === null || typeof entry.label === "string" ); } function parseListSessions(value: unknown): ListSession[] { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("invalid response"); } const payload = value as Record; if (payload.op !== "list_ok") { throw new Error("invalid response"); } const sessions = payload.sessions; if (!Array.isArray(sessions) || !sessions.every(isListSession)) { throw new Error("invalid response"); } return sessions as ListSession[]; } function formatListSessions(sessions: ListSession[]): string { return [...sessions] .sort((left, right) => left.name === right.name ? 0 : left.name < right.name ? -1 : 1, ) .map( (session) => `• ${session.name}${session.label ? ` (${session.label})` : ""}`, ) .join("\n"); } // Compact one-line summary for the collapsed message renderer. function messageSummary(details: { from?: string; text?: string; toInfo?: string; outgoing?: boolean; }): string | null { const text = details.text ?? ""; const chars = text.length; if (details.outgoing) { const toInfo = details.toInfo ?? ""; if (toInfo.startsWith("to ")) { return `sent to ${toInfo.slice(3)} • ${chars} chars`; } if (toInfo === "broadcast") { return `broadcast • ${chars} chars`; } if (toInfo.startsWith("on ")) { return `published on ${toInfo.slice(3)} • ${chars} chars`; } return `sent ${toInfo} • ${chars} chars`; } if (details.from) { return `from ${details.from} • ${chars} chars`; } return null; } function renderOutgoingToolCall(toolName: string, theme: Theme): Component { return new Text(theme.fg("toolTitle", theme.bold(toolName)), 0, 0); } function renderOutgoingToolResult( toolName: string, result: { content: ReadonlyArray<{ type: string; text?: string }> }, expanded: boolean, isPartial: boolean, isError: boolean, destination: string, message: string, theme: Theme, ): Component { if (isError) { const errorText = result.content .filter((block) => block.type === "text") .map((block) => block.text ?? "") .filter(Boolean) .join("\n"); return new Text(theme.fg("error", errorText || `${toolName} failed`), 0, 0); } if (!expanded || isPartial) return new Container(); return new Text( theme.fg("toolOutput", `\nTo: ${destination}\n\nMessage: ${message}`), 0, 0, ); } function getConnectionState(ctx: ExtensionContext): ConnectionState | null { const branch = ctx.sessionManager.getBranch(); for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "custom" && entry.customType === "inter-agent-state") { return entry.data as ConnectionState; } } return null; } function persistState(pi: ExtensionAPI, state: ConnectionState) { pi.appendEntry("inter-agent-state", state); } function notify( title: string, body: string, type: "info" | "warning" | "error" = "info", ) { currentCtx?.ui.notify(truncate(`${title}: ${body}`, NOTIFY_MAX_LEN), type); } const PI_DOCTOR_FAILURE_HINT = "Run /inter-agent doctor for bounded diagnostics and check the Pi extension README.md for setup guidance."; const PI_SETUP_FAILURE_HINT = "Run /inter-agent setup to create or repair the managed helper environment."; function notifyCommandFailure( title: string, body: string, remedy: FailureRemedy = "doctor", ): void { const separator = /[.!?]$/.test(body) ? "" : "."; const hint = remedy === "setup" ? PI_SETUP_FAILURE_HINT : PI_DOCTOR_FAILURE_HINT; const suffix = `${separator} ${hint}`; const maxBodyLength = Math.max( 0, NOTIFY_MAX_LEN - title.length - 2 - suffix.length, ); const boundedBody = body.length <= maxBodyLength ? body : `${body.slice(0, Math.max(0, maxBodyLength - 2))} …`; notify(title, `${boundedBody}${suffix}`, "error"); } const notifyDefaultFailure: FailureNotifier = (title, body, remedy) => notifyCommandFailure(title, body, remedy); function sendConnectionStatus( pi: ExtensionAPI, status: "connected" | "disconnected", message: string, ): void { pi.sendMessage( { customType: "inter-agent-status", content: message, display: true, details: { status }, }, { deliverAs: "followUp", triggerTurn: false }, ); } function connectedMessage(name: string, label: string | null): string { return `Connected to inter-agent message bus as "${name}"${label ? ` (${label})` : ""}.`; } const DISCONNECTED_MESSAGE = "Disconnected from inter-agent message bus."; // Build an inbound peer body as a custom `inter-agent-message` payload. The // content carries the body plus bounded reply-decision guidance; the display // content is the clean body shown in the TUI. Immediate delivery reuses this // builder so direct/broadcast/channel formatting and guidance stay identical. function buildInboundMessage( from: string, text: string, toInfo: string, ): InboundImmediateMessage { const noReplyGuidance = "If no peer reply or user-facing action is needed, do not send a courtesy reply or discuss the message solely to acknowledge it."; const isChannel = toInfo.startsWith("on "); let replyInstruction: string; if (toInfo === "via broadcast") { replyInstruction = `Peer broadcast. Reply directly to ${from} only with inter_agent_send if it advances work or coordination, or to satisfy a request from the user; do not broadcast unless the user asks. ${noReplyGuidance}`; } else if (isChannel) { replyInstruction = `Peer channel message ${toInfo}. Reply to ${from} only with inter_agent_send, and only if it advances work or coordination; there is no publish tool, so reply directly rather than reposting to the channel. ${noReplyGuidance}`; } else { replyInstruction = `Peer message. Reply to ${from} only with inter_agent_send, and only if it advances work or coordination. ${noReplyGuidance}`; } const content = `[inter-agent message from agent ${from} ${toInfo}] ${text} ${replyInstruction}`; const displayContent = `[inter-agent message from agent ${from} ${toInfo}] ${text}`; return { customType: "inter-agent-message", content, display: true, details: { from, text, toInfo, displayContent }, }; } function showOutgoingInContext( pi: ExtensionAPI, from: string, text: string, toInfo: string, ) { pi.sendMessage( { customType: "inter-agent-message", content: `Outbound inter-agent history for a message sent as ${from} ${toInfo}. This records an action already completed; treat it as context, not a new request. ## BEGIN MESSAGE TRANSCRIPT ${text} ## END MESSAGE TRANSCRIPT`, display: true, details: { from, text, toInfo, outgoing: true, displayContent: `[outbound inter-agent history — sent by current agent (${from}) ${toInfo}] ${text}`, }, }, { triggerTurn: false, deliverAs: "followUp" }, ); } function execScript(script: string, args: string[]): Promise { return new Promise((resolve) => { const env = interAgentEnv(); const proc = spawnChildProcess(script, args, { stdio: ["ignore", "pipe", "pipe"], shell: false, env, }); let stdout = ""; let stderr = ""; proc.stdout?.on("data", (d: Buffer) => { stdout += d.toString(); }); proc.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); }); proc.on("close", (code) => { resolve({ stdout, stderr, code }); }); proc.on("error", (err) => { const nodeErr = err as NodeJS.ErrnoException; if (nodeErr.code === "ENOENT") { stderr += `inter-agent command was not found at ${script}. Check that inter-agent is installed and configured, then try again.`; } else { stderr += String(err); } resolve({ stdout, stderr, code: null }); }); }); } interface BoundedProcessResult extends ScriptResult { timedOut: boolean; outputExceeded: boolean; } function runBoundedProcess( command: string, args: string[], ): Promise { return new Promise((resolve) => { const env = setupEnvironment(); const proc = spawnChildProcess(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: false, env, }); let stdout = ""; let stderr = ""; let stdoutBytes = 0; let stderrBytes = 0; let timedOut = false; let outputExceeded = false; let settled = false; let timer: ReturnType | null = null; const finish = (code: number | null): void => { if (settled) return; settled = true; if (timer) clearTimeout(timer); resolve({ stdout, stderr, code, timedOut, outputExceeded }); }; const append = (chunk: Buffer, stream: "stdout" | "stderr"): void => { const currentBytes = stream === "stdout" ? stdoutBytes : stderrBytes; const remaining = SETUP_OUTPUT_MAX_BYTES - currentBytes; if (remaining <= 0) { outputExceeded = true; return; } const bounded = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk; if (stream === "stdout") { stdout += bounded.toString(); stdoutBytes += chunk.byteLength; } else { stderr += bounded.toString(); stderrBytes += chunk.byteLength; } if (chunk.byteLength > remaining) { outputExceeded = true; } }; proc.stdout?.on("data", (chunk: Buffer) => append(chunk, "stdout")); proc.stderr?.on("data", (chunk: Buffer) => append(chunk, "stderr")); proc.once("close", (code) => finish(code)); proc.once("error", (error) => { stderr = String(error); finish(null); }); timer = setTimeout(() => { timedOut = true; try { proc.kill("SIGKILL"); } catch { // The process may have exited before the timeout fired. } finish(null); }, SETUP_PROCESS_TIMEOUT_MS); timer.unref?.(); }); } function setupProcessFailure( stage: string, result: BoundedProcessResult, ): string { if (result.timedOut) return `${stage} timed out`; if (result.outputExceeded) return `${stage} produced too much output`; if (result.code === null) return `${stage} could not be started`; return `${stage} failed (exit ${result.code})`; } function execPiScript( scripts: InterAgentScripts, args: string[], ): Promise { if (scripts.unavailableMessage) { return Promise.resolve({ stdout: "", stderr: scripts.unavailableMessage, code: 127, }); } return execScript(scripts.pi, args); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function scriptFailureMessage(result: ScriptResult, operation: string): string { const output = (result.stderr || result.stdout).trim(); if (!output && result.code === null) { return "inter-agent command was not found. Check that inter-agent is installed and configured, then try again."; } if (output.includes("not found")) { return output; } return truncate(output || `inter-agent ${operation} command failed`, 200); } function managedPythonPath(): string { return join(managedRuntimeVenv(), "bin", "python"); } function setupVersionFailure(output: string): string | null { const match = output.match(/Python\s+(\d+)\.(\d+)/i); if (!match) return "selected Python did not report a usable version"; const major = Number(match[1]); const minor = Number(match[2]); return major > 3 || (major === 3 && minor >= 10) ? null : "Python 3.10 or newer is required"; } async function setupManagedRuntime(): Promise< { ok: true } | { ok: false; message: string } > { const python = setupPython(); const source = setupSource(); const state = classifyManagedRuntime(); if (state.kind === "unsafe") { return { ok: false, message: `${state.reason}; no files were changed` }; } const version = await runBoundedProcess(python, ["-I", "--version"]); if (version.code !== 0) { return { ok: false, message: setupProcessFailure("Python check", version) }; } const versionFailure = setupVersionFailure( `${version.stdout}\n${version.stderr}`, ); if (versionFailure) return { ok: false, message: versionFailure }; let targetState: SafeManagedRuntimeState = state; if (targetState.kind === "missing") { const created = await runBoundedProcess(python, [ "-I", "-m", "venv", managedRuntimeVenv(), ]); if (created.code !== 0) { return { ok: false, message: setupProcessFailure("virtual environment creation", created), }; } } else if (targetState.kind === "broken") { const repaired = await runBoundedProcess(python, [ "-I", "-m", "venv", "--clear", managedRuntimeVenv(), ]); if (repaired.code !== 0) { return { ok: false, message: setupProcessFailure("managed environment repair", repaired), }; } } else { const pipCheck = await runBoundedProcess(managedPythonPath(), [ "-I", "-m", "pip", "--isolated", "--version", ]); if (pipCheck.code !== 0) { const reclassified = classifyManagedRuntime(); if (reclassified.kind === "unsafe") { return { ok: false, message: `${reclassified.reason}; no files were changed`, }; } targetState = reclassified; if (targetState.kind !== "broken" && targetState.kind !== "healthy") { return { ok: false, message: "managed environment pip is unusable and safe repair was not verified", }; } const repaired = await runBoundedProcess(python, [ "-I", "-m", "venv", "--clear", managedRuntimeVenv(), ]); if (repaired.code !== 0) { return { ok: false, message: setupProcessFailure("managed environment repair", repaired), }; } } } const pip = await runBoundedProcess(managedPythonPath(), [ "-I", "-m", "pip", "--isolated", "--version", ]); if (pip.code !== 0) { return { ok: false, message: setupProcessFailure("managed pip check", pip), }; } const installed = await runBoundedProcess(managedPythonPath(), [ "-I", "-m", "pip", "--isolated", "install", "--upgrade", "--no-cache-dir", "--", source, ]); if (installed.code !== 0) { return { ok: false, message: setupProcessFailure("helper installation", installed), }; } if (!managedHelpersUsable()) { return { ok: false, message: "helper installation completed without all verified executable commands", }; } return { ok: true }; } async function readServerStatus( scripts: InterAgentScripts, ): Promise< | { ok: true; payload: Record } | { ok: false; message: string; remedy?: FailureRemedy } > { const result = await execPiScript(scripts, ["status", "--json"]); if (result.code !== 0) { return { ok: false, message: scripts.unavailableMessage || scriptFailureMessage(result, "status"), remedy: scripts.unavailableRemedy, }; } try { const parsed: unknown = JSON.parse(result.stdout); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return { ok: true, payload: parsed as Record }; } } catch { // Fall through to the user-facing error below. } return { ok: false, message: "inter-agent status returned an invalid response. Try /inter-agent status; if this continues, check the inter-agent installation.", }; } function statusState(payload: Record): string { return typeof payload.state === "string" ? payload.state : "unknown"; } function statusMessage(payload: Record): string { return typeof payload.message === "string" ? payload.message : statusState(payload); } function shouldAutoStartServer(payload: Record): boolean { return statusState(payload) === "unavailable"; } function statusFailureGuidance(payload: Record): string { const message = statusMessage(payload); switch (statusState(payload)) { case "auth_failed": return `${message}. Check that server and clients use the same inter-agent secret.`; case "protocol_mismatch": return `${message}. Another process may be using the inter-agent port; try /inter-agent status or restart the server.`; case "unavailable": return `${message}. Try /inter-agent status, or start inter-agent-server manually.`; default: return message; } } function startServerProcess( scripts: InterAgentScripts, ): Promise<{ ok: true; pid?: number } | { ok: false; message: string }> { if (scripts.unavailableMessage) { return Promise.resolve({ ok: false, message: scripts.unavailableMessage }); } return new Promise((resolve) => { let settled = false; const finish = ( result: { ok: true; pid?: number } | { ok: false; message: string }, ) => { if (!settled) { settled = true; resolve(result); } }; const proc = spawnChildProcess( scripts.server, ["--idle-timeout", String(AUTO_STARTED_SERVER_IDLE_TIMEOUT_S)], { stdio: "ignore", shell: false, detached: true, env: interAgentEnv(loadConfig()), }, ); proc.once("spawn", () => { proc.unref(); finish({ ok: true, pid: proc.pid }); }); proc.once("error", (err) => { const nodeErr = err as NodeJS.ErrnoException; if (nodeErr.code === "ENOENT") { finish({ ok: false, message: `inter-agent server command was not found at ${scripts.server}. Check that inter-agent is installed and configured, then try again.`, }); } else { finish({ ok: false, message: `could not start inter-agent server: ${String(err)}`, }); } }); proc.once("exit", (code, signal) => { finish({ ok: false, message: `inter-agent server exited before it was ready (code ${code ?? "none"}, signal ${signal ?? "none"}). Try /inter-agent status or start inter-agent-server manually.`, }); }); }); } async function waitForServerAvailable( scripts: InterAgentScripts, ): Promise<{ ok: true } | { ok: false; message: string }> { let lastMessage = "server unavailable"; for (let i = 0; i < SERVER_START_WAIT_ATTEMPTS; i++) { const status = await readServerStatus(scripts); if (status.ok === false) { lastMessage = status.message; } else if (statusState(status.payload) === "available") { return { ok: true }; } else { lastMessage = statusFailureGuidance(status.payload); } await sleep(SERVER_START_WAIT_MS); } return { ok: false, message: `Started the inter-agent server, but it did not become available. ${lastMessage}`, }; } async function ensureServerAvailable( scripts: InterAgentScripts, failureNotifier: FailureNotifier = notifyDefaultFailure, ): Promise { const initial = await readServerStatus(scripts); if (initial.ok === false) { failureNotifier( "[inter-agent] connect failed", initial.message, initial.remedy, ); return false; } if (statusState(initial.payload) === "available") { return true; } if (!shouldAutoStartServer(initial.payload)) { failureNotifier( "[inter-agent] connect failed", statusFailureGuidance(initial.payload), ); return false; } const started = await startServerProcess(scripts); if (started.ok === false) { failureNotifier( "[inter-agent] connect failed", started.message, scripts.unavailableRemedy, ); return false; } const ready = await waitForServerAvailable(scripts); if (ready.ok === false) { failureNotifier("[inter-agent] connect failed", ready.message); return false; } return true; } function splitCommandArgs(input: string): string[] { const matches = input.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S+/g) || []; return matches.map((part) => { if ( (part.startsWith('"') && part.endsWith('"')) || (part.startsWith("'") && part.endsWith("'")) ) { return part.slice(1, -1).replace(/\\(["'\\])/g, "$1"); } return part; }); } function parseConnectArgs( args: string, ): | { ok: true; name: string; label: string | null } | { ok: false; message: string } { const parts = splitCommandArgs(args.trim()); let name = DEFAULT_NAME; let label: string | null = null; let index = 0; if (parts[0] && parts[0] !== "--label") { name = parts[0]; index = 1; } while (index < parts.length) { const part = parts[index]; if (part === "--label") { const value = parts[index + 1]; if (!value) { return { ok: false, message: "usage: /inter-agent connect [--label