python-codex 0.1.1__py3-none-any.whl → 0.1.3__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 (59) hide show
  1. pycodex/__init__.py +5 -1
  2. pycodex/agent.py +39 -41
  3. pycodex/cli.py +51 -43
  4. pycodex/collaboration.py +6 -7
  5. pycodex/compat.py +99 -0
  6. pycodex/context.py +87 -87
  7. pycodex/doctor.py +40 -40
  8. pycodex/model.py +69 -69
  9. pycodex/portable.py +33 -33
  10. pycodex/portable_server.py +22 -21
  11. pycodex/protocol.py +84 -86
  12. pycodex/runtime.py +36 -35
  13. pycodex/runtime_services.py +72 -69
  14. pycodex/tools/agent_tool_schemas.py +0 -2
  15. pycodex/tools/apply_patch_tool.py +43 -44
  16. pycodex/tools/base_tool.py +35 -36
  17. pycodex/tools/close_agent_tool.py +2 -4
  18. pycodex/tools/code_mode_manager.py +61 -61
  19. pycodex/tools/exec_command_tool.py +5 -6
  20. pycodex/tools/exec_runtime.js +3 -3
  21. pycodex/tools/exec_tool.py +3 -5
  22. pycodex/tools/grep_files_tool.py +10 -11
  23. pycodex/tools/list_dir_tool.py +8 -9
  24. pycodex/tools/read_file_tool.py +13 -14
  25. pycodex/tools/request_permissions_tool.py +2 -4
  26. pycodex/tools/request_user_input_tool.py +13 -14
  27. pycodex/tools/resume_agent_tool.py +2 -4
  28. pycodex/tools/send_input_tool.py +8 -9
  29. pycodex/tools/shell_command_tool.py +5 -6
  30. pycodex/tools/shell_tool.py +5 -6
  31. pycodex/tools/spawn_agent_tool.py +4 -5
  32. pycodex/tools/unified_exec_manager.py +79 -61
  33. pycodex/tools/update_plan_tool.py +4 -5
  34. pycodex/tools/view_image_tool.py +4 -5
  35. pycodex/tools/wait_agent_tool.py +2 -4
  36. pycodex/tools/wait_tool.py +4 -5
  37. pycodex/tools/web_search_tool.py +1 -3
  38. pycodex/tools/write_stdin_tool.py +4 -5
  39. pycodex/utils/dotenv.py +6 -6
  40. pycodex/utils/get_env.py +57 -34
  41. pycodex/utils/random_ids.py +1 -2
  42. pycodex/utils/visualize.py +79 -79
  43. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
  44. python_codex-0.1.3.dist-info/RECORD +74 -0
  45. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
  46. responses_server/__init__.py +17 -0
  47. responses_server/__main__.py +5 -0
  48. responses_server/app.py +227 -0
  49. responses_server/config.py +63 -0
  50. responses_server/payload_processors.py +86 -0
  51. responses_server/server.py +63 -0
  52. responses_server/session_store.py +37 -0
  53. responses_server/stream_router.py +784 -0
  54. responses_server/tools/__init__.py +4 -0
  55. responses_server/tools/custom_adapter.py +235 -0
  56. responses_server/tools/web_search.py +263 -0
  57. python_codex-0.1.1.dist-info/RECORD +0 -62
  58. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
  59. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -11,8 +11,6 @@ Expected behavior:
11
11
  - Return summaries in the same textual shape Codex tools expect.
