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
@@ -13,6 +13,8 @@ Expected behavior:
13
13
  import asyncio
14
14
  import json
15
15
  import math
16
+ import threading
17
+ import typing
16
18
  import uuid
17
19
  from dataclasses import dataclass, field
18
20
  from pathlib import Path
@@ -23,7 +25,6 @@ from ..compat import is_ascii, stream_writer_is_closing
23
25
  from ..protocol import JSONDict, JSONValue, ToolCall
24
26
  from ..utils.truncation import DEFAULT_MAX_OUTPUT_TOKENS
25
27
  from .base_tool import StructuredToolOutput, ToolContext, ToolRegistry
26
- import typing
27
28
 
28
29
  DEFAULT_WAIT_YIELD_TIME_MS = 10_000
29
30
  CHARS_PER_TOKEN = 4
@@ -33,39 +34,47 @@ WAIT_COMPLETION_GRACE_SECONDS = 0.02
33
34
 
34
35
  @dataclass
35
36
  class ExecCell:
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, )
37
+ cell_id: "str"
38
+ process: "asyncio.subprocess.Process"
39
+ started_at: "float"
40
+ output_items: "typing.List[JSONDict]" = field(default_factory=list)
41
+ delivered_count: "int" = 0
42
+ reader_task: "typing.Union[asyncio.Task, None]" = None
43
+ stderr_task: "typing.Union[asyncio.Task, None]" = None
44
+ yield_event: "asyncio.Event" = field(default_factory=asyncio.Event)
45
+ output_event: "asyncio.Event" = field(default_factory=asyncio.Event)
46
+ done_event: "asyncio.Event" = field(default_factory=asyncio.Event)
47
+ completed: "bool" = False
48
+ terminated: "bool" = False
49
+ error_text: "typing.Union[str, None]" = None
50
+ stderr_chunks: "typing.List[str]" = field(default_factory=list)
51
+
52
+
53
+ @dataclass(
54
+ frozen=True,
55
+ )
53
56
  class ParsedExecSource:
54
- code: 'str'
55
- yield_time_ms: 'typing.Union[int, None]'
56
- max_output_tokens: 'typing.Union[int, None]'
57
+ code: "str"
58
+ yield_time_ms: "typing.Union[int, None]"
59
+ max_output_tokens: "typing.Union[int, None]"
57
60
 
58
61
 
59
62
  class CodeModeManager:
60
- def __init__(self, registry: 'ToolRegistry', cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
63
+ def __init__(
64
+ self,
65
+ registry: "ToolRegistry",
66
+ cwd: "typing.Union[typing.Union[str, Path], None]" = None,
67
+ ) -> "None":
61
68
  self._registry = registry
62
69
  self._default_cwd = Path(cwd or Path.cwd()).resolve()
63
70
  self._runtime_script = Path(__file__).with_name("exec_runtime.js")
64
- self._stored_values: 'typing.Dict[str, JSONValue]' = {}
65
- self._cells: 'typing.Dict[str, ExecCell]' = {}
66
- self._lock = asyncio.Lock()
71
+ self._stored_values: "typing.Dict[str, JSONValue]" = {}
72
+ self._cells: "typing.Dict[str, ExecCell]" = {}
73
+ self._lock = threading.Lock()
67
74
 
68
- async def exec(self, source: 'str', context: 'ToolContext') -> 'typing.Union[StructuredToolOutput, str]':
75
+ async def exec(
76
+ self, source: "str", context: "ToolContext"
77
+ ) -> "typing.Union[StructuredToolOutput, str]":
69
78
  try:
70
79
  parsed = self._parse_exec_source(source)
71
80
  except ValueError as exc:
@@ -77,11 +86,11 @@ class CodeModeManager:
77
86
 
78
87
  async def wait(
79
88
  self,
80
- cell_id: 'str',
81
- yield_time_ms: 'int',
82
- max_tokens: 'typing.Union[int, None]',
83
- terminate: 'bool',
84
- ) -> 'typing.Union[StructuredToolOutput, str]':
89
+ cell_id: "str",
90
+ yield_time_ms: "int",
91
+ max_tokens: "typing.Union[int, None]",
92
+ terminate: "bool",
93
+ ) -> "typing.Union[StructuredToolOutput, str]":
85
94
  cell = self._cells.get(cell_id)
