python-codex 0.1.11__py3-none-any.whl → 0.1.13__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 +226 -21
- pycodex/cli.py +199 -145
- pycodex/compat.py +8 -4
- pycodex/context.py +16 -0
- pycodex/feishu_card.py +693 -0
- pycodex/feishu_link.py +342 -0
- pycodex/model.py +102 -7
- pycodex/prompts/models.json +4 -4
- pycodex/protocol.py +17 -17
- pycodex/runtime.py +9 -14
- pycodex/runtime_services.py +45 -23
- pycodex/tools/apply_patch_tool.py +11 -12
- pycodex/tools/ipython_tool.py +144 -0
- pycodex/tools/unified_exec_manager.py +3 -0
- pycodex/utils/__init__.py +2 -13
- pycodex/utils/async_bridge.py +54 -0
- pycodex/utils/compactor.py +96 -19
- pycodex/utils/session_persist.py +57 -38
- pycodex/utils/toolcall_visualize.py +713 -0
- pycodex/utils/visualize.py +252 -837
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/METADATA +15 -2
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/RECORD +28 -23
- responses_server/app.py +7 -3
- responses_server/stream_router.py +39 -1
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/WHEEL +0 -0
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
from typing import Callable
|
|
5
6
|
|
|
6
7
|
from .context import ContextManager
|
|
@@ -22,17 +23,36 @@ import typing
|
|
|
22
23
|
|
|
23
24
|
if typing.TYPE_CHECKING:
|
|
24
25
|
from .utils.session_persist import SessionRolloutRecorder
|
|
26
|
+
from .runtime_services import AgentRuntimeEnvironment
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
EventHandler = Callable[[AgentEvent], None]
|
|
28
|
-
|
|
30
|
+
BASE_EVENT_HANDLER: 'EventHandler' = lambda _event: None
|
|
31
|
+
_REQUESTED_TOKENS_RE = re.compile(
|
|
32
|
+
r"requested\s+([0-9,]+)\s+tokens",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
_REQUESTED_TOKEN_SPLIT_RE = re.compile(
|
|
36
|
+
r"\(([0-9,]+)\s+in\s+the\s+messages,\s+([0-9,]+)\s+in\s+the\s+completion\)",
|
|
37
|
+
re.IGNORECASE,
|
|
38
|
+
)
|
|
39
|
+
_MAX_CONTEXT_TOKENS_RE = re.compile(
|
|
40
|
+
r"maximum\s+context\s+length\s+is\s+([0-9,]+)\s+tokens",
|
|
41
|
+
re.IGNORECASE,
|
|
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
|
+
)
|
|
29
49
|
|
|
30
50
|
|
|
31
51
|
class TurnInterrupted(RuntimeError):
|
|
32
52
|
pass
|
|
33
53
|
|
|
34
54
|
|
|
35
|
-
class
|
|
55
|
+
class Agent:
|
|
36
56
|
"""Minimal Python port of Codex's turn loop.
|
|
37
57
|
|
|
38
58
|
The core idea mirrors the Rust implementation:
|
|
@@ -47,9 +67,10 @@ class AgentLoop:
|
|
|
47
67
|
tool_registry: 'ToolRegistry',
|
|
48
68
|
context_manager: 'typing.Union[ContextManager, None]' = None,
|
|
49
69
|
parallel_tool_calls: 'bool' = True,
|
|
50
|
-
event_handler: 'EventHandler' =
|
|
70
|
+
event_handler: 'EventHandler' = BASE_EVENT_HANDLER,
|
|
51
71
|
initial_history: 'typing.Tuple[ConversationItem, ...]' = (),
|
|
52
72
|
rollout_recorder: 'typing.Union[SessionRolloutRecorder, None]' = None,
|
|
73
|
+
runtime_environment: 'AgentRuntimeEnvironment' = None,
|
|
53
74
|
) -> 'None':
|
|
54
75
|
self._model_client = model_client
|
|
55
76
|
self._tool_registry = tool_registry
|
|
@@ -58,6 +79,11 @@ class AgentLoop:
|
|
|
58
79
|
self._event_handler = event_handler
|
|
59
80
|
self._history: 'typing.List[ConversationItem]' = list(initial_history)
|
|
60
81
|
self._rollout_recorder = rollout_recorder
|
|
82
|
+
self._auto_compact_token_limit = (
|
|
83
|
+
self._context_manager.resolve_auto_compact_token_limit()
|
|
84
|
+
)
|
|
85
|
+
self._last_total_usage_tokens: 'typing.Union[int, None]' = None
|
|
86
|
+
self.runtime_environment = runtime_environment
|
|
61
87
|
self.interrupt_asap = False
|
|
62
88
|
|
|
63
89
|
@property
|
|
@@ -65,7 +91,7 @@ class AgentLoop:
|
|
|
65
91
|
return tuple(self._history)
|
|
66
92
|
|
|
67
93
|
def set_event_handler(
|
|
68
|
-
self, event_handler: 'EventHandler' =
|
|
94
|
+
self, event_handler: 'EventHandler' = BASE_EVENT_HANDLER
|
|
69
95
|
) -> 'None':
|
|
70
96
|
self._event_handler = event_handler
|
|
71
97
|
|
|
@@ -81,6 +107,11 @@ class AgentLoop:
|
|
|
81
107
|
) -> 'None':
|
|
82
108
|
self._rollout_recorder = rollout_recorder
|
|
83
109
|
|
|
110
|
+
def ask(self, text: 'str') -> 'TurnResult':
|
|
111
|
+
from .utils.async_bridge import run_async
|
|
112
|
+
|
|
113
|
+
return run_async(self.run_turn([text]))
|
|
114
|
+
|
|
84
115
|
def _raise_if_interrupt_requested(
|
|
85
116
|
self,
|
|
86
117
|
turn_id: 'str',
|
|
@@ -101,8 +132,6 @@ class AgentLoop:
|
|
|
101
132
|
turn_id = turn_id or uuid7_string()
|
|
102
133
|
self.interrupt_asap = False
|
|
103
134
|
new_user_messages = [UserMessage(text=text) for text in texts]
|
|
104
|
-
self._history.extend(new_user_messages)
|
|
105
|
-
self._persist_history_items(new_user_messages)
|
|
106
135
|
|
|
107
136
|
self._emit(
|
|
108
137
|
"turn_started",
|
|
@@ -110,6 +139,9 @@ class AgentLoop:
|
|
|
110
139
|
user_text="\n".join(texts),
|
|
111
140
|
user_texts=list(texts),
|
|
112
141
|
)
|
|
142
|
+
await self._maybe_auto_compact(turn_id, phase="pre_turn")
|
|
143
|
+
self._history.extend(new_user_messages)
|
|
144
|
+
self._persist_history_items(new_user_messages)
|
|
113
145
|
|
|
114
146
|
last_assistant_message: 'typing.Union[str, None]' = None
|
|
115
147
|
final_response_items: 'typing.Tuple[\n typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], ...\n]' = ()
|
|
@@ -122,23 +154,11 @@ class AgentLoop:
|
|
|
122
154
|
iteration,
|
|
123
155
|
output_text=last_assistant_message,
|
|
124
156
|
)
|
|
157
|
+
await self._maybe_auto_compact(turn_id, phase="mid_turn")
|
|
125
158
|
iteration += 1
|
|
126
|
-
|
|
127
|
-
self._history,
|
|
128
|
-
self._tool_registry.model_visible_specs(),
|
|
129
|
-
self._parallel_tool_calls,
|
|
130
|
-
turn_id=turn_id,
|
|
131
|
-
)
|
|
132
|
-
self._emit(
|
|
133
|
-
"model_called",
|
|
159
|
+
response = await self._complete_model_request(
|
|
134
160
|
turn_id,
|
|
135
|
-
iteration
|
|
136
|
-
history_size=len(prompt.input),
|
|
137
|
-
tool_count=len(prompt.tools),
|
|
138
|
-
)
|
|
139
|
-
response = await self._model_client.complete(
|
|
140
|
-
prompt,
|
|
141
|
-
lambda event: self._handle_model_stream_event(turn_id, event),
|
|
161
|
+
iteration,
|
|
142
162
|
)
|
|
143
163
|
final_response_items = tuple(response.items)
|
|
144
164
|
self._emit(
|
|
@@ -193,6 +213,10 @@ class AgentLoop:
|
|
|
193
213
|
except TurnInterrupted:
|
|
194
214
|
raise
|
|
195
215
|
except Exception as exc:
|
|
216
|
+
context_usage = _usage_from_context_length_error(str(exc))
|
|
217
|
+
if context_usage is not None:
|
|
218
|
+
self._remember_token_usage(context_usage)
|
|
219
|
+
self._emit("token_count", turn_id, usage=context_usage)
|
|
196
220
|
self._emit(
|
|
197
221
|
"turn_failed",
|
|
198
222
|
turn_id,
|
|
@@ -287,6 +311,8 @@ class AgentLoop:
|
|
|
287
311
|
return
|
|
288
312
|
|
|
289
313
|
def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
|
|
314
|
+
if event.kind == "token_count":
|
|
315
|
+
self._remember_token_usage(event.payload.get("usage"))
|
|
290
316
|
if event.kind == "assistant_delta":
|
|
291
317
|
self._emit("assistant_delta", turn_id, **event.payload)
|
|
292
318
|
elif event.kind == "tool_call":
|
|
@@ -296,6 +322,149 @@ class AgentLoop:
|
|
|
296
322
|
elif event.kind == "stream_error":
|
|
297
323
|
self._emit("stream_error", turn_id, **event.payload)
|
|
298
324
|
|
|
325
|
+
def _remember_token_usage(self, usage: 'object') -> 'None':
|
|
326
|
+
if not isinstance(usage, dict):
|
|
327
|
+
return
|
|
328
|
+
try:
|
|
329
|
+
self._last_total_usage_tokens = int(usage["total_tokens"])
|
|
330
|
+
except (KeyError, TypeError, ValueError):
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
async def _complete_model_request(
|
|
334
|
+
self,
|
|
335
|
+
turn_id: 'str',
|
|
336
|
+
iteration: 'int',
|
|
337
|
+
) -> 'typing.Any':
|
|
338
|
+
attempted_context_compact = False
|
|
339
|
+
while True:
|
|
340
|
+
prompt = self._context_manager.build_prompt(
|
|
341
|
+
self._history,
|
|
342
|
+
self._tool_registry.model_visible_specs(),
|
|
343
|
+
self._parallel_tool_calls,
|
|
344
|
+
turn_id=turn_id,
|
|
345
|
+
)
|
|
346
|
+
self._emit(
|
|
347
|
+
"model_called",
|
|
348
|
+
turn_id,
|
|
349
|
+
iteration=iteration,
|
|
350
|
+
history_size=len(prompt.input),
|
|
351
|
+
tool_count=len(prompt.tools),
|
|
352
|
+
)
|
|
353
|
+
try:
|
|
354
|
+
return await self._model_client.complete(
|
|
355
|
+
prompt,
|
|
356
|
+
lambda event: self._handle_model_stream_event(turn_id, event),
|
|
357
|
+
)
|
|
358
|
+
except Exception as exc:
|
|
359
|
+
error_message = str(exc)
|
|
360
|
+
if (
|
|
361
|
+
not _is_context_length_error_message(error_message)
|
|
362
|
+
or attempted_context_compact
|
|
363
|
+
):
|
|
364
|
+
raise
|
|
365
|
+
attempted_context_compact = True
|
|
366
|
+
context_usage = _usage_from_context_length_error(error_message)
|
|
367
|
+
if context_usage is not None:
|
|
368
|
+
self._remember_token_usage(context_usage)
|
|
369
|
+
self._emit("token_count", turn_id, usage=context_usage)
|
|
370
|
+
await self._run_auto_compact(
|
|
371
|
+
turn_id,
|
|
372
|
+
phase="context_length_exceeded",
|
|
373
|
+
total_tokens=(
|
|
374
|
+
context_usage.get("total_tokens")
|
|
375
|
+
if context_usage is not None
|
|
376
|
+
else None
|
|
377
|
+
),
|
|
378
|
+
token_limit=_context_length_error_token_limit(error_message),
|
|
379
|
+
prune_tool_results_on_context_error=True,
|
|
380
|
+
)
|
|
381
|
+
self._raise_if_interrupt_requested(turn_id, iteration)
|
|
382
|
+
|
|
383
|
+
async def _maybe_auto_compact(
|
|
384
|
+
self,
|
|
385
|
+
turn_id: 'str',
|
|
386
|
+
phase: 'str',
|
|
387
|
+
) -> 'None':
|
|
388
|
+
limit = self._auto_compact_token_limit
|
|
389
|
+
total_tokens = self._last_total_usage_tokens
|
|
390
|
+
if limit is None or total_tokens is None:
|
|
391
|
+
return
|
|
392
|
+
if total_tokens < limit or not self._history:
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
await self._run_auto_compact(
|
|
396
|
+
turn_id,
|
|
397
|
+
phase=phase,
|
|
398
|
+
total_tokens=total_tokens,
|
|
399
|
+
token_limit=limit,
|
|
400
|
+
prune_tool_results_on_context_error=True,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
async def _run_auto_compact(
|
|
404
|
+
self,
|
|
405
|
+
turn_id: 'str',
|
|
406
|
+
phase: 'str',
|
|
407
|
+
total_tokens: 'typing.Union[int, None]' = None,
|
|
408
|
+
token_limit: 'typing.Union[int, None]' = None,
|
|
409
|
+
prune_tool_results_on_context_error: 'bool' = False,
|
|
410
|
+
) -> 'None':
|
|
411
|
+
from .utils.compactor import compact_agent
|
|
412
|
+
|
|
413
|
+
payload: 'typing.Dict[str, object]' = {"phase": phase}
|
|
414
|
+
if total_tokens is not None:
|
|
415
|
+
payload["total_tokens"] = total_tokens
|
|
416
|
+
if token_limit is not None:
|
|
417
|
+
payload["token_limit"] = token_limit
|
|
418
|
+
self._emit(
|
|
419
|
+
"auto_compact_started",
|
|
420
|
+
turn_id,
|
|
421
|
+
**payload,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def handle_compact_stream_event(event: 'ModelStreamEvent') -> 'None':
|
|
425
|
+
if event.kind == "stream_error":
|
|
426
|
+
self._emit("stream_error", turn_id, **event.payload)
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
compact_result = await compact_agent(
|
|
430
|
+
self,
|
|
431
|
+
handle_compact_stream_event,
|
|
432
|
+
prune_tool_results_on_context_error,
|
|
433
|
+
)
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
failed_payload = dict(payload)
|
|
436
|
+
failed_payload.update(
|
|
437
|
+
{
|
|
438
|
+
"error": str(exc),
|
|
439
|
+
"error_type": type(exc).__name__,
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
self._emit(
|
|
443
|
+
"auto_compact_failed",
|
|
444
|
+
turn_id,
|
|
445
|
+
**failed_payload,
|
|
446
|
+
)
|
|
447
|
+
raise
|
|
448
|
+
|
|
449
|
+
self._last_total_usage_tokens = None
|
|
450
|
+
if compact_result is None:
|
|
451
|
+
return
|
|
452
|
+
completed_payload = dict(payload)
|
|
453
|
+
completed_payload.update(
|
|
454
|
+
{
|
|
455
|
+
"original_item_count": compact_result.original_item_count,
|
|
456
|
+
"retained_item_count": compact_result.retained_item_count,
|
|
457
|
+
"summary": compact_result.display_text(),
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
if compact_result.pruned_tool_results:
|
|
461
|
+
completed_payload["pruned_tool_results"] = compact_result.pruned_tool_results
|
|
462
|
+
self._emit(
|
|
463
|
+
"auto_compact_completed",
|
|
464
|
+
turn_id,
|
|
465
|
+
**completed_payload,
|
|
466
|
+
)
|
|
467
|
+
|
|
299
468
|
def _build_follow_up_messages(
|
|
300
469
|
self,
|
|
301
470
|
tool_results: 'typing.List[ToolResult]',
|
|
@@ -326,3 +495,39 @@ class AgentLoop:
|
|
|
326
495
|
)
|
|
327
496
|
)
|
|
328
497
|
return follow_ups
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _usage_from_context_length_error(
|
|
501
|
+
message: 'str',
|
|
502
|
+
) -> 'typing.Union[typing.Dict[str, int], None]':
|
|
503
|
+
if not _is_context_length_error_message(message):
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
requested_match = _REQUESTED_TOKENS_RE.search(message)
|
|
507
|
+
if requested_match is None:
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
usage = {"total_tokens": _parse_token_count(requested_match.group(1))}
|
|
511
|
+
split_match = _REQUESTED_TOKEN_SPLIT_RE.search(message)
|
|
512
|
+
if split_match is not None:
|
|
513
|
+
usage["input_tokens"] = _parse_token_count(split_match.group(1))
|
|
514
|
+
usage["output_tokens"] = _parse_token_count(split_match.group(2))
|
|
515
|
+
else:
|
|
516
|
+
usage["input_tokens"] = usage["total_tokens"]
|
|
517
|
+
return usage
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _is_context_length_error_message(message: 'str') -> 'bool':
|
|
521
|
+
lower = message.lower()
|
|
522
|
+
return any(marker in lower for marker in _CONTEXT_LENGTH_ERROR_MARKERS)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _context_length_error_token_limit(message: 'str') -> 'typing.Union[int, None]':
|
|
526
|
+
limit_match = _MAX_CONTEXT_TOKENS_RE.search(message)
|
|
527
|
+
if limit_match is None:
|
|
528
|
+
return None
|
|
529
|
+
return _parse_token_count(limit_match.group(1))
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _parse_token_count(value: 'str') -> 'int':
|
|
533
|
+
return int(value.replace(",", ""))
|