/** * CodexAgentRunner — AgentRunner impl backed by the Codex CLI (subprocess). * * Architectural pattern: SAME as KimiCodeAgentRunner/AntigravityAgentRunner — * spawn the vendor's CLI binary, parse its JSONL stream, normalize to * AgentMessage. No SDK dependency (the @openai/codex-sdk package would add * ~100MB; we use the CLI directly like we do for kimi). * * Auth: inherited from ~/.codex/auth.json (created by `codex login`). The * ChatGPT subscription flows through this credential cache. No env vars. * * Streaming / liveness — WHY INCREMENTAL (#204): * This runner originally buffered ALL of stdout until the codex subprocess * exited, then parsed the whole batch. session.ts races the FIRST pull from * this runner against a 4-minute silent-stream watchdog (SILENT_SESSION_MS), * so any turn longer than 4 minutes yielded zero messages in time — abort, * retry, kill, circuit breaker, stalled org. Same bug class as the * kimi/antigravity runners had (#204 audit). This runner now parses stdout * LINE BY LINE as data arrives: a liveness `tool_use` message is yielded * the moment the subprocess spawns (deterministically winning the * first-pull race regardless of model-thinking latency), assistant text is * yielded as each `agent_message` item lands (codex sends whole items, not * per-token deltas — no accumulation needed, unlike agy), and codex's own * tool items (command_execution, file_change, mcp_tool_call, web_search, * todo_list) are forwarded as id-carrying `tool_use` at `item.started` * and a matched `tool_result` at `item.completed` (codex-runner-tools.ts). Tool_call fences are still collected * from the raw texts and parsed at end of turn (fence parsing needs the * complete text). * * Org tools (org_send, knowledge_search, ask_human, …) — FENCE PROTOCOL: * Same approach as kimi/opencode/antigravity. Tools are rendered INTO the * first prompt; the model emits ```tool_call fences; this runner parses * them out of the agent_message text, executes the real OrgToolDef handlers * in-process (gated through canUseTool), and feeds results back as the * next prompt. * * Subprocess protocol — REWRITTEN against openai/codex's own Rust source * (issue #178). This runner previously assumed a "thread.started" / * "item.completed" event shape sourced from `openai/codex/sdk/typescript`, * which turned out to be a DIFFERENT wire format than what `codex exec * --json` actually emits. Confirmed two ways: (1) live output from an * installed codex v0.21.0 (`brew install codex`) showed events shaped like * `{"id":"1","msg":{"type":"task_started"}}` — nothing resembling * "thread.started"; (2) cross-checked against * `codex-rs/protocol/src/protocol.rs`'s `EventMsg` enum * (`#[serde(tag = "type", rename_all = "snake_case")]`) and * `codex-rs/protocol/src/legacy_events.rs`, which is EXACTLY this wire * format and is explicitly comment-labeled "v1 wire format" (still the * live default for `--json`, not a deprecated relic). * * - Invocation: `codex exec --json [--model X] [-c model_reasoning_effort=L] * -c features.shell_snapshot{,_v2}=false (security, see * CODEX_SNAPSHOT_OFF_ARGS) [--cd Y] [--skip-git-repo-check] [--sandbox ] * [resume ] -- -` with the prompt on STDIN * (the sandbox mode follows the role's policy.git level — cli-sandbox.ts; * coder mode, `access: 'full'`, passes * `--dangerously-bypass-approvals-and-sandbox` instead — see * codexExecArgs in codex-runner-stream.ts) * (see streamTurn for why argv is not used). `--experimental-json` * (the old flag name) doesn't exist in v0.21.0 — confirmed live * ("unexpected argument '--experimental-json' found"); `--json` is * correct in both v0.21.0 and the current v0.147.0 (where * `--experimental-json` IS an alias again, per current * `codex-rs/exec/src/cli.rs` — but v0.21.0 predates that alias). * - `resume` is a SUBCOMMAND of `exec`, not a positional after other exec * flags in isolation — `codex exec resume [""]`, * with `exec`'s own global flags (--json, --model, --sandbox, etc.) * specified BEFORE the `resume` token. Confirmed live (v0.147.0 AND * v0.149.0) that this exact arg order parses cleanly. v0.21.0 has no * `resume` subcommand at all (confirmed live: "resume" was consumed as * the PROMPT text itself, then the actual session id was rejected as an * unexpected extra positional) — so resume silently can't work * pre-~v0.12x. This runner doesn't detect codex's version; if resume * fails on an old install, each turn falls back to a fresh (contextless) * session rather than erroring — a real limitation, not fixed here. * - JSONL on stdout, one event per line. Confirmed real EventMsg variants * (source: `EventMsg` enum + each variant's struct, all in * `protocol.rs`): * {"type":"session_configured","session_id":"...","thread_id":"...", * "model":"...",...} — session/thread id * {"type":"task_started"} — turn started * {"type":"agent_message","message":"...","phase":...} * — assistant text * {"type":"token_count","info":{"last_token_usage": * {"input_tokens":N,"output_tokens":N,"reasoning_output_tokens":N, * "total_tokens":N,...},"total_token_usage":{...}},...} * — `last_token_usage` is per-TURN (not cumulative); * `total_token_usage` IS cumulative — use the former * {"type":"task_complete","turn_id":"...", * "last_agent_message":"...","error":{"message":"..."}|absent,...} * {"type":"error","message":"...",...} * - NO per-token streaming in this event set (whole agent_message events * only, unlike the discarded thread/item assumption). * - Session ID: captured from `session_configured.session_id` (or * `.thread_id` — both present, same value in observed live output), * NOT from a "thread.started" event, which doesn't exist in THIS * (legacy) protocol variant — see the CURRENT shape below, where it does. * - stderr is human diagnostics; buffer and surface on non-zero exit only. * * WIRE-FORMAT UPDATE (#178 follow-up, #204) — live-verified 2026-08-22 * against an authenticated codex v0.149.0 (`codex login status` → * "Logged in using ChatGPT"), three real `codex exec --json` invocations: * * 1. Plain turn (`"reply with exactly the word: pong"`): * {"type":"thread.started","thread_id":"01a02a2f-...-...-...-..."} * {"type":"turn.started"} * {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"pong"}} * {"type":"turn.completed","usage":{"input_tokens":13085,"cached_input_tokens":9984, * "cache_write_input_tokens":0,"output_tokens":5,"reasoning_output_tokens":0}} * 2. Turn with a shell tool call (`"run: echo hello-from-codex-shell"`) — adds: * {"type":"item.started","item":{"id":"item_1","type":"command_execution", * "command":"...","aggregated_output":"","exit_code":null,"status":"in_progress"}} * {"type":"item.completed","item":{"id":"item_1","type":"command_execution", * "command":"...","aggregated_output":"hello-from-codex-shell\n","exit_code":0, * "status":"completed"}} * 3. `codex exec resume "..."` × 3 in the same thread — confirms * (a) `resume` still works exactly as documented above, (b) * `turn.completed.usage.input_tokens` stays roughly flat across resumed * turns (13097 → 13112) rather than growing cumulatively, consistent * with per-turn (not cumulative) usage — same inference as legacy * `token_count.info.last_token_usage` vs `total_token_usage`, though * this wasn't cross-checked against Rust source (no `codex-rs` checkout * in this environment) the way the legacy shape originally was. * * NONE of `session_configured` / `task_started` / `agent_message` (top-level) * / `token_count` / `task_complete` — the shape this runner was originally * written against — appeared in ANY of the above. This runner was silently * broken against any codex install emitting the current shape: every switch * branch missed, so assistant text/inputTokens/outputTokens/threadId all * stayed at their zero-values for every single turn, producing no assistant * text, no token accounting, and no session resumption — while `exitCode` * was still 0, so nothing surfaced as an error either. * * Both shapes are now parsed (see CURRENT vs LEGACY branches in * `handleEvent` below). The legacy shape is kept because there's no live * evidence it was ever wrong for the v0.147.0 install it was verified * against — only that a newer install (v0.149.0) has since moved to the * item-based shape. Error/failure item types (e.g. a possible * `turn.failed`) were NOT observed live and are not guessed at here — an * error turn was only ever seen as a non-zero exit with a plain-text stderr * message (e.g. `resume`ing an unknown thread id → exit 1, no JSON on * stdout at all), which the existing `outcome.exitCode !== 0` handling * already covers unchanged. */ import type { AgentMessage, AgentRunArgs, AgentRunner } from './agent-runner.js'; export type { CodexStreamEvent } from './codex-runner-types.js'; export declare class CodexAgentRunner implements AgentRunner { private codexBin?; readonly budgetFloorGated = true; constructor(codexBin?: string | undefined); run(args: AgentRunArgs): AsyncIterable; } //# sourceMappingURL=codex-runner.d.ts.map