86
95
  if cell is None:
87
96
  return f"Error: unknown exec cell `{cell_id}`."
@@ -98,8 +107,8 @@ class CodeModeManager:
98
107
  await self._wait_for_wait(cell, yield_time_ms)
99
108
  return await self._snapshot_cell(cell, max_tokens)
100
109
 
101
- def enabled_tools(self) -> 'typing.List[typing.Dict[str, str]]':
102
- enabled: 'typing.List[typing.Dict[str, str]]' = []
110
+ def enabled_tools(self) -> "typing.List[typing.Dict[str, str]]":
111
+ enabled: "typing.List[typing.Dict[str, str]]" = []
103
112
  for tool in self._registry.tools():
104
113
  if tool.name in {"exec", "wait"}:
105
114
  continue
@@ -116,7 +125,7 @@ class CodeModeManager:
116
125
  enabled.sort(key=lambda item: item["tool_name"])
117
126
  return enabled
118
127
 
119
- async def _start_cell(self, code: 'str', context: 'ToolContext') -> 'ExecCell':
128
+ async def _start_cell(self, code: "str", context: "ToolContext") -> "ExecCell":
120
129
  cell_id = uuid.uuid4().hex[:10]
121
130
  process = await asyncio.create_subprocess_exec(
122
131
  "node",
@@ -147,7 +156,7 @@ class CodeModeManager:
147
156
  logger.debug("exec start cell_id={} cwd={}", cell_id, self._default_cwd)
148
157
  return cell
149
158
 
150
- async def _read_stdout(self, cell: 'ExecCell', context: 'ToolContext') -> 'None':
159
+ async def _read_stdout(self, cell: "ExecCell", context: "ToolContext") -> "None":
151
160
  stream = cell.process.stdout
152
161
  if stream is None:
153
162
  cell.error_text = "missing stdout pipe"
@@ -181,7 +190,7 @@ class CodeModeManager:
181
190
  cell.output_event.set()
182
191
  continue
183
192
  if msg_type == "output_image":
184
- image_item: 'JSONDict' = {
193
+ image_item: "JSONDict" = {
185
194
  "type": "input_image",
186
195
  "image_url": str(message.get("image_url", "")),
187
196
  }
@@ -202,7 +211,7 @@ class CodeModeManager:
202
211
  cell.error_text = self._coerce_optional_text(message.get("error_text"))
203
212
  stored_values = message.get("stored_values")
204
213
  if isinstance(stored_values, dict):
205
- async with self._lock:
214
+ with self._lock:
206
215
  self._stored_values = stored_values
207
216
  cell.done_event.set()
208
217
  cell.output_event.set()
@@ -220,7 +229,7 @@ class CodeModeManager:
220
229
  cell.done_event.set()
221
230
  cell.output_event.set()
222
231
 
223
- async def _read_stderr(self, cell: 'ExecCell') -> 'None':
232
+ async def _read_stderr(self, cell: "ExecCell") -> "None":
224
233
  stream = cell.process.stderr
225
234
  if stream is None:
226
235
  return
@@ -232,10 +241,10 @@ class CodeModeManager:
232
241
 
233
242
  async def _handle_nested_tool_call(
234
243
  self,
235
- cell: 'ExecCell',
236
- context: 'ToolContext',
237
- message: 'JSONDict',
238
- ) -> 'None':
244
+ cell: "ExecCell",
245
+ context: "ToolContext",
246
+ message: "JSONDict",
247
+ ) -> "None":
239
248
  tool_name = str(message.get("tool_name", ""))
240
249
  request_id = str(message.get("id", ""))
241
250
  tool = self._registry.get_tool(tool_name)
@@ -287,14 +296,16 @@ class CodeModeManager:
287
296
  }
