python-codex 0.1.2__tar.gz → 0.1.3__tar.gz

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 (100) hide show
  1. python_codex-0.1.3/.github/workflows/test.yml +82 -0
  2. {python_codex-0.1.2 → python_codex-0.1.3}/PKG-INFO +15 -9
  3. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/__init__.py +5 -1
  4. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/agent.py +39 -41
  5. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/cli.py +43 -42
  6. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/collaboration.py +6 -7
  7. python_codex-0.1.3/pycodex/compat.py +99 -0
  8. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/context.py +87 -87
  9. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/doctor.py +40 -40
  10. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/model.py +69 -69
  11. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/portable.py +33 -33
  12. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/portable_server.py +22 -21
  13. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/protocol.py +84 -86
  14. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/runtime.py +36 -35
  15. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/runtime_services.py +69 -69
  16. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/agent_tool_schemas.py +0 -2
  17. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/apply_patch_tool.py +43 -44
  18. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/base_tool.py +35 -36
  19. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/close_agent_tool.py +2 -4
  20. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/code_mode_manager.py +61 -61
  21. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/exec_command_tool.py +5 -6
  22. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/exec_runtime.js +3 -3
  23. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/exec_tool.py +2 -4
  24. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/grep_files_tool.py +10 -11
  25. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/list_dir_tool.py +8 -9
  26. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/read_file_tool.py +13 -14
  27. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/request_permissions_tool.py +2 -4
  28. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/request_user_input_tool.py +13 -14
  29. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/resume_agent_tool.py +2 -4
  30. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/send_input_tool.py +8 -9
  31. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/shell_command_tool.py +5 -6
  32. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/shell_tool.py +5 -6
  33. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/spawn_agent_tool.py +4 -5
  34. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/unified_exec_manager.py +62 -61
  35. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/update_plan_tool.py +4 -5
  36. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/view_image_tool.py +4 -5
  37. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/wait_agent_tool.py +2 -4
  38. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/wait_tool.py +4 -5
  39. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/web_search_tool.py +1 -3
  40. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/write_stdin_tool.py +4 -5
  41. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/utils/dotenv.py +6 -6
  42. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/utils/get_env.py +37 -33
  43. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/utils/random_ids.py +1 -2
  44. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/utils/visualize.py +79 -79
  45. python_codex-0.1.3/pyproject.toml +50 -0
  46. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/app.py +29 -19
  47. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/config.py +17 -17
  48. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/payload_processors.py +16 -16
  49. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/server.py +11 -11
  50. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/session_store.py +10 -10
  51. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/stream_router.py +58 -58
  52. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/tools/custom_adapter.py +12 -12
  53. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/tools/web_search.py +33 -33
  54. {python_codex-0.1.2 → python_codex-0.1.3}/tests/compare_request_user_input_roundtrip.py +71 -71
  55. {python_codex-0.1.2 → python_codex-0.1.3}/tests/compare_steer_request_bodies.py +69 -69
  56. {python_codex-0.1.2 → python_codex-0.1.3}/tests/compare_tool_schemas.py +52 -51
  57. {python_codex-0.1.2 → python_codex-0.1.3}/tests/fake_responses_server.py +43 -42
  58. {python_codex-0.1.2 → python_codex-0.1.3}/tests/fakes.py +10 -10
  59. {python_codex-0.1.2 → python_codex-0.1.3}/tests/responses_server/fake_chat_completions_server.py +51 -42
  60. {python_codex-0.1.2 → python_codex-0.1.3}/tests/responses_server/test_server.py +22 -22
  61. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_agent.py +12 -12
  62. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_builtin_tools.py +33 -33
  63. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_cli.py +143 -142
  64. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_context.py +6 -7
  65. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_doctor.py +8 -9
  66. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_fake_responses_server.py +7 -6
  67. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_model.py +32 -31
  68. {python_codex-0.1.2 → python_codex-0.1.3}/tests/test_portable.py +14 -14
  69. python_codex-0.1.2/.github/workflows/test.yml +0 -65
  70. python_codex-0.1.2/pyproject.toml +0 -41
  71. {python_codex-0.1.2 → python_codex-0.1.3}/.github/workflows/publish.yml +0 -0
  72. {python_codex-0.1.2 → python_codex-0.1.3}/.gitignore +0 -0
  73. {python_codex-0.1.2 → python_codex-0.1.3}/AGENTS.md +0 -0
  74. {python_codex-0.1.2 → python_codex-0.1.3}/LICENSE +0 -0
  75. {python_codex-0.1.2 → python_codex-0.1.3}/README.md +0 -0
  76. {python_codex-0.1.2 → python_codex-0.1.3}/README_ZH.md +0 -0
  77. {python_codex-0.1.2 → python_codex-0.1.3}/docs/ALIGNMENT.md +0 -0
  78. {python_codex-0.1.2 → python_codex-0.1.3}/docs/CONTEXT.md +0 -0
  79. {python_codex-0.1.2 → python_codex-0.1.3}/docs/responses_server/README.md +0 -0
  80. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/collaboration_default.md +0 -0
  81. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/collaboration_plan.md +0 -0
  82. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/default_base_instructions.md +0 -0
  83. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/exec_tools.json +0 -0
  84. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/models.json +0 -0
  85. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
  86. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
  87. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
  88. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
  89. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
  90. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
  91. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
  92. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
  93. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/prompts/subagent_tools.json +0 -0
  94. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/tools/__init__.py +0 -0
  95. {python_codex-0.1.2 → python_codex-0.1.3}/pycodex/utils/__init__.py +0 -0
  96. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/__init__.py +0 -0
  97. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/__main__.py +0 -0
  98. {python_codex-0.1.2 → python_codex-0.1.3}/responses_server/tools/__init__.py +0 -0
  99. {python_codex-0.1.2 → python_codex-0.1.3}/tests/TESTS.md +0 -0
  100. {python_codex-0.1.2 → python_codex-0.1.3}/tests/__init__.py +0 -0
