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
pycodex/feishu_link.py CHANGED
@@ -3,9 +3,9 @@ import threading
3
3
  import time
4
4
  import typing
5
5
 
6
+ from .events import SessionStateEvent
6
7
  from .feishu_card import PycodexCard
7
- from .protocol import AssistantMessage, UserMessage
8
- from .runtime import CliSubmissionQueue
8
+ from .runtime import AgentRuntime
9
9
 
10
10
  CARD_UPDATE_FLUSH_INTERVAL_SECONDS = 0.5
11
11
  _LISTENER = None
@@ -15,39 +15,39 @@ _LISTENER_LOCK = threading.Lock()
15
15
  class PycodexRuntimeLink:
16
16
  def __init__(
17
17
  self,
18
- queue: 'CliSubmissionQueue',
18
+ runtime: "AgentRuntime",
19
19
  target: str,
20
20
  loop=None,
21
- card: 'typing.Union[PycodexCard, None]' = None,
21
+ card: "typing.Union[PycodexCard, None]" = None,
22
22
  ) -> None:
23
- self.queue = queue
23
+ self.runtime = runtime
24
24
  self.target = target
25
25
  self.loop = loop
26
26
  self.card = card or PycodexCard.from_env()
27
27
  self.session_key = None
28
28
  self.message_id = None
29
29
  self._listener = None
30
- self._previous_event_handler = None
31
- self._event_handler = None
30
+ self._frontend_id = None
32
31
  self._detached = False
33
32
  self._update_thread = None
34
33
  self._update_stop = threading.Event()
35
34
  self._update_lock = threading.Lock()
36
35
  self._update_pending = False
37
36
 
38
- async def start_async(self) -> 'PycodexRuntimeLink':
37
+ async def start_async(self) -> "PycodexRuntimeLink":
39
38
  self.loop = asyncio.get_event_loop()
40
- return await self.loop.run_in_executor(None, self.start)
39
+ self.card.apply_event(SessionStateEvent("attach", self.runtime.snapshot()))
40
+ try:
41
+ await self.loop.run_in_executor(None, self.start)
42
+ except Exception:
43
+ await self.loop.run_in_executor(None, self.stop)
44
+ raise
45
+ self._frontend_id = self.runtime.attach(self._handle_runtime_event)
46
+ return self
41
47
 
42
- def start(self) -> 'PycodexRuntimeLink':
48
+ def start(self) -> "PycodexRuntimeLink":
43
49
  if self.loop is None:
44
50
  raise RuntimeError("PycodexRuntimeLink.start_async() is required")
45
- self.card.set_snapshot(*_last_user_assistant_pair(self.queue))
46
- if self._has_active_turn():
47
- self.card.status = "Running"
48
- self.card.status_detail = "Model request started."
49
- self.card.running = True
50
- self.card.model_name = getattr(self.queue._agent._model_client, 'model', 'pycodex')
51
51
  self.card.send(self.target)
52
52
  self.session_key = self.card.session_key
53
53
  self.message_id = self.card.message_id
@@ -55,11 +55,10 @@ class PycodexRuntimeLink:
55
55
  self._listener.register(self)
56
56
  self._listener.start()
57
57
  self._start_update_thread()
58
- self._install_event_handler()
59
58
  return self
60
59
 
61
60
  def stop(self) -> None:
62
- self._restore_event_handler()
61
+ self._detach_frontend()
63
62
  if self._listener is not None:
64
63
  _release_feishu_listener(self._listener, self)
65
64
  self._listener = None
@@ -67,7 +66,7 @@ class PycodexRuntimeLink:
67
66
 
68
67
  def detach(self) -> None:
69
68
  self._detached = True
70
- self._restore_event_handler()
69
+ self._detach_frontend()
71
70
  if self._listener is not None:
72
71
  _release_feishu_listener(self._listener, self)
73
72
  self._listener = None
@@ -75,29 +74,29 @@ class PycodexRuntimeLink:
75
74
  self._safe_update_card()
76
75
  self._stop_update_thread()
77
76
 
78
- def _restore_event_handler(self) -> None:
79
- current = getattr(self.queue, "_event_handler", None)
80
- if current is self._event_handler and self._previous_event_handler is not None:
81
- self.queue.set_event_handler(self._previous_event_handler)
82
-
83
- def _install_event_handler(self) -> None:
84
- self._previous_event_handler = getattr(self.queue, "_event_handler", None)
85
- self._event_handler = self._handle_runtime_event
86
- self.queue.set_event_handler(self._event_handler)
77
+ def _detach_frontend(self) -> None:
78
+ if self._frontend_id is not None:
79
+ self.runtime.detach(self._frontend_id)
80
+ self._frontend_id = None
87
81
 
88
82
  def _handle_runtime_event(self, event) -> None:
89
- if self._previous_event_handler is not None:
90
- self._previous_event_handler(event)
91
83
  if self._detached:
92
84
  return
93
85
  self.card.apply_event(event)
