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
|
@@ -11,6 +11,7 @@ Expected behavior:
|
|
|
11
11
|
same success/error text shape Codex expects.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import typing
|
|
14
15
|
from dataclasses import dataclass
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
@@ -18,7 +19,6 @@ from loguru import logger
|
|
|
18
19
|
|
|
19
20
|
from ..protocol import JSONValue
|
|
20
21
|
from .base_tool import BaseTool, ToolContext
|
|
21
|
-
import typing
|
|
22
22
|
|
|
23
23
|
APPLY_PATCH_LARK_GRAMMAR = """start: begin_patch hunk+ end_patch
|
|
24
24
|
begin_patch: \"*** Begin Patch\" LF
|
|
@@ -46,28 +46,36 @@ class ApplyPatchError(RuntimeError):
|
|
|
46
46
|
pass
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
@dataclass(
|
|
49
|
+
@dataclass(
|
|
50
|
+
frozen=True,
|
|
51
|
+
)
|
|
50
52
|
class _AddFileOp:
|
|
51
|
-
path:
|
|
52
|
-
content:
|
|
53
|
+
path: "str"
|
|
54
|
+
content: "str"
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
@dataclass(
|
|
57
|
+
@dataclass(
|
|
58
|
+
frozen=True,
|
|
59
|
+
)
|
|
56
60
|
class _DeleteFileOp:
|
|
57
|
-
path:
|
|
61
|
+
path: "str"
|
|
58
62
|
|
|
59
63
|
|
|
60
|
-
@dataclass(
|
|
64
|
+
@dataclass(
|
|
65
|
+
frozen=True,
|
|
66
|
+
)
|
|
61
67
|
class _UpdateSection:
|
|
62
|
-
lines:
|
|
63
|
-
anchor_end_of_file:
|
|
68
|
+
lines: "typing.Tuple[str, ...]"
|
|
69
|
+
anchor_end_of_file: "bool" = False
|
|
64
70
|
|
|
65
71
|
|
|
66
|
-
@dataclass(
|
|
72
|
+
@dataclass(
|
|
73
|
+
frozen=True,
|
|
74
|
+
)
|
|
67
75
|
class _UpdateFileOp:
|
|
68
|
-
path:
|
|
69
|
-
move_to:
|
|
70
|
-
sections:
|
|
76
|
+
path: "str"
|
|
77
|
+
move_to: "typing.Union[str, None]"
|
|
78
|
+
sections: "typing.Tuple[_UpdateSection, ...]"
|
|
71
79
|
|
|
72
80
|
|
|
73
81
|
class ApplyPatchTool(BaseTool):
|
|
@@ -84,20 +92,26 @@ class ApplyPatchTool(BaseTool):
|
|
|
84
92
|
}
|
|
85
93
|
supports_parallel = False
|
|
86
94
|
|
|
87
|
-
def __init__(
|
|
95
|
+
def __init__(
|
|
96
|
+
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
97
|
+
) -> "None":
|
|
88
98
|
self._workspace_root = Path(cwd or Path.cwd()).resolve()
|
|
89
99
|
|
|
90
|
-
async def run(self, context:
|
|
100
|
+
async def run(self, context: "ToolContext", args: "JSONValue") -> "JSONValue":
|
|
91
101
|
del context
|
|
92
102
|
patch_text = str(args)
|
|
93
|
-
logger.debug(
|
|
103
|
+
logger.debug(
|
|
104
|
+
"apply_patch workspace={} bytes={}", self._workspace_root, len(patch_text)
|
|
105
|
+
)
|
|
94
106
|
try:
|
|
95
107
|
operations = self._parse_patch(patch_text)
|
|
96
108
|
return self._format_result(self._apply_operations(operations), exit_code=0)
|
|
97
109
|
except ApplyPatchError as exc:
|
|
98
110
|
return self._format_result(str(exc), exit_code=1)
|
|
99
111
|
|
|
100
|
-
def _parse_patch(
|
|
112
|
+
def _parse_patch(
|
|
113
|
+
self, patch_text: "str"
|
|
114
|
+
) -> "typing.List[typing.Union[typing.Union[_AddFileOp, _DeleteFileOp], _UpdateFileOp]]":
|
|
101
115
|
lines = patch_text.splitlines()
|
|
102
116
|
if not lines:
|
|
103
117
|
raise ApplyPatchError("patch rejected: empty patch")
|
|
@@ -106,7 +120,7 @@ class ApplyPatchTool(BaseTool):
|
|
|
106
120
|
"apply_patch verification failed: missing '*** Begin Patch' header"
|
|
107
121
|
)
|
|
108
122
|
|
|
109
|
-
operations:
|
|
123
|
+
operations: "typing.List[typing.Union[typing.Union[_AddFileOp, _DeleteFileOp], _UpdateFileOp]]" = ([])
|
|
110
124
|
index = 1
|
|
111
125
|
while index < len(lines):
|
|
112
126
|
line = lines[index]
|
|
@@ -123,7 +137,7 @@ class ApplyPatchTool(BaseTool):
|
|
|
123
137
|
if line.startswith("*** Add File: "):
|
|
124
138
|
path = line[len("*** Add File: ") :]
|
|
125
139
|
index += 1
|
|
126
|
-
content_lines:
|
|
140
|
+
content_lines: "typing.List[str]" = []
|
|
127
141
|
while index < len(lines) and not lines[index].startswith("*** "):
|
|
128
142
|
entry = lines[index]
|
|
129
143
|
if not entry.startswith("+"):
|
|
@@ -136,7 +150,9 @@ class ApplyPatchTool(BaseTool):
|
|
|
136
150
|
raise ApplyPatchError(
|
|
137
151
|
f"apply_patch verification failed: add for {path} is missing file content"
|
|
138
152
|
)
|
|
139
|
-
operations.append(
|
|
153
|
+
operations.append(
|
|
154
|
+
_AddFileOp(path=path, content=self._join_lines(content_lines))
|
|
155
|
+
)
|
|
140
156
|
continue
|
|
141
157
|
|
|
142
158
|
if line.startswith("*** Delete File: "):
|
|
@@ -153,8 +169,8 @@ class ApplyPatchTool(BaseTool):
|
|
|
153
169
|
move_to = lines[index][len("*** Move to: ") :]
|
|
154
170
|
index += 1
|
|
155
171
|
|
|
156
|
-
sections:
|
|
157
|
-
current_lines:
|
|
172
|
+
sections: "typing.List[_UpdateSection]" = []
|
|
173
|
+
current_lines: "typing.List[str]" = []
|
|
158
174
|
saw_hunk_header = False
|
|
159
175
|
anchor_end_of_file = False
|
|
160
176
|
while index < len(lines):
|
|
@@ -216,14 +232,16 @@ class ApplyPatchTool(BaseTool):
|
|
|
216
232
|
f"apply_patch verification failed: {line!r} is not a valid hunk header"
|
|
217
233
|
)
|
|
218
234
|
|
|
219
|
-
raise ApplyPatchError(
|
|
235
|
+
raise ApplyPatchError(
|
|
236
|
+
"apply_patch verification failed: missing '*** End Patch' footer"
|
|
237
|
+
)
|
|
220
238
|
|
|
221
239
|
def _apply_operations(
|
|
222
240
|
self,
|
|
223
|
-
operations:
|
|
224
|
-
) ->
|
|
225
|
-
preview:
|
|
226
|
-
summaries:
|
|
241
|
+
operations: "typing.List[typing.Union[typing.Union[_AddFileOp, _DeleteFileOp], _UpdateFileOp]]",
|
|
242
|
+
) -> "str":
|
|
243
|
+
preview: "typing.Dict[Path, typing.Union[str, None]]" = {}
|
|
244
|
+
summaries: "typing.Dict[Path, str]" = {}
|
|
227
245
|
|
|
228
246
|
for operation in operations:
|
|
229
247
|
if isinstance(operation, _AddFileOp):
|
|
@@ -253,7 +271,9 @@ class ApplyPatchTool(BaseTool):
|
|
|
253
271
|
self._write_preview(preview)
|
|
254
272
|
return self._format_success(summaries)
|
|
255
273
|
|
|
256
|
-
def _read_preview_file(
|
|
274
|
+
def _read_preview_file(
|
|
275
|
+
self, path: "Path", preview: "typing.Dict[Path, typing.Union[str, None]]"
|
|
276
|
+
) -> "str":
|
|
257
277
|
if path in preview:
|
|
258
278
|
content = preview[path]
|
|
259
279
|
if content is None:
|
|
@@ -270,10 +290,10 @@ class ApplyPatchTool(BaseTool):
|
|
|
270
290
|
|
|
271
291
|
def _apply_update(
|
|
272
292
|
self,
|
|
273
|
-
path:
|
|
274
|
-
original_text:
|
|
275
|
-
sections:
|
|
276
|
-
) ->
|
|
293
|
+
path: "Path",
|
|
294
|
+
original_text: "str",
|
|
295
|
+
sections: "typing.Tuple[_UpdateSection, ...]",
|
|
296
|
+
) -> "str":
|
|
277
297
|
lines = original_text.splitlines()
|
|
278
298
|
cursor = 0
|
|
279
299
|
for section in sections:
|
|
@@ -282,7 +302,9 @@ class ApplyPatchTool(BaseTool):
|
|
|
282
302
|
if not old_block and not new_block:
|
|
283
303
|
continue
|
|
284
304
|
|
|
285
|
-
match_index = self._find_match(
|
|
305
|
+
match_index = self._find_match(
|
|
306
|
+
lines, old_block, cursor, section.anchor_end_of_file
|
|
307
|
+
)
|
|
286
308
|
if match_index is None:
|
|
287
309
|
raise ApplyPatchError(
|
|
288
310
|
"apply_patch verification failed: Failed to find expected lines in "
|
|
@@ -294,11 +316,11 @@ class ApplyPatchTool(BaseTool):
|
|
|
294
316
|
|
|
295
317
|
def _find_match(
|
|
296
318
|
self,
|
|
297
|
-
lines:
|
|
298
|
-
old_block:
|
|
299
|
-
cursor:
|
|
300
|
-
anchor_end_of_file:
|
|
301
|
-
) ->
|
|
319
|
+
lines: "typing.List[str]",
|
|
320
|
+
old_block: "typing.List[str]",
|
|
321
|
+
cursor: "int",
|
|
322
|
+
anchor_end_of_file: "bool",
|
|
323
|
+
) -> "typing.Union[int, None]":
|
|
302
324
|
if anchor_end_of_file:
|
|
303
325
|
start = len(lines) - len(old_block)
|
|
304
326
|
if start >= 0 and lines[start : start + len(old_block)] == old_block:
|
|
@@ -317,7 +339,9 @@ class ApplyPatchTool(BaseTool):
|
|
|
317
339
|
return start
|
|
318
340
|
return None
|
|
319
341
|
|
|
320
|
-
def _write_preview(
|
|
342
|
+
def _write_preview(
|
|
343
|
+
self, preview: "typing.Dict[Path, typing.Union[str, None]]"
|
|
344
|
+
) -> "None":
|
|
321
345
|
for path, content in preview.items():
|
|
322
346
|
if content is None:
|
|
323
347
|
if path.exists():
|
|
@@ -326,7 +350,7 @@ class ApplyPatchTool(BaseTool):
|
|
|
326
350
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
327
351
|
path.write_text(content, encoding="utf-8")
|
|
328
352
|
|
|
329
|
-
def _format_success(self, summaries:
|
|
353
|
+
def _format_success(self, summaries: "typing.Dict[Path, str]") -> "str":
|
|
330
354
|
buckets = {"A": [], "M": [], "D": []}
|
|
331
355
|
for path, status in summaries.items():
|
|
332
356
|
buckets[status].append(self._display_path(path))
|
|
@@ -336,26 +360,23 @@ class ApplyPatchTool(BaseTool):
|
|
|
336
360
|
lines.append(f"{status} {rel_path}")
|
|
337
361
|
return " ".join(lines) + "\n"
|
|
338
362
|
|
|
339
|
-
def _format_result(self, output:
|
|
363
|
+
def _format_result(self, output: "str", exit_code: "int") -> "str":
|
|
340
364
|
return (
|
|
341
|
-
f"Exit code: {exit_code}\n"
|
|
342
|
-
"Wall time: 0 seconds\n"
|
|
343
|
-
"Output:\n"
|
|
344
|
-
f"{output}"
|
|
365
|
+
f"Exit code: {exit_code}\n" "Wall time: 0 seconds\n" "Output:\n" f"{output}"
|
|
345
366
|
)
|
|
346
367
|
|
|
347
|
-
def _resolve_workspace_path(self, path_text:
|
|
368
|
+
def _resolve_workspace_path(self, path_text: "str") -> "Path":
|
|
348
369
|
path = Path(path_text).expanduser()
|
|
349
370
|
resolved = path if path.is_absolute() else self._workspace_root / path
|
|
350
371
|
return resolved.resolve()
|
|
351
372
|
|
|
352
|
-
def _display_path(self, path:
|
|
373
|
+
def _display_path(self, path: "Path") -> "str":
|
|
353
374
|
try:
|
|
354
375
|
return path.relative_to(self._workspace_root).as_posix()
|
|
355
376
|
except ValueError:
|
|
356
377
|
return str(path)
|
|
357
378
|
|
|
358
|
-
def _join_lines(self, lines:
|
|
379
|
+
def _join_lines(self, lines: "typing.List[str]") -> "str":
|
|
359
380
|
if not lines:
|
|
360
381
|
return ""
|
|
361
382
|
return "\n".join(lines) + "\n"
|
pycodex/tools/base_tool.py
CHANGED
|
@@ -10,47 +10,61 @@ Expected behavior:
|
|
|
10
10
|
model, and dispatches `ToolCall` executions back into `ToolResult`s.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import asyncio
|
|
14
14
|
import json
|
|
15
|
+
import traceback
|
|
16
|
+
import typing
|
|
15
17
|
from abc import ABC, abstractmethod
|
|
16
18
|
from dataclasses import dataclass
|
|
17
|
-
import traceback
|
|
18
19
|
|
|
19
|
-
from ..
|
|
20
|
+
from ..events import Event
|
|
21
|
+
from ..protocol import (
|
|
22
|
+
ConversationItem,
|
|
23
|
+
JSONDict,
|
|
24
|
+
JSONValue,
|
|
25
|
+
ToolCall,
|
|
26
|
+
ToolResult,
|
|
27
|
+
ToolSpec,
|
|
28
|
+
UserMessage,
|
|
29
|
+
)
|
|
30
|
+
from ..runtime_services import AgentRuntimeEnvironment
|
|
20
31
|
from ..utils import get_debug_dir
|
|
21
|
-
import typing
|
|
22
32
|
|
|
33
|
+
if typing.TYPE_CHECKING:
|
|
34
|
+
from ..agent import Agent
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
|
|
37
|
+
@dataclass(
|
|
38
|
+
frozen=True,
|
|
39
|
+
)
|
|
25
40
|
class ToolContext:
|
|
26
|
-
turn_id:
|
|
27
|
-
history:
|
|
28
|
-
collaboration_mode: 'str' = "default"
|
|
41
|
+
turn_id: "str"
|
|
42
|
+
history: "typing.Tuple[ConversationItem, ...]"
|
|
29
43
|
|
|
30
44
|
|
|
31
45
|
class StructuredToolOutput:
|
|
32
46
|
def __init__(
|
|
33
47
|
self,
|
|
34
|
-
output:
|
|
35
|
-
content_items:
|
|
36
|
-
success:
|
|
37
|
-
) ->
|
|
48
|
+
output: "JSONValue",
|
|
49
|
+
content_items: "typing.Union[typing.Union[typing.Tuple[JSONDict, ...], typing.List[JSONDict]], None]" = None,
|
|
50
|
+
success: "typing.Union[bool, None]" = None,
|
|
51
|
+
) -> "None":
|
|
38
52
|
self.output = output
|
|
39
53
|
self.content_items = None if content_items is None else tuple(content_items)
|
|
40
54
|
self.success = success
|
|
41
55
|
|
|
42
56
|
|
|
43
57
|
class BaseTool(ABC):
|
|
44
|
-
name:
|
|
45
|
-
description:
|
|
46
|
-
input_schema:
|
|
47
|
-
tool_type:
|
|
48
|
-
format:
|
|
49
|
-
options:
|
|
50
|
-
output_schema:
|
|
51
|
-
supports_parallel:
|
|
52
|
-
|
|
53
|
-
def spec(self) ->
|
|
58
|
+
name: "str"
|
|
59
|
+
description: "str"
|
|
60
|
+
input_schema: "typing.Union[JSONDict, None]" = None
|
|
61
|
+
tool_type: "str" = "function"
|
|
62
|
+
format: "typing.Union[JSONDict, None]" = None
|
|
63
|
+
options: "typing.Union[JSONDict, None]" = None
|
|
64
|
+
output_schema: "typing.Union[JSONDict, None]" = None
|
|
65
|
+
supports_parallel: "bool" = True
|
|
66
|
+
|
|
67
|
+
def spec(self) -> "ToolSpec":
|
|
54
68
|
return ToolSpec(
|
|
55
69
|
name=self.name,
|
|
56
70
|
description=self.description,
|
|
@@ -62,29 +76,51 @@ class BaseTool(ABC):
|
|
|
62
76
|
supports_parallel=self.supports_parallel,
|
|
63
77
|
)
|
|
64
78
|
|
|
65
|
-
def serialize(self) ->
|
|
79
|
+
def serialize(self) -> "JSONDict":
|
|
66
80
|
return self.spec().serialize()
|
|
67
81
|
|
|
68
82
|
@abstractmethod
|
|
69
|
-
async def run(
|
|
83
|
+
async def run(
|
|
84
|
+
self, context: "ToolContext", args: "JSONValue"
|
|
85
|
+
) -> "typing.Union[JSONValue, StructuredToolOutput]":
|
|
70
86
|
raise NotImplementedError
|
|
71
87
|
|
|
88
|
+
def follow_up_messages(
|
|
89
|
+
self, output: "JSONValue"
|
|
90
|
+
) -> "typing.Tuple[UserMessage, ...]":
|
|
91
|
+
return ()
|
|
92
|
+
|
|
93
|
+
def bind_agent(self, agent: "Agent") -> "None":
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def handle_agent_event(self, event: "Event") -> "None":
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def shutdown(self) -> "None":
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def background_work_count(self, after_reply: "bool") -> "int":
|
|
103
|
+
return 0
|
|
104
|
+
|
|
72
105
|
|
|
73
106
|
class ToolRegistry:
|
|
74
|
-
def __init__(
|
|
75
|
-
self
|
|
107
|
+
def __init__(
|
|
108
|
+
self, runtime_environment: "typing.Union[AgentRuntimeEnvironment, None]" = None
|
|
109
|
+
) -> "None":
|
|
110
|
+
self._tools: "typing.Dict[str, BaseTool]" = {}
|
|
111
|
+
self.runtime_environment = runtime_environment or AgentRuntimeEnvironment()
|
|
76
112
|
|
|
77
|
-
def register(self, tool:
|
|
113
|
+
def register(self, tool: "BaseTool") -> "None":
|
|
78
114
|
self._tools[tool.name] = tool
|
|
79
115
|
|
|
80
|
-
def model_visible_specs(self) ->
|
|
116
|
+
def model_visible_specs(self) -> "typing.List[ToolSpec]":
|
|
81
117
|
return [tool.spec() for tool in self._tools.values()]
|
|
82
118
|
|
|
83
|
-
def supports_parallel(self, tool_name:
|
|
119
|
+
def supports_parallel(self, tool_name: "str") -> "bool":
|
|
84
120
|
tool = self._tools.get(tool_name)
|
|
85
121
|
return False if tool is None else tool.supports_parallel
|
|
86
122
|
|
|
87
|
-
async def execute(self, call:
|
|
123
|
+
async def execute(self, call: "ToolCall", context: "ToolContext") -> "ToolResult":
|
|
88
124
|
tool = self._tools.get(call.name)
|
|
89
125
|
if tool is None:
|
|
90
126
|
return ToolResult(
|
|
@@ -96,11 +132,7 @@ class ToolRegistry:
|
|
|
96
132
|
)
|
|
97
133
|
|
|
98
134
|
try:
|
|
99
|
-
|
|
100
|
-
if inspect.isawaitable(maybe_result):
|
|
101
|
-
output = await maybe_result
|
|
102
|
-
else:
|
|
103
|
-
output = maybe_result
|
|
135
|
+
output = await tool.run(context, call.arguments)
|
|
104
136
|
if isinstance(output, StructuredToolOutput):
|
|
105
137
|
return ToolResult(
|
|
106
138
|
call_id=call.call_id,
|
|
@@ -116,10 +148,14 @@ class ToolRegistry:
|
|
|
116
148
|
output=output,
|
|
117
149
|
tool_type=call.tool_type,
|
|
118
150
|
)
|
|
119
|
-
except
|
|
151
|
+
except asyncio.CancelledError:
|
|
152
|
+
raise
|
|
153
|
+
except Exception as exc:
|
|
120
154
|
debug_dir = get_debug_dir()
|
|
121
155
|
if debug_dir is not None:
|
|
122
|
-
with (debug_dir / "tool_errors.jsonl").open(
|
|
156
|
+
with (debug_dir / "tool_errors.jsonl").open(
|
|
157
|
+
"a", encoding="utf-8"
|
|
158
|
+
) as handle:
|
|
123
159
|
handle.write(
|
|
124
160
|
json.dumps(
|
|
125
161
|
{
|
|
@@ -140,19 +176,30 @@ class ToolRegistry:
|
|
|
140
176
|
tool_type=call.tool_type,
|
|
141
177
|
)
|
|
142
178
|
|
|
143
|
-
def
|
|
179
|
+
def follow_up_messages(
|
|
180
|
+
self, results: "typing.Iterable[ToolResult]"
|
|
181
|
+
) -> "typing.Tuple[UserMessage, ...]":
|
|
182
|
+
messages = []
|
|
183
|
+
for result in results:
|
|
184
|
+
if not result.is_error:
|
|
185
|
+
messages.extend(
|
|
186
|
+
self._tools[result.name].follow_up_messages(result.output)
|
|
187
|
+
)
|
|
188
|
+
return tuple(messages)
|
|
189
|
+
|
|
190
|
+
def __contains__(self, tool_name: "str") -> "bool":
|
|
144
191
|
return tool_name in self._tools
|
|
145
192
|
|
|
146
|
-
def __len__(self) ->
|
|
193
|
+
def __len__(self) -> "int":
|
|
147
194
|
return len(self._tools)
|
|
148
195
|
|
|
149
|
-
def names(self) ->
|
|
196
|
+
def names(self) -> "typing.Tuple[str, ...]":
|
|
150
197
|
return tuple(self._tools)
|
|
151
198
|
|
|
152
|
-
def get_tool(self, tool_name:
|
|
199
|
+
def get_tool(self, tool_name: "str") -> "typing.Union[BaseTool, None]":
|
|
153
200
|
return self._tools.get(tool_name)
|
|
154
201
|
|
|
155
|
-
def tools(self) ->
|
|
202
|
+
def tools(self) -> "typing.Tuple[BaseTool, ...]":
|
|
156
203
|
return tuple(self._tools.values())
|
|
157
204
|
|
|
158
205
|
|
pycodex/tools/clock_tool.py
CHANGED
|
@@ -6,13 +6,19 @@ cancels the pending countdown, and every successful reply starts a fresh one.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
from datetime import datetime
|
|
10
9
|
import math
|
|
10
|
+
import typing
|
|
11
|
+
from datetime import datetime
|
|
11
12
|
|
|
13
|
+
from ..events import (
|
|
14
|
+
CompactCompletedEvent,
|
|
15
|
+
CompactStartedEvent,
|
|
16
|
+
Event,
|
|
17
|
+
TurnCompletedEvent,
|
|
18
|
+
TurnStartedEvent,
|
|
19
|
+
)
|
|
12
20
|
from ..protocol import JSONDict, JSONValue
|
|
13
21
|
from .base_tool import BaseTool, ToolContext
|
|
14
|
-
import typing
|
|
15
|
-
|
|
16
22
|
|
|
17
23
|
CLOCK_STATE_OUTPUT_SCHEMA = {
|
|
18
24
|
"type": "object",
|
|
@@ -34,25 +40,25 @@ CLOCK_STATE_OUTPUT_SCHEMA = {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
|
|
37
|
-
def _current_time() ->
|
|
43
|
+
def _current_time() -> "str":
|
|
38
44
|
return datetime.now().astimezone().isoformat(timespec="seconds")
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
class ClockManager:
|
|
42
|
-
def __init__(self, seconds_per_minute:
|
|
48
|
+
def __init__(self, seconds_per_minute: "float" = 60.0) -> "None":
|
|
43
49
|
self._seconds_per_minute = seconds_per_minute
|
|
44
|
-
self._period_m:
|
|
45
|
-
self._timer_task:
|
|
50
|
+
self._period_m: "typing.Union[float, None]" = None
|
|
51
|
+
self._timer_task: "typing.Union[asyncio.Task, None]" = None
|
|
46
52
|
self._generation = 0
|
|
47
|
-
self._notify_hook:
|
|
53
|
+
self._notify_hook: "typing.Union[typing.Callable[[typing.Dict[str, object]], typing.Awaitable[typing.Any]], None]" = (None)
|
|
48
54
|
|
|
49
55
|
def set_notify_hook(
|
|
50
56
|
self,
|
|
51
|
-
callback:
|
|
52
|
-
) ->
|
|
57
|
+
callback: "typing.Union[typing.Callable[[typing.Dict[str, object]], typing.Awaitable[typing.Any]], None]",
|
|
58
|
+
) -> "None":
|
|
53
59
|
self._notify_hook = callback
|
|
54
60
|
|
|
55
|
-
def set_period(self, value:
|
|
61
|
+
def set_period(self, value: "object") -> "JSONDict":
|
|
56
62
|
self._cancel_pending()
|
|
57
63
|
if value is None:
|
|
58
64
|
self._period_m = None
|
|
@@ -65,20 +71,20 @@ class ClockManager:
|
|
|
65
71
|
self._period_m = period_m
|
|
66
72
|
return self.snapshot()
|
|
67
73
|
|
|
68
|
-
def snapshot(self) ->
|
|
74
|
+
def snapshot(self) -> "JSONDict":
|
|
69
75
|
return {
|
|
70
76
|
"enabled": self.enabled,
|
|
71
77
|
"period_m": self._period_m,
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
@property
|
|
75
|
-
def enabled(self) ->
|
|
81
|
+
def enabled(self) -> "bool":
|
|
76
82
|
return self._period_m is not None
|
|
77
83
|
|
|
78
|
-
def turn_started(self) ->
|
|
84
|
+
def turn_started(self) -> "None":
|
|
79
85
|
self._cancel_pending()
|
|
80
86
|
|
|
81
|
-
def arm_after_reply(self) ->
|
|
87
|
+
def arm_after_reply(self) -> "None":
|
|
82
88
|
self._cancel_pending()
|
|
83
89
|
period_m = self._period_m
|
|
84
90
|
if period_m is None or self._notify_hook is None:
|
|
@@ -91,17 +97,19 @@ class ClockManager:
|
|
|
91
97
|
lambda task: None if task.cancelled() else task.exception()
|
|
92
98
|
)
|
|
93
99
|
|
|
94
|
-
def cancel(self) ->
|
|
100
|
+
def cancel(self) -> "None":
|
|
95
101
|
self.set_period(None)
|
|
96
102
|
|
|
97
|
-
def _cancel_pending(self) ->
|
|
103
|
+
def _cancel_pending(self) -> "None":
|
|
98
104
|
self._generation += 1
|
|
99
105
|
task = self._timer_task
|
|
100
106
|
self._timer_task = None
|
|
101
107
|
if task is not None and not task.done():
|
|
102
108
|
task.cancel()
|
|
103
109
|
|
|
104
|
-
async def _wait_and_notify(self, generation:
|
|
110
|
+
async def _wait_and_notify(self, generation: "int", period_m: "float") -> "None":
|
|
111
|
+
from ..agent import TurnInterrupted
|
|
112
|
+
|
|
105
113
|
try:
|
|
106
114
|
await asyncio.sleep(period_m * self._seconds_per_minute)
|
|
107
115
|
except asyncio.CancelledError:
|
|
@@ -121,8 +129,19 @@ class ClockManager:
|
|
|
121
129
|
"current_time": _current_time(),
|
|
122
130
|
}
|
|
123
131
|
)
|
|
124
|
-
except
|
|
125
|
-
|
|
132
|
+
except TurnInterrupted:
|
|
133
|
+
# The next successful reply will arm the clock after a steer.
|
|
134
|
+
return
|
|
135
|
+
except asyncio.CancelledError:
|
|
136
|
+
raise
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
139
|
+
{
|
|
140
|
+
"message": "Clock notification failed",
|
|
141
|
+
"exception": exc,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
return
|
|
126
145
|
|
|
127
146
|
if (
|
|
128
147
|
not started
|
|
@@ -147,9 +166,7 @@ class ClockTool(BaseTool):
|
|
|
147
166
|
{"type": "number"},
|
|
148
167
|
{"type": "null"},
|
|
149
168
|
],
|
|
150
|
-
"description": (
|
|
151
|
-
"Positive period in minutes, or null to cancel."
|
|
152
|
-
),
|
|
169
|
+
"description": ("Positive period in minutes, or null to cancel."),
|
|
153
170
|
},
|
|
154
171
|
},
|
|
155
172
|
"required": ["period_m"],
|
|
@@ -158,10 +175,26 @@ class ClockTool(BaseTool):
|
|
|
158
175
|
output_schema = CLOCK_STATE_OUTPUT_SCHEMA
|
|
159
176
|
supports_parallel = False
|
|
160
177
|
|
|
161
|
-
def __init__(self, manager:
|
|
178
|
+
def __init__(self, manager: "ClockManager") -> "None":
|
|
162
179
|
self._manager = manager
|
|
163
180
|
|
|
164
|
-
|
|
181
|
+
def bind_agent(self, agent) -> "None":
|
|
182
|
+
self._manager.set_notify_hook(agent.maybe_invoke)
|
|
183
|
+
|
|
184
|
+
def handle_agent_event(self, event: "Event") -> "None":
|
|
185
|
+
if isinstance(event, (TurnStartedEvent, CompactStartedEvent)):
|
|
186
|
+
self._manager.turn_started()
|
|
187
|
+
elif isinstance(event, (TurnCompletedEvent, CompactCompletedEvent)):
|
|
188
|
+
self._manager.arm_after_reply()
|
|
189
|
+
|
|
190
|
+
def shutdown(self) -> "None":
|
|
191
|
+
self._manager.cancel()
|
|
192
|
+
self._manager.set_notify_hook(None)
|
|
193
|
+
|
|
194
|
+
def background_work_count(self, after_reply: "bool") -> "int":
|
|
195
|
+
return int(after_reply and self._manager.enabled)
|
|
196
|
+
|
|
197
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
165
198
|
del context
|
|
166
199
|
if not isinstance(args, dict) or "period_m" not in args:
|
|
167
200
|
raise ValueError("clock requires period_m")
|
|
@@ -49,10 +49,10 @@ class CloseAgentTool(BaseTool):
|
|
|
49
49
|
output_schema = CLOSE_AGENT_OUTPUT_SCHEMA
|
|
50
50
|
supports_parallel = False
|
|
51
51
|
|
|
52
|
-
def __init__(self, subagent_manager:
|
|
52
|
+
def __init__(self, subagent_manager: "SubAgentManager") -> "None":
|
|
53
53
|
self._subagent_manager = subagent_manager
|
|
54
54
|
|
|
55
|
-
async def run(self, context:
|
|
55
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
56
56
|
del context
|
|
57
57
|
agent_id = str(args.get("id", "")).strip()
|
|
58
58
|
if not agent_id:
|