python-codex 0.2.1__tar.gz → 0.2.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.2.1 → python_codex-0.2.3}/PKG-INFO +21 -4
- {python_codex-0.2.1 → python_codex-0.2.3}/README.md +20 -3
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/cli.py +21 -13
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/context.py +4 -1
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/feishu_link.py +32 -14
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/interactive_session.py +5 -1
- {python_codex-0.2.1 → python_codex-0.2.3}/pyproject.toml +5 -2
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_cli.py +17 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_context.py +12 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_feishu_link.py +98 -6
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_workspace_server.py +507 -40
- python_codex-0.2.3/workspace_server/__init__.py +39 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/workspace_server/app.py +591 -369
- {python_codex-0.2.1 → python_codex-0.2.3}/workspace_server/workspace.html +377 -32
- python_codex-0.2.3/workspace_server/workspaces.html +551 -0
- python_codex-0.2.3/workspace_server/workspaces.py +561 -0
- python_codex-0.2.1/workspace_server/__init__.py +0 -23
- {python_codex-0.2.1 → python_codex-0.2.3}/.github/workflows/publish.yml +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/.github/workflows/test.yml +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/.gitignore +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/AGENTS.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/LICENSE +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/README_ZH.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/docs/ALIGNMENT.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/docs/CONTEXT.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/docs/responses_server/README.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/__init__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/agent.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/collaboration.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/compat.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/doctor.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/feishu_card.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/model.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/portable.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/portable_server.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/collaboration_default.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/collaboration_plan.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/models.json +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/protocol.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/runtime.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/runtime_services.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/agent_tool_schemas.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/apply_patch_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/base_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/close_agent_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/code_mode_manager.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/exec_command_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/exec_runtime.js +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/exec_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/grep_files_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/ipython_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/list_dir_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/read_file_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/request_permissions_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/request_user_input_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/resume_agent_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/send_input_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/shell_command_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/shell_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/spawn_agent_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/unified_exec_manager.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/update_plan_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/view_image_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/wait_agent_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/wait_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/web_search_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/tools/write_stdin_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/__init__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/async_bridge.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/compactor.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/debug.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/dotenv.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/get_env.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/random_ids.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/session_persist.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/toolcall_visualize.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/truncation.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/pycodex/utils/visualize.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/__init__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/__main__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/app.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/config.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/messages_api.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/payload_processors.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/server.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/session_store.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/stream_router.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/tools/custom_adapter.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/tools/web_search.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/responses_server/trajectory_dump.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/TESTS.md +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/__init__.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/compare_request_user_input_roundtrip.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/compare_steer_request_bodies.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/compare_tool_schemas.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/fake_responses_server.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/fakes.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/responses_server/fake_chat_completions_server.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/responses_server/test_server.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_agent.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_async_bridge.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_builtin_tools.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_compactor.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_doctor.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_fake_responses_server.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_feishu_card.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_ipython_tool.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_model.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_portable.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_py36_syntax.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tests/test_visualize.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/tools/feishu_oauth.py +0 -0
- {python_codex-0.2.1 → python_codex-0.2.3}/workspace_server/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A minimal Python extraction of Codex's main agent loop
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.6.2
|
|
@@ -165,15 +165,16 @@ pycodex --put @127.0.0.1:5577
|
|
|
165
165
|
pycodex --put /data/.codex/@127.0.0.1:5577
|
|
166
166
|
pycodex --call SECRET-CALLID@127.0.0.1:5577 "Reply with exactly OK."
|
|
167
167
|
pycodex doctor
|
|
168
|
-
pycodex-ws --listen 0.0.0.0:6007 --
|
|
168
|
+
pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json
|
|
169
|
+
pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json --password 12345
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
Current behavior:
|
|
172
173
|
|
|
173
174
|
- with no argv prompt and a TTY stdin, enter interactive mode
|
|
174
175
|
- with an argv prompt or piped stdin, run a single turn
|
|
175
|
-
- `pycodex-ws` starts the standalone browser workspace
|
|
176
|
-
pycodex session pane
|
|
176
|
+
- `pycodex-ws` starts the standalone browser workspace manager and serves each
|
|
177
|
+
workspace with a board pane and a pycodex session pane
|
|
177
178
|
- interactive mode supports `/exit` and `/quit`
|
|
178
179
|
- interactive mode shows a compact event stream for user-visible phases such as
|
|
179
180
|
tool execution and model follow-up after tool results
|
|
@@ -204,6 +205,22 @@ Current behavior:
|
|
|
204
205
|
non-persisted follow-up session and submits the file contents as the next
|
|
205
206
|
user instruction; this is intended for side-effect follow-ups such as
|
|
206
207
|
Feishu notifications
|
|
208
|
+
- `/link <feishu-email|open_id|chat_id>` attaches the current interactive
|
|
209
|
+
session to a Feishu card; multiple sessions in the same pycodex process share
|
|
210
|
+
one Feishu long-connection listener and route card actions by message id
|
|
211
|
+
- `pycodex-ws --workspace-config workspaces.json` serves workspace boards from
|
|
212
|
+
one process. The JSON file can be either a list or
|
|
213
|
+
`{"workspaces": [...]}`; each entry uses `board` and `work_dir`, with optional
|
|
214
|
+
display/URL `id`, and is exposed at `/w/<id>/`. Internally the resolved
|
|
215
|
+
`board` path is the workspace identity, so adding the same board path twice is
|
|
216
|
+
rejected even with a different name. Relative `board` and `work_dir` paths
|
|
217
|
+
resolve from the config file directory. The root path `/` shows a workspaces
|
|
218
|
+
management page with `add_workspace(name=None, dir="./", board=None)` and
|
|
219
|
+
`delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
|
|
220
|
+
etc. If `board` is omitted when adding a workspace, pycodex assigns a random
|
|
221
|
+
writable `/tmp/pcws-*.html` board path. Add/delete actions and later
|
|
222
|
+
session-state saves refresh the JSON file. `--password <value>` enables a
|
|
223
|
+
password-only login page for workspace pages, APIs, and websocket connections.
|
|
207
224
|
- steer is enabled by default in interactive mode: normal input goes into the
|
|
208
225
|
runtime steer path, the current request stops at the next safe boundary, and
|
|
209
226
|
later steer text is appended to the next model request's `input` in order;
|
|
@@ -144,15 +144,16 @@ pycodex --put @127.0.0.1:5577
|
|
|
144
144
|
pycodex --put /data/.codex/@127.0.0.1:5577
|
|
145
145
|
pycodex --call SECRET-CALLID@127.0.0.1:5577 "Reply with exactly OK."
|
|
146
146
|
pycodex doctor
|
|
147
|
-
pycodex-ws --listen 0.0.0.0:6007 --
|
|
147
|
+
pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json
|
|
148
|
+
pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json --password 12345
|
|
148
149
|
```
|
|
149
150
|
|
|
150
151
|
Current behavior:
|
|
151
152
|
|
|
152
153
|
- with no argv prompt and a TTY stdin, enter interactive mode
|
|
153
154
|
- with an argv prompt or piped stdin, run a single turn
|
|
154
|
-
- `pycodex-ws` starts the standalone browser workspace
|
|
155
|
-
pycodex session pane
|
|
155
|
+
- `pycodex-ws` starts the standalone browser workspace manager and serves each
|
|
156
|
+
workspace with a board pane and a pycodex session pane
|
|
156
157
|
- interactive mode supports `/exit` and `/quit`
|
|
157
158
|
- interactive mode shows a compact event stream for user-visible phases such as
|
|
158
159
|
tool execution and model follow-up after tool results
|
|
@@ -183,6 +184,22 @@ Current behavior:
|
|
|
183
184
|
non-persisted follow-up session and submits the file contents as the next
|
|
184
185
|
user instruction; this is intended for side-effect follow-ups such as
|
|
185
186
|
Feishu notifications
|
|
187
|
+
- `/link <feishu-email|open_id|chat_id>` attaches the current interactive
|
|
188
|
+
session to a Feishu card; multiple sessions in the same pycodex process share
|
|
189
|
+
one Feishu long-connection listener and route card actions by message id
|
|
190
|
+
- `pycodex-ws --workspace-config workspaces.json` serves workspace boards from
|
|
191
|
+
one process. The JSON file can be either a list or
|
|
192
|
+
`{"workspaces": [...]}`; each entry uses `board` and `work_dir`, with optional
|
|
193
|
+
display/URL `id`, and is exposed at `/w/<id>/`. Internally the resolved
|
|
194
|
+
`board` path is the workspace identity, so adding the same board path twice is
|
|
195
|
+
rejected even with a different name. Relative `board` and `work_dir` paths
|
|
196
|
+
resolve from the config file directory. The root path `/` shows a workspaces
|
|
197
|
+
management page with `add_workspace(name=None, dir="./", board=None)` and
|
|
198
|
+
`delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
|
|
199
|
+
etc. If `board` is omitted when adding a workspace, pycodex assigns a random
|
|
200
|
+
writable `/tmp/pcws-*.html` board path. Add/delete actions and later
|
|
201
|
+
session-state saves refresh the JSON file. `--password <value>` enables a
|
|
202
|
+
password-only login page for workspace pages, APIs, and websocket connections.
|
|
186
203
|
- steer is enabled by default in interactive mode: normal input goes into the
|
|
187
204
|
runtime steer path, the current request stops at the next safe boundary, and
|
|
188
205
|
later steer text is appended to the next model request's `input` in order;
|
|
@@ -165,6 +165,7 @@ def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
|
|
|
165
165
|
def get_tools(
|
|
166
166
|
runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
|
|
167
167
|
exec_mode: 'bool' = False,
|
|
168
|
+
cwd: 'typing.Union[str, Path, None]' = None,
|
|
168
169
|
):
|
|
169
170
|
from .tools import (
|
|
170
171
|
ApplyPatchTool,
|
|
@@ -194,8 +195,8 @@ def get_tools(
|
|
|
194
195
|
|
|
195
196
|
runtime_environment = runtime_environment or create_agent_runtime_environment()
|
|
196
197
|
registry = Registry()
|
|
197
|
-
code_mode_manager = CodeModeManager(registry)
|
|
198
|
-
unified_exec_manager = UnifiedExecManager()
|
|
198
|
+
code_mode_manager = CodeModeManager(registry, cwd=cwd)
|
|
199
|
+
unified_exec_manager = UnifiedExecManager(cwd=cwd)
|
|
199
200
|
exec_tool = ExecTool(code_mode_manager)
|
|
200
201
|
wait_tool = WaitTool(code_mode_manager)
|
|
201
202
|
web_search_tool = WebSearchTool()
|
|
@@ -211,15 +212,15 @@ def get_tools(
|
|
|
211
212
|
resume_agent_tool = ResumeAgentTool(runtime_environment.subagent_manager)
|
|
212
213
|
wait_agent_tool = WaitAgentTool(runtime_environment.subagent_manager)
|
|
213
214
|
close_agent_tool = CloseAgentTool(runtime_environment.subagent_manager)
|
|
214
|
-
apply_patch_tool = ApplyPatchTool()
|
|
215
|
-
shell_tool = ShellTool()
|
|
216
|
-
shell_command_tool = ShellCommandTool()
|
|
215
|
+
apply_patch_tool = ApplyPatchTool(cwd=cwd)
|
|
216
|
+
shell_tool = ShellTool(cwd=cwd)
|
|
217
|
+
shell_command_tool = ShellCommandTool(cwd=cwd)
|
|
217
218
|
exec_command_tool = ExecCommandTool(unified_exec_manager)
|
|
218
219
|
write_stdin_tool = WriteStdinTool(unified_exec_manager)
|
|
219
|
-
grep_files_tool = GrepFilesTool()
|
|
220
|
+
grep_files_tool = GrepFilesTool(cwd=cwd)
|
|
220
221
|
read_file_tool = ReadFileTool()
|
|
221
222
|
list_dir_tool = ListDirTool()
|
|
222
|
-
view_image_tool = ViewImageTool()
|
|
223
|
+
view_image_tool = ViewImageTool(cwd=cwd)
|
|
223
224
|
if exec_mode:
|
|
224
225
|
registry.register(exec_command_tool)
|
|
225
226
|
registry.register(write_stdin_tool)
|
|
@@ -258,7 +259,10 @@ def get_tools(
|
|
|
258
259
|
return registry
|
|
259
260
|
|
|
260
261
|
|
|
261
|
-
def get_subagent_tools(
|
|
262
|
+
def get_subagent_tools(
|
|
263
|
+
runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
|
|
264
|
+
cwd: 'typing.Union[str, Path, None]' = None,
|
|
265
|
+
):
|
|
262
266
|
from .tools import (
|
|
263
267
|
ApplyPatchTool,
|
|
264
268
|
ExecCommandTool,
|
|
@@ -272,13 +276,13 @@ def get_subagent_tools(runtime_environment: 'typing.Union[AgentRuntimeEnvironmen
|
|
|
272
276
|
|
|
273
277
|
runtime_environment = runtime_environment or create_agent_runtime_environment()
|
|
274
278
|
registry = Registry()
|
|
275
|
-
unified_exec_manager = UnifiedExecManager()
|
|
279
|
+
unified_exec_manager = UnifiedExecManager(cwd=cwd)
|
|
276
280
|
registry.register(ExecCommandTool(unified_exec_manager))
|
|
277
281
|
registry.register(WriteStdinTool(unified_exec_manager))
|
|
278
282
|
registry.register(UpdatePlanTool(runtime_environment.plan_store))
|
|
279
|
-
registry.register(ApplyPatchTool())
|
|
283
|
+
registry.register(ApplyPatchTool(cwd=cwd))
|
|
280
284
|
registry.register(WebSearchTool())
|
|
281
|
-
registry.register(ViewImageTool())
|
|
285
|
+
registry.register(ViewImageTool(cwd=cwd))
|
|
282
286
|
return registry
|
|
283
287
|
|
|
284
288
|
|
|
@@ -290,8 +294,10 @@ def build_agent(
|
|
|
290
294
|
session_mode: 'CliSessionMode' = "exec",
|
|
291
295
|
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
292
296
|
extra_contextual_user_messages: 'typing.Iterable[str]' = (),
|
|
297
|
+
cwd: 'typing.Union[str, Path, None]' = None,
|
|
293
298
|
) -> 'Agent':
|
|
294
299
|
config_path = str(config_path)
|
|
300
|
+
resolved_cwd = Path(cwd or Path.cwd()).resolve()
|
|
295
301
|
context_manager = ContextManager.from_codex_config(
|
|
296
302
|
config_path,
|
|
297
303
|
profile,
|
|
@@ -299,6 +305,7 @@ def build_agent(
|
|
|
299
305
|
collaboration_mode=collaboration_mode,
|
|
300
306
|
include_collaboration_instructions=session_mode == "tui",
|
|
301
307
|
extra_contextual_user_messages=extra_contextual_user_messages,
|
|
308
|
+
cwd=resolved_cwd,
|
|
302
309
|
)
|
|
303
310
|
session_id = getattr(client, "_session_id", None) or uuid7_string()
|
|
304
311
|
if hasattr(client, "_session_id"):
|
|
@@ -309,6 +316,7 @@ def build_agent(
|
|
|
309
316
|
base_instructions_override=system_prompt,
|
|
310
317
|
include_collaboration_instructions=False,
|
|
311
318
|
extra_contextual_user_messages=extra_contextual_user_messages,
|
|
319
|
+
cwd=resolved_cwd,
|
|
312
320
|
)
|
|
313
321
|
runtime_environment = create_agent_runtime_environment()
|
|
314
322
|
runtime_environment.request_user_input_manager.set_handler(None)
|
|
@@ -343,7 +351,7 @@ def build_agent(
|
|
|
343
351
|
)
|
|
344
352
|
sub_agent = Agent(
|
|
345
353
|
nested_client,
|
|
346
|
-
get_subagent_tools(subagent_agent_runtime_environment),
|
|
354
|
+
get_subagent_tools(subagent_agent_runtime_environment, cwd=resolved_cwd),
|
|
347
355
|
subagent_context_manager,
|
|
348
356
|
initial_history=tuple(initial_history),
|
|
349
357
|
runtime_environment=subagent_agent_runtime_environment,
|
|
@@ -357,7 +365,7 @@ def build_agent(
|
|
|
357
365
|
)
|
|
358
366
|
return Agent(
|
|
359
367
|
client,
|
|
360
|
-
get_tools(runtime_environment, exec_mode=True),
|
|
368
|
+
get_tools(runtime_environment, exec_mode=True, cwd=resolved_cwd),
|
|
361
369
|
context_manager,
|
|
362
370
|
rollout_recorder=rollout_recorder,
|
|
363
371
|
runtime_environment=runtime_environment,
|
|
@@ -150,8 +150,9 @@ class ContextManager:
|
|
|
150
150
|
include_skills_instructions: 'bool' = True,
|
|
151
151
|
network_access: 'str' = "enabled",
|
|
152
152
|
extra_contextual_user_messages: 'typing.Iterable[str]' = (),
|
|
153
|
+
cwd: 'typing.Union[str, Path, None]' = None,
|
|
153
154
|
) -> 'None':
|
|
154
|
-
self.cwd = Path.cwd().resolve()
|
|
155
|
+
self.cwd = Path(cwd or Path.cwd()).resolve()
|
|
155
156
|
self._shell = get_shell_name()
|
|
156
157
|
self._current_date = datetime.now().date().isoformat()
|
|
157
158
|
self._timezone_name = get_timezone_name()
|
|
@@ -193,6 +194,7 @@ class ContextManager:
|
|
|
193
194
|
include_skills_instructions: 'bool' = True,
|
|
194
195
|
network_access: 'str' = "enabled",
|
|
195
196
|
extra_contextual_user_messages: 'typing.Iterable[str]' = (),
|
|
197
|
+
cwd: 'typing.Union[str, Path, None]' = None,
|
|
196
198
|
) -> 'ContextManager':
|
|
197
199
|
config = ContextConfig.from_codex_config(config_path, profile)
|
|
198
200
|
return cls(
|
|
@@ -204,6 +206,7 @@ class ContextManager:
|
|
|
204
206
|
include_skills_instructions=include_skills_instructions,
|
|
205
207
|
network_access=network_access,
|
|
206
208
|
extra_contextual_user_messages=extra_contextual_user_messages,
|
|
209
|
+
cwd=cwd,
|
|
207
210
|
)
|
|
208
211
|
|
|
209
212
|
@property
|
|
@@ -224,7 +224,8 @@ class _FeishuCardActionListener:
|
|
|
224
224
|
self.domain = card.domain
|
|
225
225
|
self.encrypt_key = card.encrypt_key
|
|
226
226
|
self.verification_token = card.verification_token
|
|
227
|
-
self.
|
|
227
|
+
self._links = {}
|
|
228
|
+
self._links_lock = threading.Lock()
|
|
228
229
|
self._async_thread = _AsyncLoopThread()
|
|
229
230
|
self._thread = None
|
|
230
231
|
self._client = None
|
|
@@ -250,14 +251,29 @@ class _FeishuCardActionListener:
|
|
|
250
251
|
self._thread.start()
|
|
251
252
|
|
|
252
253
|
def register(self, link: PycodexRuntimeLink) -> None:
|
|
253
|
-
|
|
254
|
+
message_id = str(getattr(link, "message_id", "") or "").strip()
|
|
255
|
+
if not message_id:
|
|
256
|
+
raise RuntimeError("Feishu linked card message_id is required")
|
|
257
|
+
with self._links_lock:
|
|
258
|
+
self._links[message_id] = link
|
|
254
259
|
|
|
255
260
|
def unregister(self, link: PycodexRuntimeLink) -> None:
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
message_id = str(getattr(link, "message_id", "") or "").strip()
|
|
262
|
+
with self._links_lock:
|
|
263
|
+
if message_id and self._links.get(message_id) is link:
|
|
264
|
+
del self._links[message_id]
|
|
265
|
+
return
|
|
266
|
+
stale_ids = [
|
|
267
|
+
registered_id
|
|
268
|
+
for registered_id, registered_link in self._links.items()
|
|
269
|
+
if registered_link is link
|
|
270
|
+
]
|
|
271
|
+
for registered_id in stale_ids:
|
|
272
|
+
del self._links[registered_id]
|
|
258
273
|
|
|
259
274
|
def empty(self) -> bool:
|
|
260
|
-
|
|
275
|
+
with self._links_lock:
|
|
276
|
+
return not self._links
|
|
261
277
|
|
|
262
278
|
def stop(self) -> None:
|
|
263
279
|
self._stop_requested.set()
|
|
@@ -329,16 +345,9 @@ class _FeishuCardActionListener:
|
|
|
329
345
|
|
|
330
346
|
def _handle_card_action(self, event) -> 'typing.Dict[str, object]':
|
|
331
347
|
try:
|
|
332
|
-
link = self.link
|
|
333
|
-
if link is None or link._detached:
|
|
334
|
-
return {
|
|
335
|
-
"toast": {
|
|
336
|
-
"type": "warning",
|
|
337
|
-
"content": "pycodex is detached.",
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
348
|
message_id = _event_message_id(event)
|
|
341
|
-
|
|
349
|
+
link = self._resolve_link(message_id)
|
|
350
|
+
if link is None or link._detached:
|
|
342
351
|
return {
|
|
343
352
|
"toast": {
|
|
344
353
|
"type": "warning",
|
|
@@ -355,6 +364,15 @@ class _FeishuCardActionListener:
|
|
|
355
364
|
}
|
|
356
365
|
}
|
|
357
366
|
|
|
367
|
+
def _resolve_link(self, message_id):
|
|
368
|
+
message_id = str(message_id or "").strip()
|
|
369
|
+
with self._links_lock:
|
|
370
|
+
if message_id:
|
|
371
|
+
return self._links.get(message_id)
|
|
372
|
+
if len(self._links) == 1:
|
|
373
|
+
return next(iter(self._links.values()))
|
|
374
|
+
return None
|
|
375
|
+
|
|
358
376
|
|
|
359
377
|
def _event_message_id(event) -> "typing.Union[str, None]":
|
|
360
378
|
event_data = getattr(event, "event", None)
|
|
@@ -18,6 +18,7 @@ import typing
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
21
|
+
HELP_COMMAND = "/help"
|
|
21
22
|
HISTORY_COMMAND = "/history"
|
|
22
23
|
TITLE_COMMAND = "/title"
|
|
23
24
|
MODEL_COMMAND = "/model"
|
|
@@ -27,7 +28,7 @@ COMPACT_COMMAND = "/compact"
|
|
|
27
28
|
LINK_COMMAND = "/link"
|
|
28
29
|
UNLINK_COMMAND = "/unlink"
|
|
29
30
|
EXTRA_COMMANDS_LINE = (
|
|
30
|
-
"Extra commands: /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
31
|
+
"Extra commands: /help, /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
|
|
@@ -232,6 +233,9 @@ async def run_interactive_session(
|
|
|
232
233
|
continue
|
|
233
234
|
if prompt_text in EXIT_COMMANDS:
|
|
234
235
|
break
|
|
236
|
+
if prompt_text == HELP_COMMAND:
|
|
237
|
+
view.write_line(EXTRA_COMMANDS_LINE)
|
|
238
|
+
continue
|
|
235
239
|
if prompt_text == HISTORY_COMMAND:
|
|
236
240
|
view.show_history()
|
|
237
241
|
continue
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-codex"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "A minimal Python extraction of Codex's main agent loop"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6.2"
|
|
@@ -45,7 +45,10 @@ default-groups = []
|
|
|
45
45
|
|
|
46
46
|
[tool.hatch.build.targets.wheel]
|
|
47
47
|
packages = ["pycodex", "responses_server", "workspace_server"]
|
|
48
|
-
artifacts = [
|
|
48
|
+
artifacts = [
|
|
49
|
+
"workspace_server/workspace.html",
|
|
50
|
+
"workspace_server/workspaces.html",
|
|
51
|
+
]
|
|
49
52
|
|
|
50
53
|
[tool.hatch.build.targets.sdist]
|
|
51
54
|
exclude = [".venv*"]
|
|
@@ -975,6 +975,23 @@ def test_get_tools_exec_mode_matches_codex_exec_subset() -> 'None':
|
|
|
975
975
|
)
|
|
976
976
|
|
|
977
977
|
|
|
978
|
+
@pytest.mark.asyncio
|
|
979
|
+
async def test_get_tools_exec_mode_uses_explicit_cwd(tmp_path) -> 'None':
|
|
980
|
+
registry = get_tools(exec_mode=True, cwd=tmp_path)
|
|
981
|
+
tool = registry.get_tool("exec_command")
|
|
982
|
+
assert tool is not None
|
|
983
|
+
|
|
984
|
+
output = await tool.run(
|
|
985
|
+
None,
|
|
986
|
+
{
|
|
987
|
+
"cmd": "pwd",
|
|
988
|
+
"yield_time_ms": 1000,
|
|
989
|
+
},
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
assert str(tmp_path) in str(output)
|
|
993
|
+
|
|
994
|
+
|
|
978
995
|
def _tool_payload_by_name(registry: 'ToolRegistry') -> 'typing.Dict[str, typing.Dict[str, object]]':
|
|
979
996
|
payloads: 'typing.Dict[str, typing.Dict[str, object]]' = {}
|
|
980
997
|
for payload in [spec.serialize() for spec in registry.model_visible_specs()]:
|
|
@@ -86,6 +86,18 @@ def test_context_manager_reads_auto_compact_limit_from_codex_config(tmp_path) ->
|
|
|
86
86
|
assert manager.resolve_auto_compact_token_limit() == 12345
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def test_context_manager_accepts_explicit_cwd(tmp_path, monkeypatch) -> 'None':
|
|
90
|
+
explicit_cwd = tmp_path / "workspace"
|
|
91
|
+
explicit_cwd.mkdir()
|
|
92
|
+
other_cwd = tmp_path / "other"
|
|
93
|
+
other_cwd.mkdir()
|
|
94
|
+
monkeypatch.chdir(other_cwd)
|
|
95
|
+
|
|
96
|
+
manager = ContextManager(cwd=explicit_cwd)
|
|
97
|
+
|
|
98
|
+
assert manager.cwd == explicit_cwd.resolve()
|
|
99
|
+
|
|
100
|
+
|
|
89
101
|
@pytest.mark.parametrize(
|
|
90
102
|
"model",
|
|
91
103
|
["step-3.5-flash", "step-3.5-flash-2603", "step-3.7-flash"],
|
|
@@ -88,18 +88,34 @@ async def _forever():
|
|
|
88
88
|
await asyncio.sleep(3600)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
def
|
|
91
|
+
def test_feishu_listener_tracks_active_links_by_message_id() -> None:
|
|
92
92
|
listener = _FeishuCardActionListener(PycodexCard(app_id="app", app_secret="secret"))
|
|
93
93
|
first = _Object(message_id="om_first")
|
|
94
94
|
second = _Object(message_id="om_second")
|
|
95
95
|
|
|
96
96
|
listener.register(first)
|
|
97
|
-
assert listener.
|
|
97
|
+
assert listener._resolve_link("om_first") is first
|
|
98
|
+
listener.register(second)
|
|
99
|
+
assert listener._resolve_link("om_first") is first
|
|
100
|
+
assert listener._resolve_link("om_second") is second
|
|
101
|
+
|
|
98
102
|
listener.unregister(first)
|
|
99
|
-
assert listener.
|
|
103
|
+
assert listener._resolve_link("om_first") is None
|
|
104
|
+
assert listener._resolve_link("om_second") is second
|
|
100
105
|
|
|
101
|
-
listener.
|
|
102
|
-
assert listener.
|
|
106
|
+
listener.unregister(second)
|
|
107
|
+
assert listener.empty()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_feishu_listener_unregisters_link_if_message_id_changes() -> None:
|
|
111
|
+
listener = _FeishuCardActionListener(PycodexCard(app_id="app", app_secret="secret"))
|
|
112
|
+
link = _Object(message_id="om_first")
|
|
113
|
+
|
|
114
|
+
listener.register(link)
|
|
115
|
+
link.message_id = "om_second"
|
|
116
|
+
listener.unregister(link)
|
|
117
|
+
|
|
118
|
+
assert listener.empty()
|
|
103
119
|
|
|
104
120
|
|
|
105
121
|
def test_feishu_listener_stops_when_last_link_is_released(monkeypatch) -> None:
|
|
@@ -112,7 +128,29 @@ def test_feishu_listener_stops_when_last_link_is_released(monkeypatch) -> None:
|
|
|
112
128
|
|
|
113
129
|
_release_feishu_listener(listener, link)
|
|
114
130
|
|
|
115
|
-
assert listener.
|
|
131
|
+
assert listener.empty()
|
|
132
|
+
assert stopped == [True]
|
|
133
|
+
assert feishu_link._LISTENER is None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_feishu_listener_does_not_stop_until_all_links_are_released(monkeypatch) -> None:
|
|
137
|
+
listener = _FeishuCardActionListener(PycodexCard(app_id="app", app_secret="secret"))
|
|
138
|
+
first = _Object(message_id="om_first")
|
|
139
|
+
second = _Object(message_id="om_second")
|
|
140
|
+
stopped = []
|
|
141
|
+
listener.stop = lambda: stopped.append(True)
|
|
142
|
+
listener.register(first)
|
|
143
|
+
listener.register(second)
|
|
144
|
+
monkeypatch.setattr(feishu_link, "_LISTENER", listener)
|
|
145
|
+
|
|
146
|
+
_release_feishu_listener(listener, first)
|
|
147
|
+
|
|
148
|
+
assert stopped == []
|
|
149
|
+
assert feishu_link._LISTENER is listener
|
|
150
|
+
assert listener._resolve_link("om_second") is second
|
|
151
|
+
|
|
152
|
+
_release_feishu_listener(listener, second)
|
|
153
|
+
|
|
116
154
|
assert stopped == [True]
|
|
117
155
|
assert feishu_link._LISTENER is None
|
|
118
156
|
|
|
@@ -131,6 +169,60 @@ def test_feishu_listener_relink_creates_fresh_listener(monkeypatch) -> None:
|
|
|
131
169
|
assert feishu_link._LISTENER is second
|
|
132
170
|
|
|
133
171
|
|
|
172
|
+
def test_feishu_listener_routes_card_action_by_message_id(monkeypatch) -> None:
|
|
173
|
+
listener = _FeishuCardActionListener(PycodexCard(app_id="app", app_secret="secret"))
|
|
174
|
+
first = _Object(
|
|
175
|
+
message_id="om_first",
|
|
176
|
+
_detached=False,
|
|
177
|
+
card=_Object(parse_action=lambda event: {"action": "send", "prompt": "first"}),
|
|
178
|
+
)
|
|
179
|
+
second = _Object(
|
|
180
|
+
message_id="om_second",
|
|
181
|
+
_detached=False,
|
|
182
|
+
card=_Object(parse_action=lambda event: {"action": "send", "prompt": "second"}),
|
|
183
|
+
)
|
|
184
|
+
submitted = []
|
|
185
|
+
|
|
186
|
+
async def submit(action):
|
|
187
|
+
submitted.append(action["prompt"])
|
|
188
|
+
return {"toast": {"type": "info", "content": action["prompt"]}}
|
|
189
|
+
|
|
190
|
+
first._submit = submit
|
|
191
|
+
second._submit = submit
|
|
192
|
+
|
|
193
|
+
def run_coro(coro):
|
|
194
|
+
loop = asyncio.new_event_loop()
|
|
195
|
+
try:
|
|
196
|
+
return loop.run_until_complete(coro)
|
|
197
|
+
finally:
|
|
198
|
+
loop.close()
|
|
199
|
+
|
|
200
|
+
listener._async_thread.run = run_coro
|
|
201
|
+
|
|
202
|
+
listener.register(first)
|
|
203
|
+
listener.register(second)
|
|
204
|
+
|
|
205
|
+
result = listener._handle_card_action(_event("om_second"))
|
|
206
|
+
|
|
207
|
+
assert submitted == ["second"]
|
|
208
|
+
assert result["toast"]["content"] == "second"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_feishu_listener_rejects_unknown_card_action_message_id() -> None:
|
|
212
|
+
listener = _FeishuCardActionListener(PycodexCard(app_id="app", app_secret="secret"))
|
|
213
|
+
listener.register(_Object(message_id="om_first", _detached=False))
|
|
214
|
+
listener.register(_Object(message_id="om_second", _detached=False))
|
|
215
|
+
|
|
216
|
+
result = listener._handle_card_action(_event("om_stale"))
|
|
217
|
+
|
|
218
|
+
assert result == {
|
|
219
|
+
"toast": {
|
|
220
|
+
"type": "warning",
|
|
221
|
+
"content": "pycodex is detached.",
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
134
226
|
def test_feishu_listener_replaces_running_sdk_module_loop(monkeypatch) -> None:
|
|
135
227
|
client_module = types.ModuleType("lark_oapi.ws.client")
|
|
136
228
|
started_loops = []
|