@@ -0,0 +1,82 @@
1
+ name: test
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ pytest-modern:
12
+ name: pytest (Python ${{ matrix.python-version }})
13
+ runs-on: ubuntu-22.04
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version:
18
+ - "3.7"
19
+ - "3.8"
20
+ - "3.10"
21
+ steps:
22
+ - name: Check out repository
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+
30
+ - name: Set up uv
31
+ uses: astral-sh/setup-uv@v5
32
+
33
+ - name: Sync dependencies
34
+ run: uv sync --dev
35
+
36
+ - name: Run pytest
37
+ run: uv run pytest
38
+
39
+ pytest-py36:
40
+ name: pytest (Python 3.6)
41
+ runs-on: ubuntu-22.04
42
+ container:
43
+ image: python:3.6.15-slim-bullseye
44
+ steps:
45
+ - name: Install system dependencies
46
+ run: |
47
+ apt-get update
48
+ apt-get install -y --no-install-recommends git nodejs npm
49
+
50
+ - name: Check out repository
51
+ uses: actions/checkout@v4
52
+
53
+ - name: Mark workspace as safe for git
54
+ run: git config --global --add safe.directory "$PWD"
55
+
56
+ - name: Show runtime versions
57
+ run: |
58
+ python --version
59
+ pip --version
60
+ git --version
61
+ node --version
62
+
63
+ - name: Install Python 3.6 compatibility dependencies
64
+ run: |
65
+ python -m pip install -i https://pypi.org/simple \
66
+ "dataclasses>=0.8" \
67
+ "typing_extensions>=4.1.1,<4.2" \
68
+ "importlib_metadata>=4.8.3,<5" \
69
+ "tomli>=1.2.3,<2" \
70
+ "requests>=2.27.1" \
71
+ "prompt-toolkit>=3.0.36,<3.1" \
72
+ "loguru>=0.7.3,<1" \
73
+ "cryptography>=40.0.2,<41" \
74
+ "fastapi>=0.83,<0.84" \
75
+ "uvicorn>=0.16,<0.17" \
76
+ "pytest>=6.2.5,<7" \
77
+ "pytest-asyncio>=0.16,<0.17"
78
+
79
+ - name: Run Python 3.6 pytest
80
+ env:
81
+ PYTHONPATH: ${{ github.workspace }}
82
+ run: python -m pytest -c /dev/null
@@ -1,16 +1,22 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
- Requires-Python: >=3.10
7
- Requires-Dist: cryptography>=3.4
8
- Requires-Dist: fastapi>=0.115
6
+ Requires-Python: >=3.6.2
7
+ Requires-Dist: cryptography<41,>=40.0.2; python_version < '3.7'
8
+ Requires-Dist: cryptography>=40.0.2; python_version >= '3.7'
9
+ Requires-Dist: dataclasses>=0.8; python_version < '3.7'
10
+ Requires-Dist: fastapi<0.84,>=0.83.0; python_version < '3.7'
11
+ Requires-Dist: fastapi>=0.83.0; python_version >= '3.7'
12
+ Requires-Dist: importlib-metadata>=4.8.3; python_version < '3.8'
9
13
  Requires-Dist: loguru>=0.7.3
