|
| 1 | +# Plan: Python Interface to Codex |
| 2 | + |
| 3 | +## Goal |
| 4 | +Provide a clean Python interface to Codex suitable for apps and automation, with a quick path today (no native build) and a deeper path later (native bindings) while tracking the upstream protocol. |
| 5 | + |
| 6 | +## Phase 1 — Subprocess + JSON (Implemented) |
| 7 | +- Approach |
| 8 | + - Invoke the Codex CLI (`codex exec`) with JSON output and parse NDJSON lines. |
| 9 | + - Validate event payloads using generated Pydantic models derived from the upstream protocol. |
| 10 | +- Components |
| 11 | + - `codex.api.run_exec(prompt, *, json=False, model=None, full_auto=False, cd=None, ...) -> str` |
| 12 | + - Adds `--json` when `json=True`. |
| 13 | + - `codex.protocol.runtime.stream_exec_events(prompt, *, model=None, full_auto=False, cd=None, ...) -> Iterator[Event]` |
| 14 | + - Spawns `codex exec --json` and yields validated `Event` envelopes (`id: str`, `msg: EventMsg`). |
| 15 | + - `codex.protocol.types` — Pydantic models (generated) for protocol request/response/event types. |
| 16 | +- Usage |
| 17 | + - One‑shot: `from codex import run_exec` |
| 18 | + - Streaming: `from codex.protocol.runtime import stream_exec_events` |
| 19 | +- Pros |
| 20 | + - No native build or wheel distribution required. |
| 21 | + - Works anywhere the CLI works; simple integration surface. |
| 22 | +- Cons |
| 23 | + - Separate process; IPC via stdout/stderr. |
| 24 | + - Less direct control than embedding codex directly. |
| 25 | + |
| 26 | +## Phase 2 — Native PyO3 Bindings (Optional, Future) |
| 27 | +- Motivation |
| 28 | + - Single process; lower latency; richer control (interrupt/shutdown, approvals, patch streaming) exposed as Python calls. |
| 29 | +- Scope |
| 30 | + - Create a Rust crate (e.g., `crates/codex_py`) using `pyo3`. |
| 31 | + - Depend on upstream `codex-rs` crates via path deps under `codex-proj/codex-rs`. |
| 32 | + - Expose minimal API initially: |
| 33 | + - `start_session(config) -> Session` |
| 34 | + - `Session.send_user_message(text, images=...) -> Iterator[Event]` |
| 35 | + - `Session.interrupt()` / `Session.shutdown()` |
| 36 | +- Build & Publish |
| 37 | + - Use `maturin` (or `hatch-maturin`) to build wheels. |
| 38 | + - GH Actions matrix to build manylinux + macOS + Windows wheels on tags (`v*`). |
| 39 | +- Risks |
| 40 | + - Cross‑platform wheel complexity. |
| 41 | + - API surface alignment with evolving upstream. |
| 42 | + |
| 43 | +## Protocol Models — Generation Flow |
| 44 | +- Source of truth: `codex-proj/codex-rs/protocol-ts` (Rust → TypeScript via `ts-rs`). |
| 45 | +- Generation steps (Make target): |
| 46 | + - `make gen-protocol` |
| 47 | + - Runs the TS generator to `.generated/ts/`. |
| 48 | + - Converts TS → Python Pydantic models at `codex/protocol/types.py`. |
| 49 | +- Notes |
| 50 | + - Generated code forbids extra fields (`model_config = ConfigDict(extra='forbid')`). |
| 51 | + - Optional TS fields (`| null`) map to Optional fields with default `None`. |
| 52 | + |
| 53 | +## CI & Release |
| 54 | +- CI runs `ruff`, `mypy`, and `pytest` on pushes/PRs. |
| 55 | +- PyPI publish via Trusted Publishing (OIDC) on tags matching `v*`. |
| 56 | + |
| 57 | +## Open Questions / Next Steps |
| 58 | +- Should we emit discriminated unions in Pydantic (e.g., `Field(discriminator="method")`) for simpler parsing of request/notification unions? |
| 59 | +- Add higher‑level convenience API: e.g., `CodexSession` in Python wrapping `stream_exec_events` with callbacks. |
| 60 | +- Windows and shell escaping: ensure prompts and cwd are robust on Windows. |
| 61 | +- Back‑pressure & large outputs: expose an async-friendly variant in the future (if needed). |
| 62 | + |
| 63 | +## Acceptance Criteria |
| 64 | +- Phase 1 usable APIs documented and tested: |
| 65 | + - `run_exec(..., json=True)` returns JSON mode output. |
| 66 | + - `stream_exec_events(...)` yields validated `Event` instances for typical flows. |
| 67 | +- Protocol generator (`make gen-protocol`) succeeds and passes lint/mypy. |
| 68 | +- Publishing workflow remains functional after additions. |
0 commit comments