12
12
  """
13
13
 
14
- from __future__ import annotations
15
-
16
14
  import asyncio
17
15
  import os
18
16
  import shlex
@@ -22,6 +20,9 @@ from pathlib import Path
22
20
 
23
21
  from loguru import logger
24
22
 
23
+ from ..compat import shlex_join, stream_writer_is_closing
24
+ import typing
25
+
25
26
  DEFAULT_EXEC_YIELD_TIME_MS = 10_000
26
27
  DEFAULT_WRITE_STDIN_YIELD_TIME_MS = 250
27
28
  DEFAULT_MAX_OUTPUT_TOKENS = 10_000
@@ -63,33 +64,33 @@ UNIFIED_EXEC_OUTPUT_SCHEMA = {
63
64
  }
64
65
 
65
66
 
66
- def _approx_token_count(text: str) -> int:
67
+ def _approx_token_count(text: 'str') -> 'int':
67
68
  if not text:
68
69
  return 0
69
70
  byte_length = len(text.encode("utf-8"))
70
71
  return max(1, (byte_length + APPROX_BYTES_PER_TOKEN - 1) // APPROX_BYTES_PER_TOKEN)
71
72
 
72
73
 
73
- def _approx_bytes_for_tokens(token_count: int) -> int:
74
+ def _approx_bytes_for_tokens(token_count: 'int') -> 'int':
74
75
  return max(token_count, 0) * APPROX_BYTES_PER_TOKEN
75
76
 
76
77
 
77
- def _approx_tokens_from_byte_count(byte_count: int) -> int:
78
+ def _approx_tokens_from_byte_count(byte_count: 'int') -> 'int':
78
79
  if byte_count <= 0:
79
80
  return 0
80
81
  return (byte_count + APPROX_BYTES_PER_TOKEN - 1) // APPROX_BYTES_PER_TOKEN
81
82
 
82
83
 
83
- def _split_budget(byte_budget: int) -> tuple[int, int]:
84
+ def _split_budget(byte_budget: 'int') -> 'typing.Tuple[int, int]':
84
85
  left_budget = byte_budget // 2
85
86
  return left_budget, byte_budget - left_budget
86
87
 
87
88
 
88
89
  def _split_string(
89
- text: str,
90
- beginning_bytes: int,
91
- end_bytes: int,
92
- ) -> tuple[str, str]:
90
+ text: 'str',
91
+ beginning_bytes: 'int',
92
+ end_bytes: 'int',
93
+ ) -> 'typing.Tuple[str, str]':
93
94
  if not text:
94
95
  return "", ""
95
96
 
@@ -122,7 +123,7 @@ def _split_string(
122
123
  return text[:prefix_end], text[suffix_start:]
123
124
 
124
125
 
125
- def _truncate_text(text: str, max_tokens: int) -> str:
126
+ def _truncate_text(text: 'str', max_tokens: 'int') -> 'str':
126
127
  if not text:
127
128
  return ""
128
129
 
@@ -141,7 +142,7 @@ def _truncate_text(text: str, max_tokens: int) -> str:
141
142
  return f"{prefix}{marker}{suffix}"
142
143
 
143
144
 
144
- def _formatted_truncate_text(text: str, max_tokens: int) -> str:
145
+ def _formatted_truncate_text(text: 'str', max_tokens: 'int') -> 'str':
145
146
  byte_budget = _approx_bytes_for_tokens(max_tokens)
146
147
  if len(text.encode("utf-8")) <= byte_budget:
147
148
  return text
@@ -150,13 +151,13 @@ def _formatted_truncate_text(text: str, max_tokens: int) -> str:
150
151
  return f"Total output lines: {total_lines}\n\n{_truncate_text(text, max_tokens)}"
151
152
 
152
153
 
153
- @dataclass(slots=True)
154
+ @dataclass
154
155
  class _HeadTailBuffer:
155
- max_bytes: int = UNIFIED_EXEC_OUTPUT_MAX_BYTES
156
- head: bytearray = field(default_factory=bytearray)
157
- tail: bytearray = field(default_factory=bytearray)
156
+ max_bytes: 'int' = UNIFIED_EXEC_OUTPUT_MAX_BYTES
157
+ head: 'bytearray' = field(default_factory=bytearray)
158
+ tail: 'bytearray' = field(default_factory=bytearray)
158
159
 
159
- def push_chunk(self, chunk: bytes) -> None:
160
+ def push_chunk(self, chunk: 'bytes') -> 'None':
160
161
  if not chunk or self.max_bytes <= 0:
161
162
  return
162
163
 
@@ -178,41 +179,45 @@ class _HeadTailBuffer:
178
179
  excess = len(self.tail) - tail_budget
179
180
  del self.tail[:excess]
180
181
 
181
- def drain_bytes(self) -> bytes:
182
+ def drain_bytes(self) -> 'bytes':
182
183
  combined = bytes(self.head) + bytes(self.tail)
183
184
  self.head.clear()
184
185
  self.tail.clear()
185
186
  return combined
186
187
 
188
+ def has_data(self) -> 'bool':
189
+ return bool(self.head or self.tail)
190
+
187
191
 
188
- @dataclass(slots=True)
192
+ @dataclass
189
193
  class UnifiedExecSession:
190
- session_id: int
191
- process: asyncio.subprocess.Process
192
- start_time: float
193
- command_display: str
194
- tty: bool
195
- unread_output: _HeadTailBuffer = field(default_factory=_HeadTailBuffer)
196
- reader_task: asyncio.Task | None = None
194
+ session_id: 'int'
195
+ process: 'asyncio.subprocess.Process'
196
+ start_time: 'float'
197
+ command_display: 'str'
198
+ tty: 'bool'
199
+ unread_output: '_HeadTailBuffer' = field(default_factory=_HeadTailBuffer)
200
+ reader_task: 'typing.Union[asyncio.Task, None]' = None
201
+ output_event: 'asyncio.Event' = field(default_factory=asyncio.Event)
197
202
 
198
203
 
199
204
  class UnifiedExecManager:
200
- def __init__(self, cwd: str | Path | None = None) -> None:
205
+ def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
201
206
  self._default_cwd = Path(cwd or Path.cwd()).resolve()
202
207
  self._next_session_id = DEFAULT_SESSION_ID_START
203
- self._sessions: dict[int, UnifiedExecSession] = {}
208
+ self._sessions: 'typing.Dict[int, UnifiedExecSession]' = {}
204
209
  self._lock = asyncio.Lock()
205
210
 
206
211
  async def exec_command(
207
212
  self,
208
- cmd: str,
209
- workdir: str | None = None,
210
- shell: str | None = None,
211
- login: bool = DEFAULT_LOGIN,
212
- tty: bool = DEFAULT_TTY,
213
- yield_time_ms: int = DEFAULT_EXEC_YIELD_TIME_MS,
214
- max_output_tokens: int | None = None,
215
- ) -> str:
213
+ cmd: 'str',
214
+ workdir: 'typing.Union[str, None]' = None,
215
+ shell: 'typing.Union[str, None]' = None,
216
+ login: 'bool' = DEFAULT_LOGIN,
217
+ tty: 'bool' = DEFAULT_TTY,
218
+ yield_time_ms: 'int' = DEFAULT_EXEC_YIELD_TIME_MS,
219
+ max_output_tokens: 'typing.Union[int, None]' = None,
220
+ ) -> 'str':
216
221
  session_id = await self._allocate_session_id()
217
222
  command = self._build_shell_command(cmd, shell, login)
218
223
  cwd = self._resolve_workdir(workdir)
@@ -234,7 +239,7 @@ class UnifiedExecManager:
234
239
  session_id=session_id,
235
240
  process=process,
236
241
  start_time=asyncio.get_running_loop().time(),
237
- command_display=shlex.join(command),
242
+ command_display=shlex_join(command),
238
243
  tty=tty,
239
244
  )
240
245
  session.reader_task = asyncio.create_task(self._pump_output(session))
@@ -250,11 +255,11 @@ class UnifiedExecManager:
250
255
 
251
256
  async def write_stdin(
252
257
  self,
253
- session_id: int,
254
- chars: str = "",
255
- yield_time_ms: int = DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
256
- max_output_tokens: int | None = None,
257
- ) -> str:
258
+ session_id: 'int',
259
+ chars: 'str' = "",
260
+ yield_time_ms: 'int' = DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
261
+ max_output_tokens: 'typing.Union[int, None]' = None,
262
+ ) -> 'str':
258
263
  session = await self._get_session(session_id)
259
264
  if session is None:
260
265
  return f"Error: session_id {session_id} is not running."
@@ -274,31 +279,42 @@ class UnifiedExecManager:
274
279
  max_output_tokens,
275
280
  )
276
281
 
277
- async def _allocate_session_id(self) -> int:
282
+ async def _allocate_session_id(self) -> 'int':
278
283
  async with self._lock:
279
284
  session_id = self._next_session_id
280
285
  self._next_session_id += 1
281
286
  return session_id
282
287
 
283
- async def _get_session(self, session_id: int) -> UnifiedExecSession | None:
288
+ async def _get_session(self, session_id: 'int') -> 'typing.Union[UnifiedExecSession, None]':
284
289
  async with self._lock:
285
290
  return self._sessions.get(session_id)
286
291
 
287
292
  async def _wait_and_snapshot(
288
293
  self,
289
- session_id: int,
290
- yield_time_ms: int,
291
- max_output_tokens: int | None,
292
- ) -> str:
294
+ session_id: 'int',
295
+ yield_time_ms: 'int',
296
+ max_output_tokens: 'typing.Union[int, None]',
297
+ ) -> 'str':
293
298
  session = await self._get_session(session_id)
294
299
  if session is None:
295
300
  return f"Error: session_id {session_id} is not running."
296
301
 
297
- start_wait = asyncio.get_running_loop().time()
302
+ loop = asyncio.get_running_loop()
303
+ start_wait = loop.time()
298
304
  try:
299
305
  await asyncio.wait_for(session.process.wait(), timeout=yield_time_ms / 1000.0)
300
306
  except asyncio.TimeoutError:
301
- pass
307
+ remaining_seconds = (yield_time_ms / 1000.0) - (loop.time() - start_wait)
308
+ if (
309
+ session.process.returncode is None
310
+ and not session.unread_output.has_data()
311
+ and remaining_seconds > 0
312
+ ):
313
+ session.output_event.clear()
314
+ try:
315
+ await asyncio.wait_for(session.output_event.wait(), timeout=remaining_seconds)
316
+ except asyncio.TimeoutError:
317
+ pass
302
318
 
303
319
  if session.reader_task is not None and session.process.returncode is not None:
304
320
  await session.reader_task
@@ -328,15 +344,15 @@ class UnifiedExecManager:
328
344
 
329
345
  return "\n".join(lines)
330
346
 
331
- async def _close_session(self, session_id: int) -> None:
347
+ async def _close_session(self, session_id: 'int') -> 'None':
332
348
  async with self._lock:
333
349
  session = self._sessions.pop(session_id, None)
334
350
  if session is None:
335
351
  return
336
- if session.process.stdin is not None and not session.process.stdin.is_closing():
352
+ if session.process.stdin is not None and not stream_writer_is_closing(session.process.stdin):
337
353
  session.process.stdin.close()
338
354
 
339
- async def _pump_output(self, session: UnifiedExecSession) -> None:
355
+ async def _pump_output(self, session: 'UnifiedExecSession') -> 'None':
340
356
  stream = session.process.stdout
341
357
  if stream is None:
342
358
  return
@@ -345,8 +361,10 @@ class UnifiedExecManager:
345
361
  if not chunk:
346
362
  break
347
363
  session.unread_output.push_chunk(chunk)
364
+ session.output_event.set()
365
+ session.output_event.set()
348
366
 
349
- def _resolve_workdir(self, workdir: str | None) -> Path:
367
+ def _resolve_workdir(self, workdir: 'typing.Union[str, None]') -> 'Path':
350
368
  if not workdir:
351
369
  return self._default_cwd
352
370
  path = Path(workdir)
@@ -356,10 +374,10 @@ class UnifiedExecManager:
356
374
 
357
375
  def _build_shell_command(
358
376
  self,
359
- cmd: str,
360
- shell: str | None,
361
- login: bool,
362
- ) -> list[str]:
377
+ cmd: 'str',
378
+ shell: 'typing.Union[str, None]',
379
+ login: 'bool',
380
+ ) -> 'typing.List[str]':
363
381
  shell_path = shell or os.environ.get("SHELL") or "/bin/bash"
364
382
  shell_name = Path(shell_path).name.lower()
365
383
  if shell_name in {"cmd", "cmd.exe"}:
@@ -368,13 +386,13 @@ class UnifiedExecManager:
368
386
  return [shell_path, "-NoProfile", "-Command", cmd]
369
387
  return [shell_path, "-lc" if login else "-c", cmd]
370
388
 
371
- def _estimate_token_count(self, output: str) -> int | None:
389
+ def _estimate_token_count(self, output: 'str') -> 'typing.Union[int, None]':
372
390
  return _approx_token_count(output)
373
391
 
374
- def _truncate_output(self, output: str, max_output_tokens: int | None) -> str:
392
+ def _truncate_output(self, output: 'str', max_output_tokens: 'typing.Union[int, None]') -> 'str':
375
393
  token_budget = DEFAULT_MAX_OUTPUT_TOKENS if max_output_tokens is None else max_output_tokens
376
394
  return _formatted_truncate_text(output, max(token_budget, 0))
377
395
 
378
- def _tty_echo(self, chars: str) -> bytes:
396
+ def _tty_echo(self, chars: 'str') -> 'bytes':
379
397
  normalized = chars.replace("\n", "\r\n")
380
398
  return normalized.encode("utf-8")
@@ -10,11 +10,10 @@ Expected behavior:
10
10
  confirmation text Codex uses.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  from ..protocol import JSONDict, JSONValue
16
14
  from ..runtime_services import PlanItem, PlanStore
17
15
  from .base_tool import BaseTool, ToolContext
16
+ import typing
18
17
 
19
18
  VALID_PLAN_STATUSES = {"pending", "in_progress", "completed"}
20
19
 
@@ -52,16 +51,16 @@ class UpdatePlanTool(BaseTool):
52
51
  }
53
52
  supports_parallel = False
54
53
 
55
- def __init__(self, plan_store: PlanStore) -> None:
54
+ def __init__(self, plan_store: 'PlanStore') -> 'None':
56
55
  self._plan_store = plan_store
57
56
 
58
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
57
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
59
58
  del context
60
59
  raw_plan = args.get("plan")
61
60
  if not isinstance(raw_plan, list):
62
61
  return "Error: `plan` must be a list."
63
62
 
64
- plan_items: list[PlanItem] = []
63
+ plan_items: 'typing.List[PlanItem]' = []
65
64
  for item in raw_plan:
66
65
  if not isinstance(item, dict):
67
66
  return "Error: each `plan` item must be an object."
@@ -12,14 +12,13 @@ Expected behavior:
12
12
  item that Codex uses when feeding image tool output back to the model.
13
13
  """
