python-codex 0.4.7__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycodex/agent.py +8 -0
- pycodex/bootstrap.py +23 -26
- pycodex/context.py +10 -0
- pycodex/runtime.py +11 -0
- pycodex/tools/apply_patch_tool.py +24 -26
- pycodex/tools/base_tool.py +5 -0
- pycodex/tools/code_mode_manager.py +4 -9
- pycodex/tools/exec_command_tool.py +1 -2
- pycodex/tools/grep_files_tool.py +2 -16
- pycodex/tools/shell_command_tool.py +1 -17
- pycodex/tools/shell_tool.py +1 -17
- pycodex/tools/unified_exec_manager.py +2 -14
- pycodex/tools/view_image_tool.py +1 -12
- pycodex/utils/event_helpers.py +2 -0
- python_codex-0.5.0.dist-info/METADATA +193 -0
- {python_codex-0.4.7.dist-info → python_codex-0.5.0.dist-info}/RECORD +19 -19
- python_codex-0.4.7.dist-info/METADATA +0 -905
- {python_codex-0.4.7.dist-info → python_codex-0.5.0.dist-info}/WHEEL +0 -0
- {python_codex-0.4.7.dist-info → python_codex-0.5.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.4.7.dist-info → python_codex-0.5.0.dist-info}/licenses/LICENSE +0 -0
pycodex/agent.py
CHANGED
|
@@ -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))
|
pycodex/bootstrap.py
CHANGED
|
@@ -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(
|
pycodex/context.py
CHANGED
|
@@ -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
|
pycodex/runtime.py
CHANGED
|
@@ -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
|
|
pycodex/tools/base_tool.py
CHANGED
|
@@ -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)),
|
pycodex/tools/grep_files_tool.py
CHANGED
|
@@ -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
|
pycodex/tools/shell_tool.py
CHANGED
|
@@ -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",
|
pycodex/tools/view_image_tool.py
CHANGED
|
@@ -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():
|
pycodex/utils/event_helpers.py
CHANGED
|
@@ -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: "
|