python-codex 0.1.2__py3-none-any.whl → 0.1.4__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 (60) hide show
  1. pycodex/__init__.py +5 -1
  2. pycodex/agent.py +89 -51
  3. pycodex/cli.py +152 -45
  4. pycodex/collaboration.py +6 -7
  5. pycodex/compat.py +99 -0
  6. pycodex/context.py +110 -87
  7. pycodex/doctor.py +40 -40
  8. pycodex/model.py +429 -90
  9. pycodex/portable.py +33 -33
  10. pycodex/portable_server.py +22 -21
  11. pycodex/prompts/models.json +30 -0
  12. pycodex/protocol.py +84 -86
  13. pycodex/runtime.py +36 -35
  14. pycodex/runtime_services.py +69 -69
  15. pycodex/tools/agent_tool_schemas.py +0 -2
  16. pycodex/tools/apply_patch_tool.py +45 -46
  17. pycodex/tools/base_tool.py +35 -36
  18. pycodex/tools/close_agent_tool.py +2 -4
  19. pycodex/tools/code_mode_manager.py +61 -61
  20. pycodex/tools/exec_command_tool.py +5 -6
  21. pycodex/tools/exec_runtime.js +3 -3
  22. pycodex/tools/exec_tool.py +2 -4
  23. pycodex/tools/grep_files_tool.py +10 -11
  24. pycodex/tools/list_dir_tool.py +8 -9
  25. pycodex/tools/read_file_tool.py +13 -14
  26. pycodex/tools/request_permissions_tool.py +2 -4
  27. pycodex/tools/request_user_input_tool.py +13 -14
  28. pycodex/tools/resume_agent_tool.py +2 -4
  29. pycodex/tools/send_input_tool.py +8 -9
  30. pycodex/tools/shell_command_tool.py +5 -6
  31. pycodex/tools/shell_tool.py +5 -6
  32. pycodex/tools/spawn_agent_tool.py +4 -5
  33. pycodex/tools/unified_exec_manager.py +62 -61
  34. pycodex/tools/update_plan_tool.py +4 -5
  35. pycodex/tools/view_image_tool.py +4 -5
  36. pycodex/tools/wait_agent_tool.py +2 -4
  37. pycodex/tools/wait_tool.py +4 -5
  38. pycodex/tools/web_search_tool.py +1 -3
  39. pycodex/tools/write_stdin_tool.py +4 -5
  40. pycodex/utils/__init__.py +4 -0
  41. pycodex/utils/compactor.py +189 -0
  42. pycodex/utils/dotenv.py +6 -6
  43. pycodex/utils/get_env.py +37 -33
  44. pycodex/utils/random_ids.py +1 -2
  45. pycodex/utils/session_persist.py +483 -0
  46. pycodex/utils/visualize.py +197 -83
  47. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/METADATA +32 -11
  48. python_codex-0.1.4.dist-info/RECORD +76 -0
  49. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/WHEEL +1 -1
  50. responses_server/app.py +32 -20
  51. responses_server/config.py +17 -17
  52. responses_server/payload_processors.py +26 -17
  53. responses_server/server.py +11 -11
  54. responses_server/session_store.py +10 -10
  55. responses_server/stream_router.py +83 -64
  56. responses_server/tools/custom_adapter.py +12 -12
  57. responses_server/tools/web_search.py +33 -33
  58. python_codex-0.1.2.dist-info/RECORD +0 -73
  59. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/entry_points.txt +0 -0
  60. {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/licenses/LICENSE +0 -0
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  model, and dispatches `ToolCall` executions back into `ToolResult`s.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  import inspect
16
14
  from abc import ABC, abstractmethod
17
15
  from dataclasses import dataclass
@@ -20,6 +18,7 @@ import json
20
18
  from pathlib import Path
21
19
 
22
20
  from ..protocol import ConversationItem, JSONDict, JSONValue, ToolCall, ToolResult, ToolSpec
21
+ import typing
23
22
 
24
23
  EXEC_TOOLS_SNAPSHOT_PATH = (
25
24
  Path(__file__).resolve().parent.parent / "prompts" / "exec_tools.json"
@@ -27,8 +26,8 @@ EXEC_TOOLS_SNAPSHOT_PATH = (
27
26
 
28
27
 
29
28
  @lru_cache(maxsize=1)
30
- def _load_exec_tool_payloads() -> dict[str, JSONDict]:
31
- payloads: dict[str, JSONDict] = {}
29
+ def _load_exec_tool_payloads() -> 'typing.Dict[str, JSONDict]':
30
+ payloads: 'typing.Dict[str, JSONDict]' = {}
32
31
  for payload in json.loads(EXEC_TOOLS_SNAPSHOT_PATH.read_text()):
33
32
  if not isinstance(payload, dict):
34
33
  continue
@@ -41,36 +40,36 @@ def _load_exec_tool_payloads() -> dict[str, JSONDict]:
41
40
  return payloads
42
41
 
43
42
 
44
- @dataclass(frozen=True, slots=True)
43
+ @dataclass(frozen=True, )
45
44
  class ToolContext:
46
- turn_id: str
47
- history: tuple[ConversationItem, ...]
48
- collaboration_mode: str = "default"
45
+ turn_id: 'str'
46
+ history: 'typing.Tuple[ConversationItem, ...]'
47
+ collaboration_mode: 'str' = "default"
49
48
 
50
49
 
51
50
  class StructuredToolOutput:
52
51
  def __init__(
53
52
  self,
54
- output: JSONValue,
55
- content_items: tuple[JSONDict, ...] | list[JSONDict] | None = None,
56
- success: bool | None = None,
57
- ) -> None:
53
+ output: 'JSONValue',
54
+ content_items: 'typing.Union[typing.Union[typing.Tuple[JSONDict, ...], typing.List[JSONDict]], None]' = None,
55
+ success: 'typing.Union[bool, None]' = None,
56
+ ) -> 'None':
58
57
  self.output = output
59
58
  self.content_items = None if content_items is None else tuple(content_items)
60
59
  self.success = success
61
60
 
62
61
 
63
62
  class BaseTool(ABC):
64
- name: str
65
- description: str
66
- input_schema: JSONDict | None = None
67
- tool_type: str = "function"
68
- format: JSONDict | None = None
69
- options: JSONDict | None = None
70
- output_schema: JSONDict | None = None
71
- supports_parallel: bool = True
72
-
73
- def spec(self) -> ToolSpec:
63
+ name: 'str'
64
+ description: 'str'
65
+ input_schema: 'typing.Union[JSONDict, None]' = None
66
+ tool_type: 'str' = "function"
67
+ format: 'typing.Union[JSONDict, None]' = None
68
+ options: 'typing.Union[JSONDict, None]' = None
69
+ output_schema: 'typing.Union[JSONDict, None]' = None
70
+ supports_parallel: 'bool' = True
71
+
72
+ def spec(self) -> 'ToolSpec':
74
73
  return ToolSpec(
75
74
  name=self.name,
76
75
  description=self.description,
@@ -83,32 +82,32 @@ class BaseTool(ABC):
83
82
  raw_payload=self.raw_payload(),
84
83
  )
85
84
 
86
- def serialize(self) -> JSONDict:
85
+ def serialize(self) -> 'JSONDict':
87
86
  return self.spec().serialize()
88
87
 
89
- def raw_payload(self) -> JSONDict | None:
88
+ def raw_payload(self) -> 'typing.Union[JSONDict, None]':
90
89
  return _load_exec_tool_payloads().get(self.name)
91
90
 
92
91
  @abstractmethod
93
- async def run(self, context: ToolContext, args: JSONValue) -> JSONValue:
92
+ async def run(self, context: 'ToolContext', args: 'JSONValue') -> 'JSONValue':
94
93
  raise NotImplementedError
95
94
 
96
95
 
97
96
  class ToolRegistry:
98
- def __init__(self) -> None:
99
- self._tools: dict[str, BaseTool] = {}
97
+ def __init__(self) -> 'None':
98
+ self._tools: 'typing.Dict[str, BaseTool]' = {}
100
99
 
101
- def register(self, tool: BaseTool) -> None:
100
+ def register(self, tool: 'BaseTool') -> 'None':
102
101
  self._tools[tool.name] = tool
103
102
 
104
- def model_visible_specs(self) -> list[ToolSpec]:
103
+ def model_visible_specs(self) -> 'typing.List[ToolSpec]':
105
104
  return [tool.spec() for tool in self._tools.values()]
106
105
 
107
- def supports_parallel(self, tool_name: str) -> bool:
106
+ def supports_parallel(self, tool_name: 'str') -> 'bool':
108
107
  tool = self._tools.get(tool_name)
109
108
  return False if tool is None else tool.supports_parallel
110
109
 
111
- async def execute(self, call: ToolCall, context: ToolContext) -> ToolResult:
110
+ async def execute(self, call: 'ToolCall', context: 'ToolContext') -> 'ToolResult':
112
111
  tool = self._tools.get(call.name)
113
112
  if tool is None:
114
113
  return ToolResult(
@@ -149,19 +148,19 @@ class ToolRegistry:
149
148
  tool_type=call.tool_type,
150
149
  )
151
150
 
152
- def __contains__(self, tool_name: str) -> bool:
151
+ def __contains__(self, tool_name: 'str') -> 'bool':
153
152
  return tool_name in self._tools
154
153
 
155
- def __len__(self) -> int:
154
+ def __len__(self) -> 'int':
156
155
  return len(self._tools)
157
156
 
158
- def names(self) -> tuple[str, ...]:
157
+ def names(self) -> 'typing.Tuple[str, ...]':
159
158
  return tuple(self._tools)
160
159
 
161
- def get_tool(self, tool_name: str) -> BaseTool | None:
160
+ def get_tool(self, tool_name: 'str') -> 'typing.Union[BaseTool, None]':
162
161
  return self._tools.get(tool_name)
163
162
 
164
- def tools(self) -> tuple[BaseTool, ...]:
163
+ def tools(self) -> 'typing.Tuple[BaseTool, ...]':
165
164
  return tuple(self._tools.values())
166
165
 
167
166
 
@@ -8,8 +8,6 @@ Expected behavior:
8
8
  - Return the agent status observed at close time.
9
9
  """
10
10
 
11
- from __future__ import annotations
12
-
13
11
  from ..protocol import JSONDict, JSONValue
14
12
  from ..runtime_services import SubAgentManager
15
13
  from .agent_tool_schemas import AGENT_STATUS_SCHEMA
@@ -44,10 +42,10 @@ class CloseAgentTool(BaseTool):
44
42
  output_schema = CLOSE_AGENT_OUTPUT_SCHEMA
45
43
  supports_parallel = False
46
44
 
47
- def __init__(self, subagent_manager: SubAgentManager) -> None:
45
+ def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
48
46
  self._subagent_manager = subagent_manager
49
47
 
50
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
48
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
51
49
  del context
52
50
  agent_id = str(args.get("id", "")).strip()
53
51
  if not agent_id:
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  `wait`.
11
11
  """
12
12
 
13
- from __future__ import annotations
14
-
15
13
  import asyncio
16
14
  import json
17
15
  import math
@@ -21,8 +19,10 @@ from pathlib import Path
21
19
 
22
20
  from loguru import logger
23
21
 
22
+ from ..compat import is_ascii, stream_writer_is_closing
24
23
  from ..protocol import JSONDict, JSONValue, ToolCall
25
24
  from .base_tool import StructuredToolOutput, ToolContext, ToolRegistry
25
+ import typing
26
26
 
27
27
  DEFAULT_WAIT_YIELD_TIME_MS = 10_000
28
28
  DEFAULT_MAX_OUTPUT_TOKENS = 10_000
@@ -31,41 +31,41 @@ EXEC_PRAGMA_PREFIX = "// @exec:"
31
31
  WAIT_COMPLETION_GRACE_SECONDS = 0.02
32
32
 
33
33
 
34
- @dataclass(slots=True)
34
+ @dataclass
35
35
  class ExecCell:
36
- cell_id: str
37
- process: asyncio.subprocess.Process
38
- started_at: float
39
- output_items: list[JSONDict] = field(default_factory=list)
40
- delivered_count: int = 0
41
- reader_task: asyncio.Task | None = None
42
- stderr_task: asyncio.Task | None = None
43
- yield_event: asyncio.Event = field(default_factory=asyncio.Event)
44
- output_event: asyncio.Event = field(default_factory=asyncio.Event)
45
- done_event: asyncio.Event = field(default_factory=asyncio.Event)
46
- completed: bool = False
47
- terminated: bool = False
48
- error_text: str | None = None
49
- stderr_chunks: list[str] = field(default_factory=list)
50
-
51
-
52
- @dataclass(frozen=True, slots=True)
36
+ cell_id: 'str'
37
+ process: 'asyncio.subprocess.Process'
38
+ started_at: 'float'
39
+ output_items: 'typing.List[JSONDict]' = field(default_factory=list)
40
+ delivered_count: 'int' = 0
41
+ reader_task: 'typing.Union[asyncio.Task, None]' = None
42
+ stderr_task: 'typing.Union[asyncio.Task, None]' = None
43
+ yield_event: 'asyncio.Event' = field(default_factory=asyncio.Event)
44
+ output_event: 'asyncio.Event' = field(default_factory=asyncio.Event)
45
+ done_event: 'asyncio.Event' = field(default_factory=asyncio.Event)
46
+ completed: 'bool' = False
47
+ terminated: 'bool' = False
48
+ error_text: 'typing.Union[str, None]' = None
49
+ stderr_chunks: 'typing.List[str]' = field(default_factory=list)
50
+
51
+
52
+ @dataclass(frozen=True, )
53
53
  class ParsedExecSource:
54
- code: str
55
- yield_time_ms: int | None
56
- max_output_tokens: int | None
54
+ code: 'str'
55
+ yield_time_ms: 'typing.Union[int, None]'
56
+ max_output_tokens: 'typing.Union[int, None]'
57
57
 
58
58
 
59
59
  class CodeModeManager:
60
- def __init__(self, registry: ToolRegistry, cwd: str | Path | None = None) -> None:
60
+ def __init__(self, registry: 'ToolRegistry', cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
61
61
  self._registry = registry
62
62
  self._default_cwd = Path(cwd or Path.cwd()).resolve()
63
63
  self._runtime_script = Path(__file__).with_name("exec_runtime.js")
64
- self._stored_values: dict[str, JSONValue] = {}
65
- self._cells: dict[str, ExecCell] = {}
64
+ self._stored_values: 'typing.Dict[str, JSONValue]' = {}
65
+ self._cells: 'typing.Dict[str, ExecCell]' = {}
66
66
  self._lock = asyncio.Lock()
67
67
 
68
- async def exec(self, source: str, context: ToolContext) -> StructuredToolOutput | str:
68
+ async def exec(self, source: 'str', context: 'ToolContext') -> 'typing.Union[StructuredToolOutput, str]':
69
69
  try:
70
70
  parsed = self._parse_exec_source(source)
71
71
  except ValueError as exc:
@@ -77,11 +77,11 @@ class CodeModeManager:
77
77
 
78
78
  async def wait(
79
79
  self,
80
- cell_id: str,
81
- yield_time_ms: int,
82
- max_tokens: int | None,
83
- terminate: bool,
84
- ) -> StructuredToolOutput | str:
80
+ cell_id: 'str',
81
+ yield_time_ms: 'int',
82
+ max_tokens: 'typing.Union[int, None]',
83
+ terminate: 'bool',
84
+ ) -> 'typing.Union[StructuredToolOutput, str]':
85
85
  cell = self._cells.get(cell_id)
86
86
  if cell is None:
87
87
  return f"Error: unknown exec cell `{cell_id}`."
@@ -98,8 +98,8 @@ class CodeModeManager:
98
98
  await self._wait_for_wait(cell, yield_time_ms)
99
99
  return await self._snapshot_cell(cell, max_tokens)
100
100
 
101
- def enabled_tools(self) -> list[dict[str, str]]:
102
- enabled: list[dict[str, str]] = []
101
+ def enabled_tools(self) -> 'typing.List[typing.Dict[str, str]]':
102
+ enabled: 'typing.List[typing.Dict[str, str]]' = []
103
103
  for tool in self._registry.tools():
104
104
  if tool.name in {"exec", "wait"}:
105
105
  continue
@@ -116,7 +116,7 @@ class CodeModeManager:
116
116
  enabled.sort(key=lambda item: item["tool_name"])
117
117
  return enabled
118
118
 
119
- async def _start_cell(self, code: str, context: ToolContext) -> ExecCell:
119
+ async def _start_cell(self, code: 'str', context: 'ToolContext') -> 'ExecCell':
120
120
  cell_id = uuid.uuid4().hex[:10]
121
121
  process = await asyncio.create_subprocess_exec(
122
122
  "node",
@@ -147,7 +147,7 @@ class CodeModeManager:
147
147
  logger.debug("exec start cell_id={} cwd={}", cell_id, self._default_cwd)
148
148
  return cell
149
149
 
150
- async def _read_stdout(self, cell: ExecCell, context: ToolContext) -> None:
150
+ async def _read_stdout(self, cell: 'ExecCell', context: 'ToolContext') -> 'None':
151
151
  stream = cell.process.stdout
152
152
  if stream is None:
153
153
  cell.error_text = "missing stdout pipe"
@@ -181,7 +181,7 @@ class CodeModeManager:
181
181
  cell.output_event.set()
182
182
  continue
183
183
  if msg_type == "output_image":
184
- image_item: JSONDict = {
184
+ image_item: 'JSONDict' = {
185
185
  "type": "input_image",
186
186
  "image_url": str(message.get("image_url", "")),
187
187
  }
@@ -220,7 +220,7 @@ class CodeModeManager:
220
220
  cell.done_event.set()
221
221
  cell.output_event.set()
222
222
 
223
- async def _read_stderr(self, cell: ExecCell) -> None:
223
+ async def _read_stderr(self, cell: 'ExecCell') -> 'None':
224
224
  stream = cell.process.stderr
225
225
  if stream is None:
226
226
  return
@@ -232,10 +232,10 @@ class CodeModeManager:
232
232
 
233
233
  async def _handle_nested_tool_call(
234
234
  self,
235
- cell: ExecCell,
236
- context: ToolContext,
237
- message: JSONDict,
238
- ) -> None:
235
+ cell: 'ExecCell',
236
+ context: 'ToolContext',
237
+ message: 'JSONDict',
238
+ ) -> 'None':
239
239
  tool_name = str(message.get("tool_name", ""))
240
240
  request_id = str(message.get("id", ""))
241
241
  tool = self._registry.get_tool(tool_name)
@@ -287,14 +287,14 @@ class CodeModeManager:
287
287
  }
288
288
  await self._send_message(cell, payload)
289
289
 
290
- async def _send_message(self, cell: ExecCell, payload: JSONDict) -> None:
290
+ async def _send_message(self, cell: 'ExecCell', payload: 'JSONDict') -> 'None':
291
291
  stdin = cell.process.stdin
292
- if stdin is None or stdin.is_closing():
292
+ if stdin is None or stream_writer_is_closing(stdin):
293
293
  return
294
294
  stdin.write((json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
295
295
  await stdin.drain()
296
296
 
297
- async def _wait_for_exec(self, cell: ExecCell, yield_time_ms: int | None) -> None:
297
+ async def _wait_for_exec(self, cell: 'ExecCell', yield_time_ms: 'typing.Union[int, None]') -> 'None':
298
298
  done_task = asyncio.create_task(cell.done_event.wait())
299
299
  yield_task = asyncio.create_task(cell.yield_event.wait())
300
300
  tasks = {done_task, yield_task}
@@ -314,7 +314,7 @@ class CodeModeManager:
314
314
  task.cancel()
315
315
  cell.yield_event.clear()
316
316
 
317
- async def _wait_for_wait(self, cell: ExecCell, yield_time_ms: int) -> None:
317
+ async def _wait_for_wait(self, cell: 'ExecCell', yield_time_ms: 'int') -> 'None':
318
318
  loop = asyncio.get_running_loop()
319
319
  deadline = loop.time() + max(yield_time_ms, 1) / 1000.0
320
320
  initial_count = cell.delivered_count
@@ -357,9 +357,9 @@ class CodeModeManager:
357
357
 
358
358
  async def _wait_for_completion_grace(
359
359
  self,
360
- cell: ExecCell,
361
- timeout_seconds: float,
362
- ) -> None:
360
+ cell: 'ExecCell',
361
+ timeout_seconds: 'float',
362
+ ) -> 'None':
363
363
  if timeout_seconds <= 0:
364
364
  return
365
365
  done_task = asyncio.create_task(cell.done_event.wait())
@@ -378,9 +378,9 @@ class CodeModeManager:
378
378
 
379
379
  async def _snapshot_cell(
380
380
  self,
381
- cell: ExecCell,
382
- max_tokens: int | None,
383
- ) -> StructuredToolOutput:
381
+ cell: 'ExecCell',
382
+ max_tokens: 'typing.Union[int, None]',
383
+ ) -> 'StructuredToolOutput':
384
384
  if cell.process.returncode is not None and cell.reader_task is not None:
385
385
  await cell.reader_task
386
386
 
@@ -421,13 +421,13 @@ class CodeModeManager:
421
421
 
422
422
  def _truncate_content_items(
423
423
  self,
424
- items: list[JSONDict],
425
- max_tokens: int | None,
426
- ) -> list[JSONDict]:
424
+ items: 'typing.List[JSONDict]',
425
+ max_tokens: 'typing.Union[int, None]',
426
+ ) -> 'typing.List[JSONDict]':
427
427
  token_budget = DEFAULT_MAX_OUTPUT_TOKENS if max_tokens is None else max_tokens
428
428
  max_chars = max(1, token_budget) * CHARS_PER_TOKEN
429
429
  total_chars = 0
430
- truncated: list[JSONDict] = []
430
+ truncated: 'typing.List[JSONDict]' = []
431
431
  for item in items:
432
432
  if item.get("type") != "input_text":
433
433
  truncated.append(item)
@@ -449,7 +449,7 @@ class CodeModeManager:
449
449
  total_chars += len(text)
450
450
  return truncated
451
451
 
452
- def _status_text(self, cell: ExecCell) -> str:
452
+ def _status_text(self, cell: 'ExecCell') -> 'str':
453
453
  if cell.terminated:
454
454
  return "Script terminated"
455
455
  if not cell.done_event.is_set():
@@ -458,7 +458,7 @@ class CodeModeManager:
458
458
  return "Script failed"
459
459
  return "Script completed"
460
460
 
461
- def _parse_exec_source(self, input_text: str) -> ParsedExecSource:
461
+ def _parse_exec_source(self, input_text: 'str') -> 'ParsedExecSource':
462
462
  if not input_text.strip():
463
463
  raise ValueError(
464
464
  "exec expects raw JavaScript source text (non-empty)."
@@ -499,13 +499,13 @@ class CodeModeManager:
499
499
  max_output_tokens=max_output_tokens,
500
500
  )
501
501
 
502
- def _normalize_identifier(self, tool_name: str) -> str:
502
+ def _normalize_identifier(self, tool_name: 'str') -> 'str':
503
503
  identifier = []
504
504
  for index, char in enumerate(tool_name):
505
505
  is_valid = (
506
506
  char == "_"
507
507
  or char == "$"
508
- or (char.isascii() and char.isalnum() and (index != 0 or char.isalpha()))
508
+ or (is_ascii(char) and char.isalnum() and (index != 0 or char.isalpha()))
509
509
  )
510
510
  if is_valid:
511
511
  identifier.append(char)
@@ -513,7 +513,7 @@ class CodeModeManager:
513
513
  identifier.append("_")
514
514
  return "".join(identifier) or "_"
515
515
 
516
- def _coerce_optional_text(self, value: JSONValue) -> str | None:
516
+ def _coerce_optional_text(self, value: 'JSONValue') -> 'typing.Union[str, None]':
517
517
  if value in (None, ""):
518
518
  return None
519
519
  return str(value)
@@ -10,8 +10,6 @@ Expected behavior:
10
10
  `write_stdin`.
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 (
@@ -21,6 +19,7 @@ from .unified_exec_manager import (
21
19
  UNIFIED_EXEC_OUTPUT_SCHEMA,
22
20
  UnifiedExecManager,
23
21
  )
22
+ import typing
24
23
 
25
24
 
26
25
  class ExecCommandTool(BaseTool):
@@ -64,10 +63,10 @@ class ExecCommandTool(BaseTool):
64
63
  output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
65
64
  supports_parallel = False
66
65
 
67
- def __init__(self, manager: UnifiedExecManager) -> None:
66
+ def __init__(self, manager: 'UnifiedExecManager') -> 'None':
68
67
  self._manager = manager
69
68
 
70
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
69
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
71
70
  del context
72
71
  cmd = str(args.get("cmd", "")).strip()
73
72
  if not cmd:
@@ -83,13 +82,13 @@ class ExecCommandTool(BaseTool):
83
82
  max_output_tokens=self._optional_int(args, "max_output_tokens"),
84
83
  )
85
84
 
86
- def _optional_string(self, args: JSONDict, key: str) -> str | None:
85
+ def _optional_string(self, args: 'JSONDict', key: 'str') -> 'typing.Union[str, None]':
87
86
  value = args.get(key)
88
87
  if value in (None, ""):
89
88
  return None
90
89
  return str(value)
91
90
 
92
- def _optional_int(self, args: JSONDict, key: str) -> int | None:
91
+ def _optional_int(self, args: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
93
92
  value = args.get(key)
94
93
  if value in (None, ""):
95
94
  return None
@@ -1,5 +1,5 @@
1
- const readline = require('node:readline');
2
- const { stdin, stdout } = require('node:process');
1
+ const readline = require('readline');
2
+ const { stdin, stdout } = require('process');
3
3
 
4
4
  const pending = new Map();
5
5
  let storedValues = {};
@@ -41,7 +41,7 @@ function image(value) {
41
41
  send({
42
42
  type: 'output_image',
43
43
  image_url: value.image_url,
44
- detail: value.detail ?? null,
44
+ detail: value.detail === undefined ? null : value.detail,
45
45
  });
46
46
  return;
47
47
  }
@@ -11,8 +11,6 @@ Expected behavior:
11
11
  resumed via `wait`.
12
12
  """
13
13
 
14
- from __future__ import annotations
15
-
16
14
  from ..protocol import JSONValue
17
15
  from .base_tool import BaseTool, ToolContext
18
16
  from .code_mode_manager import CodeModeManager
@@ -41,8 +39,8 @@ class ExecTool(BaseTool):
41
39
  }
42
40
  supports_parallel = False
43
41
 
44
- def __init__(self, manager: CodeModeManager) -> None:
42
+ def __init__(self, manager: 'CodeModeManager') -> 'None':
45
43
  self._manager = manager
46
44
 
47
- async def run(self, context: ToolContext, args: JSONValue) -> JSONValue:
45
+ async def run(self, context: 'ToolContext', args: 'JSONValue') -> 'JSONValue':
48
46
  return await self._manager.exec(str(args), context)
@@ -9,8 +9,6 @@ Expected behavior:
9
9
  parameters rather than delegating to a shell transcript.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  import asyncio
15
13
  import fnmatch
16
14
  import re
@@ -18,6 +16,7 @@ from pathlib import Path
18
16
 
19
17
  from ..protocol import JSONDict, JSONValue
20
18
  from .base_tool import BaseTool, ToolContext
19
+ import typing
21
20
 
22
21
  DEFAULT_LIMIT = 100
23
22
  MAX_LIMIT = 2000
@@ -41,10 +40,10 @@ class GrepFilesTool(BaseTool):
41
40
  "required": ["pattern"],
42
41
  }
43
42
 
44
- def __init__(self, cwd: str | Path | None = None) -> None:
43
+ def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
45
44
  self._working_directory = Path(cwd or Path.cwd()).resolve()
46
45
 
47
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
46
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
48
47
  del context
49
48
  pattern = str(args.get("pattern", "")).strip()
50
49
  include = str(args.get("include", "")).strip() or None
@@ -111,13 +110,13 @@ class GrepFilesTool(BaseTool):
111
110
 
112
111
  def _search_with_python(
113
112
  self,
114
- pattern: str,
115
- include: str | None,
116
- search_path: Path,
117
- limit: int,
118
- ) -> list[str]:
113
+ pattern: 'str',
114
+ include: 'typing.Union[str, None]',
115
+ search_path: 'Path',
116
+ limit: 'int',
117
+ ) -> 'typing.List[str]':
119
118
  regex = re.compile(pattern)
120
- candidates: list[Path] = []
119
+ candidates: 'typing.List[Path]' = []
121
120
 
122
121
  if search_path.is_file():
123
122
  candidates = [search_path]
@@ -141,7 +140,7 @@ class GrepFilesTool(BaseTool):
141
140
  matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
142
141
  return [str(path) for path in matches[:limit]]
143
142
 
144
- def _resolve_path(self, path_arg) -> Path:
143
+ def _resolve_path(self, path_arg) -> 'Path':
145
144
  if path_arg in (None, ""):
146
145
  return self._working_directory
147
146
  path = Path(str(path_arg))
@@ -9,13 +9,12 @@ Expected behavior:
9
9
  commands like `find` or `ls -R`.
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from collections import deque
15
13
  from pathlib import Path
16
14
 
17
15
  from ..protocol import JSONDict, JSONValue
18
16
  from .base_tool import BaseTool, ToolContext
17
+ import typing
19
18
 
20
19
  MAX_ENTRY_LENGTH = 500
21
20
  INDENTATION_SPACES = 2
@@ -38,7 +37,7 @@ class ListDirTool(BaseTool):
38
37
  "required": ["dir_path"],
39
38
  }
40
39
 
41
- async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
40
+ async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
42
41
  del context
43
42
  dir_path = Path(str(args.get("dir_path", "")))
44
43
  offset = int(args.get("offset", 1))
@@ -74,8 +73,8 @@ class ListDirTool(BaseTool):
74
73
  lines.append(f"More than {len(selected)} entries found")
75
74
  return "\n".join(lines)
76
75
 
77
- def _collect_entries(self, root: Path, depth: int) -> list[dict[str, object]]:
78
- entries: list[dict[str, object]] = []
76
+ def _collect_entries(self, root: 'Path', depth: 'int') -> 'typing.List[typing.Dict[str, object]]':
77
+ entries: 'typing.List[typing.Dict[str, object]]' = []
79
78
  queue = deque([(root, Path(), depth)])
80
79
 
81
80
  while queue:
@@ -106,7 +105,7 @@ class ListDirTool(BaseTool):
106
105
  entries.sort(key=lambda entry: entry["name"])
107
106
  return entries
108
107
 
109
- def _entry_kind(self, path: Path) -> str:
108
+ def _entry_kind(self, path: 'Path') -> 'str':
110
109
  if path.is_symlink():
111
110
  return "symlink"
112
111
  if path.is_dir():
@@ -115,14 +114,14 @@ class ListDirTool(BaseTool):
115
114
  return "file"
116
115
  return "other"
117
116
 
118
- def _format_entry_name(self, path: Path) -> str:
117
+ def _format_entry_name(self, path: 'Path') -> 'str':
119
118
  text = path.as_posix()
120
119
  return text[:MAX_ENTRY_LENGTH]
121
120
 
122
- def _format_component(self, name: str) -> str:
121
+ def _format_component(self, name: 'str') -> 'str':
123
122
  return name[:MAX_ENTRY_LENGTH]
124
123
 
125
- def _format_entry_line(self, entry: dict[str, object]) -> str:
124
+ def _format_entry_line(self, entry: 'typing.Dict[str, object]') -> 'str':
126
125
  indent = " " * (int(entry["depth"]) * INDENTATION_SPACES)
127
126
  name = str(entry["display_name"])
128
127
  kind = str(entry["kind"])