14
14
 
15
- from __future__ import annotations
16
-
17
15
  import base64
18
16
  import mimetypes
19
17
  from pathlib import Path
20
18
 
21
19
  from ..protocol import JSONDict, JSONValue
22
20
  from .base_tool import BaseTool, StructuredToolOutput, ToolContext
21
+ import typing
23
22
 
24
23
  VIEW_IMAGE_OUTPUT_SCHEMA = {
25
24
  "type": "object",
@@ -62,10 +61,10 @@ class ViewImageTool(BaseTool):
62
61
  }
63
62
  output_schema = VIEW_IMAGE_OUTPUT_SCHEMA
64
63
 
65
- def __init__(self, cwd: str | Path | None = None) -> None:
64
+ def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
66
65
  self._workspace_root = Path(cwd or Path.cwd()).resolve()
67
66
 
68
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
67
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
69
68
  del context
70
69
  path_value = str(args.get("path", "")).strip()
71
70
  if not path_value:
@@ -102,7 +101,7 @@ class ViewImageTool(BaseTool):
102
101
  "image_url": image_url,
103
102
  "detail": detail,
104
103
  }
105
- image_item: JSONDict = {
104
+ image_item: 'JSONDict' = {
106
105
  "type": "input_image",
107
106
  "image_url": image_url,
108
107
  }
@@ -9,8 +9,6 @@ Expected behavior:
9
9
  wait times out.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from ..protocol import JSONDict, JSONValue
15
13
  from ..runtime_services import SubAgentManager
16
14
  from .agent_tool_schemas import AGENT_STATUS_SCHEMA
@@ -60,10 +58,10 @@ class WaitAgentTool(BaseTool):
60
58
  output_schema = WAIT_AGENT_OUTPUT_SCHEMA
61
59
  supports_parallel = False
62
60
 
63
- def __init__(self, subagent_manager: SubAgentManager) -> None:
61
+ def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
64
62
  self._subagent_manager = subagent_manager
65
63
 
66
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
64
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
67
65
  del context
68
66
  ids = args.get("ids")
69
67
  if not isinstance(ids, list) or not ids:
@@ -9,11 +9,10 @@ Expected behavior:
9
9
  - Return only the new output since the previous `exec` / `wait` snapshot.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from ..protocol import JSONDict, JSONValue
15
13
  from .base_tool import BaseTool, ToolContext
16
14
  from .code_mode_manager import DEFAULT_WAIT_YIELD_TIME_MS, CodeModeManager
15
+ import typing
17
16
 
18
17
 
19
18
  class WaitTool(BaseTool):
@@ -46,10 +45,10 @@ class WaitTool(BaseTool):
46
45
  }