288
297
  await self._send_message(cell, payload)
289
298
 
290
- async def _send_message(self, cell: 'ExecCell', payload: 'JSONDict') -> 'None':
299
+ async def _send_message(self, cell: "ExecCell", payload: "JSONDict") -> "None":
291
300
  stdin = cell.process.stdin
292
301
  if stdin is None or stream_writer_is_closing(stdin):
293
302
  return
294
303
  stdin.write((json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
295
304
  await stdin.drain()
296
305
 
297
- async def _wait_for_exec(self, cell: 'ExecCell', yield_time_ms: 'typing.Union[int, None]') -> 'None':
306
+ async def _wait_for_exec(
307
+ self, cell: "ExecCell", yield_time_ms: "typing.Union[int, None]"
308
+ ) -> "None":
298
309
  done_task = asyncio.create_task(cell.done_event.wait())
299
310
  yield_task = asyncio.create_task(cell.yield_event.wait())
300
311
  tasks = {done_task, yield_task}
@@ -314,7 +325,7 @@ class CodeModeManager:
314
325
  task.cancel()
315
326
  cell.yield_event.clear()
316
327
 
317
- async def _wait_for_wait(self, cell: 'ExecCell', yield_time_ms: 'int') -> 'None':
328
+ async def _wait_for_wait(self, cell: "ExecCell", yield_time_ms: "int") -> "None":
318
329
  loop = asyncio.get_running_loop()
319
330
  deadline = loop.time() + max(yield_time_ms, 1) / 1000.0
320
331
  initial_count = cell.delivered_count
@@ -357,9 +368,9 @@ class CodeModeManager:
357
368
 
358
369
  async def _wait_for_completion_grace(
359
370
  self,
360
- cell: 'ExecCell',
361
- timeout_seconds: 'float',
362
- ) -> 'None':
371
+ cell: "ExecCell",
372
+ timeout_seconds: "float",
373
+ ) -> "None":
363
374
  if timeout_seconds <= 0:
364
375
  return
365
376
  done_task = asyncio.create_task(cell.done_event.wait())
@@ -378,9 +389,9 @@ class CodeModeManager:
378
389
 
379
390
  async def _snapshot_cell(
380
391
  self,
381
- cell: 'ExecCell',
382
- max_tokens: 'typing.Union[int, None]',
383
- ) -> 'StructuredToolOutput':
392
+ cell: "ExecCell",
393
+ max_tokens: "typing.Union[int, None]",
394
+ ) -> "StructuredToolOutput":
384
395
  if cell.process.returncode is not None and cell.reader_task is not None:
385
396
  await cell.reader_task
386
397
 
@@ -417,17 +428,19 @@ class CodeModeManager:
417
428
  if cell.done_event.is_set():
418
429
  self._cells.pop(cell.cell_id, None)
419
430
 
420
- return StructuredToolOutput(output=output_text, content_items=tuple(content_items))
431
+ return StructuredToolOutput(
432
+ output=output_text, content_items=tuple(content_items)
433
+ )
421
434
 
422
435
  def _truncate_content_items(
423
436
  self,
424
- items: 'typing.List[JSONDict]',
425
- max_tokens: 'typing.Union[int, None]',
426
- ) -> 'typing.List[JSONDict]':
437
+ items: "typing.List[JSONDict]",
438
+ max_tokens: "typing.Union[int, None]",
439
+ ) -> "typing.List[JSONDict]":
427
440
  token_budget = DEFAULT_MAX_OUTPUT_TOKENS if max_tokens is None else max_tokens
428
441
  max_chars = max(1, token_budget) * CHARS_PER_TOKEN