10
- Requires-Dist: prompt-toolkit>=3.0
11
- Requires-Dist: requests>=2.31
12
- Requires-Dist: tomli>=2.0; python_version < '3.11'
13
- Requires-Dist: uvicorn>=0.32
14
+ Requires-Dist: prompt-toolkit>=3.0.36
15
+ Requires-Dist: requests>=2.27.1
16
+ Requires-Dist: tomli<2,>=1.2.3; python_version < '3.11'
17
+ Requires-Dist: typing-extensions>=4.1.1; python_version < '3.8'
18
+ Requires-Dist: uvicorn<0.17,>=0.16.0; python_version < '3.7'
19
+ Requires-Dist: uvicorn>=0.16.0; python_version >= '3.7'
14
20
  Description-Content-Type: text/markdown
15
21
 
16
22
  # pycodex
@@ -1,3 +1,7 @@
1
+ from .compat import patch_asyncio
2
+
3
+ patch_asyncio()
4
+
1
5
  from .agent import AgentLoop
2
6
  from .context import ContextConfig, ContextManager
3
7
  from .model import (
@@ -60,7 +64,7 @@ from .tools import (
60
64
  WriteStdinTool,
61
65
  )
62
66
 
63
- def debug(stop: bool = False):
67
+ def debug(stop: 'bool' = False):
64
68
 
65
69
  import socket
66
70
 
@@ -1,8 +1,7 @@
1
- from __future__ import annotations
2
1
 
3
2
  import asyncio
4
3
  import json
5
- from collections.abc import Callable
4
+ from typing import Callable
6
5
 
7
6
  from .context import ContextManager
8
7
  from .model import ModelClient
@@ -19,10 +18,11 @@ from .protocol import (
19
18
  )
20
19
  from .tools import ToolContext, ToolRegistry
21
20
  from .utils import uuid7_string
21
+ import typing
22
22
 
23
23
 
24
24
  EventHandler = Callable[[AgentEvent], None]
25
- NOOP_EVENT_HANDLER: EventHandler = lambda _event: None
25
+ NOOP_EVENT_HANDLER: 'EventHandler' = lambda _event: None
26
26
 
27
27
 
28
28
  class TurnInterrupted(RuntimeError):
@@ -40,47 +40,47 @@ class AgentLoop:
40
40
 
41
41
  def __init__(
42
42
  self,
43
- model_client: ModelClient,
44
- tool_registry: ToolRegistry,
45
- context_manager: ContextManager | None = None,
46
- parallel_tool_calls: bool = True,
47
- event_handler: EventHandler = NOOP_EVENT_HANDLER,
48
- initial_history: tuple[ConversationItem, ...] = (),
49
- ) -> None:
43
+ model_client: 'ModelClient',
44
+ tool_registry: 'ToolRegistry',
45
+ context_manager: 'typing.Union[ContextManager, None]' = None,
46
+ parallel_tool_calls: 'bool' = True,
47
+ event_handler: 'EventHandler' = NOOP_EVENT_HANDLER,
48
+ initial_history: 'typing.Tuple[ConversationItem, ...]' = (),
49
+ ) -> 'None':
50
50
  self._model_client = model_client
51
51
  self._tool_registry = tool_registry
52
52
  self._context_manager = context_manager or ContextManager()
53
53
  self._parallel_tool_calls = parallel_tool_calls
54
54
  self._event_handler = event_handler
