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.
Files changed (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {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: 'int' = UNIFIED_EXEC_OUTPUT_MAX_BYTES
74
- head: 'bytearray' = field(default_factory=bytearray)
75
- tail: 'bytearray' = field(default_factory=bytearray)
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: 'bytes') -> 'None':
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) -> 'bytes':
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) -> 'bool':
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: 'int'
112
- process: 'asyncio.subprocess.Process'
113
- start_time: 'float'
114
- command_display: 'str'
115
- tty: 'bool'
116
- unread_output: '_HeadTailBuffer' = field(default_factory=_HeadTailBuffer)
117
- reader_task: 'typing.Union[asyncio.Task, None]' = None
118
- output_event: 'asyncio.Event' = field(default_factory=asyncio.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__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
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: 'typing.Dict[int, UnifiedExecSession]' = {}
126
- self._lock = asyncio.Lock()
127
- self._notify_hook: 'typing.Union[typing.Callable[[typing.Dict[str, object]], typing.Awaitable[typing.Any]], None]' = None
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: 'typing.Union[typing.Callable[[typing.Dict[str, object]], typing.Awaitable[typing.Any]], None]',
132
- ) -> 'None':
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) -> 'int':
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: 'str',
145
- workdir: 'typing.Union[str, None]' = None,
146
- shell: 'typing.Union[str, None]' = None,
147
- login: 'bool' = DEFAULT_LOGIN,
148
- tty: 'bool' = DEFAULT_TTY,
149
- yield_time_ms: 'int' = DEFAULT_EXEC_YIELD_TIME_MS,
150
- max_output_tokens: 'typing.Union[int, None]' = None,
151
- ) -> 'str':
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
- async with self._lock:
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: 'int',
197
- chars: 'str' = "",
198
- yield_time_ms: 'int' = DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
199
- max_output_tokens: 'typing.Union[int, None]' = None,
200
- ) -> 'str':
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("write_stdin session_id={} chars_len={}", session_id, len(chars))
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) -> 'int':
221
- async with self._lock:
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(self, session_id: 'int') -> 'typing.Union[UnifiedExecSession, None]':
227
- async with self._lock:
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: 'int',
233
- yield_time_ms: 'int',
234
- max_output_tokens: 'typing.Union[int, None]',
235
- ) -> 'str':
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(session.process.wait(), timeout=yield_time_ms / 1000.0)
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(session.output_event.wait(), timeout=remaining_seconds)
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: 'int') -> 'None':
291
- async with self._lock:
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(session.process.stdin):
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: 'UnifiedExecSession',
301
- timeout_seconds: 'float',
302
- ) -> 'None':
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: 'UnifiedExecSession') -> 'None':
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: 'int') -> 'None':
335
- callback = self._notify_hook
336
- if callback is None:
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
- async with self._lock:
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 Exception: # pragma: no cover - background notification must not break tools
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: 'typing.Union[str, None]') -> 'Path':
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: 'str',
368
- shell: 'typing.Union[str, None]',
369
- login: 'bool',
370
- ) -> 'typing.List[str]':
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: 'str') -> 'typing.Union[int, None]':
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(self, output: 'str', max_output_tokens: 'typing.Union[int, None]') -> 'str':
383
- token_budget = DEFAULT_MAX_OUTPUT_TOKENS if max_output_tokens is None else max_output_tokens
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: 'str') -> 'bytes':
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: 'PlanStore') -> 'None':
63
+ def __init__(self, plan_store: "PlanStore") -> "None":
63
64
  self._plan_store = plan_store
64
65
 
65
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
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: 'typing.List[PlanItem]' = []
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 = None if explanation_value in (None, "") else str(explanation_value)
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"
@@ -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__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
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: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
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: 'JSONDict' = {
106
+ image_item: "JSONDict" = {
105
107
  "type": "input_image",
106
108
  "image_url": image_url,
107
109
  "detail": detail,
@@ -9,7 +9,10 @@ Expected behavior:
9
9
  wait times out.
10
10
  """
11
11
 
12
- from ..protocol import JSONDict, JSONValue
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: 'SubAgentManager') -> 'None':
69
+ def __init__(self, subagent_manager: "SubAgentManager") -> "None":
67
70
  self._subagent_manager = subagent_manager
68
71
 
69
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
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 _timeout_ms(self, args: 'JSONDict') -> 'int':
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),
@@ -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: 'CodeModeManager') -> 'None':
63
+ def __init__(self, manager: "CodeModeManager") -> "None":
63
64
  self._manager = manager
64
65
 
65
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
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: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
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
@@ -24,6 +24,8 @@ class WebSearchTool(BaseTool):
24
24
  }
25
25
  supports_parallel = False
26
26
 
27
- async def run(self, context: 'ToolContext', args: 'JSONValue') -> 'JSONValue':
27
+ async def run(self, context: "ToolContext", args: "JSONValue") -> "JSONValue":
28
28
  del context, args
29
- return "Error: web_search is provider-native and should not be executed locally."
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: 'UnifiedExecManager') -> 'None':
58
+ def __init__(self, manager: "UnifiedExecManager") -> "None":
58
59
  self._manager = manager
59
60
 
60
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
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: 'JSONDict', chars: 'str') -> 'int':
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: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
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: 'JSONDict',
100
- key: 'str',
101
- default: 'int',
102
- minimum: 'int',
103
- maximum: 'int',
104
- ) -> 'int':
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 .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
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
  ]