python-codex 0.1.12__py3-none-any.whl → 0.1.14__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/__init__.py +10 -8
- pycodex/agent.py +118 -29
- pycodex/cli.py +97 -387
- pycodex/compat.py +8 -4
- pycodex/feishu_card.py +739 -0
- pycodex/feishu_link.py +462 -0
- pycodex/interactive_session.py +397 -0
- pycodex/model.py +71 -7
- pycodex/prompts/models.json +4 -4
- pycodex/protocol.py +17 -22
- pycodex/runtime.py +22 -14
- pycodex/runtime_services.py +47 -25
- pycodex/tools/agent_tool_schemas.py +1 -1
- pycodex/tools/apply_patch_tool.py +12 -13
- pycodex/tools/base_tool.py +1 -27
- pycodex/tools/close_agent_tool.py +11 -4
- 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 +145 -0
- 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 +45 -1
- 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/__init__.py +2 -13
- pycodex/utils/async_bridge.py +54 -0
- pycodex/utils/compactor.py +29 -10
- pycodex/utils/session_persist.py +57 -38
- pycodex/utils/toolcall_visualize.py +713 -0
- pycodex/utils/visualize.py +253 -872
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/METADATA +4 -1
- python_codex-0.1.14.dist-info/RECORD +87 -0
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/entry_points.txt +1 -0
- workspace_server/__init__.py +21 -0
- workspace_server/__main__.py +5 -0
- workspace_server/app.py +983 -0
- workspace_server/workspace.html +790 -0
- pycodex/prompts/exec_tools.json +0 -411
- pycodex/prompts/subagent_tools.json +0 -163
- python_codex-0.1.12.dist-info/RECORD +0 -79
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/WHEEL +0 -0
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/licenses/LICENSE +0 -0
pycodex/__init__.py
CHANGED
|
@@ -2,12 +2,13 @@ from .compat import patch_asyncio
|
|
|
2
2
|
|
|
3
3
|
patch_asyncio()
|
|
4
4
|
|
|
5
|
-
from .agent import
|
|
5
|
+
from .agent import Agent
|
|
6
6
|
from .context import ContextConfig, ContextManager
|
|
7
7
|
from .model import (
|
|
8
8
|
ModelClient,
|
|
9
9
|
NOOP_MODEL_STREAM_EVENT_HANDLER,
|
|
10
10
|
ResponsesApiError,
|
|
11
|
+
ResponsesIncompleteError,
|
|
11
12
|
ResponsesModelClient,
|
|
12
13
|
ResponsesProviderConfig,
|
|
13
14
|
)
|
|
@@ -26,14 +27,14 @@ from .protocol import (
|
|
|
26
27
|
TurnResult,
|
|
27
28
|
UserMessage,
|
|
28
29
|
)
|
|
29
|
-
from .runtime import
|
|
30
|
+
from .runtime import CliSubmissionQueue
|
|
30
31
|
from .runtime_services import (
|
|
31
32
|
PlanStore,
|
|
32
33
|
RequestPermissionsManager,
|
|
33
34
|
RequestUserInputManager,
|
|
34
35
|
SubAgentManager,
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
create_agent_runtime_environment,
|
|
37
|
+
get_agent_runtime_environment,
|
|
37
38
|
)
|
|
38
39
|
from .tools import (
|
|
39
40
|
ApplyPatchTool,
|
|
@@ -90,13 +91,13 @@ def debug(stop: 'bool' = False):
|
|
|
90
91
|
|
|
91
92
|
__all__ = [
|
|
92
93
|
"AgentEvent",
|
|
93
|
-
"
|
|
94
|
-
"
|
|
94
|
+
"Agent",
|
|
95
|
+
"CliSubmissionQueue",
|
|
95
96
|
"ApplyPatchTool",
|
|
96
97
|
"AssistantMessage",
|
|
97
98
|
"BaseTool",
|
|
98
99
|
"CloseAgentTool",
|
|
99
|
-
"
|
|
100
|
+
"create_agent_runtime_environment",
|
|
100
101
|
"CodeModeManager",
|
|
101
102
|
"ContextConfig",
|
|
102
103
|
"ContextManager",
|
|
@@ -120,6 +121,7 @@ __all__ = [
|
|
|
120
121
|
"RequestUserInputManager",
|
|
121
122
|
"ResumeAgentTool",
|
|
122
123
|
"ResponsesApiError",
|
|
124
|
+
"ResponsesIncompleteError",
|
|
123
125
|
"ResponsesModelClient",
|
|
124
126
|
"ResponsesProviderConfig",
|
|
125
127
|
"SendInputTool",
|
|
@@ -142,5 +144,5 @@ __all__ = [
|
|
|
142
144
|
"WaitTool",
|
|
143
145
|
"WebSearchTool",
|
|
144
146
|
"WriteStdinTool",
|
|
145
|
-
"
|
|
147
|
+
"get_agent_runtime_environment",
|
|
146
148
|
]
|
pycodex/agent.py
CHANGED
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
from typing import Callable
|
|
6
6
|
|
|
7
7
|
from .context import ContextManager
|
|
8
|
-
from .model import ModelClient
|
|
8
|
+
from .model import ModelClient, ResponsesIncompleteError
|
|
9
9
|
from .protocol import (
|
|
10
10
|
AgentEvent,
|
|
11
11
|
AssistantMessage,
|
|
@@ -17,16 +17,17 @@ from .protocol import (
|
|
|
17
17
|
TurnResult,
|
|
18
18
|
UserMessage,
|
|
19
19
|
)
|
|
20
|
-
from .tools import ToolContext, ToolRegistry
|
|
20
|
+
from .tools import ExecCommandTool, ToolContext, ToolRegistry, UnifiedExecManager
|
|
21
21
|
from .utils import uuid7_string
|
|
22
22
|
import typing
|
|
23
23
|
|
|
24
24
|
if typing.TYPE_CHECKING:
|
|
25
25
|
from .utils.session_persist import SessionRolloutRecorder
|
|
26
|
+
from .runtime_services import AgentRuntimeEnvironment
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
EventHandler = Callable[[AgentEvent], None]
|
|
29
|
-
|
|
30
|
+
BASE_EVENT_HANDLER: 'EventHandler' = lambda _event: None
|
|
30
31
|
_REQUESTED_TOKENS_RE = re.compile(
|
|
31
32
|
r"requested\s+([0-9,]+)\s+tokens",
|
|
32
33
|
re.IGNORECASE,
|
|
@@ -39,13 +40,20 @@ _MAX_CONTEXT_TOKENS_RE = re.compile(
|
|
|
39
40
|
r"maximum\s+context\s+length\s+is\s+([0-9,]+)\s+tokens",
|
|
40
41
|
re.IGNORECASE,
|
|
41
42
|
)
|
|
43
|
+
_CONTEXT_LENGTH_ERROR_MARKERS = (
|
|
44
|
+
"context_length_exceeded",
|
|
45
|
+
"maximum context length",
|
|
46
|
+
"exceeds the context window",
|
|
47
|
+
"exceeded the context window",
|
|
48
|
+
)
|
|
49
|
+
TERMINAL_TURN_EVENTS = {"turn_completed", "turn_failed", "turn_interrupted"}
|
|
42
50
|
|
|
43
51
|
|
|
44
52
|
class TurnInterrupted(RuntimeError):
|
|
45
53
|
pass
|
|
46
54
|
|
|
47
55
|
|
|
48
|
-
class
|
|
56
|
+
class Agent:
|
|
49
57
|
"""Minimal Python port of Codex's turn loop.
|
|
50
58
|
|
|
51
59
|
The core idea mirrors the Rust implementation:
|
|
@@ -60,9 +68,10 @@ class AgentLoop:
|
|
|
60
68
|
tool_registry: 'ToolRegistry',
|
|
61
69
|
context_manager: 'typing.Union[ContextManager, None]' = None,
|
|
62
70
|
parallel_tool_calls: 'bool' = True,
|
|
63
|
-
event_handler: 'EventHandler' =
|
|
71
|
+
event_handler: 'EventHandler' = BASE_EVENT_HANDLER,
|
|
64
72
|
initial_history: 'typing.Tuple[ConversationItem, ...]' = (),
|
|
65
73
|
rollout_recorder: 'typing.Union[SessionRolloutRecorder, None]' = None,
|
|
74
|
+
runtime_environment: 'AgentRuntimeEnvironment' = None,
|
|
66
75
|
) -> 'None':
|
|
67
76
|
self._model_client = model_client
|
|
68
77
|
self._tool_registry = tool_registry
|
|
@@ -75,14 +84,24 @@ class AgentLoop:
|
|
|
75
84
|
self._context_manager.resolve_auto_compact_token_limit()
|
|
76
85
|
)
|
|
77
86
|
self._last_total_usage_tokens: 'typing.Union[int, None]' = None
|
|
87
|
+
self.runtime_environment = runtime_environment
|
|
78
88
|
self.interrupt_asap = False
|
|
89
|
+
self._turn_running = False
|
|
90
|
+
exec_command_tool = self._tool_registry.get_tool("exec_command")
|
|
91
|
+
self._exec_manager = (
|
|
92
|
+
exec_command_tool._manager
|
|
93
|
+
if isinstance(exec_command_tool, ExecCommandTool)
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
96
|
+
if self._exec_manager is not None:
|
|
97
|
+
self._exec_manager.set_notify_hook(self.maybe_invoke)
|
|
79
98
|
|
|
80
99
|
@property
|
|
81
100
|
def history(self) -> 'typing.Tuple[ConversationItem, ...]':
|
|
82
101
|
return tuple(self._history)
|
|
83
102
|
|
|
84
103
|
def set_event_handler(
|
|
85
|
-
self, event_handler: 'EventHandler' =
|
|
104
|
+
self, event_handler: 'EventHandler' = BASE_EVENT_HANDLER
|
|
86
105
|
) -> 'None':
|
|
87
106
|
self._event_handler = event_handler
|
|
88
107
|
|
|
@@ -98,6 +117,11 @@ class AgentLoop:
|
|
|
98
117
|
) -> 'None':
|
|
99
118
|
self._rollout_recorder = rollout_recorder
|
|
100
119
|
|
|
120
|
+
def ask(self, text: 'str') -> 'TurnResult':
|
|
121
|
+
from .utils.async_bridge import run_async
|
|
122
|
+
|
|
123
|
+
return run_async(self.run_turn([text]))
|
|
124
|
+
|
|
101
125
|
def _raise_if_interrupt_requested(
|
|
102
126
|
self,
|
|
103
127
|
turn_id: 'str',
|
|
@@ -115,6 +139,7 @@ class AgentLoop:
|
|
|
115
139
|
async def run_turn(
|
|
116
140
|
self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
|
|
117
141
|
) -> 'TurnResult':
|
|
142
|
+
self._turn_running = True
|
|
118
143
|
turn_id = turn_id or uuid7_string()
|
|
119
144
|
self.interrupt_asap = False
|
|
120
145
|
new_user_messages = [UserMessage(text=text) for text in texts]
|
|
@@ -154,16 +179,10 @@ class AgentLoop:
|
|
|
154
179
|
item_count=len(response.items),
|
|
155
180
|
)
|
|
156
181
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
persisted_response_items.append(item)
|
|
162
|
-
if isinstance(item, AssistantMessage):
|
|
163
|
-
last_assistant_message = item.text
|
|
164
|
-
elif isinstance(item, ToolCall):
|
|
165
|
-
tool_calls.append(item)
|
|
166
|
-
self._persist_history_items(persisted_response_items)
|
|
182
|
+
recorded_items = self._record_model_response_items(response.items)
|
|
183
|
+
tool_calls = recorded_items[1]
|
|
184
|
+
if recorded_items[2] is not None:
|
|
185
|
+
last_assistant_message = recorded_items[2]
|
|
167
186
|
|
|
168
187
|
if not tool_calls:
|
|
169
188
|
self._raise_if_interrupt_requested(
|
|
@@ -177,6 +196,7 @@ class AgentLoop:
|
|
|
177
196
|
iteration=iteration,
|
|
178
197
|
output_text=last_assistant_message,
|
|
179
198
|
)
|
|
199
|
+
self._turn_running = False
|
|
180
200
|
return TurnResult(
|
|
181
201
|
turn_id=turn_id,
|
|
182
202
|
output_text=last_assistant_message,
|
|
@@ -197,6 +217,7 @@ class AgentLoop:
|
|
|
197
217
|
output_text=last_assistant_message,
|
|
198
218
|
)
|
|
199
219
|
except TurnInterrupted:
|
|
220
|
+
self._turn_running = False
|
|
200
221
|
raise
|
|
201
222
|
except Exception as exc:
|
|
202
223
|
context_usage = _usage_from_context_length_error(str(exc))
|
|
@@ -210,8 +231,29 @@ class AgentLoop:
|
|
|
210
231
|
error=str(exc),
|
|
211
232
|
error_type=type(exc).__name__,
|
|
212
233
|
)
|
|
234
|
+
self._turn_running = False
|
|
213
235
|
raise
|
|
214
236
|
|
|
237
|
+
async def maybe_invoke(self, event: 'typing.Dict[str, object]') -> 'bool':
|
|
238
|
+
if self._turn_running or event.get("type") != "exec_command_completed":
|
|
239
|
+
return False
|
|
240
|
+
payload = {
|
|
241
|
+
"session_id": event.get("session_id"),
|
|
242
|
+
"exit_code": event.get("exit_code"),
|
|
243
|
+
"command": event.get("command"),
|
|
244
|
+
}
|
|
245
|
+
text = (
|
|
246
|
+
"<exec_command_completed>\n"
|
|
247
|
+
f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
|
|
248
|
+
"</exec_command_completed>"
|
|
249
|
+
)
|
|
250
|
+
self._turn_running = True
|
|
251
|
+
task = asyncio.create_task(self.run_turn([text]))
|
|
252
|
+
task.add_done_callback(
|
|
253
|
+
lambda task: None if task.cancelled() else task.exception()
|
|
254
|
+
)
|
|
255
|
+
return True
|
|
256
|
+
|
|
215
257
|
async def _execute_tool_batch(
|
|
216
258
|
self,
|
|
217
259
|
turn_id: 'str',
|
|
@@ -280,10 +322,18 @@ class AgentLoop:
|
|
|
280
322
|
return result
|
|
281
323
|
|
|
282
324
|
def _emit(self, kind: 'str', turn_id: 'str', **payload: 'object') -> 'None':
|
|
325
|
+
if kind in TERMINAL_TURN_EVENTS:
|
|
326
|
+
payload["background_exec_count"] = self._background_exec_count()
|
|
283
327
|
self._event_handler(
|
|
284
328
|
AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
|
|
285
329
|
)
|
|
286
330
|
|
|
331
|
+
def _background_exec_count(self) -> 'int':
|
|
332
|
+
manager: 'typing.Union[UnifiedExecManager, None]' = self._exec_manager
|
|
333
|
+
if manager is None:
|
|
334
|
+
return 0
|
|
335
|
+
return manager.running_session_count()
|
|
336
|
+
|
|
287
337
|
def _persist_history_items(
|
|
288
338
|
self,
|
|
289
339
|
items: 'typing.Iterable[ConversationItem]',
|
|
@@ -296,6 +346,28 @@ class AgentLoop:
|
|
|
296
346
|
except Exception: # pragma: no cover - persistence should not break turns
|
|
297
347
|
return
|
|
298
348
|
|
|
349
|
+
def _record_model_response_items(
|
|
350
|
+
self,
|
|
351
|
+
items: 'typing.Iterable[object]',
|
|
352
|
+
include_tool_calls: 'bool' = True,
|
|
353
|
+
) -> 'typing.Tuple[typing.Tuple[ConversationItem, ...], typing.List[ToolCall], typing.Union[str, None]]':
|
|
354
|
+
persisted_response_items: 'typing.List[ConversationItem]' = []
|
|
355
|
+
tool_calls: 'typing.List[ToolCall]' = []
|
|
356
|
+
last_assistant_message = None
|
|
357
|
+
for item in items:
|
|
358
|
+
if isinstance(item, ToolCall) and not include_tool_calls:
|
|
359
|
+
continue
|
|
360
|
+
if not isinstance(item, (AssistantMessage, ToolCall, ReasoningItem)):
|
|
361
|
+
continue
|
|
362
|
+
self._history.append(item)
|
|
363
|
+
persisted_response_items.append(item)
|
|
364
|
+
if isinstance(item, AssistantMessage):
|
|
365
|
+
last_assistant_message = item.text
|
|
366
|
+
elif isinstance(item, ToolCall):
|
|
367
|
+
tool_calls.append(item)
|
|
368
|
+
self._persist_history_items(persisted_response_items)
|
|
369
|
+
return tuple(persisted_response_items), tool_calls, last_assistant_message
|
|
370
|
+
|
|
299
371
|
def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
|
|
300
372
|
if event.kind == "token_count":
|
|
301
373
|
self._remember_token_usage(event.payload.get("usage"))
|
|
@@ -341,18 +413,34 @@ class AgentLoop:
|
|
|
341
413
|
prompt,
|
|
342
414
|
lambda event: self._handle_model_stream_event(turn_id, event),
|
|
343
415
|
)
|
|
416
|
+
except ResponsesIncompleteError as exc:
|
|
417
|
+
if exc.reason == "max_output_tokens":
|
|
418
|
+
self._record_model_response_items(
|
|
419
|
+
exc.partial_items,
|
|
420
|
+
include_tool_calls=False,
|
|
421
|
+
)
|
|
422
|
+
raise
|
|
344
423
|
except Exception as exc:
|
|
345
|
-
|
|
346
|
-
if
|
|
424
|
+
error_message = str(exc)
|
|
425
|
+
if (
|
|
426
|
+
not _is_context_length_error_message(error_message)
|
|
427
|
+
or attempted_context_compact
|
|
428
|
+
):
|
|
347
429
|
raise
|
|
348
430
|
attempted_context_compact = True
|
|
349
|
-
|
|
350
|
-
|
|
431
|
+
context_usage = _usage_from_context_length_error(error_message)
|
|
432
|
+
if context_usage is not None:
|
|
433
|
+
self._remember_token_usage(context_usage)
|
|
434
|
+
self._emit("token_count", turn_id, usage=context_usage)
|
|
351
435
|
await self._run_auto_compact(
|
|
352
436
|
turn_id,
|
|
353
437
|
phase="context_length_exceeded",
|
|
354
|
-
total_tokens=
|
|
355
|
-
|
|
438
|
+
total_tokens=(
|
|
439
|
+
context_usage.get("total_tokens")
|
|
440
|
+
if context_usage is not None
|
|
441
|
+
else None
|
|
442
|
+
),
|
|
443
|
+
token_limit=_context_length_error_token_limit(error_message),
|
|
356
444
|
prune_tool_results_on_context_error=True,
|
|
357
445
|
)
|
|
358
446
|
self._raise_if_interrupt_requested(turn_id, iteration)
|
|
@@ -385,7 +473,7 @@ class AgentLoop:
|
|
|
385
473
|
token_limit: 'typing.Union[int, None]' = None,
|
|
386
474
|
prune_tool_results_on_context_error: 'bool' = False,
|
|
387
475
|
) -> 'None':
|
|
388
|
-
from .utils.compactor import
|
|
476
|
+
from .utils.compactor import compact_agent
|
|
389
477
|
|
|
390
478
|
payload: 'typing.Dict[str, object]' = {"phase": phase}
|
|
391
479
|
if total_tokens is not None:
|
|
@@ -403,7 +491,7 @@ class AgentLoop:
|
|
|
403
491
|
self._emit("stream_error", turn_id, **event.payload)
|
|
404
492
|
|
|
405
493
|
try:
|
|
406
|
-
compact_result = await
|
|
494
|
+
compact_result = await compact_agent(
|
|
407
495
|
self,
|
|
408
496
|
handle_compact_stream_event,
|
|
409
497
|
prune_tool_results_on_context_error,
|
|
@@ -477,11 +565,7 @@ class AgentLoop:
|
|
|
477
565
|
def _usage_from_context_length_error(
|
|
478
566
|
message: 'str',
|
|
479
567
|
) -> 'typing.Union[typing.Dict[str, int], None]':
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
"context_length_exceeded" not in lower
|
|
483
|
-
and "maximum context length" not in lower
|
|
484
|
-
):
|
|
568
|
+
if not _is_context_length_error_message(message):
|
|
485
569
|
return None
|
|
486
570
|
|
|
487
571
|
requested_match = _REQUESTED_TOKENS_RE.search(message)
|
|
@@ -498,6 +582,11 @@ def _usage_from_context_length_error(
|
|
|
498
582
|
return usage
|
|
499
583
|
|
|
500
584
|
|
|
585
|
+
def _is_context_length_error_message(message: 'str') -> 'bool':
|
|
586
|
+
lower = message.lower()
|
|
587
|
+
return any(marker in lower for marker in _CONTEXT_LENGTH_ERROR_MARKERS)
|
|
588
|
+
|
|
589
|
+
|
|
501
590
|
def _context_length_error_token_limit(message: 'str') -> 'typing.Union[int, None]':
|
|
502
591
|
limit_match = _MAX_CONTEXT_TOKENS_RE.search(message)
|
|
503
592
|
if limit_match is None:
|