429
442
  total_chars = 0
430
- truncated: 'typing.List[JSONDict]' = []
443
+ truncated: "typing.List[JSONDict]" = []
431
444
  for item in items:
432
445
  if item.get("type") != "input_text":
433
446
  truncated.append(item)
@@ -449,7 +462,7 @@ class CodeModeManager:
449
462
  total_chars += len(text)
450
463
  return truncated
451
464
 
452
- def _status_text(self, cell: 'ExecCell') -> 'str':
465
+ def _status_text(self, cell: "ExecCell") -> "str":
453
466
  if cell.terminated:
454
467
  return "Script terminated"
455
468
  if not cell.done_event.is_set():
@@ -458,11 +471,9 @@ class CodeModeManager:
458
471
  return "Script failed"
459
472
  return "Script completed"
460
473
 
461
- def _parse_exec_source(self, input_text: 'str') -> 'ParsedExecSource':
474
+ def _parse_exec_source(self, input_text: "str") -> "ParsedExecSource":
462
475
  if not input_text.strip():
463
- raise ValueError(
464
- "exec expects raw JavaScript source text (non-empty)."
465
- )
476
+ raise ValueError("exec expects raw JavaScript source text (non-empty).")
466
477
  code = input_text
467
478
  yield_time_ms = None
468
479
  max_output_tokens = None
@@ -499,13 +510,15 @@ class CodeModeManager:
499
510
  max_output_tokens=max_output_tokens,
500
511
  )
501
512
 
502
- def _normalize_identifier(self, tool_name: 'str') -> 'str':
513
+ def _normalize_identifier(self, tool_name: "str") -> "str":
503
514
  identifier = []
504
515
  for index, char in enumerate(tool_name):
505
516
  is_valid = (
506
517
  char == "_"
507
518
  or char == "$"
508
- or (is_ascii(char) and char.isalnum() and (index != 0 or char.isalpha()))
519
+ or (
520
+ is_ascii(char) and char.isalnum() and (index != 0 or char.isalpha())
521
+ )
509
522
  )
510
523
  if is_valid:
511
524
  identifier.append(char)
@@ -513,7 +526,7 @@ class CodeModeManager:
513
526
  identifier.append("_")
514
527
  return "".join(identifier) or "_"
515
528
 
516
- def _coerce_optional_text(self, value: 'JSONValue') -> 'typing.Union[str, None]':
529
+ def _coerce_optional_text(self, value: "JSONValue") -> "typing.Union[str, None]":
517
530
  if value in (None, ""):
518
531
  return None
519
532
  return str(value)
@@ -10,6 +10,8 @@ Expected behavior:
10
10
  `write_stdin`.
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 (
@@ -19,7 +21,9 @@ from .unified_exec_manager import (
19
21
  UNIFIED_EXEC_OUTPUT_SCHEMA,
20
22
  UnifiedExecManager,
21
23
  )
22
- import typing
24
+
25
+ if typing.TYPE_CHECKING:
26
+ from ..agent import Agent
23
27
 
24
28
  MIN_EXEC_YIELD_TIME_MS = 250
25
29
  MAX_EXEC_YIELD_TIME_MS = 30_000
@@ -70,10 +74,19 @@ class ExecCommandTool(BaseTool):
70
74
  output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
71
75
  supports_parallel = False
72
76
 
73
- def __init__(self, manager: 'UnifiedExecManager') -> 'None':
77
+ def __init__(self, manager: "UnifiedExecManager") -> "None":
74
78
  self._manager = manager
75
79
 
76
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
80
+ def bind_agent(self, agent: "Agent") -> "None":
81
+ self._manager.set_notify_hook(agent.maybe_invoke)
82
+
83
+ def shutdown(self) -> "None":
84
+ self._manager.set_notify_hook(None)
85
+
86
+ def background_work_count(self, after_reply: "bool") -> "int":
87
+ return self._manager.running_session_count()
88
+
89
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
77
90
  del context
78
91
  cmd = str(args.get("cmd", "")).strip()
79
92
  if not cmd:
@@ -95,13 +108,15 @@ class ExecCommandTool(BaseTool):
95
108
  max_output_tokens=self._optional_int(args, "max_output_tokens"),
96
109
  )
