python-codex 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. pycodex/__init__.py +5 -1
  2. pycodex/agent.py +39 -41
  3. pycodex/cli.py +43 -42
  4. pycodex/collaboration.py +6 -7
  5. pycodex/compat.py +99 -0
  6. pycodex/context.py +87 -87
  7. pycodex/doctor.py +40 -40
  8. pycodex/model.py +69 -69
  9. pycodex/portable.py +33 -33
  10. pycodex/portable_server.py +22 -21
  11. pycodex/protocol.py +84 -86
  12. pycodex/runtime.py +36 -35
  13. pycodex/runtime_services.py +69 -69
  14. pycodex/tools/agent_tool_schemas.py +0 -2
  15. pycodex/tools/apply_patch_tool.py +43 -44
  16. pycodex/tools/base_tool.py +35 -36
  17. pycodex/tools/close_agent_tool.py +2 -4
  18. pycodex/tools/code_mode_manager.py +61 -61
  19. pycodex/tools/exec_command_tool.py +5 -6
  20. pycodex/tools/exec_runtime.js +3 -3
  21. pycodex/tools/exec_tool.py +2 -4
  22. pycodex/tools/grep_files_tool.py +10 -11
  23. pycodex/tools/list_dir_tool.py +8 -9
  24. pycodex/tools/read_file_tool.py +13 -14
  25. pycodex/tools/request_permissions_tool.py +2 -4
  26. pycodex/tools/request_user_input_tool.py +13 -14
  27. pycodex/tools/resume_agent_tool.py +2 -4
  28. pycodex/tools/send_input_tool.py +8 -9
  29. pycodex/tools/shell_command_tool.py +5 -6
  30. pycodex/tools/shell_tool.py +5 -6
  31. pycodex/tools/spawn_agent_tool.py +4 -5
  32. pycodex/tools/unified_exec_manager.py +62 -61
  33. pycodex/tools/update_plan_tool.py +4 -5
  34. pycodex/tools/view_image_tool.py +4 -5
  35. pycodex/tools/wait_agent_tool.py +2 -4
  36. pycodex/tools/wait_tool.py +4 -5
  37. pycodex/tools/web_search_tool.py +1 -3
  38. pycodex/tools/write_stdin_tool.py +4 -5
  39. pycodex/utils/dotenv.py +6 -6
  40. pycodex/utils/get_env.py +37 -33
  41. pycodex/utils/random_ids.py +1 -2
  42. pycodex/utils/visualize.py +79 -79
  43. {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
  44. python_codex-0.1.3.dist-info/RECORD +74 -0
  45. {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
  46. responses_server/app.py +29 -19
  47. responses_server/config.py +17 -17
  48. responses_server/payload_processors.py +16 -16
  49. responses_server/server.py +11 -11
  50. responses_server/session_store.py +10 -10
  51. responses_server/stream_router.py +58 -58
  52. responses_server/tools/custom_adapter.py +12 -12
  53. responses_server/tools/web_search.py +33 -33
  54. python_codex-0.1.2.dist-info/RECORD +0 -73
  55. {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
  56. {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -10,11 +10,10 @@ Expected behavior:
10
10
  confirmation text Codex uses.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  from ..protocol import JSONDict, JSONValue
16
14
  from ..runtime_services import PlanItem, PlanStore
17
15
  from .base_tool import BaseTool, ToolContext
16
+ import typing
18
17
 
19
18
  VALID_PLAN_STATUSES = {"pending", "in_progress", "completed"}
20
19
 
@@ -52,16 +51,16 @@ class UpdatePlanTool(BaseTool):
52
51
  }
53
52
  supports_parallel = False
54
53
 
55
- def __init__(self, plan_store: PlanStore) -> None:
54
+ def __init__(self, plan_store: 'PlanStore') -> 'None':
56
55
  self._plan_store = plan_store
57
56
 
58
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
57
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
59
58
  del context
60
59
  raw_plan = args.get("plan")
61
60
  if not isinstance(raw_plan, list):
62
61
  return "Error: `plan` must be a list."
63
62
 
64
- plan_items: list[PlanItem] = []
63
+ plan_items: 'typing.List[PlanItem]' = []
65
64
  for item in raw_plan:
66
65
  if not isinstance(item, dict):
67
66
  return "Error: each `plan` item must be an object."
@@ -12,14 +12,13 @@ Expected behavior:
12
12
  item that Codex uses when feeding image tool output back to the model.
13
13
  """
14
14
 
15
- from __future__ import annotations
16
-
17
15
  import base64
18
16
  import mimetypes
19
17
  from pathlib import Path
20
18
 
21
19
  from ..protocol import JSONDict, JSONValue
22
20
  from .base_tool import BaseTool, StructuredToolOutput, ToolContext
21
+ import typing
23
22
 
24
23
  VIEW_IMAGE_OUTPUT_SCHEMA = {
25
24
  "type": "object",
@@ -62,10 +61,10 @@ class ViewImageTool(BaseTool):
62
61
  }
63
62
  output_schema = VIEW_IMAGE_OUTPUT_SCHEMA
64
63
 
65
- def __init__(self, cwd: str | Path | None = None) -> None:
64
+ def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
66
65
  self._workspace_root = Path(cwd or Path.cwd()).resolve()
67
66
 
68
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
67
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
69
68
  del context
70
69
  path_value = str(args.get("path", "")).strip()
71
70
  if not path_value:
@@ -102,7 +101,7 @@ class ViewImageTool(BaseTool):
102
101
  "image_url": image_url,
103
102
  "detail": detail,
104
103
  }
105
- image_item: JSONDict = {
104
+ image_item: 'JSONDict' = {
106
105
  "type": "input_image",
107
106
  "image_url": image_url,
108
107
  }
@@ -9,8 +9,6 @@ Expected behavior:
9
9
  wait times out.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from ..protocol import JSONDict, JSONValue
15
13
  from ..runtime_services import SubAgentManager
16
14
  from .agent_tool_schemas import AGENT_STATUS_SCHEMA
@@ -60,10 +58,10 @@ class WaitAgentTool(BaseTool):
60
58
  output_schema = WAIT_AGENT_OUTPUT_SCHEMA
61
59
  supports_parallel = False
62
60
 
63
- def __init__(self, subagent_manager: SubAgentManager) -> None:
61
+ def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
64
62
  self._subagent_manager = subagent_manager
65
63
 
66
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
64
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
67
65
  del context
68
66
  ids = args.get("ids")
69
67
  if not isinstance(ids, list) or not ids:
@@ -9,11 +9,10 @@ Expected behavior:
9
9
  - Return only the new output since the previous `exec` / `wait` snapshot.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from ..protocol import JSONDict, JSONValue
15
13
  from .base_tool import BaseTool, ToolContext
16
14
  from .code_mode_manager import DEFAULT_WAIT_YIELD_TIME_MS, CodeModeManager
15
+ import typing
17
16
 
18
17
 
19
18
  class WaitTool(BaseTool):
@@ -46,10 +45,10 @@ class WaitTool(BaseTool):
46
45
  }
47
46
  supports_parallel = False
48
47
 
49
- def __init__(self, manager: CodeModeManager) -> None:
48
+ def __init__(self, manager: 'CodeModeManager') -> 'None':
50
49
  self._manager = manager
51
50
 
52
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
51
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
53
52
  del context
54
53
  cell_id = str(args.get("cell_id", "")).strip()
55
54
  if not cell_id:
@@ -61,7 +60,7 @@ class WaitTool(BaseTool):
61
60
  terminate=bool(args.get("terminate", False)),
62
61
  )
63
62
 
64
- def _optional_int(self, args: JSONDict, key: str) -> int | None:
63
+ def _optional_int(self, args: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
65
64
  value = args.get(key)
66
65
  if value in (None, ""):
67
66
  return None
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  - Never expect a local tool-call round-trip result from the model.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  from ..protocol import JSONDict, JSONValue
16
14
  from .base_tool import BaseTool, ToolContext
17
15
 
@@ -25,6 +23,6 @@ class WebSearchTool(BaseTool):
25
23
  }
26
24
  supports_parallel = False
27
25
 
28
- async def run(self, context: ToolContext, args: JSONValue) -> JSONValue:
26
+ async def run(self, context: 'ToolContext', args: 'JSONValue') -> 'JSONValue':
29
27
  del context, args
30
28
  return "Error: web_search is provider-native and should not be executed locally."
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  - Reuse the same `session_id` until the process exits.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  from ..protocol import JSONDict, JSONValue
16
14
  from .base_tool import BaseTool, ToolContext
17
15
  from .unified_exec_manager import (
@@ -19,6 +17,7 @@ from .unified_exec_manager import (
19
17
  UNIFIED_EXEC_OUTPUT_SCHEMA,
20
18
  UnifiedExecManager,
21
19
  )
20
+ import typing
22
21
 
23
22
 
24
23
  class WriteStdinTool(BaseTool):
@@ -50,10 +49,10 @@ class WriteStdinTool(BaseTool):
50
49
  output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
51
50
  supports_parallel = False
52
51
 
53
- def __init__(self, manager: UnifiedExecManager) -> None:
52
+ def __init__(self, manager: 'UnifiedExecManager') -> 'None':
54
53
  self._manager = manager
55
54
 
56
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
55
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
57
56
  del context
58
57
  session_id = args.get("session_id")
59
58
  if session_id is None:
@@ -68,7 +67,7 @@ class WriteStdinTool(BaseTool):
68
67
  max_output_tokens=self._optional_int(args, "max_output_tokens"),
69
68
  )
70
69
 
71
- def _optional_int(self, args: JSONDict, key: str) -> int | None:
70
+ def _optional_int(self, args: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
72
71
  value = args.get(key)
73
72
  if value in (None, ""):
74
73
  return None
pycodex/utils/dotenv.py CHANGED
@@ -1,14 +1,14 @@
1
- from __future__ import annotations
2
1
 
3
2
  import os
4
3
  from pathlib import Path
4
+ import typing
5
5
 
6
6
  ILLEGAL_ENV_VAR_PREFIX = "CODEX_"
7
7
  DOTENV_FILENAME = ".env"
8
- _LOADED_CODEX_DOTENV_HOMES: set[str] = set()
8
+ _LOADED_CODEX_DOTENV_HOMES: 'typing.Set[str]' = set()
9
9
 
10
10
 
11
- def load_codex_dotenv(config_path: str | Path) -> None:
11
+ def load_codex_dotenv(config_path: 'typing.Union[str, Path]') -> 'None':
12
12
  codex_home = str(Path(config_path).resolve().parent)
13
13
  if codex_home in _LOADED_CODEX_DOTENV_HOMES:
14
14
  return
@@ -26,8 +26,8 @@ def load_codex_dotenv(config_path: str | Path) -> None:
26
26
  _LOADED_CODEX_DOTENV_HOMES.add(codex_home)
27
27
 
28
28
 
29
- def parse_dotenv(text: str) -> dict[str, str]:
30
- values: dict[str, str] = {}
29
+ def parse_dotenv(text: 'str') -> 'typing.Dict[str, str]':
30
+ values: 'typing.Dict[str, str]' = {}
31
31
  for raw_line in text.splitlines():
32
32
  line = raw_line.strip()
33
33
  if not line or line.startswith("#"):
@@ -45,7 +45,7 @@ def parse_dotenv(text: str) -> dict[str, str]:
45
45
  return values
46
46
 
47
47
 
48
- def parse_dotenv_value(raw_value: str) -> str:
48
+ def parse_dotenv_value(raw_value: 'str') -> 'str':
49
49
  if not raw_value:
50
50
  return ""
51
51
 
pycodex/utils/get_env.py CHANGED
@@ -1,22 +1,22 @@
1
- from __future__ import annotations
2
-
3
- import importlib.metadata
4
1
  import os
5
2
  import platform
6
3
  import re
7
4
  from datetime import datetime
8
5
  from pathlib import Path
9
6
  import subprocess
7
+ import typing
8
+
9
+ from ..compat import importlib_metadata
10
10
 
11
11
 
12
- def get_shell_name() -> str:
12
+ def get_shell_name() -> 'str':
13
13
  shell_path = os.environ.get("SHELL")
14
14
  if shell_path:
15
15
  return Path(shell_path).name or shell_path
16
16
  return "bash"
17
17
 
18
18
 
19
- def get_timezone_name() -> str:
19
+ def get_timezone_name() -> 'str':
20
20
  timezone_env = os.environ.get("TZ")
21
21
  if timezone_env:
22
22
  return timezone_env
@@ -33,7 +33,7 @@ def get_timezone_name() -> str:
33
33
  return "Etc/UTC"
34
34
  name = str(timezone)
35
35
  return name or "Etc/UTC"
36
- def get_sandbox_tag(sandbox_mode: str | None) -> str:
36
+ def get_sandbox_tag(sandbox_mode: 'typing.Union[str, None]') -> 'str':
37
37
  if sandbox_mode == "danger-full-access":
38
38
  return "none"
39
39
  if sandbox_mode == "read-only":
@@ -43,7 +43,7 @@ def get_sandbox_tag(sandbox_mode: str | None) -> str:
43
43
  return "none"
44
44
 
45
45
 
46
- def get_workspace_turn_metadata(cwd: str | Path) -> dict[str, object] | None:
46
+ def get_workspace_turn_metadata(cwd: 'typing.Union[str, Path]') -> 'typing.Union[typing.Dict[str, object], None]':
47
47
  resolved_cwd = Path(cwd).resolve()
48
48
  repo_root = _git_output(
49
49
  resolved_cwd,
@@ -52,7 +52,7 @@ def get_workspace_turn_metadata(cwd: str | Path) -> dict[str, object] | None:
52
52
  if repo_root is None:
53
53
  return None
54
54
 
55
- workspace: dict[str, object] = {}
55
+ workspace: 'typing.Dict[str, object]' = {}
56
56
  head = _git_output(resolved_cwd, ["rev-parse", "HEAD"])
57
57
  if head is not None:
58
58
  workspace["latest_git_commit_hash"] = head
@@ -70,7 +70,7 @@ def get_workspace_turn_metadata(cwd: str | Path) -> dict[str, object] | None:
70
70
  return {"workspaces": {repo_root: workspace}}
71
71
 
72
72
 
73
- def build_user_agent(originator: str) -> str:
73
+ def build_user_agent(originator: 'str') -> 'str':
74
74
  version = get_package_version()
75
75
  terminal = get_terminal_user_agent_token()
76
76
  os_name, os_version = get_os_info()
@@ -79,14 +79,14 @@ def build_user_agent(originator: str) -> str:
79
79
  return f"{originator}/{version} ({os_name} {os_version}; {arch}) {terminal}{suffix}"
80
80
 
81
81
 
82
- def get_package_version() -> str:
82
+ def get_package_version() -> 'str':
83
83
  detected = _detect_upstream_codex_version()
84
84
  if detected is not None:
85
85
  return detected
86
86
  for distribution_name in ("python-codex", "pycodex"):
87
87
  try:
88
- return importlib.metadata.version(distribution_name)
89
- except importlib.metadata.PackageNotFoundError:
88
+ return importlib_metadata.version(distribution_name)
89
+ except importlib_metadata.PackageNotFoundError:
90
90
  continue
91
91
  local_version = _read_local_package_version()
92
92
  if local_version is not None:
@@ -94,10 +94,10 @@ def get_package_version() -> str:
94
94
  return "0.1.0"
95
95
 
96
96
 
97
- def get_os_info() -> tuple[str, str]:
97
+ def get_os_info() -> 'typing.Tuple[str, str]':
98
98
  os_release = Path("/etc/os-release")
99
99
  if os_release.is_file():
100
- values: dict[str, str] = {}
100
+ values: 'typing.Dict[str, str]' = {}
101
101
  for line in os_release.read_text().splitlines():
102
102
  if "=" not in line:
103
103
  continue
@@ -110,7 +110,7 @@ def get_os_info() -> tuple[str, str]:
110
110
  return platform.system(), platform.release()
111
111
 
112
112
 
113
- def get_terminal_user_agent_token() -> str:
113
+ def get_terminal_user_agent_token() -> 'str':
114
114
  term_program = os.environ.get("TERM_PROGRAM", "")
115
115
  if term_program.lower() == "tmux":
116
116
  client_termname = _tmux_display_message("#{client_termname}")
@@ -123,14 +123,15 @@ def get_terminal_user_agent_token() -> str:
123
123
  return "unknown"
124
124
 
125
125
 
126
- def _git_output(cwd: Path, args: list[str]) -> str | None:
126
+ def _git_output(cwd: 'Path', args: 'typing.List[str]') -> 'typing.Union[str, None]':
127
127
  try:
128
128
  completed = subprocess.run(
129
129
  ["git", *args],
130
130
  cwd=str(cwd),
131
131
  check=True,
132
- capture_output=True,
133
- text=True,
132
+ stdout=subprocess.PIPE,
133
+ stderr=subprocess.PIPE,
134
+ universal_newlines=True,
134
135
  )
135
136
  except (OSError, subprocess.CalledProcessError):
136
137
  return None
@@ -138,11 +139,11 @@ def _git_output(cwd: Path, args: list[str]) -> str | None:
138
139
  return value or None
139
140
 
140
141
 
141
- def _git_remote_urls(cwd: Path) -> dict[str, str]:
142
+ def _git_remote_urls(cwd: 'Path') -> 'typing.Dict[str, str]':
142
143
  remote_names = _git_output(cwd, ["remote"])
143
144
  if remote_names is None:
144
145
  return {}
145
- remotes: dict[str, str] = {}
146
+ remotes: 'typing.Dict[str, str]' = {}
146
147
  for name in remote_names.splitlines():
147
148
  remote_name = name.strip()
148
149
  if not remote_name:
@@ -153,21 +154,22 @@ def _git_remote_urls(cwd: Path) -> dict[str, str]:
153
154
  return remotes
154
155
 
155
156
 
156
- def _git_has_changes(cwd: Path) -> bool | None:
157
+ def _git_has_changes(cwd: 'Path') -> 'typing.Union[bool, None]':
157
158
  try:
158
159
  completed = subprocess.run(
159
160
  ["git", "status", "--porcelain"],
160
161
  cwd=str(cwd),
161
162
  check=True,
162
- capture_output=True,
163
- text=True,
163
+ stdout=subprocess.PIPE,
164
+ stderr=subprocess.PIPE,
165
+ universal_newlines=True,
164
166
  )
165
167
  except (OSError, subprocess.CalledProcessError):
166
168
  return None
167
169
  return bool(completed.stdout.strip())
168
170
 
169
171
 
170
- def _user_agent_suffix(originator: str, version: str) -> str:
172
+ def _user_agent_suffix(originator: 'str', version: 'str') -> 'str':
171
173
  if originator == "codex_exec":
172
174
  return f" (codex-exec; {version})"
173
175
  if originator == "codex-tui":
@@ -175,7 +177,7 @@ def _user_agent_suffix(originator: str, version: str) -> str:
175
177
  return ""
176
178
 
177
179
 
178
- def _normalize_os_version(version: str) -> str:
180
+ def _normalize_os_version(version: 'str') -> 'str':
179
181
  parts = version.split(".")
180
182
  if len(parts) == 2 and all(part.isdigit() for part in parts):
181
183
  major, minor = parts
@@ -183,7 +185,7 @@ def _normalize_os_version(version: str) -> str:
183
185
  return version
184
186
 
185
187
 
186
- def _read_local_package_version() -> str | None:
188
+ def _read_local_package_version() -> 'typing.Union[str, None]':
187
189
  pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
188
190
  if not pyproject_path.is_file():
189
191
  return None
@@ -197,13 +199,14 @@ def _read_local_package_version() -> str | None:
197
199
  return match.group(1).strip() or None
198
200
 
199
201
 
200
- def _tmux_display_message(fmt: str) -> str | None:
202
+ def _tmux_display_message(fmt: 'str') -> 'typing.Union[str, None]':
201
203
  try:
202
204
  output = subprocess.run(
203
205
  ["tmux", "display-message", "-p", fmt],
204
206
  check=True,
205
- capture_output=True,
206
- text=True,
207
+ stdout=subprocess.PIPE,
208
+ stderr=subprocess.PIPE,
209
+ universal_newlines=True,
207
210
  )
208
211
  except (OSError, subprocess.CalledProcessError):
209
212
  return None
@@ -211,7 +214,7 @@ def _tmux_display_message(fmt: str) -> str | None:
211
214
  return value or None
212
215
 
213
216
 
214
- def _sanitize_header_token(value: str) -> str:
217
+ def _sanitize_header_token(value: 'str') -> 'str':
215
218
  return "".join(
216
219
  character
217
220
  if (character.isalnum() or character in {"-", "_", ".", "/"})
@@ -220,13 +223,14 @@ def _sanitize_header_token(value: str) -> str:
220
223
  )
221
224
 
222
225
 
223
- def _detect_upstream_codex_version() -> str | None:
226
+ def _detect_upstream_codex_version() -> 'typing.Union[str, None]':
224
227
  try:
225
228
  output = subprocess.run(
226
229
  ["codex", "--version"],
227
230
  check=True,
228
- capture_output=True,
229
- text=True,
231
+ stdout=subprocess.PIPE,
232
+ stderr=subprocess.PIPE,
233
+ universal_newlines=True,
230
234
  )
231
235
  except (OSError, subprocess.CalledProcessError):
232
236
  return None
@@ -1,11 +1,10 @@
1
- from __future__ import annotations
2
1
 
3
2
  import random
4
3
  import time
5
4
  import uuid
6
5
 
7
6
 
8
- def uuid7_string() -> str:
7
+ def uuid7_string() -> 'str':
9
8
  timestamp_ms = int(time.time() * 1000) & ((1 << 48) - 1)
10
9
  rand_a = random.getrandbits(12)
11
10
  rand_b = random.getrandbits(62)