python-codex 0.1.1__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.
- python_codex-0.1.3/.github/workflows/test.yml +82 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/PKG-INFO +15 -9
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/__init__.py +5 -1
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/agent.py +39 -41
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/cli.py +51 -43
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/collaboration.py +6 -7
- python_codex-0.1.3/pycodex/compat.py +99 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/context.py +87 -87
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/doctor.py +40 -40
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/model.py +69 -69
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/portable.py +33 -33
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/portable_server.py +22 -21
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/protocol.py +84 -86
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/runtime.py +36 -35
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/runtime_services.py +72 -69
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/agent_tool_schemas.py +0 -2
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/apply_patch_tool.py +43 -44
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/base_tool.py +35 -36
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/close_agent_tool.py +2 -4
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/code_mode_manager.py +61 -61
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/exec_command_tool.py +5 -6
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/exec_runtime.js +3 -3
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/exec_tool.py +3 -5
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/grep_files_tool.py +10 -11
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/list_dir_tool.py +8 -9
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/read_file_tool.py +13 -14
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/request_permissions_tool.py +2 -4
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/request_user_input_tool.py +13 -14
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/resume_agent_tool.py +2 -4
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/send_input_tool.py +8 -9
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/shell_command_tool.py +5 -6
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/shell_tool.py +5 -6
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/spawn_agent_tool.py +4 -5
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/unified_exec_manager.py +79 -61
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/update_plan_tool.py +4 -5
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/view_image_tool.py +4 -5
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/wait_agent_tool.py +2 -4
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/wait_tool.py +4 -5
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/web_search_tool.py +1 -3
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/write_stdin_tool.py +4 -5
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/utils/dotenv.py +6 -6
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/utils/get_env.py +57 -34
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/utils/random_ids.py +1 -2
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/utils/visualize.py +79 -79
- python_codex-0.1.3/pyproject.toml +50 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/app.py +29 -19
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/config.py +17 -17
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/payload_processors.py +16 -16
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/server.py +11 -11
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/session_store.py +10 -10
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/stream_router.py +58 -58
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/tools/custom_adapter.py +12 -12
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/tools/web_search.py +33 -33
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/compare_request_user_input_roundtrip.py +71 -71
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/compare_steer_request_bodies.py +69 -69
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/compare_tool_schemas.py +52 -51
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/fake_responses_server.py +43 -42
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/fakes.py +10 -10
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/responses_server/fake_chat_completions_server.py +51 -42
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/responses_server/test_server.py +22 -22
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_agent.py +34 -17
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_builtin_tools.py +67 -35
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_cli.py +174 -142
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_context.py +6 -7
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_doctor.py +8 -9
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_fake_responses_server.py +7 -6
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_model.py +44 -19
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/test_portable.py +14 -14
- python_codex-0.1.1/pyproject.toml +0 -41
- {python_codex-0.1.1 → python_codex-0.1.3}/.github/workflows/publish.yml +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/.gitignore +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/AGENTS.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/LICENSE +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/README.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/README_ZH.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/docs/ALIGNMENT.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/docs/CONTEXT.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/docs/responses_server/README.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/collaboration_default.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/collaboration_plan.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/exec_tools.json +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/models.json +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/prompts/subagent_tools.json +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/pycodex/utils/__init__.py +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/__init__.py +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/__main__.py +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.1.1 → python_codex-0.1.3}/tests/TESTS.md +0 -0
- {python_codex-0.1.1 → 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.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
7
|
-
Requires-Dist: cryptography
|
|
8
|
-
Requires-Dist:
|
|
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.
|
|
12
|
-
Requires-Dist: tomli
|
|
13
|
-
Requires-Dist:
|
|
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
|
|
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
|
|
46
|
-
parallel_tool_calls: bool = True,
|
|
47
|
-
event_handler: EventHandler = NOOP_EVENT_HANDLER,
|
|
48
|
-
initial_history:
|
|
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:
|
|
55
|
+
self._history: 'typing.List[ConversationItem]' = list(initial_history)
|
|
56
56
|
self.interrupt_asap = False
|
|
57
57
|
|
|
58
58
|
@property
|
|
59
|
-
def history(self) ->
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
97
|
-
final_response_items:
|
|
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:
|
|
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:
|
|
187
|
-
) ->
|
|
188
|
-
results:
|
|
189
|
-
parallel_batch:
|
|
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:
|
|
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:
|
|
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:
|
|
264
|
-
) ->
|
|
265
|
-
follow_ups:
|
|
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
|
|
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,7 +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
|
-
|
|
24
|
+
import typing
|
|
25
25
|
|
|
26
26
|
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
27
27
|
HISTORY_COMMAND = "/history"
|
|
@@ -33,7 +33,15 @@ LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
|
|
|
33
33
|
CLI_ORIGINATOR = "codex-tui"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def launch_chat_completion_compat_server(*args, **kwargs):
|
|
37
|
+
from responses_server import (
|
|
38
|
+
launch_chat_completion_compat_server as launch_compat_server,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return launch_compat_server(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def configure_loguru() -> 'None':
|
|
37
45
|
try:
|
|
38
46
|
from loguru import logger
|
|
39
47
|
except ImportError: # pragma: no cover - dependency may be absent in minimal envs
|
|
@@ -54,7 +62,7 @@ def configure_loguru() -> None:
|
|
|
54
62
|
logger.add(sys.stderr, level="DEBUG")
|
|
55
63
|
|
|
56
64
|
|
|
57
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
65
|
+
def build_parser() -> 'argparse.ArgumentParser':
|
|
58
66
|
parser = argparse.ArgumentParser(
|
|
59
67
|
prog="pycodex",
|
|
60
68
|
description="Minimal Codex-style local CLI backed by ~/.codex/config.toml.",
|
|
@@ -124,11 +132,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
124
132
|
return parser
|
|
125
133
|
|
|
126
134
|
|
|
127
|
-
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':
|
|
128
136
|
return not prompt_parts and stdin_is_tty
|
|
129
137
|
|
|
130
138
|
|
|
131
|
-
def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
|
|
139
|
+
def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
|
|
132
140
|
if prompt_parts:
|
|
133
141
|
return " ".join(prompt_parts).strip()
|
|
134
142
|
|
|
@@ -141,8 +149,8 @@ def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
|
|
|
141
149
|
|
|
142
150
|
|
|
143
151
|
def get_tools(
|
|
144
|
-
runtime_environment: RuntimeEnvironment
|
|
145
|
-
exec_mode: bool = False,
|
|
152
|
+
runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None,
|
|
153
|
+
exec_mode: 'bool' = False,
|
|
146
154
|
):
|
|
147
155
|
from .tools import (
|
|
148
156
|
ApplyPatchTool,
|
|
@@ -236,7 +244,7 @@ def get_tools(
|
|
|
236
244
|
return registry
|
|
237
245
|
|
|
238
246
|
|
|
239
|
-
def get_subagent_tools(runtime_environment: RuntimeEnvironment
|
|
247
|
+
def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None):
|
|
240
248
|
from .tools import (
|
|
241
249
|
ApplyPatchTool,
|
|
242
250
|
ExecCommandTool,
|
|
@@ -261,13 +269,13 @@ def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
|
|
|
261
269
|
|
|
262
270
|
|
|
263
271
|
def build_runtime(
|
|
264
|
-
config_path: str,
|
|
265
|
-
profile: str
|
|
266
|
-
system_prompt: str
|
|
272
|
+
config_path: 'str',
|
|
273
|
+
profile: 'typing.Union[str, None]',
|
|
274
|
+
system_prompt: 'typing.Union[str, None]',
|
|
267
275
|
client,
|
|
268
|
-
session_mode: CliSessionMode = "exec",
|
|
269
|
-
collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
|
|
270
|
-
) -> AgentRuntime:
|
|
276
|
+
session_mode: 'CliSessionMode' = "exec",
|
|
277
|
+
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
278
|
+
) -> 'AgentRuntime':
|
|
271
279
|
use_tui_context = session_mode == "tui"
|
|
272
280
|
context_manager = ContextManager.from_codex_config(
|
|
273
281
|
config_path,
|
|
@@ -288,11 +296,11 @@ def build_runtime(
|
|
|
288
296
|
|
|
289
297
|
def make_subagent_runtime_builder(base_client):
|
|
290
298
|
def build_subagent_runtime(
|
|
291
|
-
model_override: str
|
|
292
|
-
reasoning_effort_override: str
|
|
299
|
+
model_override: 'typing.Union[str, None]',
|
|
300
|
+
reasoning_effort_override: 'typing.Union[str, None]',
|
|
293
301
|
initial_history=(),
|
|
294
|
-
session_id: str
|
|
295
|
-
) -> AgentRuntime:
|
|
302
|
+
session_id: 'typing.Union[str, None]' = None,
|
|
303
|
+
) -> 'AgentRuntime':
|
|
296
304
|
nested_client = base_client.with_overrides(
|
|
297
305
|
model_override,
|
|
298
306
|
reasoning_effort_override,
|
|
@@ -328,19 +336,19 @@ def build_runtime(
|
|
|
328
336
|
)
|
|
329
337
|
|
|
330
338
|
|
|
331
|
-
def format_turn_output(result, json_mode: bool) -> str:
|
|
339
|
+
def format_turn_output(result, json_mode: 'bool') -> 'str':
|
|
332
340
|
if json_mode:
|
|
333
341
|
return json.dumps(asdict(result), ensure_ascii=False, indent=2)
|
|
334
342
|
return result.output_text or ""
|
|
335
343
|
|
|
336
344
|
|
|
337
345
|
def _build_model_client(
|
|
338
|
-
config_path: str,
|
|
339
|
-
profile: str
|
|
340
|
-
timeout_seconds: float,
|
|
341
|
-
managed_responses_base_url: str
|
|
342
|
-
vllm_endpoint: str
|
|
343
|
-
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,
|
|
344
352
|
):
|
|
345
353
|
load_codex_dotenv(config_path)
|
|
346
354
|
provider_config = ResponsesProviderConfig.from_codex_config(
|
|
@@ -386,13 +394,13 @@ def _build_model_client(
|
|
|
386
394
|
|
|
387
395
|
|
|
388
396
|
async def prompt_request_user_input(
|
|
389
|
-
view: CliSessionView,
|
|
390
|
-
payload:
|
|
391
|
-
) ->
|
|
397
|
+
view: 'CliSessionView',
|
|
398
|
+
payload: 'typing.Dict[str, object]',
|
|
399
|
+
) -> 'typing.Union[typing.Dict[str, object], None]':
|
|
392
400
|
view.finish_stream()
|
|
393
401
|
view.pause_spinner()
|
|
394
402
|
view.write_line("[request_user_input] waiting for user response")
|
|
395
|
-
answers:
|
|
403
|
+
answers: 'typing.Dict[str, typing.Dict[str, typing.List[str]]]' = {}
|
|
396
404
|
try:
|
|
397
405
|
for question in payload.get("questions", []):
|
|
398
406
|
if not isinstance(question, dict):
|
|
@@ -449,9 +457,9 @@ async def prompt_request_user_input(
|
|
|
449
457
|
|
|
450
458
|
|
|
451
459
|
async def prompt_request_permissions(
|
|
452
|
-
view: CliSessionView,
|
|
453
|
-
payload:
|
|
454
|
-
) ->
|
|
460
|
+
view: 'CliSessionView',
|
|
461
|
+
payload: 'typing.Dict[str, object]',
|
|
462
|
+
) -> 'typing.Union[typing.Dict[str, object], None]':
|
|
455
463
|
view.finish_stream()
|
|
456
464
|
view.pause_spinner()
|
|
457
465
|
view.write_line("[request_permissions] user approval required")
|
|
@@ -488,14 +496,14 @@ async def prompt_request_permissions(
|
|
|
488
496
|
|
|
489
497
|
|
|
490
498
|
async def run_interactive_session(
|
|
491
|
-
runtime: AgentRuntime,
|
|
492
|
-
json_mode: bool,
|
|
493
|
-
) -> int:
|
|
499
|
+
runtime: 'AgentRuntime',
|
|
500
|
+
json_mode: 'bool',
|
|
501
|
+
) -> 'int':
|
|
494
502
|
worker = asyncio.create_task(runtime.run_forever())
|
|
495
503
|
view = CliSessionView()
|
|
496
504
|
model_client = runtime._agent_loop._model_client
|
|
497
505
|
runtime.set_event_handler(view.handle_event)
|
|
498
|
-
pending_turn_tasks:
|
|
506
|
+
pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
|
|
499
507
|
runtime_environment = runtime.runtime_environment
|
|
500
508
|
if runtime_environment is None:
|
|
501
509
|
runtime_environment = create_runtime_environment()
|
|
@@ -510,13 +518,13 @@ async def run_interactive_session(
|
|
|
510
518
|
view.write_line("Extra commands: /history, /title, /model")
|
|
511
519
|
try:
|
|
512
520
|
|
|
513
|
-
def has_pending_turn_tasks() -> bool:
|
|
521
|
+
def has_pending_turn_tasks() -> 'bool':
|
|
514
522
|
pending_turn_tasks.difference_update(
|
|
515
523
|
task for task in tuple(pending_turn_tasks) if task.done()
|
|
516
524
|
)
|
|
517
525
|
return bool(pending_turn_tasks)
|
|
518
526
|
|
|
519
|
-
async def wait_for_turn_result(future) -> None:
|
|
527
|
+
async def wait_for_turn_result(future) -> 'None':
|
|
520
528
|
try:
|
|
521
529
|
result = await future
|
|
522
530
|
except Exception as exc: # pragma: no cover - defensive surface
|
|
@@ -611,7 +619,7 @@ async def run_interactive_session(
|
|
|
611
619
|
return 0
|
|
612
620
|
|
|
613
621
|
|
|
614
|
-
async def run_cli(args: argparse.Namespace) -> int:
|
|
622
|
+
async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
615
623
|
runtime = None
|
|
616
624
|
worker = None
|
|
617
625
|
try:
|
|
@@ -621,7 +629,7 @@ async def run_cli(args: argparse.Namespace) -> int:
|
|
|
621
629
|
raise ValueError("--put does not accept prompt text")
|
|
622
630
|
configure_loguru()
|
|
623
631
|
if args.put is not None:
|
|
624
|
-
def emit_put_log(message: str) -> None:
|
|
632
|
+
def emit_put_log(message: 'str') -> 'None':
|
|
625
633
|
print(message, flush=True)
|
|
626
634
|
|
|
627
635
|
call_spec = upload_codex_home(args.put, event_handler=emit_put_log)
|
|
@@ -671,7 +679,7 @@ async def run_cli(args: argparse.Namespace) -> int:
|
|
|
671
679
|
await worker
|
|
672
680
|
|
|
673
681
|
|
|
674
|
-
def main(argv: Sequence[str]
|
|
682
|
+
def main(argv: 'typing.Union[Sequence[str], None]' = None) -> 'int':
|
|
675
683
|
raw_args = list(argv) if argv is not None else None
|
|
676
684
|
if raw_args is None:
|
|
677
685
|
raw_args = sys.argv[1:]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
|
-
from
|
|
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:
|
|
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
|
|
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
|