97
110
 
98
- def _optional_string(self, args: 'JSONDict', key: 'str') -> 'typing.Union[str, None]':
111
+ def _optional_string(
112
+ self, args: "JSONDict", key: "str"
113
+ ) -> "typing.Union[str, None]":
99
114
  value = args.get(key)
100
115
  if value in (None, ""):
101
116
  return None
102
117
  return str(value)
103
118
 
104
- def _optional_int(self, args: 'JSONDict', key: 'str') -> 'typing.Union[int, None]':
119
+ def _optional_int(self, args: "JSONDict", key: "str") -> "typing.Union[int, None]":
105
120
  value = args.get(key)
106
121
  if value in (None, ""):
107
122
  return None
@@ -109,11 +124,11 @@ class ExecCommandTool(BaseTool):
109
124
 
110
125
  def _bounded_int(
111
126
  self,
112
- args: 'JSONDict',
113
- key: 'str',
114
- default: 'int',
115
- minimum: 'int',
116
- maximum: 'int',
117
- ) -> 'int':
127
+ args: "JSONDict",
128
+ key: "str",
129
+ default: "int",
130
+ minimum: "int",
131
+ maximum: "int",
132
+ ) -> "int":
118
133
  value = int(args.get(key, default))
119
134
  return min(max(value, minimum), maximum)
@@ -40,8 +40,8 @@ class ExecTool(BaseTool):
40
40
  "- Accepts raw JavaScript source text, not JSON, quoted strings, or "
41
41
  "markdown code fences.\n"
42
42
  "- You may optionally start the tool input with a first-line pragma "
43
- "like `// @exec: {\"yield_time_ms\": 10000, "
44
- "\"max_output_tokens\": 1000}`.\n"
43
+ 'like `// @exec: {"yield_time_ms": 10000, '
44
+ '"max_output_tokens": 1000}`.\n'
45
45
  "- `yield_time_ms` asks `exec` to yield early if the script is still "
46
46
  "running. Defaults to 10000 ms.\n"
47
47
  "- `max_output_tokens` sets the token budget for direct `exec` results. "
@@ -55,8 +55,8 @@ class ExecTool(BaseTool):
55
55
  }
56
56
  supports_parallel = False
57
57
 
58
- def __init__(self, manager: 'CodeModeManager') -> 'None':
58
+ def __init__(self, manager: "CodeModeManager") -> "None":
59
59
  self._manager = manager
60
60
 
61
- async def run(self, context: 'ToolContext', args: 'JSONValue') -> 'JSONValue':
61
+ async def run(self, context: "ToolContext", args: "JSONValue") -> "JSONValue":
62
62
  return await self._manager.exec(str(args), context)
@@ -12,11 +12,11 @@ Expected behavior:
12
12
  import asyncio
13
13
  import fnmatch
14
14
  import re
15
+ import typing
15
16
  from pathlib import Path
16
17
 
17
18
  from ..protocol import JSONDict, JSONValue
18
19
  from .base_tool import BaseTool, ToolContext
19
- import typing
20
20
 
21
21
  DEFAULT_LIMIT = 100
22
22
  MAX_LIMIT = 2000
@@ -53,10 +53,12 @@ class GrepFilesTool(BaseTool):
53
53
  "additionalProperties": False,
54
54
  }
55
55
 
56
- def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
56
+ def __init__(
57
+ self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
58
+ ) -> "None":
57
59
  self._working_directory = Path(cwd or Path.cwd()).resolve()
58
60
 
59
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
61
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
60
62
  del context
