python-codex 0.2.7__py3-none-any.whl → 0.3.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/__init__.py +14 -14
- pycodex/agent.py +465 -499
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -510
- pycodex/compat.py +19 -5
- pycodex/context.py +222 -212
- pycodex/doctor.py +52 -48
- pycodex/events.py +857 -0
- pycodex/feishu_card.py +217 -163
- pycodex/feishu_link.py +43 -83
- pycodex/model.py +324 -253
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +76 -45
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +245 -983
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -176
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +1 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +58 -25
- pycodex/tools/close_agent_tool.py +2 -2
- pycodex/tools/code_mode_manager.py +77 -64
- pycodex/tools/exec_command_tool.py +26 -11
- pycodex/tools/exec_tool.py +4 -4
- pycodex/tools/grep_files_tool.py +12 -10
- pycodex/tools/ipython_tool.py +10 -13
- pycodex/tools/list_dir_tool.py +13 -9
- pycodex/tools/read_file_tool.py +29 -17
- pycodex/tools/request_permissions_tool.py +15 -5
- pycodex/tools/request_user_input_tool.py +13 -104
- pycodex/tools/resume_agent_tool.py +2 -2
- pycodex/tools/send_input_tool.py +11 -8
- pycodex/tools/shell_command_tool.py +7 -5
- pycodex/tools/shell_tool.py +7 -5
- pycodex/tools/spawn_agent_tool.py +7 -4
- pycodex/tools/unified_exec_manager.py +102 -69
- pycodex/tools/update_plan_tool.py +8 -5
- pycodex/tools/view_image_tool.py +7 -5
- pycodex/tools/wait_agent_tool.py +27 -4
- pycodex/tools/wait_tool.py +5 -4
- pycodex/tools/web_search_tool.py +4 -2
- pycodex/tools/write_stdin_tool.py +12 -11
- pycodex/utils/__init__.py +2 -17
- pycodex/utils/compactor.py +41 -72
- pycodex/utils/debug.py +2 -2
- pycodex/utils/dotenv.py +6 -7
- pycodex/utils/event_helpers.py +190 -0
- pycodex/utils/get_env.py +27 -70
- pycodex/{image_utils.py → utils/image_utils.py} +8 -11
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +217 -163
- pycodex/utils/truncation.py +21 -45
- python_codex-0.3.0.dist-info/METADATA +704 -0
- python_codex-0.3.0.dist-info/RECORD +90 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +36 -31
- responses_server/config.py +23 -23
- responses_server/messages_api.py +51 -53
- responses_server/payload_processors.py +25 -20
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +101 -98
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +36 -14
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +461 -375
- workspace_server/workspace.html +852 -228
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +137 -79
- pycodex/collaboration.py +0 -20
- pycodex/interactive_session.py +0 -415
- pycodex/prompts/collaboration_default.md +0 -11
- pycodex/prompts/collaboration_plan.md +0 -128
- pycodex/utils/toolcall_visualize.py +0 -713
- pycodex/utils/visualize.py +0 -560
- python_codex-0.2.7.dist-info/METADATA +0 -455
- python_codex-0.2.7.dist-info/RECORD +0 -93
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,6 +13,8 @@ Expected behavior:
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import json
|
|
15
15
|
import math
|
|
16
|
+
import threading
|
|
17
|
+
import typing
|
|
16
18
|
import uuid
|
|
17
19
|
from dataclasses import dataclass, field
|
|
18
20
|
from pathlib import Path
|
|
@@ -23,7 +25,6 @@ from ..compat import is_ascii, stream_writer_is_closing
|
|
|
23
25
|
from ..protocol import JSONDict, JSONValue, ToolCall
|
|
24
26
|
from ..utils.truncation import DEFAULT_MAX_OUTPUT_TOKENS
|
|
25
27
|
from .base_tool import StructuredToolOutput, ToolContext, ToolRegistry
|
|
26
|
-
import typing
|
|
27
28
|
|
|
28
29
|
DEFAULT_WAIT_YIELD_TIME_MS = 10_000
|
|
29
30
|
CHARS_PER_TOKEN = 4
|
|
@@ -33,39 +34,47 @@ WAIT_COMPLETION_GRACE_SECONDS = 0.02
|
|
|
33
34
|
|
|
34
35
|
@dataclass
|
|
35
36
|
class ExecCell:
|
|
36
|
-
cell_id:
|
|
37
|
-
process:
|
|
38
|
-
started_at:
|
|
39
|
-
output_items:
|
|
40
|
-
delivered_count:
|
|
41
|
-
reader_task:
|
|
42
|
-
stderr_task:
|
|
43
|
-
yield_event:
|
|
44
|
-
output_event:
|
|
45
|
-
done_event:
|
|
46
|
-
completed:
|
|
47
|
-
terminated:
|
|
48
|
-
error_text:
|
|
49
|
-
stderr_chunks:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass(
|
|
37
|
+
cell_id: "str"
|
|
38
|
+
process: "asyncio.subprocess.Process"
|
|
39
|
+
started_at: "float"
|
|
40
|
+
output_items: "typing.List[JSONDict]" = field(default_factory=list)
|
|
41
|
+
delivered_count: "int" = 0
|
|
42
|
+
reader_task: "typing.Union[asyncio.Task, None]" = None
|
|
43
|
+
stderr_task: "typing.Union[asyncio.Task, None]" = None
|
|
44
|
+
yield_event: "asyncio.Event" = field(default_factory=asyncio.Event)
|
|
45
|
+
output_event: "asyncio.Event" = field(default_factory=asyncio.Event)
|
|
46
|
+
done_event: "asyncio.Event" = field(default_factory=asyncio.Event)
|
|
47
|
+
completed: "bool" = False
|
|
48
|
+
terminated: "bool" = False
|
|
49
|
+
error_text: "typing.Union[str, None]" = None
|
|
50
|
+
stderr_chunks: "typing.List[str]" = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(
|
|
54
|
+
frozen=True,
|
|
55
|
+
)
|
|
53
56
|
class ParsedExecSource:
|
|
54
|
-
code:
|
|
55
|
-
yield_time_ms:
|
|
56
|
-
max_output_tokens:
|
|
57
|
+
code: "str"
|
|
58
|
+
yield_time_ms: "typing.Union[int, None]"
|
|
59
|
+
max_output_tokens: "typing.Union[int, None]"
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
class CodeModeManager:
|
|
60
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
registry: "ToolRegistry",
|
|
66
|
+
cwd: "typing.Union[typing.Union[str, Path], None]" = None,
|
|
67
|
+
) -> "None":
|
|
61
68
|
self._registry = registry
|
|
62
69
|
self._default_cwd = Path(cwd or Path.cwd()).resolve()
|
|
63
70
|
self._runtime_script = Path(__file__).with_name("exec_runtime.js")
|
|
64
|
-
self._stored_values:
|
|
65
|
-
self._cells:
|
|
66
|
-
self._lock =
|
|
71
|
+
self._stored_values: "typing.Dict[str, JSONValue]" = {}
|
|
72
|
+
self._cells: "typing.Dict[str, ExecCell]" = {}
|
|
73
|
+
self._lock = threading.Lock()
|
|
67
74
|
|
|
68
|
-
async def exec(
|
|
75
|
+
async def exec(
|
|
76
|
+
self, source: "str", context: "ToolContext"
|
|
77
|
+
) -> "typing.Union[StructuredToolOutput, str]":
|
|
69
78
|
try:
|
|
70
79
|
parsed = self._parse_exec_source(source)
|
|
71
80
|
except ValueError as exc:
|
|
@@ -77,11 +86,11 @@ class CodeModeManager:
|
|
|
77
86
|
|
|
78
87
|
async def wait(
|
|
79
88
|
self,
|
|
80
|
-
cell_id:
|
|
81
|
-
yield_time_ms:
|
|
82
|
-
max_tokens:
|
|
83
|
-
terminate:
|
|
84
|
-
) ->
|
|
89
|
+
cell_id: "str",
|
|
90
|
+
yield_time_ms: "int",
|
|
91
|
+
max_tokens: "typing.Union[int, None]",
|
|
92
|
+
terminate: "bool",
|
|
93
|
+
) -> "typing.Union[StructuredToolOutput, str]":
|
|
85
94
|
cell = self._cells.get(cell_id)
|
|
86
95
|
if cell is None:
|
|
87
96
|
return f"Error: unknown exec cell `{cell_id}`."
|
|
@@ -98,8 +107,8 @@ class CodeModeManager:
|
|
|
98
107
|
await self._wait_for_wait(cell, yield_time_ms)
|
|
99
108
|
return await self._snapshot_cell(cell, max_tokens)
|
|
100
109
|
|
|
101
|
-
def enabled_tools(self) ->
|
|
102
|
-
enabled:
|
|
110
|
+
def enabled_tools(self) -> "typing.List[typing.Dict[str, str]]":
|
|
111
|
+
enabled: "typing.List[typing.Dict[str, str]]" = []
|
|
103
112
|
for tool in self._registry.tools():
|
|
104
113
|
if tool.name in {"exec", "wait"}:
|
|
105
114
|
continue
|
|
@@ -116,7 +125,7 @@ class CodeModeManager:
|
|
|
116
125
|
enabled.sort(key=lambda item: item["tool_name"])
|
|
117
126
|
return enabled
|
|
118
127
|
|
|
119
|
-
async def _start_cell(self, code:
|
|
128
|
+
async def _start_cell(self, code: "str", context: "ToolContext") -> "ExecCell":
|
|
120
129
|
cell_id = uuid.uuid4().hex[:10]
|
|
121
130
|
process = await asyncio.create_subprocess_exec(
|
|
122
131
|
"node",
|
|
@@ -147,7 +156,7 @@ class CodeModeManager:
|
|
|
147
156
|
logger.debug("exec start cell_id={} cwd={}", cell_id, self._default_cwd)
|
|
148
157
|
return cell
|
|
149
158
|
|
|
150
|
-
async def _read_stdout(self, cell:
|
|
159
|
+
async def _read_stdout(self, cell: "ExecCell", context: "ToolContext") -> "None":
|
|
151
160
|
stream = cell.process.stdout
|
|
152
161
|
if stream is None:
|
|
153
162
|
cell.error_text = "missing stdout pipe"
|
|
@@ -181,7 +190,7 @@ class CodeModeManager:
|
|
|
181
190
|
cell.output_event.set()
|
|
182
191
|
continue
|
|
183
192
|
if msg_type == "output_image":
|
|
184
|
-
image_item:
|
|
193
|
+
image_item: "JSONDict" = {
|
|
185
194
|
"type": "input_image",
|
|
186
195
|
"image_url": str(message.get("image_url", "")),
|
|
187
196
|
}
|
|
@@ -202,7 +211,7 @@ class CodeModeManager:
|
|
|
202
211
|
cell.error_text = self._coerce_optional_text(message.get("error_text"))
|
|
203
212
|
stored_values = message.get("stored_values")
|
|
204
213
|
if isinstance(stored_values, dict):
|
|
205
|
-
|
|
214
|
+
with self._lock:
|
|
206
215
|
self._stored_values = stored_values
|
|
207
216
|
cell.done_event.set()
|
|
208
217
|
cell.output_event.set()
|
|
@@ -220,7 +229,7 @@ class CodeModeManager:
|
|
|
220
229
|
cell.done_event.set()
|
|
221
230
|
cell.output_event.set()
|
|
222
231
|
|
|
223
|
-
async def _read_stderr(self, cell:
|
|
232
|
+
async def _read_stderr(self, cell: "ExecCell") -> "None":
|
|
224
233
|
stream = cell.process.stderr
|
|
225
234
|
if stream is None:
|
|
226
235
|
return
|
|
@@ -232,10 +241,10 @@ class CodeModeManager:
|
|
|
232
241
|
|
|
233
242
|
async def _handle_nested_tool_call(
|
|
234
243
|
self,
|
|
235
|
-
cell:
|
|
236
|
-
context:
|
|
237
|
-
message:
|
|
238
|
-
) ->
|
|
244
|
+
cell: "ExecCell",
|
|
245
|
+
context: "ToolContext",
|
|
246
|
+
message: "JSONDict",
|
|
247
|
+
) -> "None":
|
|
239
248
|
tool_name = str(message.get("tool_name", ""))
|
|
240
249
|
request_id = str(message.get("id", ""))
|
|
241
250
|
tool = self._registry.get_tool(tool_name)
|
|
@@ -287,14 +296,16 @@ class CodeModeManager:
|
|
|
287
296
|
}
|
|
288
297
|
await self._send_message(cell, payload)
|
|
289
298
|
|
|
290
|
-
async def _send_message(self, cell:
|
|
299
|
+
async def _send_message(self, cell: "ExecCell", payload: "JSONDict") -> "None":
|
|
291
300
|
stdin = cell.process.stdin
|
|
292
301
|
if stdin is None or stream_writer_is_closing(stdin):
|
|
293
302
|
return
|
|
294
303
|
stdin.write((json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
295
304
|
await stdin.drain()
|
|
296
305
|
|
|
297
|
-
async def _wait_for_exec(
|
|
306
|
+
async def _wait_for_exec(
|
|
307
|
+
self, cell: "ExecCell", yield_time_ms: "typing.Union[int, None]"
|
|
308
|
+
) -> "None":
|
|
298
309
|
done_task = asyncio.create_task(cell.done_event.wait())
|
|
299
310
|
yield_task = asyncio.create_task(cell.yield_event.wait())
|
|
300
311
|
tasks = {done_task, yield_task}
|
|
@@ -314,7 +325,7 @@ class CodeModeManager:
|
|
|
314
325
|
task.cancel()
|
|
315
326
|
cell.yield_event.clear()
|
|
316
327
|
|
|
317
|
-
async def _wait_for_wait(self, cell:
|
|
328
|
+
async def _wait_for_wait(self, cell: "ExecCell", yield_time_ms: "int") -> "None":
|
|
318
329
|
loop = asyncio.get_running_loop()
|
|
319
330
|
deadline = loop.time() + max(yield_time_ms, 1) / 1000.0
|
|
320
331
|
initial_count = cell.delivered_count
|
|
@@ -357,9 +368,9 @@ class CodeModeManager:
|
|
|
357
368
|
|
|
358
369
|
async def _wait_for_completion_grace(
|
|
359
370
|
self,
|
|
360
|
-
cell:
|
|
361
|
-
timeout_seconds:
|
|
362
|
-
) ->
|
|
371
|
+
cell: "ExecCell",
|
|
372
|
+
timeout_seconds: "float",
|
|
373
|
+
) -> "None":
|
|
363
374
|
if timeout_seconds <= 0:
|
|
364
375
|
return
|
|
365
376
|
done_task = asyncio.create_task(cell.done_event.wait())
|
|
@@ -378,9 +389,9 @@ class CodeModeManager:
|
|
|
378
389
|
|
|
379
390
|
async def _snapshot_cell(
|
|
380
391
|
self,
|
|
381
|
-
cell:
|
|
382
|
-
max_tokens:
|
|
383
|
-
) ->
|
|
392
|
+
cell: "ExecCell",
|
|
393
|
+
max_tokens: "typing.Union[int, None]",
|
|
394
|
+
) -> "StructuredToolOutput":
|
|
384
395
|
if cell.process.returncode is not None and cell.reader_task is not None:
|
|
385
396
|
await cell.reader_task
|
|
386
397
|
|
|
@@ -417,17 +428,19 @@ class CodeModeManager:
|
|
|
417
428
|
if cell.done_event.is_set():
|
|
418
429
|
self._cells.pop(cell.cell_id, None)
|
|
419
430
|
|
|
420
|
-
return StructuredToolOutput(
|
|
431
|
+
return StructuredToolOutput(
|
|
432
|
+
output=output_text, content_items=tuple(content_items)
|
|
433
|
+
)
|
|
421
434
|
|
|
422
435
|
def _truncate_content_items(
|
|
423
436
|
self,
|
|
424
|
-
items:
|
|
425
|
-
max_tokens:
|
|
426
|
-
) ->
|
|
437
|
+
items: "typing.List[JSONDict]",
|
|
438
|
+
max_tokens: "typing.Union[int, None]",
|
|
439
|
+
) -> "typing.List[JSONDict]":
|
|
427
440
|
token_budget = DEFAULT_MAX_OUTPUT_TOKENS if max_tokens is None else max_tokens
|
|
428
441
|
max_chars = max(1, token_budget) * CHARS_PER_TOKEN
|
|
429
442
|
total_chars = 0
|
|
430
|
-
truncated:
|
|
443
|
+
truncated: "typing.List[JSONDict]" = []
|
|
431
444
|
for item in items:
|
|
432
445
|
if item.get("type") != "input_text":
|
|
433
446
|
truncated.append(item)
|
|
@@ -449,7 +462,7 @@ class CodeModeManager:
|
|
|
449
462
|
total_chars += len(text)
|
|
450
463
|
return truncated
|
|
451
464
|
|
|
452
|
-
def _status_text(self, cell:
|
|
465
|
+
def _status_text(self, cell: "ExecCell") -> "str":
|
|
453
466
|
if cell.terminated:
|
|
454
467
|
return "Script terminated"
|
|
455
468
|
if not cell.done_event.is_set():
|
|
@@ -458,11 +471,9 @@ class CodeModeManager:
|
|
|
458
471
|
return "Script failed"
|
|
459
472
|
return "Script completed"
|
|
460
473
|
|
|
461
|
-
def _parse_exec_source(self, input_text:
|
|
474
|
+
def _parse_exec_source(self, input_text: "str") -> "ParsedExecSource":
|
|
462
475
|
if not input_text.strip():
|
|
463
|
-
raise ValueError(
|
|
464
|
-
"exec expects raw JavaScript source text (non-empty)."
|
|
465
|
-
)
|
|
476
|
+
raise ValueError("exec expects raw JavaScript source text (non-empty).")
|
|
466
477
|
code = input_text
|
|
467
478
|
yield_time_ms = None
|
|
468
479
|
max_output_tokens = None
|
|
@@ -499,13 +510,15 @@ class CodeModeManager:
|
|
|
499
510
|
max_output_tokens=max_output_tokens,
|
|
500
511
|
)
|
|
501
512
|
|
|
502
|
-
def _normalize_identifier(self, tool_name:
|
|
513
|
+
def _normalize_identifier(self, tool_name: "str") -> "str":
|
|
503
514
|
identifier = []
|
|
504
515
|
for index, char in enumerate(tool_name):
|
|
505
516
|
is_valid = (
|
|
506
517
|
char == "_"
|
|
507
518
|
or char == "$"
|
|
508
|
-
or (
|
|
519
|
+
or (
|
|
520
|
+
is_ascii(char) and char.isalnum() and (index != 0 or char.isalpha())
|
|
521
|
+
)
|
|
509
522
|
)
|
|
510
523
|
if is_valid:
|
|
511
524
|
identifier.append(char)
|
|
@@ -513,7 +526,7 @@ class CodeModeManager:
|
|
|
513
526
|
identifier.append("_")
|
|
514
527
|
return "".join(identifier) or "_"
|
|
515
528
|
|
|
516
|
-
def _coerce_optional_text(self, value:
|
|
529
|
+
def _coerce_optional_text(self, value: "JSONValue") -> "typing.Union[str, None]":
|
|
517
530
|
if value in (None, ""):
|
|
518
531
|
return None
|
|
519
532
|
return str(value)
|
|
@@ -10,6 +10,8 @@ Expected behavior:
|
|
|
10
10
|
`write_stdin`.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import typing
|
|
14
|
+
|
|
13
15
|
from ..protocol import JSONDict, JSONValue
|
|
14
16
|
from .base_tool import BaseTool, ToolContext
|
|
15
17
|
from .unified_exec_manager import (
|
|
@@ -19,7 +21,9 @@ from .unified_exec_manager import (
|
|
|
19
21
|
UNIFIED_EXEC_OUTPUT_SCHEMA,
|
|
20
22
|
UnifiedExecManager,
|
|
21
23
|
)
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
if typing.TYPE_CHECKING:
|
|
26
|
+
from ..agent import Agent
|
|
23
27
|
|
|
24
28
|
MIN_EXEC_YIELD_TIME_MS = 250
|
|
25
29
|
MAX_EXEC_YIELD_TIME_MS = 30_000
|
|
@@ -70,10 +74,19 @@ class ExecCommandTool(BaseTool):
|
|
|
70
74
|
output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
|
|
71
75
|
supports_parallel = False
|
|
72
76
|
|
|
73
|
-
def __init__(self, manager:
|
|
77
|
+
def __init__(self, manager: "UnifiedExecManager") -> "None":
|
|
74
78
|
self._manager = manager
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
def bind_agent(self, agent: "Agent") -> "None":
|
|
81
|
+
self._manager.set_notify_hook(agent.maybe_invoke)
|
|
82
|
+
|
|
83
|
+
def shutdown(self) -> "None":
|
|
84
|
+
self._manager.set_notify_hook(None)
|
|
85
|
+
|
|
86
|
+
def background_work_count(self, after_reply: "bool") -> "int":
|
|
87
|
+
return self._manager.running_session_count()
|
|
88
|
+
|
|
89
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
77
90
|
del context
|
|
78
91
|
cmd = str(args.get("cmd", "")).strip()
|
|
79
92
|
if not cmd:
|
|
@@ -95,13 +108,15 @@ class ExecCommandTool(BaseTool):
|
|
|
95
108
|
max_output_tokens=self._optional_int(args, "max_output_tokens"),
|
|
96
109
|
)
|
|
97
110
|
|
|
98
|
-
def _optional_string(
|
|
111
|
+
def _optional_string(
|
|
112
|
+
self, args: "JSONDict", key: "str"
|
|
113
|
+
) -> "typing.Union[str, None]":
|
|
99
114
|
value = args.get(key)
|
|
100
115
|
if value in (None, ""):
|
|
101
116
|
return None
|
|
102
117
|
return str(value)
|
|
103
118
|
|
|
104
|
-
def _optional_int(self, args:
|
|
119
|
+
def _optional_int(self, args: "JSONDict", key: "str") -> "typing.Union[int, None]":
|
|
105
120
|
value = args.get(key)
|
|
106
121
|
if value in (None, ""):
|
|
107
122
|
return None
|
|
@@ -109,11 +124,11 @@ class ExecCommandTool(BaseTool):
|
|
|
109
124
|
|
|
110
125
|
def _bounded_int(
|
|
111
126
|
self,
|
|
112
|
-
args:
|
|
113
|
-
key:
|
|
114
|
-
default:
|
|
115
|
-
minimum:
|
|
116
|
-
maximum:
|
|
117
|
-
) ->
|
|
127
|
+
args: "JSONDict",
|
|
128
|
+
key: "str",
|
|
129
|
+
default: "int",
|
|
130
|
+
minimum: "int",
|
|
131
|
+
maximum: "int",
|
|
132
|
+
) -> "int":
|
|
118
133
|
value = int(args.get(key, default))
|
|
119
134
|
return min(max(value, minimum), maximum)
|
pycodex/tools/exec_tool.py
CHANGED
|
@@ -40,8 +40,8 @@ class ExecTool(BaseTool):
|
|
|
40
40
|
"- Accepts raw JavaScript source text, not JSON, quoted strings, or "
|
|
41
41
|
"markdown code fences.\n"
|
|
42
42
|
"- You may optionally start the tool input with a first-line pragma "
|
|
43
|
-
|
|
44
|
-
"
|
|
43
|
+
'like `// @exec: {"yield_time_ms": 10000, '
|
|
44
|
+
'"max_output_tokens": 1000}`.\n'
|
|
45
45
|
"- `yield_time_ms` asks `exec` to yield early if the script is still "
|
|
46
46
|
"running. Defaults to 10000 ms.\n"
|
|
47
47
|
"- `max_output_tokens` sets the token budget for direct `exec` results. "
|
|
@@ -55,8 +55,8 @@ class ExecTool(BaseTool):
|
|
|
55
55
|
}
|
|
56
56
|
supports_parallel = False
|
|
57
57
|
|
|
58
|
-
def __init__(self, manager:
|
|
58
|
+
def __init__(self, manager: "CodeModeManager") -> "None":
|
|
59
59
|
self._manager = manager
|
|
60
60
|
|
|
61
|
-
async def run(self, context:
|
|
61
|
+
async def run(self, context: "ToolContext", args: "JSONValue") -> "JSONValue":
|
|
62
62
|
return await self._manager.exec(str(args), context)
|
pycodex/tools/grep_files_tool.py
CHANGED
|
@@ -12,11 +12,11 @@ Expected behavior:
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import fnmatch
|
|
14
14
|
import re
|
|
15
|
+
import typing
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
from ..protocol import JSONDict, JSONValue
|
|
18
19
|
from .base_tool import BaseTool, ToolContext
|
|
19
|
-
import typing
|
|
20
20
|
|
|
21
21
|
DEFAULT_LIMIT = 100
|
|
22
22
|
MAX_LIMIT = 2000
|
|
@@ -53,10 +53,12 @@ class GrepFilesTool(BaseTool):
|
|
|
53
53
|
"additionalProperties": False,
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
58
|
+
) -> "None":
|
|
57
59
|
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
58
60
|
|
|
59
|
-
async def run(self, context:
|
|
61
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
60
62
|
del context
|
|
61
63
|
pattern = str(args.get("pattern", "")).strip()
|
|
62
64
|
include = str(args.get("include", "")).strip() or None
|
|
@@ -123,13 +125,13 @@ class GrepFilesTool(BaseTool):
|
|
|
123
125
|
|
|
124
126
|
def _search_with_python(
|
|
125
127
|
self,
|
|
126
|
-
pattern:
|
|
127
|
-
include:
|
|
128
|
-
search_path:
|
|
129
|
-
limit:
|
|
130
|
-
) ->
|
|
128
|
+
pattern: "str",
|
|
129
|
+
include: "typing.Union[str, None]",
|
|
130
|
+
search_path: "Path",
|
|
131
|
+
limit: "int",
|
|
132
|
+
) -> "typing.List[str]":
|
|
131
133
|
regex = re.compile(pattern)
|
|
132
|
-
candidates:
|
|
134
|
+
candidates: "typing.List[Path]" = []
|
|
133
135
|
|
|
134
136
|
if search_path.is_file():
|
|
135
137
|
candidates = [search_path]
|
|
@@ -153,7 +155,7 @@ class GrepFilesTool(BaseTool):
|
|
|
153
155
|
matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
|
|
154
156
|
return [str(path) for path in matches[:limit]]
|
|
155
157
|
|
|
156
|
-
def _resolve_path(self, path_arg) ->
|
|
158
|
+
def _resolve_path(self, path_arg) -> "Path":
|
|
157
159
|
if path_arg in (None, ""):
|
|
158
160
|
return self._working_directory
|
|
159
161
|
path = Path(str(path_arg))
|
pycodex/tools/ipython_tool.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Optional tool that executes code in the current IPython kernel."""
|
|
2
2
|
|
|
3
|
-
from ..
|
|
3
|
+
from ..events import ToolCompletedEvent
|
|
4
|
+
from ..utils.event_helpers import colorize_tool_message
|
|
4
5
|
from .base_tool import BaseTool, ToolContext
|
|
5
6
|
|
|
6
7
|
|
|
@@ -28,7 +29,7 @@ class IPythonTool(BaseTool):
|
|
|
28
29
|
self._shell = shell
|
|
29
30
|
self.name = name
|
|
30
31
|
|
|
31
|
-
async def run(self, context:
|
|
32
|
+
async def run(self, context: "ToolContext", args):
|
|
32
33
|
del context
|
|
33
34
|
if not isinstance(args, dict):
|
|
34
35
|
return {"error": "arguments must be an object"}
|
|
@@ -55,9 +56,7 @@ class IPythonTool(BaseTool):
|
|
|
55
56
|
data = getattr(item, "data", None)
|
|
56
57
|
if isinstance(data, dict):
|
|
57
58
|
display_outputs.append(
|
|
58
|
-
data.get("text/plain")
|
|
59
|
-
or data.get("text/html")
|
|
60
|
-
or repr(data)
|
|
59
|
+
data.get("text/plain") or data.get("text/html") or repr(data)
|
|
61
60
|
)
|
|
62
61
|
else:
|
|
63
62
|
display_outputs.append(repr(item))
|
|
@@ -106,9 +105,7 @@ def _install_agent_shortcut(shell):
|
|
|
106
105
|
if not prompt.strip():
|
|
107
106
|
return [f"{indent}print('Usage: @{agent_name} <prompt>')\n"]
|
|
108
107
|
|
|
109
|
-
return [
|
|
110
|
-
f"{indent}print({agent_name}.ask({prompt!r}).output_text)\n"
|
|
111
|
-
]
|
|
108
|
+
return [f"{indent}print({agent_name}.ask({prompt!r}).output_text)\n"]
|
|
112
109
|
|
|
113
110
|
shell.input_transformers_cleanup.append(transform)
|
|
114
111
|
shell.user_ns["_pycodex_agent_shortcut_transform"] = transform
|
|
@@ -117,13 +114,13 @@ def _install_agent_shortcut(shell):
|
|
|
117
114
|
|
|
118
115
|
def attach_ipython_event_printer(agent, color=True):
|
|
119
116
|
def handle_event(event):
|
|
120
|
-
if event
|
|
121
|
-
tool_name =
|
|
122
|
-
message =
|
|
117
|
+
if isinstance(event, ToolCompletedEvent):
|
|
118
|
+
tool_name = event.call.name
|
|
119
|
+
message = event.visualize()
|
|
123
120
|
for line in message.splitlines() or [""]:
|
|
124
121
|
print(colorize_tool_message(line, color, tool_name), flush=True)
|
|
125
122
|
|
|
126
|
-
agent.
|
|
123
|
+
agent.event_handler = handle_event
|
|
127
124
|
return handle_event
|
|
128
125
|
|
|
129
126
|
|
|
@@ -137,7 +134,7 @@ def attach_ipython_tool(
|
|
|
137
134
|
raise RuntimeError("not running inside IPython")
|
|
138
135
|
|
|
139
136
|
tool = IPythonTool(shell, name=name)
|
|
140
|
-
agent.
|
|
137
|
+
agent.tool_registry.register(tool)
|
|
141
138
|
if shortcut:
|
|
142
139
|
_install_agent_shortcut(shell)
|
|
143
140
|
if print_tool_events:
|
pycodex/tools/list_dir_tool.py
CHANGED
|
@@ -9,12 +9,12 @@ Expected behavior:
|
|
|
9
9
|
commands like `find` or `ls -R`.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import typing
|
|
12
13
|
from collections import deque
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
from ..protocol import JSONDict, JSONValue
|
|
16
17
|
from .base_tool import BaseTool, ToolContext
|
|
17
|
-
import typing
|
|
18
18
|
|
|
19
19
|
MAX_ENTRY_LENGTH = 500
|
|
20
20
|
INDENTATION_SPACES = 2
|
|
@@ -50,7 +50,7 @@ class ListDirTool(BaseTool):
|
|
|
50
50
|
"additionalProperties": False,
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async def run(self, context:
|
|
53
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
54
54
|
del context
|
|
55
55
|
dir_path = Path(str(args.get("dir_path", "")))
|
|
56
56
|
offset = int(args.get("offset", 1))
|
|
@@ -86,15 +86,19 @@ class ListDirTool(BaseTool):
|
|
|
86
86
|
lines.append(f"More than {len(selected)} entries found")
|
|
87
87
|
return "\n".join(lines)
|
|
88
88
|
|
|
89
|
-
def _collect_entries(
|
|
90
|
-
|
|
89
|
+
def _collect_entries(
|
|
90
|
+
self, root: "Path", depth: "int"
|
|
91
|
+
) -> "typing.List[typing.Dict[str, object]]":
|
|
92
|
+
entries: "typing.List[typing.Dict[str, object]]" = []
|
|
91
93
|
queue = deque([(root, Path(), depth)])
|
|
92
94
|
|
|
93
95
|
while queue:
|
|
94
96
|
current_dir, prefix, remaining_depth = queue.popleft()
|
|
95
97
|
dir_entries = []
|
|
96
98
|
for child in current_dir.iterdir():
|
|
97
|
-
relative_path =
|
|
99
|
+
relative_path = (
|
|
100
|
+
prefix / child.name if prefix.parts else Path(child.name)
|
|
101
|
+
)
|
|
98
102
|
kind = self._entry_kind(child)
|
|
99
103
|
dir_entries.append(
|
|
100
104
|
(
|
|
@@ -118,7 +122,7 @@ class ListDirTool(BaseTool):
|
|
|
118
122
|
entries.sort(key=lambda entry: entry["name"])
|
|
119
123
|
return entries
|
|
120
124
|
|
|
121
|
-
def _entry_kind(self, path:
|
|
125
|
+
def _entry_kind(self, path: "Path") -> "str":
|
|
122
126
|
if path.is_symlink():
|
|
123
127
|
return "symlink"
|
|
124
128
|
if path.is_dir():
|
|
@@ -127,14 +131,14 @@ class ListDirTool(BaseTool):
|
|
|
127
131
|
return "file"
|
|
128
132
|
return "other"
|
|
129
133
|
|
|
130
|
-
def _format_entry_name(self, path:
|
|
134
|
+
def _format_entry_name(self, path: "Path") -> "str":
|
|
131
135
|
text = path.as_posix()
|
|
132
136
|
return text[:MAX_ENTRY_LENGTH]
|
|
133
137
|
|
|
134
|
-
def _format_component(self, name:
|
|
138
|
+
def _format_component(self, name: "str") -> "str":
|
|
135
139
|
return name[:MAX_ENTRY_LENGTH]
|
|
136
140
|
|
|
137
|
-
def _format_entry_line(self, entry:
|
|
141
|
+
def _format_entry_line(self, entry: "typing.Dict[str, object]") -> "str":
|
|
138
142
|
indent = " " * (int(entry["depth"]) * INDENTATION_SPACES)
|
|
139
143
|
name = str(entry["display_name"])
|
|
140
144
|
kind = str(entry["kind"])
|