Skip to content

Commit 6ad5550

Browse files
committed
revert: remove codex/protocol/runtime.py (undo last edit)
1 parent 37a415c commit 6ad5550

7 files changed

Lines changed: 200 additions & 314 deletions

File tree

codex/__init__.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,29 @@
44
55
Usage:
66
from codex import run_exec
7-
output = run_exec("explain this codebase to me")
7+
events = run_exec("explain this codebase to me")
88
"""
99

1010
from .api import (
1111
CodexClient,
1212
CodexError,
13-
CodexNotFoundError,
14-
CodexProcessError,
15-
find_binary,
13+
CodexNativeError,
14+
Conversation,
1615
run_exec,
1716
)
17+
from .config import CodexConfig
18+
from .event import Event
1819

1920
__all__ = [
2021
"__version__",
2122
"CodexError",
22-
"CodexNotFoundError",
23-
"CodexProcessError",
23+
"CodexNativeError",
2424
"CodexClient",
25-
"find_binary",
25+
"Conversation",
2626
"run_exec",
27+
"Event",
28+
"CodexConfig",
2729
]
2830

2931
# Managed by Hatch via pyproject.toml [tool.hatch.version]
30-
__version__ = "0.1.2"
32+
__version__ = "0.1.2"

codex/api.py

Lines changed: 63 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,96 @@
11
from __future__ import annotations
22

3-
import os
4-
import shutil
5-
import subprocess
63
from collections.abc import Iterable, Mapping, Sequence
74
from dataclasses import dataclass
5+
from typing import Iterator
6+
7+
from .config import CodexConfig
8+
from .event import Event
9+
from .native import run_exec_collect as native_run_exec_collect
10+
from .native import start_exec_stream as native_start_exec_stream
811

912

1013
class CodexError(Exception):
1114
"""Base exception for codex-python."""
1215

1316

14-
class CodexNotFoundError(CodexError):
15-
"""Raised when the 'codex' binary cannot be found or executed."""
17+
class CodexNativeError(CodexError):
18+
"""Raised when the native extension is not available or fails."""
1619

17-
def __init__(self, executable: str = "codex") -> None:
20+
def __init__(self) -> None:
1821
super().__init__(
19-
f"Codex CLI not found: '{executable}'.\n"
20-
"Install from https://github.com/openai/codex or ensure it is on PATH."
22+
"codex_native extension not installed or failed to run. "
23+
"Run `make dev-native` or ensure native wheels are installed."
2124
)
22-
self.executable = executable
2325

2426

2527
@dataclass(slots=True)
26-
class CodexProcessError(CodexError):
27-
"""Raised when the codex process exits with a non‑zero status."""
28-
29-
returncode: int
30-
cmd: Sequence[str]
31-
stdout: str
32-
stderr: str
33-
34-
def __str__(self) -> str: # pragma: no cover - repr is sufficient
35-
return (
36-
f"Codex process failed with exit code {self.returncode}.\n"
37-
f"Command: {' '.join(self.cmd)}\n"
38-
f"stderr:\n{self.stderr.strip()}"
39-
)
40-
28+
class Conversation:
29+
"""A stateful conversation with Codex, streaming events natively."""
4130

42-
def find_binary(executable: str = "codex") -> str:
43-
"""Return the absolute path to the Codex CLI binary or raise if not found."""
44-
path = shutil.which(executable)
45-
if not path:
46-
raise CodexNotFoundError(executable)
47-
return path
31+
_stream: Iterable[dict]
4832

49-
50-
def run_exec(
51-
prompt: str,
52-
*,
53-
model: str | None = None,
54-
oss: bool = False,
55-
full_auto: bool = False,
56-
cd: str | None = None,
57-
skip_git_repo_check: bool = False,
58-
timeout: float | None = None,
59-
env: Mapping[str, str] | None = None,
60-
executable: str = "codex",
61-
extra_args: Iterable[str] | None = None,
62-
json: bool = False,
63-
) -> str:
64-
"""
65-
Run `codex exec` with the given prompt and return stdout as text.
66-
67-
- Raises CodexNotFoundError if the binary is unavailable.
68-
- Raises CodexProcessError on non‑zero exit with captured stdout/stderr.
69-
"""
70-
bin_path = find_binary(executable)
71-
72-
cmd: list[str] = [bin_path]
73-
74-
if cd:
75-
cmd.extend(["--cd", cd])
76-
if model:
77-
cmd.extend(["-m", model])
78-
if oss:
79-
cmd.append("--oss")
80-
if full_auto:
81-
cmd.append("--full-auto")
82-
if skip_git_repo_check:
83-
cmd.append("--skip-git-repo-check")
84-
if extra_args:
85-
cmd.extend(list(extra_args))
86-
87-
cmd.append("exec")
88-
if json:
89-
cmd.append("--json")
90-
cmd.append(prompt)
91-
92-
completed = subprocess.run(
93-
cmd,
94-
capture_output=True,
95-
text=True,
96-
timeout=timeout,
97-
env={**os.environ, **(dict(env) if env else {})},
98-
check=False,
99-
)
100-
101-
stdout = completed.stdout or ""
102-
stderr = completed.stderr or ""
103-
if completed.returncode != 0:
104-
raise CodexProcessError(
105-
returncode=completed.returncode,
106-
cmd=tuple(cmd),
107-
stdout=stdout,
108-
stderr=stderr,
109-
)
110-
return stdout
33+
def __iter__(self) -> Iterator[Event]:
34+
"""Yield `Event` objects from the native stream."""
35+
for item in self._stream:
36+
yield Event.model_validate(item)
11137

11238

11339
@dataclass(slots=True)
11440
class CodexClient:
115-
"""Lightweight, synchronous client for the Codex CLI.
41+
"""Lightweight, synchronous client for the native Codex core.
11642
117-
Provides defaults for repeated invocations and convenience helpers.
43+
Provides defaults for repeated invocations and conversation management.
11844
"""
11945