61
63
  pattern = str(args.get("pattern", "")).strip()
62
64
  include = str(args.get("include", "")).strip() or None
@@ -123,13 +125,13 @@ class GrepFilesTool(BaseTool):
123
125
 
124
126
  def _search_with_python(
125
127
  self,
126
- pattern: 'str',
127
- include: 'typing.Union[str, None]',
128
- search_path: 'Path',
129
- limit: 'int',
130
- ) -> 'typing.List[str]':
128
+ pattern: "str",
129
+ include: "typing.Union[str, None]",
130
+ search_path: "Path",
131
+ limit: "int",
132
+ ) -> "typing.List[str]":
131
133
  regex = re.compile(pattern)
132
- candidates: 'typing.List[Path]' = []
134
+ candidates: "typing.List[Path]" = []
133
135
 
134
136
  if search_path.is_file():
135
137
  candidates = [search_path]
@@ -153,7 +155,7 @@ class GrepFilesTool(BaseTool):
153
155
  matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
154
156
  return [str(path) for path in matches[:limit]]
155
157
 
156
- def _resolve_path(self, path_arg) -> 'Path':
158
+ def _resolve_path(self, path_arg) -> "Path":
157
159
  if path_arg in (None, ""):
158
160
  return self._working_directory
159
161
  path = Path(str(path_arg))
@@ -1,6 +1,7 @@
1
1
  """Optional tool that executes code in the current IPython kernel."""
2
2
 
3
- from ..utils.toolcall_visualize import colorize_tool_message, tool_summary
3
+ from ..events import ToolCompletedEvent
4
+ from ..utils.event_helpers import colorize_tool_message
4
5
  from .base_tool import BaseTool, ToolContext
5
6
 
6
7
 
@@ -28,7 +29,7 @@ class IPythonTool(BaseTool):
28
29
  self._shell = shell
29
30
  self.name = name
30
31
 
31
- async def run(self, context: 'ToolContext', args):
32
+ async def run(self, context: "ToolContext", args):
32
33
  del context
33
34
  if not isinstance(args, dict):
34
35
  return {"error": "arguments must be an object"}
@@ -55,9 +56,7 @@ class IPythonTool(BaseTool):
55
56
  data = getattr(item, "data", None)
56
57
  if isinstance(data, dict):
57
58
  display_outputs.append(
58
- data.get("text/plain")
59
- or data.get("text/html")
60
- or repr(data)
59
+ data.get("text/plain") or data.get("text/html") or repr(data)
61
60
  )
62
61
  else:
63
62
  display_outputs.append(repr(item))
@@ -106,9 +105,7 @@ def _install_agent_shortcut(shell):
106
105
  if not prompt.strip():
107
106
  return [f"{indent}print('Usage: @{agent_name} <prompt>')\n"]
108
107
 
109
- return [
110
- f"{indent}print({agent_name}.ask({prompt!r}).output_text)\n"
111
- ]
108
+ return [f"{indent}print({agent_name}.ask({prompt!r}).output_text)\n"]
112
109
 
113
110
  shell.input_transformers_cleanup.append(transform)
114
111
  shell.user_ns["_pycodex_agent_shortcut_transform"] = transform
@@ -117,13 +114,13 @@ def _install_agent_shortcut(shell):
117
114
 
118
115
  def attach_ipython_event_printer(agent, color=True):
119
116
  def handle_event(event):
120
- if event.kind == "tool_completed":
121
- tool_name = str(event.payload.get("tool_name", "")).strip()
122
- message = tool_summary(event.payload)
117
+ if isinstance(event, ToolCompletedEvent):
118
+ tool_name = event.call.name
119
+ message = event.visualize()
123
120
  for line in message.splitlines() or [""]:
124
121
  print(colorize_tool_message(line, color, tool_name), flush=True)
125
122
 
