python-codex 0.4.7__tar.gz → 0.4.8__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.4.7 → python_codex-0.4.8}/PKG-INFO +13 -2
- {python_codex-0.4.7 → python_codex-0.4.8}/README.md +12 -1
- {python_codex-0.4.7 → python_codex-0.4.8}/README_ZH.md +6 -1
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/agent.py +8 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/bootstrap.py +23 -26
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/context.py +10 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/runtime.py +11 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/apply_patch_tool.py +24 -26
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/base_tool.py +5 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/code_mode_manager.py +4 -9
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/exec_command_tool.py +1 -2
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/grep_files_tool.py +2 -16
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/shell_command_tool.py +1 -17
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/shell_tool.py +1 -17
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/unified_exec_manager.py +2 -14
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/view_image_tool.py +1 -12
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/event_helpers.py +2 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pyproject.toml +2 -2
- {python_codex-0.4.7 → python_codex-0.4.8}/.gitignore +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/LICENSE +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/__main__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/cli.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/events.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/model.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/portable/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/portable/client.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/portable/server.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/models.json +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/protocol.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/agent_tool_schemas.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/clock_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/close_agent_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/exec_runtime.js +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/exec_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/list_dir_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/read_file_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/request_permissions_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/request_user_input_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/resume_agent_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/runtime_services.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/send_input_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/spawn_agent_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/update_plan_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/wait_agent_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/wait_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/web_search_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/tools/write_stdin_tool.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/_aes_gcm/LICENSE +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/_aes_gcm/README.md +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/_aes_gcm/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/_aes_gcm/aes.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/_aes_gcm/gcm.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/_aes_gcm/source-sha256.json +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/async_bridge.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/compactor.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/compat.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/crypto.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/debug.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/doctor.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/dotenv.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/get_env.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/image_utils.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/model_metadata.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/random_ids.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/session_persist.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/pycodex/utils/truncation.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/__main__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/app.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/config.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/messages_api.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/payload_processors.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/server.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/session_store.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/stream_router.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/tools/custom_adapter.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/tools/web_search.py +0 -0
- {python_codex-0.4.7 → python_codex-0.4.8}/responses_server/trajectory_dump.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
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
|
|
@@ -378,10 +378,13 @@ Current behavior:
|
|
|
378
378
|
tool execution and model follow-up after tool results
|
|
379
379
|
- startup shows a welcome, model, effort, endpoint and available commands.
|
|
380
380
|
- assistant text is printed from streaming deltas directly
|
|
381
|
-
- interactive mode supports `/history`, `/title`, `/model`, `/effort`, `/resume`,
|
|
381
|
+
- interactive mode supports `/history`, `/title`, `/cd`, `/model`, `/effort`, `/resume`,
|
|
382
382
|
`/compact`, and `/fork`
|
|
383
383
|
- `/model <name>` switches the model used by later turns in the current
|
|
384
384
|
interactive session; `/model` shows the current model and available choices
|
|
385
|
+
- `/cd <directory>` changes the current session's working directory while idle;
|
|
386
|
+
`/cd` shows it. Relative paths and `~` are supported; paths containing spaces
|
|
387
|
+
can be entered directly without quotes. Later tools and context use the new directory.
|
|
385
388
|
- `/effort low` switches effort while idle; `/effort` shows the current value,
|
|
386
389
|
and `/effort default` restores the model default. Frontend labels show
|
|
387
390
|
`model:effort`.
|
|
@@ -454,6 +457,11 @@ Current behavior:
|
|
|
454
457
|
original state paths and are saved in the new format. See the
|
|
455
458
|
[workspace configuration example](packages/pycodex-ws/README.md).
|
|
456
459
|
Add/delete actions and later session-state saves refresh the JSON file.
|
|
460
|
+
Each session starts in `work_dir` and can switch independently with `/cd`.
|
|
461
|
+
A small folder indicator appears inside the composer only when the session
|
|
462
|
+
directory differs from `work_dir`; hover for the full path. Directories persist
|
|
463
|
+
across workspace restarts, including for empty sessions. Relative chat/Canvas file links use the selected
|
|
464
|
+
session's directory; Canvas and workspace state files stay at their configured paths.
|
|
457
465
|
Board HTML can reference local
|
|
458
466
|
images beside the board (including nested paths) with relative URLs; only
|
|
459
467
|
`image/*` files contained by the board directory are served.
|
|
@@ -813,6 +821,9 @@ event JSON format.
|
|
|
813
821
|
uses `agent.tool_registry.runtime_environment`, not a forwarding Agent property.
|
|
814
822
|
The global `get_agent_runtime_environment()` and
|
|
815
823
|
separate `Agent(..., runtime_environment=...)` entry points are removed.
|
|
824
|
+
- Configure cwd once with `ContextConfig(cwd=...)` or `build_agent(cwd=...)`.
|
|
825
|
+
Tools use the required `ToolContext.cwd` for each call; tool constructors
|
|
826
|
+
and `get_tools` do not take a separate default directory.
|
|
816
827
|
- Each sub-agent gets its own context manager and runtime services. The sub-agent
|
|
817
828
|
service retains terminal event status and queues one notification per terminal
|
|
818
829
|
event. The parent consumes these notifications before its next normal model
|
|
@@ -358,10 +358,13 @@ Current behavior:
|
|
|
358
358
|
tool execution and model follow-up after tool results
|
|
359
359
|
- startup shows a welcome, model, effort, endpoint and available commands.
|
|
360
360
|
- assistant text is printed from streaming deltas directly
|
|
361
|
-
- interactive mode supports `/history`, `/title`, `/model`, `/effort`, `/resume`,
|
|
361
|
+
- interactive mode supports `/history`, `/title`, `/cd`, `/model`, `/effort`, `/resume`,
|
|
362
362
|
`/compact`, and `/fork`
|
|
363
363
|
- `/model <name>` switches the model used by later turns in the current
|
|
364
364
|
interactive session; `/model` shows the current model and available choices
|
|
365
|
+
- `/cd <directory>` changes the current session's working directory while idle;
|
|
366
|
+
`/cd` shows it. Relative paths and `~` are supported; paths containing spaces
|
|
367
|
+
can be entered directly without quotes. Later tools and context use the new directory.
|
|
365
368
|
- `/effort low` switches effort while idle; `/effort` shows the current value,
|
|
366
369
|
and `/effort default` restores the model default. Frontend labels show
|
|
367
370
|
`model:effort`.
|
|
@@ -434,6 +437,11 @@ Current behavior:
|
|
|
434
437
|
original state paths and are saved in the new format. See the
|
|
435
438
|
[workspace configuration example](packages/pycodex-ws/README.md).
|
|
436
439
|
Add/delete actions and later session-state saves refresh the JSON file.
|
|
440
|
+
Each session starts in `work_dir` and can switch independently with `/cd`.
|
|
441
|
+
A small folder indicator appears inside the composer only when the session
|
|
442
|
+
directory differs from `work_dir`; hover for the full path. Directories persist
|
|
443
|
+
across workspace restarts, including for empty sessions. Relative chat/Canvas file links use the selected
|
|
444
|
+
session's directory; Canvas and workspace state files stay at their configured paths.
|
|
437
445
|
Board HTML can reference local
|
|
438
446
|
images beside the board (including nested paths) with relative URLs; only
|
|
439
447
|
`image/*` files contained by the board directory are served.
|
|
@@ -793,6 +801,9 @@ event JSON format.
|
|
|
793
801
|
uses `agent.tool_registry.runtime_environment`, not a forwarding Agent property.
|
|
794
802
|
The global `get_agent_runtime_environment()` and
|
|
795
803
|
separate `Agent(..., runtime_environment=...)` entry points are removed.
|
|
804
|
+
- Configure cwd once with `ContextConfig(cwd=...)` or `build_agent(cwd=...)`.
|
|
805
|
+
Tools use the required `ToolContext.cwd` for each call; tool constructors
|
|
806
|
+
and `get_tools` do not take a separate default directory.
|
|
796
807
|
- Each sub-agent gets its own context manager and runtime services. The sub-agent
|
|
797
808
|
service retains terminal event status and queues one notification per terminal
|
|
798
809
|
event. The parent consumes these notifications before its next normal model
|
|
@@ -309,7 +309,8 @@ pycodex doctor
|
|
|
309
309
|
- 交互模式下会显示简洁阶段事件流,例如工具执行状态和模型回看工具结果
|
|
310
310
|
- 开场保留欢迎语,显示 model、effort、endpoint 和命令列表。
|
|
311
311
|
- assistant 文本会按流式 delta 直接打印
|
|
312
|
-
- 交互模式下支持 `/history`、`/title`、`/model`、`/effort` 和 `/resume`
|
|
312
|
+
- 交互模式下支持 `/history`、`/title`、`/cd`、`/model`、`/effort` 和 `/resume`
|
|
313
|
+
- `/cd <目录>` 在空闲时切换当前 session 的工作目录,`/cd` 查看当前目录;支持相对路径、`~` 和直接输入含空格的路径(不加引号)。之后的工具和上下文使用新目录。
|
|
313
314
|
- `/model <name>` 会切换当前交互会话后续请求使用的模型;`/model` 会显示当前模型和可选模型
|
|
314
315
|
- `/effort low` 在空闲时切换 effort,`/effort` 查看当前值,`/effort default` 恢复模型默认值;前端模型标签显示 `model:effort`。
|
|
315
316
|
- `/resume` 不带参数时会按首条用户消息预览列出当前可恢复的 session;`/resume 1`
|
|
@@ -337,6 +338,10 @@ pycodex doctor
|
|
|
337
338
|
`enable_board: false` 关闭 Canvas 标签、路由、轮询和上下文注入,会话及文件视图
|
|
338
339
|
仍照常持久化和恢复。旧 `board: "demo.html"` / `board: false` 配置读取时转换,
|
|
339
340
|
继续使用原状态文件,后续保存采用新格式。管理页新建时不填前缀会分配 `/tmp/pcws-*`。
|
|
341
|
+
- 每个 session 初始继承 `work_dir`,之后可通过 `/cd` 各自切换。仅当目录与默认值不同时,
|
|
342
|
+
输入框内显示小型文件夹提示,悬停可查看完整路径;切回默认目录后隐藏。
|
|
343
|
+
目录随会话索引保存,空会话重启后也能恢复。聊天和 Canvas 中的相对文件链接按当前选中
|
|
344
|
+
session 的目录解析;Canvas 文件及 workspace 状态文件的位置保持固定。
|
|
340
345
|
- workspace 整个左侧展示区称为看板,包含可选的实时 HTML Canvas 首标签和只读文件/diff 标签;
|
|
341
346
|
所有标签在同一行共同滚动,溢出时可用鼠标滚轮横向滚动。点击普通 Markdown 文件链接
|
|
342
347
|
`[源码](src/app.py:42)` 或 `[源码](/data/project/app.py#L42)` 即可打开视图。
|
|
@@ -152,6 +152,13 @@ class Agent:
|
|
|
152
152
|
self.context_manager.set_model(model)
|
|
153
153
|
self._last_total_usage_tokens = None
|
|
154
154
|
|
|
155
|
+
def set_cwd(self, cwd: "typing.Union[str, Path]") -> "None":
|
|
156
|
+
if self.is_running:
|
|
157
|
+
raise RuntimeError("cannot change directory while agent is running")
|
|
158
|
+
if not self.accepts_input:
|
|
159
|
+
raise RuntimeError("cannot change directory while agent is closing or closed")
|
|
160
|
+
self.context_manager.set_cwd(cwd)
|
|
161
|
+
|
|
155
162
|
def resume(
|
|
156
163
|
self, session_file_path: "typing.Union[str, Path, None]" = None
|
|
157
164
|
) -> "typing.Union[typing.Dict[str, object], None]":
|
|
@@ -400,6 +407,7 @@ class Agent:
|
|
|
400
407
|
context = ToolContext(
|
|
401
408
|
turn_id=turn_id,
|
|
402
409
|
history=self.history,
|
|
410
|
+
cwd=self.context_manager.cwd,
|
|
403
411
|
)
|
|
404
412
|
if len(batch) == 1:
|
|
405
413
|
results.append(await self._run_single_tool(turn_id, batch[0], context))
|
|
@@ -86,7 +86,6 @@ def configure_loguru() -> "None":
|
|
|
86
86
|
def get_tools(
|
|
87
87
|
runtime_environment: "typing.Union[AgentRuntimeEnvironment, None]" = None,
|
|
88
88
|
exec_mode: "bool" = False,
|
|
89
|
-
cwd: "typing.Union[str, Path, None]" = None,
|
|
90
89
|
toolset: "typing.Union[typing.Iterable[str], None]" = None,
|
|
91
90
|
):
|
|
92
91
|
from .tools import (
|
|
@@ -119,8 +118,8 @@ def get_tools(
|
|
|
119
118
|
|
|
120
119
|
runtime_environment = runtime_environment or create_agent_runtime_environment()
|
|
121
120
|
registry = Registry(runtime_environment)
|
|
122
|
-
code_mode_manager = CodeModeManager(registry
|
|
123
|
-
unified_exec_manager = UnifiedExecManager(
|
|
121
|
+
code_mode_manager = CodeModeManager(registry)
|
|
122
|
+
unified_exec_manager = UnifiedExecManager()
|
|
124
123
|
clock_manager = ClockManager()
|
|
125
124
|
exec_tool = ExecTool(code_mode_manager)
|
|
126
125
|
wait_tool = WaitTool(code_mode_manager)
|
|
@@ -135,16 +134,16 @@ def get_tools(
|
|
|
135
134
|
resume_agent_tool = ResumeAgentTool(runtime_environment.subagent_manager)
|
|
136
135
|
wait_agent_tool = WaitAgentTool(runtime_environment.subagent_manager)
|
|
137
136
|
close_agent_tool = CloseAgentTool(runtime_environment.subagent_manager)
|
|
138
|
-
apply_patch_tool = ApplyPatchTool(
|
|
139
|
-
shell_tool = ShellTool(
|
|
140
|
-
shell_command_tool = ShellCommandTool(
|
|
137
|
+
apply_patch_tool = ApplyPatchTool()
|
|
138
|
+
shell_tool = ShellTool()
|
|
139
|
+
shell_command_tool = ShellCommandTool()
|
|
141
140
|
exec_command_tool = ExecCommandTool(unified_exec_manager)
|
|
142
141
|
write_stdin_tool = WriteStdinTool(unified_exec_manager)
|
|
143
142
|
clock_tool = ClockTool(clock_manager)
|
|
144
|
-
grep_files_tool = GrepFilesTool(
|
|
143
|
+
grep_files_tool = GrepFilesTool()
|
|
145
144
|
read_file_tool = ReadFileTool()
|
|
146
145
|
list_dir_tool = ListDirTool()
|
|
147
|
-
view_image_tool = ViewImageTool(
|
|
146
|
+
view_image_tool = ViewImageTool()
|
|
148
147
|
tools = (
|
|
149
148
|
shell_tool,
|
|
150
149
|
shell_command_tool,
|
|
@@ -200,7 +199,6 @@ def get_tools(
|
|
|
200
199
|
|
|
201
200
|
def get_subagent_tools(
|
|
202
201
|
runtime_environment: "typing.Union[AgentRuntimeEnvironment, None]" = None,
|
|
203
|
-
cwd: "typing.Union[str, Path, None]" = None,
|
|
204
202
|
):
|
|
205
203
|
from .tools import (
|
|
206
204
|
ApplyPatchTool,
|
|
@@ -215,13 +213,13 @@ def get_subagent_tools(
|
|
|
215
213
|
|
|
216
214
|
runtime_environment = runtime_environment or create_agent_runtime_environment()
|
|
217
215
|
registry = Registry(runtime_environment)
|
|
218
|
-
unified_exec_manager = UnifiedExecManager(
|
|
216
|
+
unified_exec_manager = UnifiedExecManager()
|
|
219
217
|
registry.register(ExecCommandTool(unified_exec_manager))
|
|
220
218
|
registry.register(WriteStdinTool(unified_exec_manager))
|
|
221
219
|
registry.register(UpdatePlanTool(runtime_environment.plan_store))
|
|
222
|
-
registry.register(ApplyPatchTool(
|
|
220
|
+
registry.register(ApplyPatchTool())
|
|
223
221
|
registry.register(WebSearchTool())
|
|
224
|
-
registry.register(ViewImageTool(
|
|
222
|
+
registry.register(ViewImageTool())
|
|
225
223
|
return registry
|
|
226
224
|
|
|
227
225
|
|
|
@@ -244,46 +242,41 @@ def build_agent(
|
|
|
244
242
|
)
|
|
245
243
|
runtime_environment = create_agent_runtime_environment()
|
|
246
244
|
|
|
247
|
-
def make_subagent_runtime_builder(
|
|
245
|
+
def make_subagent_runtime_builder(parent_agent):
|
|
248
246
|
def build_subagent_runtime(
|
|
249
247
|
model_override: "typing.Union[str, None]",
|
|
250
248
|
reasoning_effort_override: "typing.Union[str, None]",
|
|
251
249
|
initial_history=(),
|
|
252
250
|
session_id: "typing.Union[str, None]" = None,
|
|
253
251
|
) -> "AgentRuntime":
|
|
254
|
-
nested_client =
|
|
252
|
+
nested_client = parent_agent.model_client.with_overrides(
|
|
255
253
|
model_override,
|
|
256
254
|
reasoning_effort_override,
|
|
257
255
|
session_id=session_id,
|
|
258
256
|
openai_subagent="collab_spawn",
|
|
259
257
|
)
|
|
260
258
|
subagent_agent_runtime_environment = create_agent_runtime_environment()
|
|
261
|
-
|
|
262
|
-
make_subagent_runtime_builder(nested_client)
|
|
263
|
-
)
|
|
259
|
+
inherited_cwd = parent_agent.context_manager.cwd
|
|
264
260
|
sub_agent = Agent(
|
|
265
261
|
nested_client,
|
|
266
|
-
get_subagent_tools(
|
|
267
|
-
|
|
268
|
-
),
|
|
269
|
-
context_config,
|
|
262
|
+
get_subagent_tools(subagent_agent_runtime_environment),
|
|
263
|
+
replace(context_config, cwd=inherited_cwd),
|
|
270
264
|
initial_history=tuple(initial_history),
|
|
271
265
|
session_id=session_id,
|
|
272
266
|
)
|
|
267
|
+
subagent_agent_runtime_environment.subagent_manager.set_runtime_builder(
|
|
268
|
+
make_subagent_runtime_builder(sub_agent)
|
|
269
|
+
)
|
|
273
270
|
return AgentRuntime(sub_agent)
|
|
274
271
|
|
|
275
272
|
return build_subagent_runtime
|
|
276
273
|
|
|
277
|
-
runtime_environment.subagent_manager.set_runtime_builder(
|
|
278
|
-
make_subagent_runtime_builder(client)
|
|
279
|
-
)
|
|
280
274
|
session_id = uuid7_string()
|
|
281
|
-
|
|
275
|
+
agent = Agent(
|
|
282
276
|
client,
|
|
283
277
|
get_tools(
|
|
284
278
|
runtime_environment,
|
|
285
279
|
exec_mode=True,
|
|
286
|
-
cwd=resolved_cwd,
|
|
287
280
|
toolset=toolset,
|
|
288
281
|
),
|
|
289
282
|
context_config,
|
|
@@ -292,6 +285,10 @@ def build_agent(
|
|
|
292
285
|
),
|
|
293
286
|
session_id=session_id,
|
|
294
287
|
)
|
|
288
|
+
runtime_environment.subagent_manager.set_runtime_builder(
|
|
289
|
+
make_subagent_runtime_builder(agent)
|
|
290
|
+
)
|
|
291
|
+
return agent
|
|
295
292
|
|
|
296
293
|
|
|
297
294
|
def build_model(
|
|
@@ -244,6 +244,16 @@ class ContextManager:
|
|
|
244
244
|
def set_model(self, model: "str") -> "None":
|
|
245
245
|
self._config = replace(self._config, model=model)
|
|
246
246
|
|
|
247
|
+
def set_cwd(self, cwd: "typing.Union[str, Path]") -> "None":
|
|
248
|
+
path = Path(cwd).expanduser()
|
|
249
|
+
path = (self.cwd / path).resolve(strict=True)
|
|
250
|
+
if not path.is_dir():
|
|
251
|
+
raise NotADirectoryError(str(path))
|
|
252
|
+
self.cwd = path
|
|
253
|
+
self._config = replace(self._config, cwd=path)
|
|
254
|
+
self._workspace_metadata_turn_id = None
|
|
255
|
+
self._workspace_metadata_cache = None
|
|
256
|
+
|
|
247
257
|
def resolve_auto_compact_token_limit(self) -> "typing.Union[int, None]":
|
|
248
258
|
if self._config.model_auto_compact_token_limit is not None:
|
|
249
259
|
return self._config.model_auto_compact_token_limit
|
|
@@ -124,6 +124,7 @@ class AgentRuntime:
|
|
|
124
124
|
"help",
|
|
125
125
|
"history",
|
|
126
126
|
"title",
|
|
127
|
+
"cd",
|
|
127
128
|
"model",
|
|
128
129
|
"effort",
|
|
129
130
|
"resume",
|
|
@@ -139,6 +140,7 @@ class AgentRuntime:
|
|
|
139
140
|
recorded_path = self.agent.recorded_session_file_path
|
|
140
141
|
return {
|
|
141
142
|
"session_id": self.agent.session_id,
|
|
143
|
+
"cwd": str(self.agent.context_manager.cwd),
|
|
142
144
|
"rollout_path": str(rollout_path) if rollout_path is not None else None,
|
|
143
145
|
"recorded_rollout_path": (
|
|
144
146
|
str(recorded_path) if recorded_path is not None else None
|
|
@@ -187,6 +189,11 @@ class AgentRuntime:
|
|
|
187
189
|
self.agent.set_model(model)
|
|
188
190
|
self.publish_state("model")
|
|
189
191
|
|
|
192
|
+
def set_cwd(self, cwd):
|
|
193
|
+
self.require_idle("change directory")
|
|
194
|
+
self.agent.set_cwd(cwd)
|
|
195
|
+
self.publish_state("cwd")
|
|
196
|
+
|
|
190
197
|
def resume(self, path=None, title=""):
|
|
191
198
|
self.require_idle("resume")
|
|
192
199
|
resumed = self.agent.resume(path)
|
|
@@ -275,6 +282,10 @@ class AgentRuntime:
|
|
|
275
282
|
return {"kind": "help", "commands": self.commands()}
|
|
276
283
|
if command == "history":
|
|
277
284
|
return {"kind": "history", "state": self.snapshot()}
|
|
285
|
+
if command == "cd":
|
|
286
|
+
if argument:
|
|
287
|
+
self.set_cwd(argument)
|
|
288
|
+
return {"kind": "cwd", "cwd": str(self.agent.context_manager.cwd)}
|
|
278
289
|
if command == "title":
|
|
279
290
|
if argument:
|
|
280
291
|
self.set_title(argument)
|
|
@@ -92,20 +92,15 @@ class ApplyPatchTool(BaseTool):
|
|
|
92
92
|
}
|
|
93
93
|
supports_parallel = False
|
|
94
94
|
|
|
95
|
-
def __init__(
|
|
96
|
-
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
97
|
-
) -> "None":
|
|
98
|
-
self._workspace_root = Path(cwd or Path.cwd()).resolve()
|
|
99
|
-
|
|
100
95
|
async def run(self, context: "ToolContext", args: "JSONValue") -> "JSONValue":
|
|
101
|
-
|
|
96
|
+
cwd = context.cwd
|
|
102
97
|
patch_text = str(args)
|
|
103
98
|
logger.debug(
|
|
104
|
-
"apply_patch workspace={} bytes={}",
|
|
99
|
+
"apply_patch workspace={} bytes={}", cwd, len(patch_text)
|
|
105
100
|
)
|
|
106
101
|
try:
|
|
107
102
|
operations = self._parse_patch(patch_text)
|
|
108
|
-
return self._format_result(self._apply_operations(operations), exit_code=0)
|
|
103
|
+
return self._format_result(self._apply_operations(operations, cwd), exit_code=0)
|
|
109
104
|
except ApplyPatchError as exc:
|
|
110
105
|
return self._format_result(str(exc), exit_code=1)
|
|
111
106
|
|
|
@@ -239,52 +234,54 @@ class ApplyPatchTool(BaseTool):
|
|
|
239
234
|
def _apply_operations(
|
|
240
235
|
self,
|
|
241
236
|
operations: "typing.List[typing.Union[typing.Union[_AddFileOp, _DeleteFileOp], _UpdateFileOp]]",
|
|
237
|
+
cwd: "Path",
|
|
242
238
|
) -> "str":
|
|
243
239
|
preview: "typing.Dict[Path, typing.Union[str, None]]" = {}
|
|
244
240
|
summaries: "typing.Dict[Path, str]" = {}
|
|
245
241
|
|
|
246
242
|
for operation in operations:
|
|
247
243
|
if isinstance(operation, _AddFileOp):
|
|
248
|
-
path = self._resolve_workspace_path(operation.path)
|
|
244
|
+
path = self._resolve_workspace_path(operation.path, cwd)
|
|
249
245
|
preview[path] = operation.content
|
|
250
246
|
summaries[path] = "A"
|
|
251
247
|
continue
|
|
252
248
|
|
|
253
249
|
if isinstance(operation, _DeleteFileOp):
|
|
254
|
-
path = self._resolve_workspace_path(operation.path)
|
|
255
|
-
self._read_preview_file(path, preview)
|
|
250
|
+
path = self._resolve_workspace_path(operation.path, cwd)
|
|
251
|
+
self._read_preview_file(path, preview, cwd)
|
|
256
252
|
preview[path] = None
|
|
257
253
|
summaries[path] = "D"
|
|
258
254
|
continue
|
|
259
255
|
|
|
260
|
-
path = self._resolve_workspace_path(operation.path)
|
|
261
|
-
original = self._read_preview_file(path, preview)
|
|
262
|
-
updated = self._apply_update(path, original, operation.sections)
|
|
256
|
+
path = self._resolve_workspace_path(operation.path, cwd)
|
|
257
|
+
original = self._read_preview_file(path, preview, cwd)
|
|
258
|
+
updated = self._apply_update(path, original, operation.sections, cwd)
|
|
263
259
|
destination = path
|
|
264
260
|
if operation.move_to is not None:
|
|
265
|
-
destination = self._resolve_workspace_path(operation.move_to)
|
|
261
|
+
destination = self._resolve_workspace_path(operation.move_to, cwd)
|
|
266
262
|
preview[path] = None
|
|
267
263
|
summaries.pop(path, None)
|
|
268
264
|
preview[destination] = updated
|
|
269
265
|
summaries[destination] = "M"
|
|
270
266
|
|
|
271
267
|
self._write_preview(preview)
|
|
272
|
-
return self._format_success(summaries)
|
|
268
|
+
return self._format_success(summaries, cwd)
|
|
273
269
|
|
|
274
270
|
def _read_preview_file(
|
|
275
|
-
self, path: "Path", preview: "typing.Dict[Path, typing.Union[str, None]]"
|
|
271
|
+
self, path: "Path", preview: "typing.Dict[Path, typing.Union[str, None]]",
|
|
272
|
+
cwd: "Path",
|
|
276
273
|
) -> "str":
|
|
277
274
|
if path in preview:
|
|
278
275
|
content = preview[path]
|
|
279
276
|
if content is None:
|
|
280
277
|
raise ApplyPatchError(
|
|
281
|
-
f"apply_patch verification failed: Failed to read {self._display_path(path)}"
|
|
278
|
+
f"apply_patch verification failed: Failed to read {self._display_path(path, cwd)}"
|
|
282
279
|
)
|
|
283
280
|
return content
|
|
284
281
|
|
|
285
282
|
if not path.exists() or not path.is_file():
|
|
286
283
|
raise ApplyPatchError(
|
|
287
|
-
f"apply_patch verification failed: Failed to read {self._display_path(path)}"
|
|
284
|
+
f"apply_patch verification failed: Failed to read {self._display_path(path, cwd)}"
|
|
288
285
|
)
|
|
289
286
|
return path.read_text(encoding="utf-8", errors="replace")
|
|
290
287
|
|
|
@@ -293,6 +290,7 @@ class ApplyPatchTool(BaseTool):
|
|
|
293
290
|
path: "Path",
|
|
294
291
|
original_text: "str",
|
|
295
292
|
sections: "typing.Tuple[_UpdateSection, ...]",
|
|
293
|
+
cwd: "Path",
|
|
296
294
|
) -> "str":
|
|
297
295
|
lines = original_text.splitlines()
|
|
298
296
|
cursor = 0
|
|
@@ -308,7 +306,7 @@ class ApplyPatchTool(BaseTool):
|
|
|
308
306
|
if match_index is None:
|
|
309
307
|
raise ApplyPatchError(
|
|
310
308
|
"apply_patch verification failed: Failed to find expected lines in "
|
|
311
|
-
f"{self._display_path(path)}"
|
|
309
|
+
f"{self._display_path(path, cwd)}"
|
|
312
310
|
)
|
|
313
311
|
lines[match_index : match_index + len(old_block)] = new_block
|
|
314
312
|
cursor = match_index + len(new_block)
|
|
@@ -350,10 +348,10 @@ class ApplyPatchTool(BaseTool):
|
|
|
350
348
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
351
349
|
path.write_text(content, encoding="utf-8")
|
|
352
350
|
|
|
353
|
-
def _format_success(self, summaries: "typing.Dict[Path, str]") -> "str":
|
|
351
|
+
def _format_success(self, summaries: "typing.Dict[Path, str]", cwd: "Path") -> "str":
|
|
354
352
|
buckets = {"A": [], "M": [], "D": []}
|
|
355
353
|
for path, status in summaries.items():
|
|
356
|
-
buckets[status].append(self._display_path(path))
|
|
354
|
+
buckets[status].append(self._display_path(path, cwd))
|
|
357
355
|
lines = ["Success:"]
|
|
358
356
|
for status in ("A", "M", "D"):
|
|
359
357
|
for rel_path in sorted(buckets[status]):
|
|
@@ -365,14 +363,14 @@ class ApplyPatchTool(BaseTool):
|
|
|
365
363
|
f"Exit code: {exit_code}\n" "Wall time: 0 seconds\n" "Output:\n" f"{output}"
|
|
366
364
|
)
|
|
367
365
|
|
|
368
|
-
def _resolve_workspace_path(self, path_text: "str") -> "Path":
|
|
366
|
+
def _resolve_workspace_path(self, path_text: "str", cwd: "Path") -> "Path":
|
|
369
367
|
path = Path(path_text).expanduser()
|
|
370
|
-
resolved = path if path.is_absolute() else
|
|
368
|
+
resolved = path if path.is_absolute() else cwd / path
|
|
371
369
|
return resolved.resolve()
|
|
372
370
|
|
|
373
|
-
def _display_path(self, path: "Path") -> "str":
|
|
371
|
+
def _display_path(self, path: "Path", cwd: "Path") -> "str":
|
|
374
372
|
try:
|
|
375
|
-
return path.relative_to(
|
|
373
|
+
return path.relative_to(cwd).as_posix()
|
|
376
374
|
except ValueError:
|
|
377
375
|
return str(path)
|
|
378
376
|
|
|
@@ -16,6 +16,7 @@ import traceback
|
|
|
16
16
|
import typing
|
|
17
17
|
from abc import ABC, abstractmethod
|
|
18
18
|
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
from ..events import Event
|
|
21
22
|
from ..protocol import (
|
|
@@ -40,6 +41,10 @@ if typing.TYPE_CHECKING:
|
|
|
40
41
|
class ToolContext:
|
|
41
42
|
turn_id: "str"
|
|
42
43
|
history: "typing.Tuple[ConversationItem, ...]"
|
|
44
|
+
cwd: "Path"
|
|
45
|
+
|
|
46
|
+
def resolve_path(self, path=None) -> "Path":
|
|
47
|
+
return (self.cwd / Path("." if path is None else path).expanduser()).resolve()
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
class StructuredToolOutput:
|
|
@@ -60,13 +60,8 @@ class ParsedExecSource:
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class CodeModeManager:
|
|
63
|
-
def __init__(
|
|
64
|
-
self,
|
|
65
|
-
registry: "ToolRegistry",
|
|
66
|
-
cwd: "typing.Union[typing.Union[str, Path], None]" = None,
|
|
67
|
-
) -> "None":
|
|
63
|
+
def __init__(self, registry: "ToolRegistry") -> "None":
|
|
68
64
|
self._registry = registry
|
|
69
|
-
self._default_cwd = Path(cwd or Path.cwd()).resolve()
|
|
70
65
|
self._runtime_script = Path(__file__).with_name("exec_runtime.js")
|
|
71
66
|
self._stored_values: "typing.Dict[str, JSONValue]" = {}
|
|
72
67
|
self._cells: "typing.Dict[str, ExecCell]" = {}
|
|
@@ -130,7 +125,7 @@ class CodeModeManager:
|
|
|
130
125
|
process = await asyncio.create_subprocess_exec(
|
|
131
126
|
"node",
|
|
132
127
|
str(self._runtime_script),
|
|
133
|
-
cwd=str(
|
|
128
|
+
cwd=str(context.cwd),
|
|
134
129
|
stdin=asyncio.subprocess.PIPE,
|
|
135
130
|
stdout=asyncio.subprocess.PIPE,
|
|
136
131
|
stderr=asyncio.subprocess.PIPE,
|
|
@@ -153,7 +148,7 @@ class CodeModeManager:
|
|
|
153
148
|
"tools": self.enabled_tools(),
|
|
154
149
|
},
|
|
155
150
|
)
|
|
156
|
-
logger.debug("exec start cell_id={} cwd={}", cell_id,
|
|
151
|
+
logger.debug("exec start cell_id={} cwd={}", cell_id, context.cwd)
|
|
157
152
|
return cell
|
|
158
153
|
|
|
159
154
|
async def _read_stdout(self, cell: "ExecCell", context: "ToolContext") -> "None":
|
|
@@ -278,7 +273,7 @@ class CodeModeManager:
|
|
|
278
273
|
arguments=message.get("arguments"),
|
|
279
274
|
tool_type=tool.tool_type,
|
|
280
275
|
),
|
|
281
|
-
|
|
276
|
+
context,
|
|
282
277
|
)
|
|
283
278
|
if result.is_error:
|
|
284
279
|
payload = {
|
|
@@ -87,14 +87,13 @@ class ExecCommandTool(BaseTool):
|
|
|
87
87
|
return self._manager.running_session_count()
|
|
88
88
|
|
|
89
89
|
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
90
|
-
del context
|
|
91
90
|
cmd = str(args.get("cmd", "")).strip()
|
|
92
91
|
if not cmd:
|
|
93
92
|
return "Error: `cmd` is required."
|
|
94
93
|
|
|
95
94
|
return await self._manager.exec_command(
|
|
96
95
|
cmd=cmd,
|
|
97
|
-
|
|
96
|
+
cwd=context.resolve_path(args.get("workdir")),
|
|
98
97
|
shell=self._optional_string(args, "shell"),
|
|
99
98
|
login=bool(args.get("login", DEFAULT_LOGIN)),
|
|
100
99
|
tty=bool(args.get("tty", DEFAULT_TTY)),
|
|
@@ -53,18 +53,12 @@ class GrepFilesTool(BaseTool):
|
|
|
53
53
|
"additionalProperties": False,
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
def __init__(
|
|
57
|
-
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
58
|
-
) -> "None":
|
|
59
|
-
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
60
|
-
|
|
61
56
|
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
62
|
-
del context
|
|
63
57
|
pattern = str(args.get("pattern", "")).strip()
|
|
64
58
|
include = str(args.get("include", "")).strip() or None
|
|
65
59
|
limit = min(int(args.get("limit", DEFAULT_LIMIT)), MAX_LIMIT)
|
|
66
60
|
path_arg = args.get("path")
|
|
67
|
-
search_path =
|
|
61
|
+
search_path = context.resolve_path(path_arg)
|
|
68
62
|
|
|
69
63
|
if not pattern:
|
|
70
64
|
return "Error: `pattern` must not be empty."
|
|
@@ -88,7 +82,7 @@ class GrepFilesTool(BaseTool):
|
|
|
88
82
|
try:
|
|
89
83
|
process = await asyncio.create_subprocess_exec(
|
|
90
84
|
*command,
|
|
91
|
-
cwd=str(
|
|
85
|
+
cwd=str(context.cwd),
|
|
92
86
|
stdout=asyncio.subprocess.PIPE,
|
|
93
87
|
stderr=asyncio.subprocess.PIPE,
|
|
94
88
|
)
|
|
@@ -154,11 +148,3 @@ class GrepFilesTool(BaseTool):
|
|
|
154
148
|
|
|
155
149
|
matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
|
|
156
150
|
return [str(path) for path in matches[:limit]]
|
|
157
|
-
|
|
158
|
-
def _resolve_path(self, path_arg) -> "Path":
|
|
159
|
-
if path_arg in (None, ""):
|
|
160
|
-
return self._working_directory
|
|
161
|
-
path = Path(str(path_arg))
|
|
162
|
-
if not path.is_absolute():
|
|
163
|
-
path = self._working_directory / path
|
|
164
|
-
return path.resolve()
|
|
@@ -12,8 +12,6 @@ Expected behavior:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
-
import typing
|
|
16
|
-
from pathlib import Path
|
|
17
15
|
|
|
18
16
|
from ..protocol import JSONDict, JSONValue
|
|
19
17
|
from .base_tool import BaseTool, ToolContext
|
|
@@ -54,13 +52,7 @@ class ShellCommandTool(BaseTool):
|
|
|
54
52
|
}
|
|
55
53
|
supports_parallel = False
|
|
56
54
|
|
|
57
|
-
def __init__(
|
|
58
|
-
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
59
|
-
) -> "None":
|
|
60
|
-
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
61
|
-
|
|
62
55
|
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
63
|
-
del context
|
|
64
56
|
command = str(args.get("command", "")).strip()
|
|
65
57
|
timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
|
|
66
58
|
if not command:
|
|
@@ -68,7 +60,7 @@ class ShellCommandTool(BaseTool):
|
|
|
68
60
|
|
|
69
61
|
login = bool(args.get("login", True))
|
|
70
62
|
workdir_arg = args.get("workdir")
|
|
71
|
-
working_directory =
|
|
63
|
+
working_directory = context.resolve_path(workdir_arg)
|
|
72
64
|
shell_args = ["bash", "-lc" if login else "-c", command]
|
|
73
65
|
|
|
74
66
|
process = await asyncio.create_subprocess_exec(
|
|
@@ -111,14 +103,6 @@ class ShellCommandTool(BaseTool):
|
|
|
111
103
|
|
|
112
104
|
return "\n".join(pieces)
|
|
113
105
|
|
|
114
|
-
def _resolve_workdir(self, workdir_arg) -> "Path":
|
|
115
|
-
if workdir_arg in (None, ""):
|
|
116
|
-
return self._working_directory
|
|
117
|
-
workdir = Path(str(workdir_arg))
|
|
118
|
-
if not workdir.is_absolute():
|
|
119
|
-
workdir = self._working_directory / workdir
|
|
120
|
-
return workdir.resolve()
|
|
121
|
-
|
|
122
106
|
def _clip_output(self, text: "str") -> "str":
|
|
123
107
|
if len(text) <= MAX_OUTPUT_CHARS:
|
|
124
108
|
return text
|
|
@@ -12,8 +12,6 @@ Expected behavior:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
-
import typing
|
|
16
|
-
from pathlib import Path
|
|
17
15
|
|
|
18
16
|
from ..protocol import JSONDict, JSONValue
|
|
19
17
|
from .base_tool import BaseTool, ToolContext
|
|
@@ -51,13 +49,7 @@ class ShellTool(BaseTool):
|
|
|
51
49
|
}
|
|
52
50
|
supports_parallel = False
|
|
53
51
|
|
|
54
|
-
def __init__(
|
|
55
|
-
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
56
|
-
) -> "None":
|
|
57
|
-
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
58
|
-
|
|
59
52
|
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
60
|
-
del context
|
|
61
53
|
command = args.get("command")
|
|
62
54
|
timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
|
|
63
55
|
if not isinstance(command, list) or not command:
|
|
@@ -66,7 +58,7 @@ class ShellTool(BaseTool):
|
|
|
66
58
|
return "Error: each `command` entry must be a non-empty string."
|
|
67
59
|
|
|
68
60
|
workdir_arg = args.get("workdir")
|
|
69
|
-
working_directory =
|
|
61
|
+
working_directory = context.resolve_path(workdir_arg)
|
|
70
62
|
|
|
71
63
|
process = await asyncio.create_subprocess_exec(
|
|
72
64
|
*command,
|
|
@@ -108,14 +100,6 @@ class ShellTool(BaseTool):
|
|
|
108
100
|
|
|
109
101
|
return "\n".join(pieces)
|
|
110
102
|
|
|
111
|
-
def _resolve_workdir(self, workdir_arg) -> "Path":
|
|
112
|
-
if workdir_arg in (None, ""):
|
|
113
|
-
return self._working_directory
|
|
114
|
-
workdir = Path(str(workdir_arg))
|
|
115
|
-
if not workdir.is_absolute():
|
|
116
|
-
workdir = self._working_directory / workdir
|
|
117
|
-
return workdir.resolve()
|
|
118
|
-
|
|
119
103
|
def _clip_output(self, text: "str") -> "str":
|
|
120
104
|
if len(text) <= MAX_OUTPUT_CHARS:
|
|
121
105
|
return text
|
|
@@ -121,10 +121,7 @@ class UnifiedExecSession:
|
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
class UnifiedExecManager:
|
|
124
|
-
def __init__(
|
|
125
|
-
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
126
|
-
) -> "None":
|
|
127
|
-
self._default_cwd = Path(cwd or Path.cwd()).resolve()
|
|
124
|
+
def __init__(self) -> "None":
|
|
128
125
|
self._next_session_id = DEFAULT_SESSION_ID_START
|
|
129
126
|
self._sessions: "typing.Dict[int, UnifiedExecSession]" = {}
|
|
130
127
|
self._lock = threading.Lock()
|
|
@@ -146,7 +143,7 @@ class UnifiedExecManager:
|
|
|
146
143
|
async def exec_command(
|
|
147
144
|
self,
|
|
148
145
|
cmd: "str",
|
|
149
|
-
|
|
146
|
+
cwd: "Path",
|
|
150
147
|
shell: "typing.Union[str, None]" = None,
|
|
151
148
|
login: "bool" = DEFAULT_LOGIN,
|
|
152
149
|
tty: "bool" = DEFAULT_TTY,
|
|
@@ -155,7 +152,6 @@ class UnifiedExecManager:
|
|
|
155
152
|
) -> "str":
|
|
156
153
|
session_id = await self._allocate_session_id()
|
|
157
154
|
command = self._build_shell_command(cmd, shell, login)
|
|
158
|
-
cwd = self._resolve_workdir(workdir)
|
|
159
155
|
logger.debug(
|
|
160
156
|
"exec_command start session_id={} shell_command={} cwd={}",
|
|
161
157
|
session_id,
|
|
@@ -381,14 +377,6 @@ class UnifiedExecManager:
|
|
|
381
377
|
}
|
|
382
378
|
)
|
|
383
379
|
|
|
384
|
-
def _resolve_workdir(self, workdir: "typing.Union[str, None]") -> "Path":
|
|
385
|
-
if not workdir:
|
|
386
|
-
return self._default_cwd
|
|
387
|
-
path = Path(workdir)
|
|
388
|
-
if not path.is_absolute():
|
|
389
|
-
path = self._default_cwd / path
|
|
390
|
-
return path.resolve()
|
|
391
|
-
|
|
392
380
|
def _build_shell_command(
|
|
393
381
|
self,
|
|
394
382
|
cmd: "str",
|
|
@@ -15,9 +15,7 @@ Expected behavior:
|
|
|
15
15
|
item that Codex uses when feeding image tool output back to the model.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
import typing
|
|
19
18
|
from importlib.util import find_spec
|
|
20
|
-
from pathlib import Path
|
|
21
19
|
|
|
22
20
|
from ..protocol import JSONDict, JSONValue
|
|
23
21
|
from ..utils.image_utils import ImageProcessingError, load_image_data_url
|
|
@@ -65,13 +63,7 @@ class ViewImageTool(BaseTool):
|
|
|
65
63
|
}
|
|
66
64
|
output_schema = VIEW_IMAGE_OUTPUT_SCHEMA
|
|
67
65
|
|
|
68
|
-
def __init__(
|
|
69
|
-
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
70
|
-
) -> "None":
|
|
71
|
-
self._workspace_root = Path(cwd or Path.cwd()).resolve()
|
|
72
|
-
|
|
73
66
|
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
74
|
-
del context
|
|
75
67
|
path_value = str(args.get("path", "")).strip()
|
|
76
68
|
if not path_value:
|
|
77
69
|
return "Error: `path` is required."
|
|
@@ -88,10 +80,7 @@ class ViewImageTool(BaseTool):
|
|
|
88
80
|
f"`{detail_value}`."
|
|
89
81
|
)
|
|
90
82
|
|
|
91
|
-
path =
|
|
92
|
-
if not path.is_absolute():
|
|
93
|
-
path = self._workspace_root / path
|
|
94
|
-
path = path.resolve()
|
|
83
|
+
path = context.resolve_path(path_value)
|
|
95
84
|
if not path.exists():
|
|
96
85
|
return f"Error: unable to locate image at `{path}`."
|
|
97
86
|
if not path.is_file():
|
|
@@ -26,6 +26,8 @@ def format_command_result(result: "typing.Dict[str, object]") -> "str":
|
|
|
26
26
|
return "Extra commands: " + ", ".join("/" + name for name in result["commands"])
|
|
27
27
|
if kind in {"title", "title_changed"}:
|
|
28
28
|
return "Session: " + (result["title"] or "untitled")
|
|
29
|
+
if kind == "cwd":
|
|
30
|
+
return "Current directory: " + result["cwd"]
|
|
29
31
|
if kind == "models":
|
|
30
32
|
return (
|
|
31
33
|
"Current model: "
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-codex"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.8"
|
|
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"
|
|
@@ -40,7 +40,7 @@ test = [
|
|
|
40
40
|
]
|
|
41
41
|
dev = [
|
|
42
42
|
{ include-group = "test" },
|
|
43
|
-
"pycodex-ws==0.4.
|
|
43
|
+
"pycodex-ws==0.4.8",
|
|
44
44
|
"black>=22.8.0",
|
|
45
45
|
"debugpy>=1.8.20; python_version >= '3.8'",
|
|
46
46
|
"isort>=5.10.1",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/never.md
RENAMED
|
File without changes
|
{python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/on_failure.md
RENAMED
|
File without changes
|
{python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/approval_policy/on_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_codex-0.4.7 → python_codex-0.4.8}/pycodex/prompts/permissions/sandbox_mode/read_only.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|