python-codex 0.1.13__py3-none-any.whl → 0.2.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.
- pycodex/agent.py +71 -11
- pycodex/cli.py +16 -356
- pycodex/context.py +12 -0
- pycodex/feishu_card.py +76 -30
- pycodex/feishu_link.py +131 -11
- pycodex/interactive_session.py +397 -0
- pycodex/model.py +11 -22
- pycodex/protocol.py +0 -5
- pycodex/runtime.py +23 -0
- pycodex/runtime_services.py +2 -2
- pycodex/tools/agent_tool_schemas.py +1 -1
- pycodex/tools/apply_patch_tool.py +1 -1
- pycodex/tools/base_tool.py +1 -27
- pycodex/tools/close_agent_tool.py +11 -4
- pycodex/tools/code_mode_manager.py +1 -1
- pycodex/tools/exec_command_tool.py +40 -16
- pycodex/tools/exec_tool.py +18 -2
- pycodex/tools/grep_files_tool.py +19 -6
- pycodex/tools/ipython_tool.py +3 -2
- pycodex/tools/list_dir_tool.py +19 -6
- pycodex/tools/read_file_tool.py +39 -9
- pycodex/tools/request_permissions_tool.py +12 -1
- pycodex/tools/request_user_input_tool.py +28 -1
- pycodex/tools/send_input_tool.py +4 -2
- pycodex/tools/shell_command_tool.py +23 -6
- pycodex/tools/shell_tool.py +13 -4
- pycodex/tools/spawn_agent_tool.py +31 -8
- pycodex/tools/unified_exec_manager.py +49 -93
- pycodex/tools/update_plan_tool.py +14 -6
- pycodex/tools/view_image_tool.py +17 -16
- pycodex/tools/wait_agent_tool.py +15 -3
- pycodex/tools/wait_tool.py +18 -4
- pycodex/tools/web_search_tool.py +2 -1
- pycodex/tools/write_stdin_tool.py +42 -10
- pycodex/utils/compactor.py +7 -1
- pycodex/utils/session_persist.py +42 -1
- pycodex/utils/truncation.py +206 -0
- pycodex/utils/visualize.py +34 -15
- {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/METADATA +4 -1
- python_codex-0.2.0.dist-info/RECORD +88 -0
- {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/entry_points.txt +1 -0
- workspace_server/__init__.py +23 -0
- workspace_server/__main__.py +5 -0
- workspace_server/app.py +1347 -0
- workspace_server/workspace.html +866 -0
- pycodex/prompts/exec_tools.json +0 -411
- pycodex/prompts/subagent_tools.json +0 -163
- python_codex-0.1.13.dist-info/RECORD +0 -84
- {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/WHEEL +0 -0
- {python_codex-0.1.13.dist-info → python_codex-0.2.0.dist-info}/licenses/LICENSE +0 -0
pycodex/feishu_card.py
CHANGED
|
@@ -45,6 +45,8 @@ class PycodexCard:
|
|
|
45
45
|
self.last_prompt = ""
|
|
46
46
|
self.model_name = "pycodex"
|
|
47
47
|
self.output_text = ""
|
|
48
|
+
self.working_output_text = ""
|
|
49
|
+
self._working_delta_active = False
|
|
48
50
|
self.error = ""
|
|
49
51
|
self.running = False
|
|
50
52
|
self.detached = False
|
|
@@ -150,6 +152,7 @@ class PycodexCard:
|
|
|
150
152
|
self.last_sender = sender or "cli"
|
|
151
153
|
self.last_prompt = prompt
|
|
152
154
|
self._mark_last_turn_output()
|
|
155
|
+
self._reset_working_output()
|
|
153
156
|
self.error = ""
|
|
154
157
|
self.running = True
|
|
155
158
|
|
|
@@ -179,6 +182,7 @@ class PycodexCard:
|
|
|
179
182
|
self.status_detail = "Model request started."
|
|
180
183
|
was_running = self.running
|
|
181
184
|
self.running = True
|
|
185
|
+
self._reset_working_output()
|
|
182
186
|
prompt = payload.get("user_text") or "\n".join(
|
|
183
187
|
str(item) for item in payload.get("user_texts", []) or []
|
|
184
188
|
)
|
|
@@ -193,16 +197,18 @@ class PycodexCard:
|
|
|
193
197
|
if kind == "assistant_delta":
|
|
194
198
|
self.status = "Responding"
|
|
195
199
|
self.status_detail = "Receiving assistant output."
|
|
196
|
-
|
|
200
|
+
self._append_working_delta(str(payload.get("delta") or ""))
|
|
197
201
|
return False
|
|
198
202
|
if kind == "tool_started":
|
|
199
203
|
self.status = "Tool"
|
|
200
204
|
self.status_detail = str(payload.get("tool_name") or "tool")
|
|
205
|
+
self._finish_working_delta_segment()
|
|
201
206
|
return False
|
|
202
207
|
if kind == "tool_completed":
|
|
203
208
|
self.status_detail = str(
|
|
204
209
|
payload.get("summary") or payload.get("tool_name") or "tool completed"
|
|
205
210
|
)
|
|
211
|
+
self._finish_working_delta_segment()
|
|
206
212
|
return False
|
|
207
213
|
if kind == "stream_error":
|
|
208
214
|
self.status = "Retrying"
|
|
@@ -217,17 +223,20 @@ class PycodexCard:
|
|
|
217
223
|
self.status = "Idle"
|
|
218
224
|
self.status_detail = "Turn completed."
|
|
219
225
|
self.running = False
|
|
226
|
+
self._reset_working_output()
|
|
220
227
|
return True
|
|
221
228
|
if kind in {"turn_failed", "submission_failed"}:
|
|
222
229
|
self.status = "Error"
|
|
223
230
|
self.error = str(payload.get("error") or kind)
|
|
224
231
|
self.status_detail = self.error
|
|
225
232
|
self.running = False
|
|
233
|
+
self._finish_working_delta_segment()
|
|
226
234
|
return True
|
|
227
235
|
if kind in {"turn_interrupted", "submission_cancelled"}:
|
|
228
236
|
self.status = "Idle"
|
|
229
237
|
self.status_detail = kind.replace("_", " ")
|
|
230
238
|
self.running = False
|
|
239
|
+
self._reset_working_output()
|
|
231
240
|
return True
|
|
232
241
|
return False
|
|
233
242
|
|
|
@@ -235,6 +244,22 @@ class PycodexCard:
|
|
|
235
244
|
if self.output_text and not self.output_text.startswith(LAST_TURN_PREFIX):
|
|
236
245
|
self.output_text = LAST_TURN_PREFIX + self.output_text
|
|
237
246
|
|
|
247
|
+
def _reset_working_output(self) -> None:
|
|
248
|
+
self.working_output_text = ""
|
|
249
|
+
self._working_delta_active = False
|
|
250
|
+
|
|
251
|
+
def _finish_working_delta_segment(self) -> None:
|
|
252
|
+
self._working_delta_active = False
|
|
253
|
+
|
|
254
|
+
def _append_working_delta(self, delta: str) -> None:
|
|
255
|
+
if not delta:
|
|
256
|
+
return
|
|
257
|
+
if self._working_delta_active:
|
|
258
|
+
self.working_output_text += delta
|
|
259
|
+
else:
|
|
260
|
+
self.working_output_text = delta
|
|
261
|
+
self._working_delta_active = True
|
|
262
|
+
|
|
238
263
|
def render(
|
|
239
264
|
self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
|
|
240
265
|
) -> "typing.Dict[str, object]":
|
|
@@ -251,7 +276,25 @@ class PycodexCard:
|
|
|
251
276
|
"Waiting for output..." if self.running else "Ready."
|
|
252
277
|
)
|
|
253
278
|
output_content = _render_output_content(output, output_mode)
|
|
279
|
+
working_output = _truncate(self.working_output_text, CARD_OUTPUT_LIMIT)
|
|
254
280
|
color, title = _status_template(self.status)
|
|
281
|
+
body_elements = [
|
|
282
|
+
{
|
|
283
|
+
"tag": "markdown",
|
|
284
|
+
"element_id": "prompt_md",
|
|
285
|
+
"content": f"> {sender}: **{_escape_code_block(prompt)}**",
|
|
286
|
+
},
|
|
287
|
+
_output_box("answer_box", "answer_md", output_content, "grey-50"),
|
|
288
|
+
]
|
|
289
|
+
if working_output:
|
|
290
|
+
body_elements.append(
|
|
291
|
+
_output_box(
|
|
292
|
+
"working_output_box",
|
|
293
|
+
"working_output_md",
|
|
294
|
+
_render_output_content(working_output, output_mode),
|
|
295
|
+
"green-50",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
255
298
|
card = {
|
|
256
299
|
"schema": "2.0",
|
|
257
300
|
"config": {"update_multi": True},
|
|
@@ -260,35 +303,7 @@ class PycodexCard:
|
|
|
260
303
|
"template": color,
|
|
261
304
|
},
|
|
262
305
|
"body": {
|
|
263
|
-
"elements":
|
|
264
|
-
{
|
|
265
|
-
"tag": "markdown",
|
|
266
|
-
"element_id": "prompt_md",
|
|
267
|
-
"content": f"> {sender}: **{_escape_code_block(prompt)}**",
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
"tag": "column_set",
|
|
271
|
-
"background_style": "grey-50",
|
|
272
|
-
"horizontal_spacing": "8px",
|
|
273
|
-
"horizontal_align": "left",
|
|
274
|
-
"columns": [
|
|
275
|
-
{
|
|
276
|
-
"tag": "column",
|
|
277
|
-
"width": "auto",
|
|
278
|
-
"elements": [
|
|
279
|
-
{
|
|
280
|
-
"tag": "markdown",
|
|
281
|
-
"element_id": "answer_md",
|
|
282
|
-
"content": output_content,
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
"vertical_spacing": "8px",
|
|
286
|
-
"horizontal_align": "left",
|
|
287
|
-
"vertical_align": "top",
|
|
288
|
-
}
|
|
289
|
-
],
|
|
290
|
-
},
|
|
291
|
-
]
|
|
306
|
+
"elements": body_elements,
|
|
292
307
|
},
|
|
293
308
|
}
|
|
294
309
|
if not self.detached:
|
|
@@ -614,6 +629,37 @@ def _render_output_content(output: str, output_mode: str) -> str:
|
|
|
614
629
|
return output
|
|
615
630
|
|
|
616
631
|
|
|
632
|
+
def _output_box(
|
|
633
|
+
box_id: str,
|
|
634
|
+
markdown_id: str,
|
|
635
|
+
content: str,
|
|
636
|
+
background_style: str,
|
|
637
|
+
) -> "typing.Dict[str, object]":
|
|
638
|
+
return {
|
|
639
|
+
"tag": "column_set",
|
|
640
|
+
"element_id": box_id,
|
|
641
|
+
"background_style": background_style,
|
|
642
|
+
"horizontal_spacing": "8px",
|
|
643
|
+
"horizontal_align": "left",
|
|
644
|
+
"columns": [
|
|
645
|
+
{
|
|
646
|
+
"tag": "column",
|
|
647
|
+
"width": "auto",
|
|
648
|
+
"elements": [
|
|
649
|
+
{
|
|
650
|
+
"tag": "markdown",
|
|
651
|
+
"element_id": markdown_id,
|
|
652
|
+
"content": content,
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
"vertical_spacing": "8px",
|
|
656
|
+
"horizontal_align": "left",
|
|
657
|
+
"vertical_align": "top",
|
|
658
|
+
}
|
|
659
|
+
],
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
|
|
617
663
|
def _is_card_content_error(exc: "BaseException") -> bool:
|
|
618
664
|
return CARD_CONTENT_ERROR_MARKER in str(exc)
|
|
619
665
|
|
pycodex/feishu_link.py
CHANGED
|
@@ -31,6 +31,7 @@ class PycodexRuntimeLink:
|
|
|
31
31
|
self._event_handler = None
|
|
32
32
|
self._detached = False
|
|
33
33
|
self._update_thread = None
|
|
34
|
+
self._update_stop = threading.Event()
|
|
34
35
|
self._update_lock = threading.Lock()
|
|
35
36
|
self._update_pending = False
|
|
36
37
|
|
|
@@ -60,17 +61,19 @@ class PycodexRuntimeLink:
|
|
|
60
61
|
def stop(self) -> None:
|
|
61
62
|
self._restore_event_handler()
|
|
62
63
|
if self._listener is not None:
|
|
63
|
-
self._listener
|
|
64
|
+
_release_feishu_listener(self._listener, self)
|
|
64
65
|
self._listener = None
|
|
66
|
+
self._stop_update_thread()
|
|
65
67
|
|
|
66
68
|
def detach(self) -> None:
|
|
67
69
|
self._detached = True
|
|
68
70
|
self._restore_event_handler()
|
|
69
71
|
if self._listener is not None:
|
|
70
|
-
self._listener
|
|
72
|
+
_release_feishu_listener(self._listener, self)
|
|
71
73
|
self._listener = None
|
|
72
74
|
self.card.detach()
|
|
73
75
|
self._safe_update_card()
|
|
76
|
+
self._stop_update_thread()
|
|
74
77
|
|
|
75
78
|
def _restore_event_handler(self) -> None:
|
|
76
79
|
current = getattr(self.queue, "_event_handler", None)
|
|
@@ -152,6 +155,7 @@ class PycodexRuntimeLink:
|
|
|
152
155
|
def _start_update_thread(self) -> None:
|
|
153
156
|
if self._update_thread is not None and self._update_thread.is_alive():
|
|
154
157
|
return
|
|
158
|
+
self._update_stop.clear()
|
|
155
159
|
self._update_thread = threading.Thread(
|
|
156
160
|
target=self._run_update_loop,
|
|
157
161
|
name="pycodex-feishu-card",
|
|
@@ -159,9 +163,15 @@ class PycodexRuntimeLink:
|
|
|
159
163
|
self._update_thread.daemon = True
|
|
160
164
|
self._update_thread.start()
|
|
161
165
|
|
|
166
|
+
def _stop_update_thread(self) -> None:
|
|
167
|
+
self._update_stop.set()
|
|
168
|
+
thread = self._update_thread
|
|
169
|
+
if thread is not None and thread.is_alive():
|
|
170
|
+
thread.join(timeout=2.0)
|
|
171
|
+
self._update_thread = None
|
|
172
|
+
|
|
162
173
|
def _run_update_loop(self) -> None:
|
|
163
|
-
while
|
|
164
|
-
time.sleep(CARD_UPDATE_FLUSH_INTERVAL_SECONDS)
|
|
174
|
+
while not self._update_stop.wait(CARD_UPDATE_FLUSH_INTERVAL_SECONDS):
|
|
165
175
|
with self._update_lock:
|
|
166
176
|
if not self._update_pending:
|
|
167
177
|
continue
|
|
@@ -194,6 +204,19 @@ def _feishu_listener(card: PycodexCard) -> '_FeishuCardActionListener':
|
|
|
194
204
|
return _LISTENER
|
|
195
205
|
|
|
196
206
|
|
|
207
|
+
def _release_feishu_listener(
|
|
208
|
+
listener: '_FeishuCardActionListener',
|
|
209
|
+
link: PycodexRuntimeLink,
|
|
210
|
+
) -> None:
|
|
211
|
+
global _LISTENER
|
|
212
|
+
with _LISTENER_LOCK:
|
|
213
|
+
listener.unregister(link)
|
|
214
|
+
if listener.empty():
|
|
215
|
+
listener.stop()
|
|
216
|
+
if _LISTENER is listener:
|
|
217
|
+
_LISTENER = None
|
|
218
|
+
|
|
219
|
+
|
|
197
220
|
class _FeishuCardActionListener:
|
|
198
221
|
def __init__(self, card: PycodexCard) -> None:
|
|
199
222
|
self.app_id = card.app_id
|
|
@@ -204,6 +227,9 @@ class _FeishuCardActionListener:
|
|
|
204
227
|
self.link = None
|
|
205
228
|
self._async_thread = _AsyncLoopThread()
|
|
206
229
|
self._thread = None
|
|
230
|
+
self._client = None
|
|
231
|
+
self._client_loop = None
|
|
232
|
+
self._stop_requested = threading.Event()
|
|
207
233
|
|
|
208
234
|
def assert_compatible(self, card: PycodexCard) -> None:
|
|
209
235
|
if self.app_id != card.app_id or self.domain != card.domain:
|
|
@@ -214,6 +240,7 @@ class _FeishuCardActionListener:
|
|
|
214
240
|
raise RuntimeError("FEISHU_APP_ID and FEISHU_APP_SECRET are required")
|
|
215
241
|
if self._thread is not None and self._thread.is_alive():
|
|
216
242
|
return
|
|
243
|
+
self._stop_requested.clear()
|
|
217
244
|
self._async_thread.start()
|
|
218
245
|
self._thread = threading.Thread(
|
|
219
246
|
target=self._listen,
|
|
@@ -229,6 +256,21 @@ class _FeishuCardActionListener:
|
|
|
229
256
|
if self.link is link:
|
|
230
257
|
self.link = None
|
|
231
258
|
|
|
259
|
+
def empty(self) -> bool:
|
|
260
|
+
return self.link is None
|
|
261
|
+
|
|
262
|
+
def stop(self) -> None:
|
|
263
|
+
self._stop_requested.set()
|
|
264
|
+
client = self._client
|
|
265
|
+
loop = self._client_loop
|
|
266
|
+
if loop is not None:
|
|
267
|
+
_stop_lark_client(client, loop)
|
|
268
|
+
thread = self._thread
|
|
269
|
+
if thread is not None and thread.is_alive():
|
|
270
|
+
thread.join(timeout=2.0)
|
|
271
|
+
self._thread = None
|
|
272
|
+
self._async_thread.stop()
|
|
273
|
+
|
|
232
274
|
def _listen(self) -> None:
|
|
233
275
|
try:
|
|
234
276
|
import lark_oapi as lark
|
|
@@ -253,13 +295,37 @@ class _FeishuCardActionListener:
|
|
|
253
295
|
.register_p2_card_action_trigger(on_card_action)
|
|
254
296
|
.build()
|
|
255
297
|
)
|
|
256
|
-
client
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
|
|
298
|
+
import lark_oapi.ws.client as lark_ws_client
|
|
299
|
+
|
|
300
|
+
client = None
|
|
301
|
+
loop = asyncio.new_event_loop()
|
|
302
|
+
self._client_loop = loop
|
|
303
|
+
asyncio.set_event_loop(loop)
|
|
304
|
+
lark_ws_client.loop = loop
|
|
305
|
+
try:
|
|
306
|
+
if self._stop_requested.is_set():
|
|
307
|
+
return
|
|
308
|
+
client = lark_ws.Client(
|
|
309
|
+
self.app_id,
|
|
310
|
+
self.app_secret,
|
|
311
|
+
event_handler=handler,
|
|
312
|
+
domain=self.domain,
|
|
313
|
+
)
|
|
314
|
+
self._client = client
|
|
315
|
+
if self._stop_requested.is_set():
|
|
316
|
+
return
|
|
317
|
+
client.start()
|
|
318
|
+
except RuntimeError as exc:
|
|
319
|
+
stopped = "Event loop stopped before Future completed" in str(exc)
|
|
320
|
+
if not self._stop_requested.is_set() or not stopped:
|
|
321
|
+
raise
|
|
322
|
+
finally:
|
|
323
|
+
if self._client is client:
|
|
324
|
+
self._client = None
|
|
325
|
+
if self._client_loop is loop:
|
|
326
|
+
self._client_loop = None
|
|
327
|
+
_close_loop(loop)
|
|
328
|
+
asyncio.set_event_loop(None)
|
|
263
329
|
|
|
264
330
|
def _handle_card_action(self, event) -> 'typing.Dict[str, object]':
|
|
265
331
|
try:
|
|
@@ -300,6 +366,60 @@ def _event_message_id(event) -> "typing.Union[str, None]":
|
|
|
300
366
|
return None
|
|
301
367
|
|
|
302
368
|
|
|
369
|
+
def _stop_lark_client(client, loop) -> None:
|
|
370
|
+
if loop.is_closed():
|
|
371
|
+
return
|
|
372
|
+
disconnect = getattr(client, "_disconnect", None) if client is not None else None
|
|
373
|
+
if loop.is_running():
|
|
374
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
375
|
+
_disconnect_then_stop(disconnect, loop),
|
|
376
|
+
loop,
|
|
377
|
+
)
|
|
378
|
+
try:
|
|
379
|
+
future.result(timeout=2.0)
|
|
380
|
+
except Exception as exc:
|
|
381
|
+
future.cancel()
|
|
382
|
+
print(exc)
|
|
383
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
384
|
+
return
|
|
385
|
+
if callable(disconnect):
|
|
386
|
+
try:
|
|
387
|
+
loop.run_until_complete(disconnect())
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
print(exc)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
async def _disconnect_then_stop(disconnect, loop) -> None:
|
|
393
|
+
try:
|
|
394
|
+
if callable(disconnect):
|
|
395
|
+
await disconnect()
|
|
396
|
+
except Exception as exc:
|
|
397
|
+
print(exc)
|
|
398
|
+
finally:
|
|
399
|
+
loop.stop()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _close_loop(loop) -> None:
|
|
403
|
+
if loop.is_closed():
|
|
404
|
+
return
|
|
405
|
+
tasks = list(_all_loop_tasks(loop))
|
|
406
|
+
for task in tasks:
|
|
407
|
+
task.cancel()
|
|
408
|
+
if tasks:
|
|
409
|
+
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
|
410
|
+
shutdown_asyncgens = getattr(loop, "shutdown_asyncgens", None)
|
|
411
|
+
if callable(shutdown_asyncgens):
|
|
412
|
+
loop.run_until_complete(shutdown_asyncgens())
|
|
413
|
+
loop.close()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _all_loop_tasks(loop):
|
|
417
|
+
all_tasks = getattr(asyncio, "all_tasks", None)
|
|
418
|
+
if callable(all_tasks):
|
|
419
|
+
return all_tasks(loop)
|
|
420
|
+
return asyncio.Task.all_tasks(loop)
|
|
421
|
+
|
|
422
|
+
|
|
303
423
|
class _AsyncLoopThread:
|
|
304
424
|
def __init__(self) -> None:
|
|
305
425
|
self.loop = None
|