import path from "node:path"; import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils"; import { asString, asNumber, asBoolean, asStringArray, parseObject, buildPaperclipEnv, redactEnvForLogs, ensureAbsoluteDirectory, ensureCommandResolvable, ensurePathInEnv, renderTemplate, runChildProcess, } from "@paperclip/adapter-utils/server-utils"; import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js"; export async function execute(ctx: AdapterExecutionContext): Promise { const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx; const promptTemplate = asString( config.promptTemplate, "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.", ); const bootstrapTemplate = asString(config.bootstrapPromptTemplate, promptTemplate); const command = asString(config.command, "codex"); const model = asString(config.model, ""); const search = asBoolean(config.search, false); const bypass = asBoolean(config.dangerouslyBypassApprovalsAndSandbox, false); const cwd = asString(config.cwd, process.cwd()); await ensureAbsoluteDirectory(cwd); const envConfig = parseObject(config.env); const hasExplicitApiKey = typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0; const env: Record = { ...buildPaperclipEnv(agent) }; env.PAPERCLIP_RUN_ID = runId; const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) || (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) || null; const wakeReason = typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0 ? context.wakeReason.trim() : null; const approvalId = typeof context.approvalId === "string" && context.approvalId.trim().length > 0 ? context.approvalId.trim() : null; const approvalStatus = typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0 ? context.approvalStatus.trim() : null; const linkedIssueIds = Array.isArray(context.issueIds) ? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0) : []; if (wakeTaskId) { env.PAPERCLIP_TASK_ID = wakeTaskId; } if (wakeReason) { env.PAPERCLIP_WAKE_REASON = wakeReason; } if (approvalId) { env.PAPERCLIP_APPROVAL_ID = approvalId; } if (approvalStatus) { env.PAPERCLIP_APPROVAL_STATUS = approvalStatus; } if (linkedIssueIds.length > 0) { env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); } for (const [k, v] of Object.entries(envConfig)) { if (typeof v === "string") env[k] = v; } if (!hasExplicitApiKey && authToken) { env.PAPERCLIP_API_KEY = authToken; } const runtimeEnv = ensurePathInEnv({ ...process.env, ...env }); await ensureCommandResolvable(command, cwd, runtimeEnv); const timeoutSec = asNumber(config.timeoutSec, 1800); const graceSec = asNumber(config.graceSec, 20); const extraArgs = (() => { const fromExtraArgs = asStringArray(config.extraArgs); if (fromExtraArgs.length > 0) return fromExtraArgs; return asStringArray(config.args); })(); const runtimeSessionParams = parseObject(runtime.sessionParams); const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? ""); const runtimeSessionCwd = asString(runtimeSessionParams.cwd, ""); const canResumeSession = runtimeSessionId.length > 0 && (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd)); const sessionId = canResumeSession ? runtimeSessionId : null; if (runtimeSessionId && !canResumeSession) { await onLog( "stderr", `[paperclip] Codex session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`, ); } const template = sessionId ? promptTemplate : bootstrapTemplate; const prompt = renderTemplate(template, { company: { id: agent.companyId }, agent, run: { id: runId, source: "on_demand" }, context, }); const buildArgs = (resumeSessionId: string | null) => { const args = ["exec", "--json"]; if (search) args.unshift("--search"); if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox"); if (model) args.push("--model", model); if (extraArgs.length > 0) args.push(...extraArgs); if (resumeSessionId) args.push("resume", resumeSessionId, prompt); else args.push(prompt); return args; }; const runAttempt = async (resumeSessionId: string | null) => { const args = buildArgs(resumeSessionId); if (onMeta) { await onMeta({ adapterType: "codex_local", command, cwd, commandArgs: args.map((value, idx) => { if (idx === args.length - 1) return ``; return value; }), env: redactEnvForLogs(env), prompt, context, }); } const proc = await runChildProcess(runId, command, args, { cwd, env, timeoutSec, graceSec, onLog, }); return { proc, parsed: parseCodexJsonl(proc.stdout), }; }; const toResult = ( attempt: { proc: { exitCode: number | null; signal: string | null; timedOut: boolean; stdout: string; stderr: string }; parsed: ReturnType }, clearSessionOnMissingSession = false, ): AdapterExecutionResult => { if (attempt.proc.timedOut) { return { exitCode: attempt.proc.exitCode, signal: attempt.proc.signal, timedOut: true, errorMessage: `Timed out after ${timeoutSec}s`, clearSession: clearSessionOnMissingSession, }; } const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null; const resolvedSessionParams = resolvedSessionId ? ({ sessionId: resolvedSessionId, cwd } as Record) : null; return { exitCode: attempt.proc.exitCode, signal: attempt.proc.signal, timedOut: false, errorMessage: (attempt.proc.exitCode ?? 0) === 0 ? null : `Codex exited with code ${attempt.proc.exitCode ?? -1}`, usage: attempt.parsed.usage, sessionId: resolvedSessionId, sessionParams: resolvedSessionParams, sessionDisplayId: resolvedSessionId, provider: "openai", model, costUsd: null, resultJson: { stdout: attempt.proc.stdout, stderr: attempt.proc.stderr, }, summary: attempt.parsed.summary, clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId), }; }; const initial = await runAttempt(sessionId); if ( sessionId && !initial.proc.timedOut && (initial.proc.exitCode ?? 0) !== 0 && isCodexUnknownSessionError(initial.proc.stdout, initial.proc.stderr) ) { await onLog( "stderr", `[paperclip] Codex resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`, ); const retry = await runAttempt(null); return toResult(retry, true); } return toResult(initial); }