94
86
  self._update_card()
95
87
 
96
- async def _submit(self, action: 'typing.Dict[str, object]') -> 'typing.Dict[str, object]':
88
+ async def _submit(
89
+ self, action: "typing.Dict[str, object]"
90
+ ) -> "typing.Dict[str, object]":
97
91
  if self._detached:
98
92
  return {"toast": {"type": "warning", "content": "pycodex is detached."}}
99
- if self.card.running or self._has_active_turn():
100
- return {"toast": {"type": "warning", "content": "pycodex is running."}}
93
+ if action.get("action") == "history":
94
+ try:
95
+ self.card.show_history(action.get("history_index"))
96
+ except ValueError as exc:
97
+ return {"toast": {"type": "warning", "content": str(exc)}}
98
+ self._update_card()
99
+ return {}
101
100
  if action.get("action") != "send":
102
101
  return {
103
102
  "toast": {
@@ -106,46 +105,22 @@ class PycodexRuntimeLink:
106
105
  }
107
106
  }
108
107
  prompt = str(action.get("prompt") or "").strip()
109
- if not prompt:
110
- return {"toast": {"type": "warning", "content": "Prompt is empty."}}
111
- self.card.set_queued(prompt, sender=str(action.get("sender") or "cli"))
112
- self._update_card()
113
- queue_name = "steer" if self._has_active_turn() else "enqueue"
114
108
  future = asyncio.run_coroutine_threadsafe(
115
- self.queue.enqueue_user_turn(prompt, queue=queue_name),
109
+ self.runtime.submit_input(
110
+ prompt, sender=str(action.get("sender") or "feishu")
111
+ ),
116
112
  self.loop,
117
113
  )
118
- submission_id, turn_future = await asyncio.wrap_future(future)
119
-
120
- def on_done(completed):
121
- if self._detached:
122
- return
123
- if completed.cancelled():
124
- self.card.status = "Idle"
125
- self.card.status_detail = "submission cancelled"
126
- self.card.running = False
127
- self._update_card()
128
- return
129
- exc = completed.exception()
130
- if exc is not None:
131
- self.card.status = "Error"
132
- self.card.status_detail = str(exc)
133
- self.card.error = str(exc)
134
- self.card.running = False
135
- self._update_card()
136
-
137
- del submission_id
138
- self.loop.call_soon_threadsafe(lambda: turn_future.add_done_callback(on_done))
139
- return {"toast": {"type": "info", "content": "pycodex is running."}}
114
+ try:
115
+ await asyncio.wrap_future(future)
116
+ except (ValueError, RuntimeError) as exc:
117
+ return {"toast": {"type": "warning", "content": str(exc)}}
118
+ return {"toast": {"type": "info", "content": "Input submitted."}}
140
119
 
141
120
  def _update_card(self) -> None:
142
121
  with self._update_lock:
143
122
  self._update_pending = True
144
123
 
145
- def _has_active_turn(self) -> bool:
146
- task = getattr(self.queue, "_current_task", None)
147
- return task is not None and not task.done()
148
-
149
124
  def _safe_update_card(self) -> None:
150
125
  try:
151
126
  self.card.update()
@@ -179,22 +154,7 @@ class PycodexRuntimeLink:
179
154
  self._safe_update_card()
180
155
 
181
156
 
182
- def _last_user_assistant_pair(queue) -> 'typing.Tuple[str, str]':
183
- agent = getattr(queue, "_agent", None)
184
- history = getattr(agent, "history", ())
185
- output = ""
186
- prompt = ""
187
- for item in reversed(history):
188
- if not output and isinstance(item, AssistantMessage):
189
- output = item.text
190
- continue
191
- if output and isinstance(item, UserMessage):
192
- prompt = item.text
193
- break
194
- return prompt, output
195
-
196
-
197
- def _feishu_listener(card: PycodexCard) -> '_FeishuCardActionListener':
157
+ def _feishu_listener(card: PycodexCard) -> "_FeishuCardActionListener":
198
158
  global _LISTENER
199
159
  with _LISTENER_LOCK:
200
160
  if _LISTENER is None:
@@ -205,7 +165,7 @@ def _feishu_listener(card: PycodexCard) -> '_FeishuCardActionListener':
205
165
 
206
166
 
207
167
  def _release_feishu_listener(
208
- listener: '_FeishuCardActionListener',
168
+ listener: "_FeishuCardActionListener",
209
169
  link: PycodexRuntimeLink,
210
170
  ) -> None:
211
171
  global _LISTENER
@@ -343,7 +303,7 @@ class _FeishuCardActionListener:
343
303
  _close_loop(loop)
344
304
  asyncio.set_event_loop(None)
345
305
 
346
- def _handle_card_action(self, event) -> 'typing.Dict[str, object]':
306
+ def _handle_card_action(self, event) -> "typing.Dict[str, object]":
347
307
  try:
348
308
  message_id = _event_message_id(event)
349
309
  link = self._resolve_link(message_id)