47
46
  supports_parallel = False
48
47
 
49
- def __init__(self, manager: CodeModeManager) -> None:
48
+ def __init__(self, manager: 'CodeModeManager') -> 'None':
50
49
  self._manager = manager
51
50
 
52
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
51
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
53
52
  del context
54
53
  cell_id = str(args.get("cell_id", "")).strip()
55
54
  if not cell_id:
@@ -61,7 +60,7 @@ class WaitTool(BaseTool):
61
60
  terminate=bool(args.get("terminate", False)),
62
61
  )
63
62
 
64
- def _optional_int(self, args: JSONDict, key: str) -> int | None:
63
+ def _optional_int(self, args: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
65
64
  value = args.get(key)
66
65
  if value in (None, ""):
67
66
  return None
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  - Never expect a local tool-call round-trip result from the model.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  from ..protocol import JSONDict, JSONValue
16
14
  from .base_tool import BaseTool, ToolContext
17
15
 
@@ -25,6 +23,6 @@ class WebSearchTool(BaseTool):
25
23
  }
26
24
  supports_parallel = False
27
25
 
28
- async def run(self, context: ToolContext, args: JSONValue) -> JSONValue:
26
+ async def run(self, context: 'ToolContext', args: 'JSONValue') -> 'JSONValue':
29
27
  del context, args