126
- agent.set_event_handler(handle_event)
123
+ agent.event_handler = handle_event
127
124
  return handle_event
128
125
 
129
126
 
@@ -137,7 +134,7 @@ def attach_ipython_tool(
137
134
  raise RuntimeError("not running inside IPython")
138
135
 
139
136
  tool = IPythonTool(shell, name=name)
140
- agent._tool_registry.register(tool)
137
+ agent.tool_registry.register(tool)
141
138
  if shortcut:
142
139
  _install_agent_shortcut(shell)
143
140
  if print_tool_events:
@@ -9,12 +9,12 @@ Expected behavior:
9
9
  commands like `find` or `ls -R`.
10
10
  """
11
11
 
12
+ import typing
12
13
  from collections import deque
13
14
  from pathlib import Path
14
15
 
15
16
  from ..protocol import JSONDict, JSONValue
16
17
  from .base_tool import BaseTool, ToolContext
17
- import typing
18
18
 
19
19
  MAX_ENTRY_LENGTH = 500
20
20
  INDENTATION_SPACES = 2
@@ -50,7 +50,7 @@ class ListDirTool(BaseTool):
50
50
  "additionalProperties": False,
51
51
  }
52
52
 
53
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
53
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
54
54
  del context
55
55
  dir_path = Path(str(args.get("dir_path", "")))
56
56
  offset = int(args.get("offset", 1))
@@ -86,15 +86,19 @@ class ListDirTool(BaseTool):
86
86
  lines.append(f"More than {len(selected)} entries found")
87
87
  return "\n".join(lines)
88
88
 
89
- def _collect_entries(self, root: 'Path', depth: 'int') -> 'typing.List[typing.Dict[str, object]]':
90
- entries: 'typing.List[typing.Dict[str, object]]' = []
89
+ def _collect_entries(
90
+ self, root: "Path", depth: "int"
91
+ ) -> "typing.List[typing.Dict[str, object]]":
92
+ entries: "typing.List[typing.Dict[str, object]]" = []
91
93
  queue = deque([(root, Path(), depth)])
92
94
 
93
95
  while queue:
94
96
  current_dir, prefix, remaining_depth = queue.popleft()
95
97
  dir_entries = []
96
98
  for child in current_dir.iterdir():
97
- relative_path = prefix / child.name if prefix.parts else Path(child.name)
99
+ relative_path = (
100
+ prefix / child.name if prefix.parts else Path(child.name)
101
+ )
98
102
  kind = self._entry_kind(child)
99
103
  dir_entries.append(
100
104
  (
@@ -118,7 +122,7 @@ class ListDirTool(BaseTool):
118
122
  entries.sort(key=lambda entry: entry["name"])
119
123
  return entries
120
124
 
121
- def _entry_kind(self, path: 'Path') -> 'str':
125
+ def _entry_kind(self, path: "Path") -> "str":
122
126
  if path.is_symlink():
123
127
  return "symlink"
124
128
  if path.is_dir():
@@ -127,14 +131,14 @@ class ListDirTool(BaseTool):
127
131
  return "file"
128
132
  return "other"
129
133
 
130
- def _format_entry_name(self, path: 'Path') -> 'str':
134
+ def _format_entry_name(self, path: "Path") -> "str":
131
135
  text = path.as_posix()
132
136
  return text[:MAX_ENTRY_LENGTH]
133
137
 
134
- def _format_component(self, name: 'str') -> 'str':
138
+ def _format_component(self, name: "str") -> "str":
135
139
  return name[:MAX_ENTRY_LENGTH]
136
140
 
137
- def _format_entry_line(self, entry: 'typing.Dict[str, object]') -> 'str':
141
+ def _format_entry_line(self, entry: "typing.Dict[str, object]") -> "str":
138
142
  indent = " " * (int(entry["depth"]) * INDENTATION_SPACES)
139
143
  name = str(entry["display_name"])
140
144
  kind = str(entry["kind"])