55
- self._history: list[ConversationItem] = list(initial_history)
55
+ self._history: 'typing.List[ConversationItem]' = list(initial_history)
56
56
  self.interrupt_asap = False
57
57
 
58
58
  @property
59
- def history(self) -> tuple[ConversationItem, ...]:
59
+ def history(self) -> 'typing.Tuple[ConversationItem, ...]':
60
60
  return tuple(self._history)
61
61
 
62
62
  def set_event_handler(
63
- self, event_handler: EventHandler = NOOP_EVENT_HANDLER
64
- ) -> None:
63
+ self, event_handler: 'EventHandler' = NOOP_EVENT_HANDLER
64
+ ) -> 'None':
65
65
  self._event_handler = event_handler
66
66
 
67
67
  def _raise_if_interrupt_requested(
68
68
  self,
69
- turn_id: str,
70
- iteration: int,
71
- output_text: str | None = None,
72
- ) -> None:
69
+ turn_id: 'str',
70
+ iteration: 'int',
71
+ output_text: 'typing.Union[str, None]' = None,
72
+ ) -> 'None':
73
73
  if self.interrupt_asap:
74
74
  self.interrupt_asap = False
75
- payload: dict[str, object] = {"iteration": iteration}
75
+ payload: 'typing.Dict[str, object]' = {"iteration": iteration}
76
76
  if output_text is not None:
77
77
  payload["output_text"] = output_text
78
78
  self._emit("turn_interrupted", turn_id, **payload)
79
79
  raise TurnInterrupted("turn interrupted")
80
80
 
81
81
  async def run_turn(
82
- self, texts: list[str], turn_id: str | None = None
83
- ) -> TurnResult:
82
+ self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
83
+ ) -> 'TurnResult':
84
84
  turn_id = turn_id or uuid7_string()
85
85
  self.interrupt_asap = False
86
86
  for text in texts:
@@ -93,10 +93,8 @@ class AgentLoop:
93
93
  user_texts=list(texts),
94
94
  )
95
95
 
96
- last_assistant_message: str | None = None
97
- final_response_items: tuple[
98
- AssistantMessage | ToolCall | ReasoningItem, ...
99
- ] = ()
96
+ last_assistant_message: 'typing.Union[str, None]' = None
97
+ final_response_items: 'typing.Tuple[\n typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], ...\n]' = ()
100
98
 
101
99
  iteration = 0
102
100
  try:
@@ -132,7 +130,7 @@ class AgentLoop:
132
130
  item_count=len(response.items),
133
131
  )
134
132
 
135
- tool_calls: list[ToolCall] = []
133
+ tool_calls: 'typing.List[ToolCall]' = []
136
134
  for item in response.items:
137
135
  self._history.append(item)
138
136
  if isinstance(item, AssistantMessage):
@@ -182,11 +180,11 @@ class AgentLoop:
182
180
 
183
181
  async def _execute_tool_batch(
184
182
  self,
185
- turn_id: str,
186
- tool_calls: list[ToolCall],
187
- ) -> list[ToolResult]:
188
- results: list[ToolResult] = []
189
- parallel_batch: list[ToolCall] = []
183
+ turn_id: 'str',
184
+ tool_calls: 'typing.List[ToolCall]',
185
+ ) -> 'typing.List[ToolResult]':
186
+ results: 'typing.List[ToolResult]' = []
187
+ parallel_batch: 'typing.List[ToolCall]' = []
190
188
 
191
189
  for call in tool_calls:
192
190
  can_run_parallel = (
@@ -224,10 +222,10 @@ class AgentLoop:
224
222
 
225
223
  async def _run_single_tool(
226
224
  self,
227
- turn_id: str,
228
- call: ToolCall,
229
- prior_results: tuple[ToolResult, ...] = (),
230
- ) -> ToolResult:
225
+ turn_id: 'str',
226
+ call: 'ToolCall',
227
+ prior_results: 'typing.Tuple[ToolResult, ...]' = (),
228
+ ) -> 'ToolResult':
231
229
  self._emit("tool_started", turn_id, tool_name=call.name, call_id=call.call_id)
232
230
  result = await self._tool_registry.execute(
233
231
  call,
@@ -237,7 +235,7 @@ class AgentLoop:
237
235
  collaboration_mode=self._context_manager.collaboration_mode,
238
236
  ),
