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
|
@@ -14,6 +14,8 @@ Expected behavior:
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import os
|
|
16
16
|
import shlex
|
|
17
|
+
import threading
|
|
18
|
+
import typing
|
|
17
19
|
import uuid
|
|
18
20
|
from dataclasses import dataclass, field
|
|
19
21
|
from pathlib import Path
|
|
@@ -26,7 +28,6 @@ from ..utils.truncation import (
|
|
|
26
28
|
approx_token_count,
|
|
27
29
|
formatted_truncate_text,
|
|
28
30
|
)
|
|
29
|
-
import typing
|
|
30
31
|
|
|
31
32
|
DEFAULT_EXEC_YIELD_TIME_MS = 10_000
|
|
32
33
|
DEFAULT_WRITE_STDIN_YIELD_TIME_MS = 250
|
|
@@ -68,13 +69,14 @@ UNIFIED_EXEC_OUTPUT_SCHEMA = {
|
|
|
68
69
|
"additionalProperties": False,
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
|
|
71
73
|
@dataclass
|
|
72
74
|
class _HeadTailBuffer:
|
|
73
|
-
max_bytes:
|
|
74
|
-
head:
|
|
75
|
-
tail:
|
|
75
|
+
max_bytes: "int" = UNIFIED_EXEC_OUTPUT_MAX_BYTES
|
|
76
|
+
head: "bytearray" = field(default_factory=bytearray)
|
|
77
|
+
tail: "bytearray" = field(default_factory=bytearray)
|
|
76
78
|
|
|
77
|
-
def push_chunk(self, chunk:
|
|
79
|
+
def push_chunk(self, chunk: "bytes") -> "None":
|
|
78
80
|
if not chunk or self.max_bytes <= 0:
|
|
79
81
|
return
|
|
80
82
|
|
|
@@ -96,43 +98,45 @@ class _HeadTailBuffer:
|
|
|
96
98
|
excess = len(self.tail) - tail_budget
|
|
97
99
|
del self.tail[:excess]
|
|
98
100
|
|
|
99
|
-
def drain_bytes(self) ->
|
|
101
|
+
def drain_bytes(self) -> "bytes":
|
|
100
102
|
combined = bytes(self.head) + bytes(self.tail)
|
|
101
103
|
self.head.clear()
|
|
102
104
|
self.tail.clear()
|
|
103
105
|
return combined
|
|
104
106
|
|
|
105
|
-
def has_data(self) ->
|
|
107
|
+
def has_data(self) -> "bool":
|
|
106
108
|
return bool(self.head or self.tail)
|
|
107
109
|
|
|
108
110
|
|
|
109
111
|
@dataclass
|
|
110
112
|
class UnifiedExecSession:
|
|
111
|
-
session_id:
|
|
112
|
-
process:
|
|
113
|
-
start_time:
|
|
114
|
-
command_display:
|
|
115
|
-
tty:
|
|
116
|
-
unread_output:
|
|
117
|
-
reader_task:
|
|
118
|
-
output_event:
|
|
113
|
+
session_id: "int"
|
|
114
|
+
process: "asyncio.subprocess.Process"
|
|
115
|
+
start_time: "float"
|
|
116
|
+
command_display: "str"
|
|
117
|
+
tty: "bool"
|
|
118
|
+
unread_output: "_HeadTailBuffer" = field(default_factory=_HeadTailBuffer)
|
|
119
|
+
reader_task: "typing.Union[asyncio.Task, None]" = None
|
|
120
|
+
output_event: "asyncio.Event" = field(default_factory=asyncio.Event)
|
|
119
121
|
|
|
120
122
|
|
|
121
123
|
class UnifiedExecManager:
|
|
122
|
-
def __init__(
|
|
124
|
+
def __init__(
|
|
125
|
+
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
126
|
+
) -> "None":
|
|
123
127
|
self._default_cwd = Path(cwd or Path.cwd()).resolve()
|
|
124
128
|
self._next_session_id = DEFAULT_SESSION_ID_START
|
|
125
|
-
self._sessions:
|
|
126
|
-
self._lock =
|
|
127
|
-
self._notify_hook:
|
|
129
|
+
self._sessions: "typing.Dict[int, UnifiedExecSession]" = {}
|
|
130
|
+
self._lock = threading.Lock()
|
|
131
|
+
self._notify_hook: "typing.Union[typing.Callable[[typing.Dict[str, object]], typing.Awaitable[typing.Any]], None]" = (None)
|
|
128
132
|
|
|
129
133
|
def set_notify_hook(
|
|
130
134
|
self,
|
|
131
|
-
callback:
|
|
132
|
-
) ->
|
|
135
|
+
callback: "typing.Union[typing.Callable[[typing.Dict[str, object]], typing.Awaitable[typing.Any]], None]",
|
|
136
|
+
) -> "None":
|
|
133
137
|
self._notify_hook = callback
|
|
134
138
|
|
|
135
|
-
def running_session_count(self) ->
|
|
139
|
+
def running_session_count(self) -> "int":
|
|
136
140
|
return sum(
|
|
137
141
|
1
|
|
138
142
|
for session in self._sessions.values()
|
|
@@ -141,14 +145,14 @@ class UnifiedExecManager:
|
|
|
141
145
|
|
|
142
146
|
async def exec_command(
|
|
143
147
|
self,
|
|
144
|
-
cmd:
|
|
145
|
-
workdir:
|
|
146
|
-
shell:
|
|
147
|
-
login:
|
|
148
|
-
tty:
|
|
149
|
-
yield_time_ms:
|
|
150
|
-
max_output_tokens:
|
|
151
|
-
) ->
|
|
148
|
+
cmd: "str",
|
|
149
|
+
workdir: "typing.Union[str, None]" = None,
|
|
150
|
+
shell: "typing.Union[str, None]" = None,
|
|
151
|
+
login: "bool" = DEFAULT_LOGIN,
|
|
152
|
+
tty: "bool" = DEFAULT_TTY,
|
|
153
|
+
yield_time_ms: "int" = DEFAULT_EXEC_YIELD_TIME_MS,
|
|
154
|
+
max_output_tokens: "typing.Union[int, None]" = None,
|
|
155
|
+
) -> "str":
|
|
152
156
|
session_id = await self._allocate_session_id()
|
|
153
157
|
command = self._build_shell_command(cmd, shell, login)
|
|
154
158
|
cwd = self._resolve_workdir(workdir)
|
|
@@ -178,7 +182,7 @@ class UnifiedExecManager:
|
|
|
178
182
|
)
|
|
179
183
|
session.reader_task = asyncio.create_task(self._pump_output(session))
|
|
180
184
|
|
|
181
|
-
|
|
185
|
+
with self._lock:
|
|
182
186
|
self._sessions[session_id] = session
|
|
183
187
|
|
|
184
188
|
output = await self._wait_and_snapshot(
|
|
@@ -193,11 +197,11 @@ class UnifiedExecManager:
|
|
|
193
197
|
|
|
194
198
|
async def write_stdin(
|
|
195
199
|
self,
|
|
196
|
-
session_id:
|
|
197
|
-
chars:
|
|
198
|
-
yield_time_ms:
|
|
199
|
-
max_output_tokens:
|
|
200
|
-
) ->
|
|
200
|
+
session_id: "int",
|
|
201
|
+
chars: "str" = "",
|
|
202
|
+
yield_time_ms: "int" = DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
|
|
203
|
+
max_output_tokens: "typing.Union[int, None]" = None,
|
|
204
|
+
) -> "str":
|
|
201
205
|
session = await self._get_session(session_id)
|
|
202
206
|
if session is None:
|
|
203
207
|
return f"Error: session_id {session_id} is not running."
|
|
@@ -205,7 +209,9 @@ class UnifiedExecManager:
|
|
|
205
209
|
if chars:
|
|
206
210
|
if session.process.stdin is None:
|
|
207
211
|
return f"Error: session_id {session_id} does not accept stdin."
|
|
208
|
-
logger.debug(
|
|
212
|
+
logger.debug(
|
|
213
|
+
"write_stdin session_id={} chars_len={}", session_id, len(chars)
|
|
214
|
+
)
|
|
209
215
|
if session.tty:
|
|
210
216
|
session.unread_output.push_chunk(self._tty_echo(chars))
|
|
211
217
|
session.process.stdin.write(chars.encode("utf-8"))
|
|
@@ -217,22 +223,24 @@ class UnifiedExecManager:
|
|
|
217
223
|
max_output_tokens,
|
|
218
224
|
)
|
|
219
225
|
|
|
220
|
-
async def _allocate_session_id(self) ->
|
|
221
|
-
|
|
226
|
+
async def _allocate_session_id(self) -> "int":
|
|
227
|
+
with self._lock:
|
|
222
228
|
session_id = self._next_session_id
|
|
223
229
|
self._next_session_id += 1
|
|
224
230
|
return session_id
|
|
225
231
|
|
|
226
|
-
async def _get_session(
|
|
227
|
-
|
|
232
|
+
async def _get_session(
|
|
233
|
+
self, session_id: "int"
|
|
234
|
+
) -> "typing.Union[UnifiedExecSession, None]":
|
|
235
|
+
with self._lock:
|
|
228
236
|
return self._sessions.get(session_id)
|
|
229
237
|
|
|
230
238
|
async def _wait_and_snapshot(
|
|
231
239
|
self,
|
|
232
|
-
session_id:
|
|
233
|
-
yield_time_ms:
|
|
234
|
-
max_output_tokens:
|
|
235
|
-
) ->
|
|
240
|
+
session_id: "int",
|
|
241
|
+
yield_time_ms: "int",
|
|
242
|
+
max_output_tokens: "typing.Union[int, None]",
|
|
243
|
+
) -> "str":
|
|
236
244
|
session = await self._get_session(session_id)
|
|
237
245
|
if session is None:
|
|
238
246
|
return f"Error: session_id {session_id} is not running."
|
|
@@ -240,7 +248,9 @@ class UnifiedExecManager:
|
|
|
240
248
|
loop = asyncio.get_running_loop()
|
|
241
249
|
start_wait = loop.time()
|
|
242
250
|
try:
|
|
243
|
-
await asyncio.wait_for(
|
|
251
|
+
await asyncio.wait_for(
|
|
252
|
+
session.process.wait(), timeout=yield_time_ms / 1000.0
|
|
253
|
+
)
|
|
244
254
|
except asyncio.TimeoutError:
|
|
245
255
|
remaining_seconds = (yield_time_ms / 1000.0) - (loop.time() - start_wait)
|
|
246
256
|
if (
|
|
@@ -250,7 +260,9 @@ class UnifiedExecManager:
|
|
|
250
260
|
):
|
|
251
261
|
session.output_event.clear()
|
|
252
262
|
try:
|
|
253
|
-
await asyncio.wait_for(
|
|
263
|
+
await asyncio.wait_for(
|
|
264
|
+
session.output_event.wait(), timeout=remaining_seconds
|
|
265
|
+
)
|
|
254
266
|
except asyncio.TimeoutError:
|
|
255
267
|
pass
|
|
256
268
|
|
|
@@ -269,7 +281,6 @@ class UnifiedExecManager:
|
|
|
269
281
|
output_text = self._truncate_output(output_text, max_output_tokens)
|
|
270
282
|
|
|
271
283
|
lines = [
|
|
272
|
-
f"Command: {session.command_display}",
|
|
273
284
|
f"Chunk ID: {uuid.uuid4().hex[:6]}",
|
|
274
285
|
f"Wall time: {wall_time:.4f} seconds",
|
|
275
286
|
]
|
|
@@ -287,19 +298,21 @@ class UnifiedExecManager:
|
|
|
287
298
|
|
|
288
299
|
return "\n".join(lines)
|
|
289
300
|
|
|
290
|
-
async def _close_session(self, session_id:
|
|
291
|
-
|
|
301
|
+
async def _close_session(self, session_id: "int") -> "None":
|
|
302
|
+
with self._lock:
|
|
292
303
|
session = self._sessions.pop(session_id, None)
|
|
293
304
|
if session is None:
|
|
294
305
|
return
|
|
295
|
-
if session.process.stdin is not None and not stream_writer_is_closing(
|
|
306
|
+
if session.process.stdin is not None and not stream_writer_is_closing(
|
|
307
|
+
session.process.stdin
|
|
308
|
+
):
|
|
296
309
|
session.process.stdin.close()
|
|
297
310
|
|
|
298
311
|
async def _drain_reader_after_exit(
|
|
299
312
|
self,
|
|
300
|
-
session:
|
|
301
|
-
timeout_seconds:
|
|
302
|
-
) ->
|
|
313
|
+
session: "UnifiedExecSession",
|
|
314
|
+
timeout_seconds: "float",
|
|
315
|
+
) -> "None":
|
|
303
316
|
reader_task = session.reader_task
|
|
304
317
|
if reader_task is None:
|
|
305
318
|
return
|
|
@@ -319,7 +332,7 @@ class UnifiedExecManager:
|
|
|
319
332
|
await asyncio.gather(reader_task, return_exceptions=True)
|
|
320
333
|
session.output_event.set()
|
|
321
334
|
|
|
322
|
-
async def _pump_output(self, session:
|
|
335
|
+
async def _pump_output(self, session: "UnifiedExecSession") -> "None":
|
|
323
336
|
stream = session.process.stdout
|
|
324
337
|
if stream is None:
|
|
325
338
|
return
|
|
@@ -331,17 +344,21 @@ class UnifiedExecManager:
|
|
|
331
344
|
session.output_event.set()
|
|
332
345
|
session.output_event.set()
|
|
333
346
|
|
|
334
|
-
async def _notify_when_session_completes(self, session_id:
|
|
335
|
-
|
|
336
|
-
|
|
347
|
+
async def _notify_when_session_completes(self, session_id: "int") -> "None":
|
|
348
|
+
from ..agent import TurnInterrupted
|
|
349
|
+
|
|
350
|
+
if self._notify_hook is None:
|
|
337
351
|
return
|
|
338
352
|
session = await self._get_session(session_id)
|
|
339
353
|
if session is None:
|
|
340
354
|
return
|
|
341
355
|
await session.process.wait()
|
|
342
|
-
|
|
356
|
+
with self._lock:
|
|
343
357
|
if self._sessions.get(session_id) is not session:
|
|
344
358
|
return
|
|
359
|
+
callback = self._notify_hook
|
|
360
|
+
if callback is None:
|
|
361
|
+
return
|
|
345
362
|
try:
|
|
346
363
|
await callback(
|
|
347
364
|
{
|
|
@@ -351,10 +368,20 @@ class UnifiedExecManager:
|
|
|
351
368
|
"command": session.command_display,
|
|
352
369
|
}
|
|
353
370
|
)
|
|
354
|
-
except
|
|
371
|
+
except TurnInterrupted:
|
|
372
|
+
# A steer ended the notification turn at a safe boundary.
|
|
355
373
|
return
|
|
374
|
+
except asyncio.CancelledError:
|
|
375
|
+
raise
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
378
|
+
{
|
|
379
|
+
"message": "Exec completion notification failed",
|
|
380
|
+
"exception": exc,
|
|
381
|
+
}
|
|
382
|
+
)
|
|
356
383
|
|
|
357
|
-
def _resolve_workdir(self, workdir:
|
|
384
|
+
def _resolve_workdir(self, workdir: "typing.Union[str, None]") -> "Path":
|
|
358
385
|
if not workdir:
|
|
359
386
|
return self._default_cwd
|
|
360
387
|
path = Path(workdir)
|
|
@@ -364,10 +391,10 @@ class UnifiedExecManager:
|
|
|
364
391
|
|
|
365
392
|
def _build_shell_command(
|
|
366
393
|
self,
|
|
367
|
-
cmd:
|
|
368
|
-
shell:
|
|
369
|
-
login:
|
|
370
|
-
) ->
|
|
394
|
+
cmd: "str",
|
|
395
|
+
shell: "typing.Union[str, None]",
|
|
396
|
+
login: "bool",
|
|
397
|
+
) -> "typing.List[str]":
|
|
371
398
|
shell_path = shell or os.environ.get("SHELL") or "/bin/bash"
|
|
372
399
|
shell_name = Path(shell_path).name.lower()
|
|
373
400
|
if shell_name in {"cmd", "cmd.exe"}:
|
|
@@ -376,13 +403,19 @@ class UnifiedExecManager:
|
|
|
376
403
|
return [shell_path, "-NoProfile", "-Command", cmd]
|
|
377
404
|
return [shell_path, "-lc" if login else "-c", cmd]
|
|
378
405
|
|
|
379
|
-
def _estimate_token_count(self, output:
|
|
406
|
+
def _estimate_token_count(self, output: "str") -> "typing.Union[int, None]":
|
|
380
407
|
return approx_token_count(output)
|
|
381
408
|
|
|
382
|
-
def _truncate_output(
|
|
383
|
-
|
|
409
|
+
def _truncate_output(
|
|
410
|
+
self, output: "str", max_output_tokens: "typing.Union[int, None]"
|
|
411
|
+
) -> "str":
|
|
412
|
+
token_budget = (
|
|
413
|
+
DEFAULT_MAX_OUTPUT_TOKENS
|
|
414
|
+
if max_output_tokens is None
|
|
415
|
+
else max_output_tokens
|
|
416
|
+
)
|
|
384
417
|
return formatted_truncate_text(output, max(token_budget, 0))
|
|
385
418
|
|
|
386
|
-
def _tty_echo(self, chars:
|
|
419
|
+
def _tty_echo(self, chars: "str") -> "bytes":
|
|
387
420
|
normalized = chars.replace("\n", "\r\n")
|
|
388
421
|
return normalized.encode("utf-8")
|
|
@@ -10,10 +10,11 @@ Expected behavior:
|
|
|
10
10
|
confirmation text Codex uses.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import typing
|
|
14
|
+
|
|
13
15
|
from ..protocol import JSONDict, JSONValue
|
|
14
16
|
from ..runtime_services import PlanItem, PlanStore
|
|
15
17
|
from .base_tool import BaseTool, ToolContext
|
|
16
|
-
import typing
|
|
17
18
|
|
|
18
19
|
VALID_PLAN_STATUSES = {"pending", "in_progress", "completed"}
|
|
19
20
|
|
|
@@ -59,16 +60,16 @@ class UpdatePlanTool(BaseTool):
|
|
|
59
60
|
}
|
|
60
61
|
supports_parallel = False
|
|
61
62
|
|
|
62
|
-
def __init__(self, plan_store:
|
|
63
|
+
def __init__(self, plan_store: "PlanStore") -> "None":
|
|
63
64
|
self._plan_store = plan_store
|
|
64
65
|
|
|
65
|
-
async def run(self, context:
|
|
66
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
66
67
|
del context
|
|
67
68
|
raw_plan = args.get("plan")
|
|
68
69
|
if not isinstance(raw_plan, list):
|
|
69
70
|
return "Error: `plan` must be a list."
|
|
70
71
|
|
|
71
|
-
plan_items:
|
|
72
|
+
plan_items: "typing.List[PlanItem]" = []
|
|
72
73
|
for item in raw_plan:
|
|
73
74
|
if not isinstance(item, dict):
|
|
74
75
|
return "Error: each `plan` item must be an object."
|
|
@@ -81,6 +82,8 @@ class UpdatePlanTool(BaseTool):
|
|
|
81
82
|
plan_items.append(PlanItem(step=step, status=status))
|
|
82
83
|
|
|
83
84
|
explanation_value = args.get("explanation")
|
|
84
|
-
explanation =
|
|
85
|
+
explanation = (
|
|
86
|
+
None if explanation_value in (None, "") else str(explanation_value)
|
|
87
|
+
)
|
|
85
88
|
self._plan_store.update(explanation, tuple(plan_items))
|
|
86
89
|
return "Plan updated"
|
pycodex/tools/view_image_tool.py
CHANGED
|
@@ -14,12 +14,12 @@ Expected behavior:
|
|
|
14
14
|
item that Codex uses when feeding image tool output back to the model.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import typing
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
|
-
from ..image_utils import ImageProcessingError, load_image_data_url
|
|
20
20
|
from ..protocol import JSONDict, JSONValue
|
|
21
|
+
from ..utils.image_utils import ImageProcessingError, load_image_data_url
|
|
21
22
|
from .base_tool import BaseTool, StructuredToolOutput, ToolContext
|
|
22
|
-
import typing
|
|
23
23
|
|
|
24
24
|
VIEW_IMAGE_OUTPUT_SCHEMA = {
|
|
25
25
|
"type": "object",
|
|
@@ -63,10 +63,12 @@ class ViewImageTool(BaseTool):
|
|
|
63
63
|
}
|
|
64
64
|
output_schema = VIEW_IMAGE_OUTPUT_SCHEMA
|
|
65
65
|
|
|
66
|
-
def __init__(
|
|
66
|
+
def __init__(
|
|
67
|
+
self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
|
|
68
|
+
) -> "None":
|
|
67
69
|
self._workspace_root = Path(cwd or Path.cwd()).resolve()
|
|
68
70
|
|
|
69
|
-
async def run(self, context:
|
|
71
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
70
72
|
del context
|
|
71
73
|
path_value = str(args.get("path", "")).strip()
|
|
72
74
|
if not path_value:
|
|
@@ -101,7 +103,7 @@ class ViewImageTool(BaseTool):
|
|
|
101
103
|
"image_url": image_url,
|
|
102
104
|
"detail": detail,
|
|
103
105
|
}
|
|
104
|
-
image_item:
|
|
106
|
+
image_item: "JSONDict" = {
|
|
105
107
|
"type": "input_image",
|
|
106
108
|
"image_url": image_url,
|
|
107
109
|
"detail": detail,
|
pycodex/tools/wait_agent_tool.py
CHANGED
|
@@ -9,7 +9,10 @@ Expected behavior:
|
|
|
9
9
|
wait times out.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import json
|
|
13
|
+
import typing
|
|
14
|
+
|
|
15
|
+
from ..protocol import JSONDict, JSONValue, UserMessage
|
|
13
16
|
from ..runtime_services import SubAgentManager
|
|
14
17
|
from .agent_tool_schemas import AGENT_STATUS_SCHEMA
|
|
15
18
|
from .base_tool import BaseTool, ToolContext
|
|
@@ -63,10 +66,10 @@ class WaitAgentTool(BaseTool):
|
|
|
63
66
|
output_schema = WAIT_AGENT_OUTPUT_SCHEMA
|
|
64
67
|
supports_parallel = False
|
|
65
68
|
|
|
66
|
-
def __init__(self, subagent_manager:
|
|
69
|
+
def __init__(self, subagent_manager: "SubAgentManager") -> "None":
|
|
67
70
|
self._subagent_manager = subagent_manager
|
|
68
71
|
|
|
69
|
-
async def run(self, context:
|
|
72
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
70
73
|
del context
|
|
71
74
|
ids = args.get("ids")
|
|
72
75
|
if not isinstance(ids, list) or not ids:
|
|
@@ -77,7 +80,27 @@ class WaitAgentTool(BaseTool):
|
|
|
77
80
|
timeout_ms = self._timeout_ms(args)
|
|
78
81
|
return await self._subagent_manager.wait_agents(agent_ids, timeout_ms)
|
|
79
82
|
|
|
80
|
-
def
|
|
83
|
+
def follow_up_messages(
|
|
84
|
+
self, output: "JSONValue"
|
|
85
|
+
) -> "typing.Tuple[UserMessage, ...]":
|
|
86
|
+
if not isinstance(output, dict):
|
|
87
|
+
return ()
|
|
88
|
+
return tuple(
|
|
89
|
+
UserMessage(
|
|
90
|
+
text=(
|
|
91
|
+
"<subagent_notification>\n"
|
|
92
|
+
+ json.dumps(
|
|
93
|
+
{"agent_id": agent_id, "status": status},
|
|
94
|
+
ensure_ascii=False,
|
|
95
|
+
separators=(",", ":"),
|
|
96
|
+
)
|
|
97
|
+
+ "\n</subagent_notification>"
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
for agent_id, status in output["status"].items()
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _timeout_ms(self, args: "JSONDict") -> "int":
|
|
81
104
|
value = int(args.get("timeout_ms", DEFAULT_WAIT_AGENT_TIMEOUT_MS))
|
|
82
105
|
return min(
|
|
83
106
|
max(value, MIN_WAIT_AGENT_TIMEOUT_MS),
|
pycodex/tools/wait_tool.py
CHANGED
|
@@ -9,10 +9,11 @@ Expected behavior:
|
|
|
9
9
|
- Return only the new output since the previous `exec` / `wait` snapshot.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import typing
|
|
13
|
+
|
|
12
14
|
from ..protocol import JSONDict, JSONValue
|
|
13
15
|
from .base_tool import BaseTool, ToolContext
|
|
14
16
|
from .code_mode_manager import DEFAULT_WAIT_YIELD_TIME_MS, CodeModeManager
|
|
15
|
-
import typing
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class WaitTool(BaseTool):
|
|
@@ -59,10 +60,10 @@ class WaitTool(BaseTool):
|
|
|
59
60
|
}
|
|
60
61
|
supports_parallel = False
|
|
61
62
|
|
|
62
|
-
def __init__(self, manager:
|
|
63
|
+
def __init__(self, manager: "CodeModeManager") -> "None":
|
|
63
64
|
self._manager = 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
|
cell_id = str(args.get("cell_id", "")).strip()
|
|
68
69
|
if not cell_id:
|
|
@@ -74,7 +75,7 @@ class WaitTool(BaseTool):
|
|
|
74
75
|
terminate=bool(args.get("terminate", False)),
|
|
75
76
|
)
|
|
76
77
|
|
|
77
|
-
def _optional_int(self, args:
|
|
78
|
+
def _optional_int(self, args: "JSONDict", key: "str") -> "typing.Union[int, None]":
|
|
78
79
|
value = args.get(key)
|
|
79
80
|
if value in (None, ""):
|
|
80
81
|
return None
|
pycodex/tools/web_search_tool.py
CHANGED
|
@@ -24,6 +24,8 @@ class WebSearchTool(BaseTool):
|
|
|
24
24
|
}
|
|
25
25
|
supports_parallel = False
|
|
26
26
|
|
|
27
|
-
async def run(self, context:
|
|
27
|
+
async def run(self, context: "ToolContext", args: "JSONValue") -> "JSONValue":
|
|
28
28
|
del context, args
|
|
29
|
-
return
|
|
29
|
+
return (
|
|
30
|
+
"Error: web_search is provider-native and should not be executed locally."
|
|
31
|
+
)
|
|
@@ -10,6 +10,8 @@ Expected behavior:
|
|
|
10
10
|
- Reuse the same `session_id` until the process exits.
|
|
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 (
|
|
@@ -17,7 +19,6 @@ from .unified_exec_manager import (
|
|
|
17
19
|
UNIFIED_EXEC_OUTPUT_SCHEMA,
|
|
18
20
|
UnifiedExecManager,
|
|
19
21
|
)
|
|
20
|
-
import typing
|
|
21
22
|
|
|
22
23
|
MIN_WRITE_YIELD_TIME_MS = 250
|
|
23
24
|
MAX_WRITE_YIELD_TIME_MS = 30_000
|
|
@@ -54,10 +55,10 @@ class WriteStdinTool(BaseTool):
|
|
|
54
55
|
output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
|
|
55
56
|
supports_parallel = False
|
|
56
57
|
|
|
57
|
-
def __init__(self, manager:
|
|
58
|
+
def __init__(self, manager: "UnifiedExecManager") -> "None":
|
|
58
59
|
self._manager = manager
|
|
59
60
|
|
|
60
|
-
async def run(self, context:
|
|
61
|
+
async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
|
|
61
62
|
del context
|
|
62
63
|
session_id = args.get("session_id")
|
|
63
64
|
if session_id is None:
|
|
@@ -71,7 +72,7 @@ class WriteStdinTool(BaseTool):
|
|
|
71
72
|
max_output_tokens=self._optional_int(args, "max_output_tokens"),
|
|
72
73
|
)
|
|
73
74
|
|
|
74
|
-
def _yield_time_ms(self, args:
|
|
75
|
+
def _yield_time_ms(self, args: "JSONDict", chars: "str") -> "int":
|
|
75
76
|
if chars:
|
|
76
77
|
return self._bounded_int(
|
|
77
78
|
args,
|
|
@@ -88,7 +89,7 @@ class WriteStdinTool(BaseTool):
|
|
|
88
89
|
MAX_WRITE_STDIN_POLL_YIELD_TIME_MS,
|
|
89
90
|
)
|
|
90
91
|
|
|
91
|
-
def _optional_int(self, args:
|
|
92
|
+
def _optional_int(self, args: "JSONDict", key: "str") -> "typing.Union[int, None]":
|
|
92
93
|
value = args.get(key)
|
|
93
94
|
if value in (None, ""):
|
|
94
95
|
return None
|
|
@@ -96,11 +97,11 @@ class WriteStdinTool(BaseTool):
|
|
|
96
97
|
|
|
97
98
|
def _bounded_int(
|
|
98
99
|
self,
|
|
99
|
-
args:
|
|
100
|
-
key:
|
|
101
|
-
default:
|
|
102
|
-
minimum:
|
|
103
|
-
maximum:
|
|
104
|
-
) ->
|
|
100
|
+
args: "JSONDict",
|
|
101
|
+
key: "str",
|
|
102
|
+
default: "int",
|
|
103
|
+
minimum: "int",
|
|
104
|
+
maximum: "int",
|
|
105
|
+
) -> "int":
|
|
105
106
|
value = int(args.get(key, default))
|
|
106
107
|
return min(max(value, minimum), maximum)
|
pycodex/utils/__init__.py
CHANGED
|
@@ -1,35 +1,20 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .compactor import DEFAULT_COMPACT_PROMPT, SUMMARY_PREFIX, compact
|
|
2
2
|
from .debug import get_debug_dir
|
|
3
|
+
from .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
|
|
3
4
|
from .get_env import build_user_agent, get_shell_name, get_timezone_name
|
|
4
5
|
from .random_ids import uuid7_string
|
|
5
|
-
from .compactor import DEFAULT_COMPACT_PROMPT, SUMMARY_PREFIX, compact
|
|
6
|
-
from .toolcall_visualize import colorize_cli_message, tool_summary
|
|
7
|
-
from .visualize import (
|
|
8
|
-
CliSessionView,
|
|
9
|
-
cli_color_enabled,
|
|
10
|
-
format_cli_tool_call_message,
|
|
11
|
-
short_id,
|
|
12
|
-
shorten_title,
|
|
13
|
-
)
|
|
14
6
|
|
|
15
7
|
__all__ = [
|
|
16
|
-
"CliSessionView",
|
|
17
8
|
"DEFAULT_COMPACT_PROMPT",
|
|
18
9
|
"DOTENV_FILENAME",
|
|
19
10
|
"SUMMARY_PREFIX",
|
|
20
11
|
"build_user_agent",
|
|
21
|
-
"cli_color_enabled",
|
|
22
|
-
"colorize_cli_message",
|
|
23
|
-
"format_cli_tool_call_message",
|
|
24
12
|
"get_debug_dir",
|
|
25
13
|
"get_shell_name",
|
|
26
14
|
"get_timezone_name",
|
|
27
15
|
"load_codex_dotenv",
|
|
28
16
|
"parse_dotenv",
|
|
29
17
|
"parse_dotenv_value",
|
|
30
|
-
"short_id",
|
|
31
|
-
"shorten_title",
|
|
32
|
-
"tool_summary",
|
|
33
18
|
"compact",
|
|
34
19
|
"uuid7_string",
|
|
35
20
|
]
|