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
pycodex/tools/read_file_tool.py
CHANGED
|
@@ -9,12 +9,12 @@ Expected behavior:
|
|
|
9
9
|
mode used to inspect code structure without shelling out to `sed` or `cat`.
|
|
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_LINE_LENGTH = 500
|
|
20
20
|
TAB_WIDTH = 4
|
|
@@ -78,7 +78,7 @@ class ReadFileTool(BaseTool):
|
|
|
78
78
|
"additionalProperties": False,
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async def run(self, context:
|
|
81
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
82
82
|
del context
|
|
83
83
|
file_path = Path(str(args.get("file_path", "")))
|
|
84
84
|
offset = int(args.get("offset", 1))
|
|
@@ -101,7 +101,7 @@ class ReadFileTool(BaseTool):
|
|
|
101
101
|
return self._read_indentation(file_path, offset, limit, indentation)
|
|
102
102
|
return self._read_slice(file_path, offset, limit)
|
|
103
103
|
|
|
104
|
-
def _read_slice(self, file_path:
|
|
104
|
+
def _read_slice(self, file_path: "Path", offset: "int", limit: "int") -> "str":
|
|
105
105
|
lines = file_path.read_text(errors="replace").splitlines()
|
|
106
106
|
if offset > len(lines):
|
|
107
107
|
return "Error: `offset` exceeds file length."
|
|
@@ -114,11 +114,11 @@ class ReadFileTool(BaseTool):
|
|
|
114
114
|
|
|
115
115
|
def _read_indentation(
|
|
116
116
|
self,
|
|
117
|
-
file_path:
|
|
118
|
-
offset:
|
|
119
|
-
limit:
|
|
120
|
-
indentation:
|
|
121
|
-
) ->
|
|
117
|
+
file_path: "Path",
|
|
118
|
+
offset: "int",
|
|
119
|
+
limit: "int",
|
|
120
|
+
indentation: "JSONDict",
|
|
121
|
+
) -> "str":
|
|
122
122
|
lines = self._collect_line_records(file_path)
|
|
123
123
|
anchor_line = int(indentation.get("anchor_line", offset))
|
|
124
124
|
max_levels = int(indentation.get("max_levels", 0))
|
|
@@ -136,7 +136,9 @@ class ReadFileTool(BaseTool):
|
|
|
136
136
|
anchor_index = anchor_line - 1
|
|
137
137
|
effective_indents = self._compute_effective_indents(lines)
|
|
138
138
|
anchor_indent = effective_indents[anchor_index]
|
|
139
|
-
min_indent =
|
|
139
|
+
min_indent = (
|
|
140
|
+
0 if max_levels == 0 else max(anchor_indent - max_levels * TAB_WIDTH, 0)
|
|
141
|
+
)
|
|
140
142
|
final_limit = min(limit, max_lines, len(lines))
|
|
141
143
|
|
|
142
144
|
if final_limit == 1:
|
|
@@ -157,8 +159,12 @@ class ReadFileTool(BaseTool):
|
|
|
157
159
|
selected.appendleft(lines[upper])
|
|
158
160
|
progressed += 1
|
|
159
161
|
if effective_indents[upper] == min_indent and not include_siblings:
|
|
160
|
-
allow_header_comment =
|
|
161
|
-
|
|
162
|
+
allow_header_comment = (
|
|
163
|
+
include_header and lines[upper]["is_comment"]
|
|
164
|
+
)
|
|
165
|
+
can_take_line = (
|
|
166
|
+
allow_header_comment or upper_min_indent_count == 0
|
|
167
|
+
)
|
|
162
168
|
if can_take_line:
|
|
163
169
|
upper_min_indent_count += 1
|
|
164
170
|
else:
|
|
@@ -200,9 +206,13 @@ class ReadFileTool(BaseTool):
|
|
|
200
206
|
f"L{record['number']}: {record['display']}" for record in selected
|
|
201
207
|
)
|
|
202
208
|
|
|
203
|
-
def _collect_line_records(
|
|
209
|
+
def _collect_line_records(
|
|
210
|
+
self, file_path: "Path"
|
|
211
|
+
) -> "typing.List[typing.Dict[str, object]]":
|
|
204
212
|
records = []
|
|
205
|
-
for number, raw in enumerate(
|
|
213
|
+
for number, raw in enumerate(
|
|
214
|
+
file_path.read_text(errors="replace").splitlines(), start=1
|
|
215
|
+
):
|
|
206
216
|
records.append(
|
|
207
217
|
{
|
|
208
218
|
"number": number,
|
|
@@ -214,7 +224,9 @@ class ReadFileTool(BaseTool):
|
|
|
214
224
|
)
|
|
215
225
|
return records
|
|
216
226
|
|
|
217
|
-
def _compute_effective_indents(
|
|
227
|
+
def _compute_effective_indents(
|
|
228
|
+
self, records: "typing.List[typing.Dict[str, object]]"
|
|
229
|
+
) -> "typing.List[int]":
|
|
218
230
|
effective = []
|
|
219
231
|
previous_indent = 0
|
|
220
232
|
for record in records:
|
|
@@ -225,7 +237,7 @@ class ReadFileTool(BaseTool):
|
|
|
225
237
|
effective.append(previous_indent)
|
|
226
238
|
return effective
|
|
227
239
|
|
|
228
|
-
def _measure_indent(self, line:
|
|
240
|
+
def _measure_indent(self, line: "str") -> "int":
|
|
229
241
|
total = 0
|
|
230
242
|
for character in line:
|
|
231
243
|
if character == " ":
|
|
@@ -236,10 +248,10 @@ class ReadFileTool(BaseTool):
|
|
|
236
248
|
break
|
|
237
249
|
return total
|
|
238
250
|
|
|
239
|
-
def _format_line(self, text:
|
|
251
|
+
def _format_line(self, text: "str") -> "str":
|
|
240
252
|
return text[:MAX_LINE_LENGTH]
|
|
241
253
|
|
|
242
|
-
def _trim_empty_lines(self, records:
|
|
254
|
+
def _trim_empty_lines(self, records: "deque[typing.Dict[str, object]]") -> "None":
|
|
243
255
|
while records and not str(records[0]["raw"]).strip():
|
|
244
256
|
records.popleft()
|
|
245
257
|
while records and not str(records[-1]["raw"]).strip():
|
|
@@ -81,10 +81,10 @@ class RequestPermissionsTool(BaseTool):
|
|
|
81
81
|
}
|
|
82
82
|
supports_parallel = False
|
|
83
83
|
|
|
84
|
-
def __init__(self, request_manager:
|
|
84
|
+
def __init__(self, request_manager: "RequestPermissionsManager") -> "None":
|
|
85
85
|
self._request_manager = request_manager
|
|
86
86
|
|
|
87
|
-
async def run(self, context:
|
|
87
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
88
88
|
del context
|
|
89
89
|
permissions = args.get("permissions")
|
|
90
90
|
if not isinstance(permissions, dict):
|
|
@@ -94,11 +94,21 @@ class RequestPermissionsTool(BaseTool):
|
|
|
94
94
|
|
|
95
95
|
response = await self._request_manager.request(
|
|
96
96
|
{
|
|
97
|
-
"reason":
|
|
98
|
-
|
|
97
|
+
"reason": (
|
|
98
|
+
None
|
|
99
|
+
if args.get("reason") in (None, "")
|
|
100
|
+
else str(args.get("reason"))
|
|
101
|
+
),
|
|
102
|
+
"environment_id": (
|
|
103
|
+
None
|
|
104
|
+
if args.get("environment_id") in (None, "")
|
|
105
|
+
else str(args.get("environment_id"))
|
|
106
|
+
),
|
|
99
107
|
"permissions": permissions,
|
|
100
108
|
}
|
|
101
109
|
)
|
|
102
110
|
if response is None:
|
|
103
|
-
return
|
|
111
|
+
return (
|
|
112
|
+
"Error: request_permissions was cancelled before receiving a response."
|
|
113
|
+
)
|
|
104
114
|
return response
|
|
@@ -4,21 +4,12 @@ Original Codex mapping:
|
|
|
4
4
|
- Corresponds to the original Codex `request_user_input` collaboration tool.
|
|
5
5
|
|
|
6
6
|
Expected behavior:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
same fixed unavailable error string as upstream Codex.
|
|
10
|
-
- In Plan mode, the tool validates its question payload, forces `isOther=true`
|
|
11
|
-
on every question, and returns a JSON-string `function_call_output` with
|
|
12
|
-
`success=true`.
|
|
7
|
+
- Keep the tool declaration available for upstream request compatibility.
|
|
8
|
+
- Return the upstream Default-mode unavailable message without requesting input.
|
|
13
9
|
"""
|
|
14
10
|
|
|
15
|
-
import json
|
|
16
|
-
|
|
17
|
-
from ..collaboration import collaboration_mode_display_name
|
|
18
11
|
from ..protocol import JSONDict, JSONValue
|
|
19
|
-
from
|
|
20
|
-
from .base_tool import BaseTool, StructuredToolOutput, ToolContext
|
|
21
|
-
import typing
|
|
12
|
+
from .base_tool import BaseTool, ToolContext
|
|
22
13
|
|
|
23
14
|
MIN_AUTO_RESOLUTION_MS = 60_000
|
|
24
15
|
MAX_AUTO_RESOLUTION_MS = 240_000
|
|
@@ -42,8 +33,8 @@ REQUEST_USER_INPUT_QUESTION_SCHEMA = {
|
|
|
42
33
|
"type": "array",
|
|
43
34
|
"description": (
|
|
44
35
|
"Provide 2-3 mutually exclusive choices. Put the recommended option "
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
'first and suffix its label with "(Recommended)". Do not include '
|
|
37
|
+
'an "Other" option in this list; the client will add a free-form '
|
|
47
38
|
"Other option automatically."
|
|
48
39
|
),
|
|
49
40
|
"items": {
|
|
@@ -68,48 +59,16 @@ REQUEST_USER_INPUT_QUESTION_SCHEMA = {
|
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
) -> 'bool':
|
|
75
|
-
normalized = mode.strip().lower()
|
|
76
|
-
return normalized == "plan" or (
|
|
77
|
-
default_mode_request_user_input and normalized == "default"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def request_user_input_unavailable_message(
|
|
82
|
-
mode: 'str',
|
|
83
|
-
default_mode_request_user_input: 'bool' = False,
|
|
84
|
-
) -> 'typing.Union[str, None]':
|
|
85
|
-
if request_user_input_is_available(mode, default_mode_request_user_input):
|
|
86
|
-
return None
|
|
87
|
-
return (
|
|
88
|
-
"request_user_input is unavailable in "
|
|
89
|
-
f"{collaboration_mode_display_name(mode)} mode"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def request_user_input_tool_description(
|
|
94
|
-
default_mode_request_user_input: 'bool' = False,
|
|
95
|
-
) -> 'str':
|
|
96
|
-
if default_mode_request_user_input:
|
|
97
|
-
allowed_modes = "Default or Plan mode"
|
|
98
|
-
else:
|
|
99
|
-
allowed_modes = "Plan mode"
|
|
100
|
-
return (
|
|
62
|
+
class RequestUserInputTool(BaseTool):
|
|
63
|
+
name = "request_user_input"
|
|
64
|
+
description = (
|
|
101
65
|
"Request user input for one to three short questions and wait for the "
|
|
102
66
|
f"response. Set autoResolutionMs, from {MIN_AUTO_RESOLUTION_MS} to "
|
|
103
67
|
f"{MAX_AUTO_RESOLUTION_MS} milliseconds, only when the question is "
|
|
104
68
|
"useful but non-blocking and continuing with best judgment is "
|
|
105
69
|
"acceptable if the user does not answer; omit it when explicit user "
|
|
106
|
-
|
|
70
|
+
"input is required. This tool is unavailable in Default mode."
|
|
107
71
|
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class RequestUserInputTool(BaseTool):
|
|
111
|
-
name = "request_user_input"
|
|
112
|
-
description = request_user_input_tool_description()
|
|
113
72
|
input_schema = {
|
|
114
73
|
"type": "object",
|
|
115
74
|
"properties": {
|
|
@@ -131,63 +90,13 @@ class RequestUserInputTool(BaseTool):
|
|
|
131
90
|
f"up to {MAX_AUTO_RESOLUTION_MS} when the answer would "
|
|
132
91
|
"materially unblock better work."
|
|
133
92
|
),
|
|
134
|
-
}
|
|
93
|
+
},
|
|
135
94
|
},
|
|
136
95
|
"required": ["questions"],
|
|
137
96
|
"additionalProperties": False,
|
|
138
97
|
}
|
|
139
98
|
supports_parallel = False
|
|
140
99
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
default_mode_request_user_input: 'bool' = False,
|
|
145
|
-
) -> 'None':
|
|
146
|
-
self._request_manager = request_manager
|
|
147
|
-
self._default_mode_request_user_input = default_mode_request_user_input
|
|
148
|
-
self.description = request_user_input_tool_description(
|
|
149
|
-
default_mode_request_user_input
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
153
|
-
unavailable = request_user_input_unavailable_message(
|
|
154
|
-
context.collaboration_mode,
|
|
155
|
-
self._default_mode_request_user_input,
|
|
156
|
-
)
|
|
157
|
-
if unavailable is not None:
|
|
158
|
-
return unavailable
|
|
159
|
-
|
|
160
|
-
questions = args.get("questions")
|
|
161
|
-
if not isinstance(questions, list):
|
|
162
|
-
raise ValueError("questions must be a list")
|
|
163
|
-
|
|
164
|
-
if any(
|
|
165
|
-
not isinstance(question, dict)
|
|
166
|
-
or not isinstance(question.get("options"), list)
|
|
167
|
-
or not question["options"]
|
|
168
|
-
for question in questions
|
|
169
|
-
):
|
|
170
|
-
return "request_user_input requires non-empty options for every question"
|
|
171
|
-
|
|
172
|
-
request_payload = {
|
|
173
|
-
"questions": [
|
|
174
|
-
{
|
|
175
|
-
**question,
|
|
176
|
-
"isOther": True,
|
|
177
|
-
}
|
|
178
|
-
for question in questions
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
auto_resolution_ms = args.get("autoResolutionMs")
|
|
182
|
-
if auto_resolution_ms not in (None, ""):
|
|
183
|
-
request_payload["autoResolutionMs"] = min(
|
|
184
|
-
max(int(auto_resolution_ms), MIN_AUTO_RESOLUTION_MS),
|
|
185
|
-
MAX_AUTO_RESOLUTION_MS,
|
|
186
|
-
)
|
|
187
|
-
response = await self._request_manager.request(request_payload)
|
|
188
|
-
if response is None:
|
|
189
|
-
return "request_user_input was cancelled before receiving a response"
|
|
190
|
-
return StructuredToolOutput(
|
|
191
|
-
json.dumps(response, ensure_ascii=False, separators=(",", ":")),
|
|
192
|
-
success=True,
|
|
193
|
-
)
|
|
100
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
101
|
+
del context, args
|
|
102
|
+
return "request_user_input is unavailable in Default mode"
|
|
@@ -43,10 +43,10 @@ class ResumeAgentTool(BaseTool):
|
|
|
43
43
|
output_schema = RESUME_AGENT_OUTPUT_SCHEMA
|
|
44
44
|
supports_parallel = False
|
|
45
45
|
|
|
46
|
-
def __init__(self, subagent_manager:
|
|
46
|
+
def __init__(self, subagent_manager: "SubAgentManager") -> "None":
|
|
47
47
|
self._subagent_manager = subagent_manager
|
|
48
48
|
|
|
49
|
-
async def run(self, context:
|
|
49
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
50
50
|
del context
|
|
51
51
|
agent_id = str(args.get("id", "")).strip()
|
|
52
52
|
if not agent_id:
|
pycodex/tools/send_input_tool.py
CHANGED
|
@@ -9,11 +9,12 @@ Expected behavior:
|
|
|
9
9
|
- Return the submission id for the queued input.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import typing
|
|
13
|
+
|
|
12
14
|
from ..protocol import JSONDict, JSONValue
|
|
13
15
|
from ..runtime_services import SubAgentManager
|
|
14
16
|
from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
|
|
15
17
|
from .base_tool import BaseTool, ToolContext
|
|
16
|
-
import typing
|
|
17
18
|
|
|
18
19
|
SEND_INPUT_OUTPUT_SCHEMA = {
|
|
19
20
|
"type": "object",
|
|
@@ -59,10 +60,10 @@ class SendInputTool(BaseTool):
|
|
|
59
60
|
output_schema = SEND_INPUT_OUTPUT_SCHEMA
|
|
60
61
|
supports_parallel = False
|
|
61
62
|
|
|
62
|
-
def __init__(self, subagent_manager:
|
|
63
|
+
def __init__(self, subagent_manager: "SubAgentManager") -> "None":
|
|
63
64
|
self._subagent_manager = subagent_manager
|
|
64
65
|
|
|
65
|
-
async def run(self, context:
|
|
66
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
66
67
|
del context
|
|
67
68
|
agent_id = str(args.get("id", "")).strip()
|
|
68
69
|
if not agent_id:
|
|
@@ -84,10 +85,10 @@ class SendInputTool(BaseTool):
|
|
|
84
85
|
|
|
85
86
|
def _compose_prompt(
|
|
86
87
|
self,
|
|
87
|
-
message:
|
|
88
|
-
items:
|
|
89
|
-
) ->
|
|
90
|
-
parts:
|
|
88
|
+
message: "typing.Union[str, None]",
|
|
89
|
+
items: "typing.Union[typing.List[typing.Dict[str, object]], None]",
|
|
90
|
+
) -> "str":
|
|
91
|
+
parts: "typing.List[str]" = []
|
|
91
92
|
if message:
|
|
92
93
|
parts.append(message.strip())
|
|
93
94
|
for item in items or []:
|
|
@@ -100,7 +101,9 @@ class SendInputTool(BaseTool):
|
|
|
100
101
|
parts.append(str(item))
|
|
101
102
|
return "\n\n".join(part for part in parts if part)
|
|
102
103
|
|
|
103
|
-
def _optional_string(
|
|
104
|
+
def _optional_string(
|
|
105
|
+
self, args: "JSONDict", key: "str"
|
|
106
|
+
) -> "typing.Union[str, None]":
|
|
104
107
|
value = args.get(key)
|
|
105
108
|
if value in (None, ""):
|
|
106
109
|
return None
|
|
@@ -12,11 +12,11 @@ Expected behavior:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
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_SHELL_TIMEOUT_MS = 10_000
|
|
22
22
|
MAX_OUTPUT_CHARS = 12_000
|
|
@@ -54,10 +54,12 @@ class ShellCommandTool(BaseTool):
|
|
|
54
54
|
}
|
|
55
55
|
supports_parallel = False
|
|
56
56
|
|
|
57
|
-
def __init__(
|
|
57
|
+
def __init__(
|
|
58
|
+
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
59
|
+
) -> "None":
|
|
58
60
|
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
59
61
|
|
|
60
|
-
async def run(self, context:
|
|
62
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
61
63
|
del context
|
|
62
64
|
command = str(args.get("command", "")).strip()
|
|
63
65
|
timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
|
|
@@ -109,7 +111,7 @@ class ShellCommandTool(BaseTool):
|
|
|
109
111
|
|
|
110
112
|
return "\n".join(pieces)
|
|
111
113
|
|
|
112
|
-
def _resolve_workdir(self, workdir_arg) ->
|
|
114
|
+
def _resolve_workdir(self, workdir_arg) -> "Path":
|
|
113
115
|
if workdir_arg in (None, ""):
|
|
114
116
|
return self._working_directory
|
|
115
117
|
workdir = Path(str(workdir_arg))
|
|
@@ -117,7 +119,7 @@ class ShellCommandTool(BaseTool):
|
|
|
117
119
|
workdir = self._working_directory / workdir
|
|
118
120
|
return workdir.resolve()
|
|
119
121
|
|
|
120
|
-
def _clip_output(self, text:
|
|
122
|
+
def _clip_output(self, text: "str") -> "str":
|
|
121
123
|
if len(text) <= MAX_OUTPUT_CHARS:
|
|
122
124
|
return text
|
|
123
125
|
return text[:MAX_OUTPUT_CHARS] + "\n...[truncated]..."
|
pycodex/tools/shell_tool.py
CHANGED
|
@@ -12,11 +12,11 @@ Expected behavior:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
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_SHELL_TIMEOUT_MS = 30_000
|
|
22
22
|
MAX_OUTPUT_CHARS = 12_000
|
|
@@ -51,10 +51,12 @@ class ShellTool(BaseTool):
|
|
|
51
51
|
}
|
|
52
52
|
supports_parallel = False
|
|
53
53
|
|
|
54
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
56
|
+
) -> "None":
|
|
55
57
|
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
56
58
|
|
|
57
|
-
async def run(self, context:
|
|
59
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
58
60
|
del context
|
|
59
61
|
command = args.get("command")
|
|
60
62
|
timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
|
|
@@ -106,7 +108,7 @@ class ShellTool(BaseTool):
|
|
|
106
108
|
|
|
107
109
|
return "\n".join(pieces)
|
|
108
110
|
|
|
109
|
-
def _resolve_workdir(self, workdir_arg) ->
|
|
111
|
+
def _resolve_workdir(self, workdir_arg) -> "Path":
|
|
110
112
|
if workdir_arg in (None, ""):
|
|
111
113
|
return self._working_directory
|
|
112
114
|
workdir = Path(str(workdir_arg))
|
|
@@ -114,7 +116,7 @@ class ShellTool(BaseTool):
|
|
|
114
116
|
workdir = self._working_directory / workdir
|
|
115
117
|
return workdir.resolve()
|
|
116
118
|
|
|
117
|
-
def _clip_output(self, text:
|
|
119
|
+
def _clip_output(self, text: "str") -> "str":
|
|
118
120
|
if len(text) <= MAX_OUTPUT_CHARS:
|
|
119
121
|
return text
|
|
120
122
|
return text[:MAX_OUTPUT_CHARS] + "\n...[truncated]..."
|
|
@@ -9,11 +9,12 @@ Expected behavior:
|
|
|
9
9
|
- Return the new agent identifier plus any user-facing nickname.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import typing
|
|
13
|
+
|
|
12
14
|
from ..protocol import JSONDict, JSONValue
|
|
13
15
|
from ..runtime_services import SubAgentManager
|
|
14
16
|
from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
|
|
15
17
|
from .base_tool import BaseTool, ToolContext
|
|
16
|
-
import typing
|
|
17
18
|
|
|
18
19
|
SPAWN_AGENT_OUTPUT_SCHEMA = {
|
|
19
20
|
"type": "object",
|
|
@@ -92,10 +93,10 @@ Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegati
|
|
|
92
93
|
output_schema = SPAWN_AGENT_OUTPUT_SCHEMA
|
|
93
94
|
supports_parallel = False
|
|
94
95
|
|
|
95
|
-
def __init__(self, subagent_manager:
|
|
96
|
+
def __init__(self, subagent_manager: "SubAgentManager") -> "None":
|
|
96
97
|
self._subagent_manager = subagent_manager
|
|
97
98
|
|
|
98
|
-
async def run(self, context:
|
|
99
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
99
100
|
message = self._optional_string(args, "message")
|
|
100
101
|
items = args.get("items")
|
|
101
102
|
if items is not None and not isinstance(items, list):
|
|
@@ -112,7 +113,9 @@ Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegati
|
|
|
112
113
|
history=context.history,
|
|
113
114
|
)
|
|
114
115
|
|
|
115
|
-
def _optional_string(
|
|
116
|
+
def _optional_string(
|
|
117
|
+
self, args: "JSONDict", key: "str"
|
|
118
|
+
) -> "typing.Union[str, None]":
|
|
116
119
|
value = args.get(key)
|
|
117
120
|
if value in (None, ""):
|
|
118
121
|
return None
|