30
28
  return "Error: web_search is provider-native and should not be executed locally."
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  - Reuse the same `session_id` until the process exits.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  from ..protocol import JSONDict, JSONValue
16
14
  from .base_tool import BaseTool, ToolContext
17
15
  from .unified_exec_manager import (
@@ -19,6 +17,7 @@ from .unified_exec_manager import (
19
17
  UNIFIED_EXEC_OUTPUT_SCHEMA,
20
18
  UnifiedExecManager,
21
19
  )
20
+ import typing
22
21
 
23
22
 
24
23
  class WriteStdinTool(BaseTool):
@@ -50,10 +49,10 @@ class WriteStdinTool(BaseTool):
50
49
  output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
51
50
  supports_parallel = False
52
51
 
53
- def __init__(self, manager: UnifiedExecManager) -> None:
52
+ def __init__(self, manager: 'UnifiedExecManager') -> 'None':
54
53
  self._manager = manager
55
54
 
56
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
55
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
57
56
  del context
58
57
  session_id = args.get("session_id")
59
58
  if session_id is None:
@@ -68,7 +67,7 @@ class WriteStdinTool(BaseTool):
68
67
  max_output_tokens=self._optional_int(args, "max_output_tokens"),
69
68
  )
70
69
 
71
- def _optional_int(self, args: JSONDict, key: str) -> int | None:
70
+ def _optional_int(self, args: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
72
71
  value = args.get(key)
73
72
  if value in (None, ""):
74
73
  return None
pycodex/utils/dotenv.py CHANGED
@@ -1,14 +1,14 @@
1
- from __future__ import annotations
2
1
 
3
2
  import os
4
3
  from pathlib import Path
4
+ import typing
5
5
 
6
6
  ILLEGAL_ENV_VAR_PREFIX = "CODEX_"
7
7
  DOTENV_FILENAME = ".env"
8
- _LOADED_CODEX_DOTENV_HOMES: set[str] = set()
8
+ _LOADED_CODEX_DOTENV_HOMES: 'typing.Set[str]' = set()
9
9
 
10
10
 
11
- def load_codex_dotenv(config_path: str | Path) -> None:
11
+ def load_codex_dotenv(config_path: 'typing.Union[str, Path]') -> 'None':
12
12
  codex_home = str(Path(config_path).resolve().parent)
13
13
  if codex_home in _LOADED_CODEX_DOTENV_HOMES:
14
14
  return
@@ -26,8 +26,8 @@ def load_codex_dotenv(config_path: str | Path) -> None:
26
26
  _LOADED_CODEX_DOTENV_HOMES.add(codex_home)
27
27
 
28
28
 
29
- def parse_dotenv(text: str) -> dict[str, str]:
30
- values: dict[str, str] = {}
29
+ def parse_dotenv(text: 'str') -> 'typing.Dict[str, str]':
30
+ values: 'typing.Dict[str, str]' = {}
31
31
  for raw_line in text.splitlines():
32
32
  line = raw_line.strip()
33
33
  if not line or line.startswith("#"):
@@ -45,7 +45,7 @@ def parse_dotenv(text: str) -> dict[str, str]:
45
45
  return values
46
46
 
47
47
 
48
- def parse_dotenv_value(raw_value: str) -> str:
48
+ def parse_dotenv_value(raw_value: 'str') -> 'str':
49
49
  if not raw_value:
50
50
  return ""
51
51