239
237
  )
240
- payload: dict[str, object] = {
238
+ payload: 'typing.Dict[str, object]' = {
241
239
  "tool_name": call.name,
242
240
  "call_id": call.call_id,
243
241
  "is_error": result.is_error,
@@ -247,12 +245,12 @@ class AgentLoop:
247
245
  self._emit("tool_completed", turn_id, **payload)
248
246
  return result
249
247
 
250
- def _emit(self, kind: str, turn_id: str, **payload: object) -> None:
248
+ def _emit(self, kind: 'str', turn_id: 'str', **payload: 'object') -> 'None':
251
249
  self._event_handler(
252
250
  AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
253
251
  )
254
252
 
255
- def _handle_model_stream_event(self, turn_id: str, event: ModelStreamEvent) -> None:
253
+ def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
256
254
  if event.kind == "assistant_delta":
257
255
  self._emit("assistant_delta", turn_id, **event.payload)
258
256
  elif event.kind == "tool_call":
@@ -260,9 +258,9 @@ class AgentLoop:
260
258
 
261
259
  def _build_follow_up_messages(
262
260
  self,
263
- tool_results: list[ToolResult],
264
- ) -> list[UserMessage]:
265
- follow_ups: list[UserMessage] = []
261
+ tool_results: 'typing.List[ToolResult]',
262
+ ) -> 'typing.List[UserMessage]':
263
+ follow_ups: 'typing.List[UserMessage]' = []
266
264
  for result in tool_results:
267
265
  statuses = None
268
266
  if (
@@ -1,4 +1,3 @@
1
- from __future__ import annotations
2
1
 
3
2
  import atexit
4
3
  import argparse
@@ -10,10 +9,11 @@ import sys
10
9
  import tempfile
11
10
  from dataclasses import asdict, replace
12
11
  from pathlib import Path
13
- from typing import Literal, Sequence
12
+ from typing import Sequence
14
13
 
15
14
  from .agent import AgentLoop
16
15
  from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
16
+ from .compat import Literal
17
17
  from .context import ContextManager
18
18
  from .model import ResponsesModelClient, ResponsesProviderConfig
19
19
  from .portable import bootstrap_called_home, upload_codex_home
@@ -21,6 +21,7 @@ from .protocol import AgentEvent
21
21
  from .runtime import AgentRuntime
22
22
  from .runtime_services import RuntimeEnvironment, create_runtime_environment
23
23
  from .utils import CliSessionView, load_codex_dotenv
24
+ import typing
24
25
 
25
26
  EXIT_COMMANDS = {"/exit", "/quit"}
26
27
  HISTORY_COMMAND = "/history"
@@ -40,7 +41,7 @@ def launch_chat_completion_compat_server(*args, **kwargs):
40
41
  return launch_compat_server(*args, **kwargs)
41
42
 
42
43
 
43
- def configure_loguru() -> None:
44
+ def configure_loguru() -> 'None':
44
45
  try:
45
46
  from loguru import logger
46
47
  except ImportError: # pragma: no cover - dependency may be absent in minimal envs
@@ -61,7 +62,7 @@ def configure_loguru() -> None:
61
62
  logger.add(sys.stderr, level="DEBUG")
62
63
 
63
64
 
64
- def build_parser() -> argparse.ArgumentParser:
65
+ def build_parser() -> 'argparse.ArgumentParser':
65
66
  parser = argparse.ArgumentParser(
66
67
  prog="pycodex",
67
68
  description="Minimal Codex-style local CLI backed by ~/.codex/config.toml.",
@@ -131,11 +132,11 @@ def build_parser() -> argparse.ArgumentParser:
131
132
  return parser
132
133
 
133
134
 
134
- def should_run_interactive(prompt_parts: Sequence[str], stdin_is_tty: bool) -> bool:
135
+ def should_run_interactive(prompt_parts: 'Sequence[str]', stdin_is_tty: 'bool') -> 'bool':
135
136
  return not prompt_parts and stdin_is_tty
136
137
 
137
138
 
138
- def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
139
+ def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
139
140
  if prompt_parts:
140
141
  return " ".join(prompt_parts).strip()
141
142
 
@@ -148,8 +149,8 @@ def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
148
149
 
149
150
 
150
151
  def get_tools(
151
- runtime_environment: RuntimeEnvironment | None = None,
152
- exec_mode: bool = False,
152
+ runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None,
153
+ exec_mode: 'bool' = False,
153
154
  ):
154
155
  from .tools import (
155
156
  ApplyPatchTool,
@@ -243,7 +244,7 @@ def get_tools(
243
244
  return registry
244
245
 
245
246
 
246
- def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
247
+ def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None):
247
248
  from .tools import (
248
249
  ApplyPatchTool,
249
250
  ExecCommandTool,
@@ -268,13 +269,13 @@ def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
268
269
 
269
270
 
270
271
  def build_runtime(
271
- config_path: str,
272
- profile: str | None,
273
- system_prompt: str | None,
272
+ config_path: 'str',
273
+ profile: 'typing.Union[str, None]',
274
+ system_prompt: 'typing.Union[str, None]',
274
275
  client,
275
- session_mode: CliSessionMode = "exec",
276
- collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
277
- ) -> AgentRuntime:
276
+ session_mode: 'CliSessionMode' = "exec",
277
+ collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
278
+ ) -> 'AgentRuntime':
278
279
  use_tui_context = session_mode == "tui"
279
280
  context_manager = ContextManager.from_codex_config(
280
281
  config_path,
@@ -295,11 +296,11 @@ def build_runtime(
295
296
 
296
297
  def make_subagent_runtime_builder(base_client):
297
298
  def build_subagent_runtime(
298
- model_override: str | None,
299
- reasoning_effort_override: str | None,
299
+ model_override: 'typing.Union[str, None]',
300
+ reasoning_effort_override: 'typing.Union[str, None]',
300
301
  initial_history=(),
301
- session_id: str | None = None,
302
- ) -> AgentRuntime:
302
+ session_id: 'typing.Union[str, None]' = None,
303
+ ) -> 'AgentRuntime':
303
304
  nested_client = base_client.with_overrides(
304
305
  model_override,
305
306
  reasoning_effort_override,
@@ -335,19 +336,19 @@ def build_runtime(
335
336
  )
336
337
 
337
338
 
338
- def format_turn_output(result, json_mode: bool) -> str:
339
+ def format_turn_output(result, json_mode: 'bool') -> 'str':
339
340
  if json_mode:
340
341
  return json.dumps(asdict(result), ensure_ascii=False, indent=2)
341
342
  return result.output_text or ""
342
343
 
343
344
 
344
345
  def _build_model_client(
345
- config_path: str,
346
- profile: str | None,
347
- timeout_seconds: float,
348
- managed_responses_base_url: str | None = None,
349
- vllm_endpoint: str | None = None,
350
- use_chat_completion: bool = False,
346
+ config_path: 'str',
347
+ profile: 'typing.Union[str, None]',
348
+ timeout_seconds: 'float',
349
+ managed_responses_base_url: 'typing.Union[str, None]' = None,
350
+ vllm_endpoint: 'typing.Union[str, None]' = None,
351
+ use_chat_completion: 'bool' = False,
351
352
  ):
352
353
  load_codex_dotenv(config_path)
353
354
  provider_config = ResponsesProviderConfig.from_codex_config(
@@ -393,13 +394,13 @@ def _build_model_client(
393
394
 
394
395
 
395
396
  async def prompt_request_user_input(
396
- view: CliSessionView,
397
- payload: dict[str, object],
398
- ) -> dict[str, object] | None:
397
+ view: 'CliSessionView',
398
+ payload: 'typing.Dict[str, object]',
399
+ ) -> 'typing.Union[typing.Dict[str, object], None]':
399
400
  view.finish_stream()
400
401
  view.pause_spinner()
401
402
  view.write_line("[request_user_input] waiting for user response")
402
- answers: dict[str, dict[str, list[str]]] = {}
403
+ answers: 'typing.Dict[str, typing.Dict[str, typing.List[str]]]' = {}
403
404
  try:
404
405
  for question in payload.get("questions", []):
405
406
  if not isinstance(question, dict):
@@ -456,9 +457,9 @@ async def prompt_request_user_input(
456
457
 
457
458
 
458
459
  async def prompt_request_permissions(
459
- view: CliSessionView,
460
- payload: dict[str, object],
461
- ) -> dict[str, object] | None:
460
+ view: 'CliSessionView',
461
+ payload: 'typing.Dict[str, object]',
462
+ ) -> 'typing.Union[typing.Dict[str, object], None]':
462
463
  view.finish_stream()
463
464
  view.pause_spinner()
464
465
  view.write_line("[request_permissions] user approval required")
@@ -495,14 +496,14 @@ async def prompt_request_permissions(
495
496
 
496
497
 
497
498
  async def run_interactive_session(
498
- runtime: AgentRuntime,
499
- json_mode: bool,
500
- ) -> int:
499
+ runtime: 'AgentRuntime',
500
+ json_mode: 'bool',
501
+ ) -> 'int':
501
502
  worker = asyncio.create_task(runtime.run_forever())
502
503
  view = CliSessionView()
503
504
  model_client = runtime._agent_loop._model_client
504
505
  runtime.set_event_handler(view.handle_event)
505
- pending_turn_tasks: set[asyncio.Task[None]] = set()
506
+ pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
506
507
  runtime_environment = runtime.runtime_environment
507
508
  if runtime_environment is None:
508
509
  runtime_environment = create_runtime_environment()
@@ -517,13 +518,13 @@ async def run_interactive_session(
517
518
  view.write_line("Extra commands: /history, /title, /model")
518
519
  try:
519
520
 
520
- def has_pending_turn_tasks() -> bool:
521
+ def has_pending_turn_tasks() -> 'bool':
521
522
  pending_turn_tasks.difference_update(
522
523
  task for task in tuple(pending_turn_tasks) if task.done()
523
524
  )
524
525
  return bool(pending_turn_tasks)
525
526
 
526
- async def wait_for_turn_result(future) -> None:
527
+ async def wait_for_turn_result(future) -> 'None':
527
528
  try:
528
529
  result = await future
529
530
  except Exception as exc: # pragma: no cover - defensive surface
@@ -618,7 +619,7 @@ async def run_interactive_session(
618
619
  return 0
619
620
 
620
621
 
621
- async def run_cli(args: argparse.Namespace) -> int:
622
+ async def run_cli(args: 'argparse.Namespace') -> 'int':
622
623
  runtime = None
623
624
  worker = None
624
625
  try:
@@ -628,7 +629,7 @@ async def run_cli(args: argparse.Namespace) -> int:
628
629
  raise ValueError("--put does not accept prompt text")
629
630
  configure_loguru()
630
631
  if args.put is not None:
631
- def emit_put_log(message: str) -> None:
632
+ def emit_put_log(message: 'str') -> 'None':
632
633
  print(message, flush=True)
633
634
 
634
635
  call_spec = upload_codex_home(args.put, event_handler=emit_put_log)
@@ -678,7 +679,7 @@ async def run_cli(args: argparse.Namespace) -> int:
678
679
  await worker
679
680
 
680
681
 
681
- def main(argv: Sequence[str] | None = None) -> int:
682
+ def main(argv: 'typing.Union[Sequence[str], None]' = None) -> 'int':
682
683
  raw_args = list(argv) if argv is not None else None
683
684
  if raw_args is None:
684
685
  raw_args = sys.argv[1:]
@@ -1,13 +1,13 @@
1
- from __future__ import annotations
2
1
 
3
- from typing import Literal
2
+ from .compat import Literal
3
+ import typing
4
4
 
5
5
  CollaborationMode = Literal["default", "plan", "execute", "pair_programming"]
6
6
 
7
- DEFAULT_COLLABORATION_MODE: CollaborationMode = "default"
8
- PLAN_COLLABORATION_MODE: CollaborationMode = "plan"
7
+ DEFAULT_COLLABORATION_MODE: 'CollaborationMode' = "default"
8
+ PLAN_COLLABORATION_MODE: 'CollaborationMode' = "plan"
9
9
 
10
- _MODE_DISPLAY_NAMES: dict[str, str] = {
10
+ _MODE_DISPLAY_NAMES: 'typing.Dict[str, str]' = {
11
11
  "default": "Default",
12
12
  "plan": "Plan",
13
13
  "execute": "Execute",
@@ -15,7 +15,6 @@ _MODE_DISPLAY_NAMES: dict[str, str] = {
15
15
  }
16
16
 
17
17
 
18
- def collaboration_mode_display_name(mode: str | None) -> str:
18
+ def collaboration_mode_display_name(mode: 'typing.Union[str, None]') -> 'str':
19
19
  normalized = (mode or DEFAULT_COLLABORATION_MODE).strip().lower()
20
20
  return _MODE_DISPLAY_NAMES.get(normalized, normalized.replace("_", " ").title())
21
-
@@ -0,0 +1,99 @@
1
+ import asyncio
2
+ import functools
3
+ import shlex
4
+
5
+ try:
6
+ from http.server import ThreadingHTTPServer
7
+ except ImportError: # pragma: no cover - Python 3.6 path
8
+ from http.server import HTTPServer
9
+ from socketserver import ThreadingMixIn
10
+
11
+ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
12
+ daemon_threads = True
13
+
14
+ try:
15
+ from importlib import metadata as importlib_metadata
16
+ except ImportError: # pragma: no cover - Python 3.6 path
17
+ import importlib_metadata # type: ignore
18
+
19
+ try:
20
+ from typing import Literal, Protocol, TypeAlias
21
+ except ImportError: # pragma: no cover - Python 3.6 path
22
+ from typing_extensions import Literal, Protocol # type: ignore
23
+ try:
24
+ from typing_extensions import TypeAlias # type: ignore
25
+ except ImportError: # pragma: no cover - old typing_extensions
26
+ TypeAlias = object
27
+
28
+
29
+ def patch_asyncio():
30
+ if not hasattr(asyncio, "create_task"):
31
+ asyncio.create_task = asyncio.ensure_future
32
+
33
+ if not hasattr(asyncio, "get_running_loop"):
34
+ def get_running_loop():
35
+ return asyncio.get_event_loop()
36
+
37
+ asyncio.get_running_loop = get_running_loop
38
+
39
+ if not hasattr(asyncio, "to_thread"):
40
+ async def to_thread(func, *args, **kwargs):
41
+ loop = asyncio.get_event_loop()
42
+ call = functools.partial(func, *args, **kwargs)
43
+ return await loop.run_in_executor(None, call)
44
+
45
+ asyncio.to_thread = to_thread
46
+
47
+ if not hasattr(asyncio, "run"):
48
+ def run(main):
49
+ loop = asyncio.new_event_loop()
50
+ try:
51
+ asyncio.set_event_loop(loop)
52
+ return loop.run_until_complete(main)
53
+ finally:
54
+ all_tasks = getattr(asyncio.Task, "all_tasks", None)
55
+ if all_tasks is not None:
56
+ pending = all_tasks(loop=loop)
57
+ else:
58
+ pending = asyncio.all_tasks(loop)
59
+ for task in pending:
60
+ task.cancel()
61
+ if pending:
62
+ loop.run_until_complete(
63
+ asyncio.gather(*pending, return_exceptions=True)
64
+ )
65
+ shutdown_asyncgens = getattr(loop, "shutdown_asyncgens", None)
66
+ if shutdown_asyncgens is not None:
67
+ loop.run_until_complete(shutdown_asyncgens())
68
+ asyncio.set_event_loop(None)
69
+ loop.close()
70
+
71
+ asyncio.run = run
72
+
73
+
74
+ def shlex_join(parts):
75
+ join = getattr(shlex, "join", None)
76
+ if join is not None:
77
+ return join(parts)
78
+ return " ".join(shlex.quote(part) for part in parts)
79
+
80
+
81
+ def stream_writer_is_closing(writer):
82
+ method = getattr(writer, "is_closing", None)
83
+ if callable(method):
84
+ return method()
85
+ transport = getattr(writer, "transport", None)
86
+ if transport is None:
87
+ return False
88
+ transport_is_closing = getattr(transport, "is_closing", None)
89
+ if callable(transport_is_closing):
90
+ return transport_is_closing()
91
+ return False
92
+
93
+
94
+ def is_ascii(text):
95
+ try:
96
+ text.encode("ascii")
97
+ except UnicodeEncodeError:
98
+ return False
99
+ return True