import { spawn } from "node:child_process"; import readline from "node:readline"; import { resolve } from "node:path"; function trimToDefined(value) { const trimmed = String(value ?? "").trim(); return trimmed ? trimmed : undefined; } function boolFromEnv(value) { const normalized = trimToDefined(value)?.toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } function listFromEnv(value) { if (!value) return undefined; try { const parsed = JSON.parse(String(value)); if (Array.isArray(parsed)) { return parsed.map((item) => String(item)); } } catch { // Fall through to whitespace splitting. } return String(value).split(/\s+/).filter(Boolean); } function resolveTaskThreadRef(task) { return trimToDefined(task?.targetCodexThreadRef || task?.targetThreadId); } function resolveTaskCwd(config, task) { return resolve( trimToDefined(task?.targetCodexFolderRef) || trimToDefined(config.masterAgentWorkdir) || process.cwd(), ); } function resolvePrompt(task) { return String(task?.executionPrompt || task?.requestText || "").trim(); } function normalizeTimeoutMs(value) { const numeric = Number(value); return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 120_000; } export function getCodexAppServerRunnerConfig(env = process.env, config = {}) { const argsFromConfig = Array.isArray(config.codexAppServerArgs) ? config.codexAppServerArgs.map((item) => String(item)) : undefined; const args = argsFromConfig ?? listFromEnv(env.BOSS_CODEX_APP_SERVER_ARGS) ?? ["app-server"]; return { enabled: config.codexAppServerEnabled === true || boolFromEnv(env.BOSS_CODEX_APP_SERVER_ENABLED), command: trimToDefined(config.codexAppServerCommand) || trimToDefined(env.BOSS_CODEX_APP_SERVER_COMMAND) || "codex", args, cwd: trimToDefined(config.codexAppServerWorkdir) || trimToDefined(env.BOSS_CODEX_APP_SERVER_WORKDIR) || trimToDefined(config.masterAgentWorkdir) || process.cwd(), timeoutMs: normalizeTimeoutMs( config.codexAppServerTimeoutMs ?? env.BOSS_CODEX_APP_SERVER_TIMEOUT_MS, ), clientName: trimToDefined(config.codexAppServerClientName) || trimToDefined(env.BOSS_CODEX_APP_SERVER_CLIENT_NAME) || "boss_local_agent", clientTitle: trimToDefined(config.codexAppServerClientTitle) || trimToDefined(env.BOSS_CODEX_APP_SERVER_CLIENT_TITLE) || "Boss Local Agent", clientVersion: trimToDefined(config.codexAppServerClientVersion) || trimToDefined(env.BOSS_CODEX_APP_SERVER_CLIENT_VERSION) || "0.1.0", model: trimToDefined(config.masterAgentModel || env.BOSS_MASTER_AGENT_MODEL), sandbox: trimToDefined(config.masterAgentSandbox), transport: "stdio", }; } export function shouldUseCodexAppServerTaskRunner(runnerConfig, task) { if (!runnerConfig?.enabled || !runnerConfig.command) { return false; } const taskType = trimToDefined(task?.taskType); if (taskType !== "conversation_reply" && taskType !== "dispatch_execution") { return false; } return Boolean(resolvePrompt(task)); } function extractAgentTextFromContent(value) { if (typeof value === "string") { return value; } if (Array.isArray(value)) { return value.map(extractAgentTextFromContent).filter(Boolean).join(""); } if (!value || typeof value !== "object") { return ""; } return ( extractAgentTextFromContent(value.text) || extractAgentTextFromContent(value.outputText) || extractAgentTextFromContent(value.content) || extractAgentTextFromContent(value.delta) ); } function extractAgentDelta(params) { if (!params || typeof params !== "object") { return ""; } return ( extractAgentTextFromContent(params.delta) || extractAgentTextFromContent(params.text) || extractAgentTextFromContent(params.messageDelta) || extractAgentTextFromContent(params.item?.delta) ); } function extractCompletedAgentMessage(params) { const item = params?.item; if (!item || typeof item !== "object") { return ""; } const itemType = String(item.type || item.kind || ""); if (!/agent|assistant/i.test(itemType)) { return ""; } return ( extractAgentTextFromContent(item.text) || extractAgentTextFromContent(item.content) || extractAgentTextFromContent(item.message) || extractAgentTextFromContent(item.output) ); } function createFailure(error, extra = {}) { return { status: "failed", errorMessage: error instanceof Error ? error.message : String(error), transport: "stdio", ...extra, }; } export async function executeCodexAppServerTask(runnerConfig, task) { if (!shouldUseCodexAppServerTaskRunner(runnerConfig, task)) { return createFailure("CODEX_APP_SERVER_DISABLED", { canFallbackToCli: true }); } const cwd = resolveTaskCwd(runnerConfig, task); const targetThreadRef = resolveTaskThreadRef(task); const prompt = resolvePrompt(task); const child = spawn(runnerConfig.command, runnerConfig.args, { cwd: runnerConfig.cwd || cwd, env: process.env, stdio: ["pipe", "pipe", "pipe"], }); let closed = false; let stderr = ""; let nextId = 1; let activeTurnStarted = false; let turnSettled = false; let replyBody = ""; let completedMessageText = ""; const pending = new Map(); let resolveTurnCompleted; let rejectTurnCompleted; const turnCompleted = new Promise((resolveTurn, rejectTurn) => { resolveTurnCompleted = (value) => { turnSettled = true; resolveTurn(value); }; rejectTurnCompleted = (error) => { turnSettled = true; rejectTurn(error); }; }); const timeout = setTimeout(() => { rejectTurnCompleted(new Error("CODEX_APP_SERVER_TIMEOUT")); for (const { reject } of pending.values()) { reject(new Error("CODEX_APP_SERVER_TIMEOUT")); } pending.clear(); if (!child.killed) { child.kill("SIGKILL"); } }, runnerConfig.timeoutMs); const rl = readline.createInterface({ input: child.stdout }); const cleanup = () => { clearTimeout(timeout); rl.close(); if (!child.killed) { child.kill("SIGTERM"); } }; const request = (method, params = {}) => { if (closed) { return Promise.reject(new Error("CODEX_APP_SERVER_CLOSED")); } const id = nextId++; const message = { method, id, params }; return new Promise((resolveRequest, rejectRequest) => { pending.set(id, { resolve: resolveRequest, reject: rejectRequest }); child.stdin.write(`${JSON.stringify(message)}\n`, (error) => { if (error) { pending.delete(id); rejectRequest(error); } }); }); }; const notify = (method, params = {}) => { if (closed) return; child.stdin.write(`${JSON.stringify({ method, params })}\n`); }; child.stderr.on("data", (chunk) => { stderr += String(chunk); }); child.on("error", (error) => { closed = true; for (const { reject } of pending.values()) { reject(error); } pending.clear(); rejectTurnCompleted(error); }); child.on("close", (code) => { closed = true; const error = new Error( stderr.trim() || `CODEX_APP_SERVER_EXITED:${code ?? "unknown"}`, ); for (const { reject } of pending.values()) { reject(error); } pending.clear(); if (code !== 0 || (activeTurnStarted && !turnSettled)) { rejectTurnCompleted(error); } }); rl.on("line", (line) => { if (!line.trim()) return; let message; try { message = JSON.parse(line); } catch { return; } if (Object.hasOwn(message, "id")) { const pendingRequest = pending.get(message.id); if (pendingRequest) { pending.delete(message.id); if (message.error) { pendingRequest.reject(new Error(message.error.message || JSON.stringify(message.error))); } else { pendingRequest.resolve(message.result ?? {}); } } return; } if (message.method === "item/agentMessage/delta") { replyBody += extractAgentDelta(message.params); return; } if (message.method === "item/completed") { completedMessageText += extractCompletedAgentMessage(message.params); return; } if (message.method === "turn/completed") { const status = message.params?.turn?.status ?? message.params?.status ?? "completed"; if (status === "completed") { resolveTurnCompleted(message.params ?? {}); } else { rejectTurnCompleted(new Error(`CODEX_APP_SERVER_TURN_${String(status).toUpperCase()}`)); } } }); try { await request("initialize", { clientInfo: { name: runnerConfig.clientName, title: runnerConfig.clientTitle, version: runnerConfig.clientVersion, }, }); notify("initialized", {}); const threadResult = targetThreadRef ? await request("thread/resume", { threadId: targetThreadRef, model: runnerConfig.model, }) : await request("thread/start", { model: runnerConfig.model, cwd, sandbox: runnerConfig.sandbox, serviceName: runnerConfig.clientName, }); const threadId = trimToDefined(threadResult?.thread?.id) || targetThreadRef; if (!threadId) { throw new Error("CODEX_APP_SERVER_THREAD_ID_MISSING"); } await request("turn/start", { threadId, input: [{ type: "text", text: prompt }], cwd, model: runnerConfig.model, }); activeTurnStarted = true; await turnCompleted; const normalizedReply = (replyBody || completedMessageText).trim(); return { status: "completed", replyBody: normalizedReply, threadId, cwd, transport: "stdio", canFallbackToCli: false, }; } catch (error) { return createFailure(error, { stderr: stderr.trim(), cwd, threadId: targetThreadRef, canFallbackToCli: !activeTurnStarted, }); } finally { cleanup(); } }