120-
executable: str = "codex"
121-
model: str | None = None
122-
full_auto: bool = False
123-
cd: str | None = None
46+
config: CodexConfig | None = None
47+
load_default_config: bool = True
12448
env: Mapping[str, str] | None = None
12549
extra_args: Sequence[str] | None = None
12650

127-
def ensure_available(self) -> str:
128-
"""Return the resolved binary path or raise CodexNotFoundError."""
129-
return find_binary(self.executable)
130-
131-
def run(
51+
def start_conversation(
13252
self,
13353
prompt: str,
13454
*,
135-
model: str | None = None,
136-
oss: bool | None = None,
137-
full_auto: bool | None = None,
138-
cd: str | None = None,
139-
skip_git_repo_check: bool | None = None,
140-
timeout: float | None = None,
141-
env: Mapping[str, str] | None = None,
142-
extra_args: Iterable[str] | None = None,
143-
) -> str:
144-
"""Execute `codex exec` and return stdout.
145-
146-
Explicit arguments override the client's defaults.
147-
"""
148-
eff_model = model if model is not None else self.model
149-
eff_full_auto = full_auto if full_auto is not None else self.full_auto
150-
eff_cd = cd if cd is not None else self.cd
151-
eff_oss = bool(oss) if oss is not None else False
152-
eff_skip_git = bool(skip_git_repo_check) if skip_git_repo_check is not None else False
153-
154-
# Merge environment overlays; run_exec will merge with os.environ
155-
merged_env: Mapping[str, str] | None
156-
if self.env and env:
157-
tmp = dict(self.env)
158-
tmp.update(env)
159-
merged_env = tmp
160-
else:
161-
merged_env = env or self.env
162-
163-
# Compose extra args
164-
eff_extra: list[str] = []
165-
if self.extra_args:
166-
eff_extra.extend(self.extra_args)
167-
if extra_args:
168-
eff_extra.extend(list(extra_args))
169-
170-
return run_exec(
55+
config: CodexConfig | None = None,
56+
load_default_config: bool | None = None,
57+
) -> Conversation:
58+
"""Start a new conversation and return a streaming iterator over events."""
59+
eff_config = config if config is not None else self.config
60+
eff_load_default_config = (
61+
load_default_config
62+
if load_default_config is not None
63+
else self.load_default_config
64+
)
65+
66+
try:
67+
stream = native_start_exec_stream(
68+
prompt,
69+
config_overrides=eff_config.to_dict() if eff_config else None,
70+
load_default_config=eff_load_default_config,
71+
)
72+
return Conversation(_stream=stream)
73+
except RuntimeError as e:
74+
raise CodexNativeError() from e
75+
76+
77+
def run_exec(
78+
prompt: str,
79+
*,
80+
config: CodexConfig | None = None,
81+
load_default_config: bool = True,
82+
) -> list[Event]:
83+
"""
84+
Run a prompt through the native Codex engine and return a list of events.
85+
86+
- Raises CodexNativeError if the native extension is unavailable or fails.
87+
"""
88+
try:
89+
events = native_run_exec_collect(
17190
prompt,
172-
model=eff_model,
173-
oss=eff_oss,
174-
full_auto=eff_full_auto,
175-
cd=eff_cd,
176-
skip_git_repo_check=eff_skip_git,
177-
timeout=timeout,
178-
env=merged_env,
179-
executable=self.executable,
180-
extra_args=eff_extra,
91+
config_overrides=config.to_dict() if config else None,
92+
load_default_config=load_default_config,
18193
)
94+
return [Event.model_validate(e) for e in events]
95+
except RuntimeError as e:
96+
raise CodexNativeError() from e

codex/config.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import asdict, dataclass
4+
from typing import Any
5+
6+
AskForApproval = "Always" | "Never" | "Auto"
7+
SandboxMode = "None" | "WorkspaceWrite" | "ReadOnly"
8+
9+
10+
@dataclass(slots=True)
11+
class CodexConfig:
12+
"""Configuration for the Codex client.
13+
14+
Attributes:
15+
model: The model to use for the conversation.
16+
model_provider: The provider of the model.
17+
approval_policy: The policy for approving tool calls.
18+
sandbox_mode: The sandbox mode to use for shell commands.
19+
cwd: The working directory to use for the conversation.
20+
"""
21+
22+
model: str | None = None
23+
model_provider: str | None = None
24+
approval_policy: AskForApproval | None = None
25+
sandbox_mode: SandboxMode | None = None
26+
cwd: str | None = None
27+
28+
def to_dict(self) -> dict[str, Any]:
29+
"""Convert the config to a dictionary, filtering out None values."""
30+
return {k: v for k, v in asdict(self).items() if v is not None}

codex/event.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel
6+
7+
8+
class Event(BaseModel):
9+
"""Protocol event envelope."""
10+
11+
id: str
12+
msg: dict[str, Any]

codex/native.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010

1111
def run_exec_collect(
12-
prompt: str, *, model: str | None = None, full_auto: bool = False, cd: str | None = None
12+
prompt: str,
13+
*,
14+
config_overrides: dict[str, Any] | None = None,
15+
load_default_config: bool = True,
1316
) -> list[dict]:
1417
"""Run Codex natively (in‑process) and return a list of events as dicts.
1518
@@ -20,15 +23,18 @@ def run_exec_collect(
2023
raise RuntimeError(
2124
"codex_native extension not installed. Run `make dev-native` or build wheels via maturin."
2225
)
23-
return [dict(e) for e in _run_exec_collect(prompt, model, full_auto, cd)]
26+
return _run_exec_collect(prompt, config_overrides, load_default_config)
2427

2528

2629
def start_exec_stream(
27-
prompt: str, *, model: str | None = None, full_auto: bool = False, cd: str | None = None
30+
prompt: str,
31+
*,
32+
config_overrides: dict[str, Any] | None = None,
33+
load_default_config: bool = True,
2834
) -> Any:
2935
"""Return a native streaming iterator over Codex events (dicts)."""
3036
if _start_exec_stream is None:
3137
raise RuntimeError(
3238
"codex_native extension not installed. Run `make dev-native` or build wheels via maturin."
3339
)
34-
return _start_exec_stream(prompt, model, full_auto, cd)
40+
return _start_exec_stream(prompt, config_overrides, load_default_config)

0 commit comments

Comments
 (0)