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.
- pycodex/__init__.py +14 -14
- pycodex/agent.py +465 -499
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -510
- pycodex/compat.py +19 -5
- pycodex/context.py +222 -212
- pycodex/doctor.py +52 -48
- pycodex/events.py +857 -0
- pycodex/feishu_card.py +217 -163
- pycodex/feishu_link.py +43 -83
- pycodex/model.py +324 -253
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +76 -45
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +245 -983
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -176
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +1 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +58 -25
- pycodex/tools/close_agent_tool.py +2 -2
- pycodex/tools/code_mode_manager.py +77 -64
- pycodex/tools/exec_command_tool.py +26 -11
- pycodex/tools/exec_tool.py +4 -4
- pycodex/tools/grep_files_tool.py +12 -10
- pycodex/tools/ipython_tool.py +10 -13
- pycodex/tools/list_dir_tool.py +13 -9
- pycodex/tools/read_file_tool.py +29 -17
- pycodex/tools/request_permissions_tool.py +15 -5
- pycodex/tools/request_user_input_tool.py +13 -104
- pycodex/tools/resume_agent_tool.py +2 -2
- pycodex/tools/send_input_tool.py +11 -8
- pycodex/tools/shell_command_tool.py +7 -5
- pycodex/tools/shell_tool.py +7 -5
- pycodex/tools/spawn_agent_tool.py +7 -4
- pycodex/tools/unified_exec_manager.py +102 -69
- pycodex/tools/update_plan_tool.py +8 -5
- pycodex/tools/view_image_tool.py +7 -5
- pycodex/tools/wait_agent_tool.py +27 -4
- pycodex/tools/wait_tool.py +5 -4
- pycodex/tools/web_search_tool.py +4 -2
- pycodex/tools/write_stdin_tool.py +12 -11
- pycodex/utils/__init__.py +2 -17
- pycodex/utils/compactor.py +41 -72
- pycodex/utils/debug.py +2 -2
- pycodex/utils/dotenv.py +6 -7
- pycodex/utils/event_helpers.py +190 -0
- pycodex/utils/get_env.py +27 -70
- pycodex/{image_utils.py → utils/image_utils.py} +8 -11
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +217 -163
- pycodex/utils/truncation.py +21 -45
- python_codex-0.3.0.dist-info/METADATA +704 -0
- python_codex-0.3.0.dist-info/RECORD +90 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +36 -31
- responses_server/config.py +23 -23
- responses_server/messages_api.py +51 -53
- responses_server/payload_processors.py +25 -20
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +101 -98
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +36 -14
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +461 -375
- workspace_server/workspace.html +852 -228
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +137 -79
- pycodex/collaboration.py +0 -20
- pycodex/interactive_session.py +0 -415
- pycodex/prompts/collaboration_default.md +0 -11
- pycodex/prompts/collaboration_plan.md +0 -128
- pycodex/utils/toolcall_visualize.py +0 -713
- pycodex/utils/visualize.py +0 -560
- python_codex-0.2.7.dist-info/METADATA +0 -455
- python_codex-0.2.7.dist-info/RECORD +0 -93
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/agent.py
CHANGED
|
@@ -1,64 +1,78 @@
|
|
|
1
|
-
|
|
2
1
|
import asyncio
|
|
3
2
|
import json
|
|
4
|
-
import
|
|
3
|
+
import typing
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Callable
|
|
6
7
|
|
|
7
|
-
from .context import ContextManager
|
|
8
|
-
from .
|
|
8
|
+
from .context import ContextConfig, ContextManager
|
|
9
|
+
from .events import (
|
|
10
|
+
AutoCompactCompletedEvent,
|
|
11
|
+
AutoCompactFailedEvent,
|
|
12
|
+
AutoCompactStartedEvent,
|
|
13
|
+
CompactCompletedEvent,
|
|
14
|
+
CompactFailedEvent,
|
|
15
|
+
CompactStartedEvent,
|
|
16
|
+
Event,
|
|
17
|
+
ModelCalledEvent,
|
|
18
|
+
ModelCompletedEvent,
|
|
19
|
+
ModelEvent,
|
|
20
|
+
StreamErrorEvent,
|
|
21
|
+
TerminalEvent,
|
|
22
|
+
TokenCountEvent,
|
|
23
|
+
ToolCompletedEvent,
|
|
24
|
+
ToolStartedEvent,
|
|
25
|
+
TurnCompletedEvent,
|
|
26
|
+
TurnEvent,
|
|
27
|
+
TurnFailedEvent,
|
|
28
|
+
TurnInterruptedEvent,
|
|
29
|
+
TurnStartedEvent,
|
|
30
|
+
)
|
|
31
|
+
from .model import (
|
|
32
|
+
DEFAULT_ORIGINATOR,
|
|
33
|
+
ContextLengthExceeded,
|
|
34
|
+
ModelClient,
|
|
35
|
+
ModelControl,
|
|
36
|
+
ResponsesIncompleteError,
|
|
37
|
+
)
|
|
9
38
|
from .protocol import (
|
|
10
|
-
AgentEvent,
|
|
11
39
|
AssistantMessage,
|
|
12
40
|
ConversationItem,
|
|
13
|
-
|
|
41
|
+
ModelResponse,
|
|
14
42
|
ReasoningItem,
|
|
15
43
|
ToolCall,
|
|
16
44
|
ToolResult,
|
|
17
45
|
TurnResult,
|
|
18
46
|
UserMessage,
|
|
19
47
|
)
|
|
20
|
-
from .tools import
|
|
21
|
-
ClockManager,
|
|
22
|
-
ClockTool,
|
|
23
|
-
ExecCommandTool,
|
|
24
|
-
ToolContext,
|
|
25
|
-
ToolRegistry,
|
|
26
|
-
UnifiedExecManager,
|
|
27
|
-
)
|
|
28
|
-
from .utils.truncation import truncate_tool_results_for_history
|
|
48
|
+
from .tools import ToolContext, ToolRegistry
|
|
29
49
|
from .utils import uuid7_string
|
|
30
|
-
import
|
|
50
|
+
from .utils.session_persist import (
|
|
51
|
+
SessionRolloutRecorder,
|
|
52
|
+
load_resumed_session_path,
|
|
53
|
+
resolve_codex_home,
|
|
54
|
+
rollout_path_for_session,
|
|
55
|
+
)
|
|
56
|
+
from .utils.truncation import truncate_tool_result_for_history
|
|
31
57
|
|
|
32
58
|
if typing.TYPE_CHECKING:
|
|
33
|
-
from .utils.
|
|
34
|
-
from .runtime_services import AgentRuntimeEnvironment
|
|
59
|
+
from .utils.compactor import CompactResult
|
|
35
60
|
|
|
36
61
|
|
|
37
|
-
EventHandler = Callable[[
|
|
38
|
-
BASE_EVENT_HANDLER:
|
|
39
|
-
_REQUESTED_TOKENS_RE = re.compile(
|
|
40
|
-
r"requested\s+([0-9,]+)\s+tokens",
|
|
41
|
-
re.IGNORECASE,
|
|
42
|
-
)
|
|
43
|
-
_REQUESTED_TOKEN_SPLIT_RE = re.compile(
|
|
44
|
-
r"\(([0-9,]+)\s+in\s+the\s+messages,\s+([0-9,]+)\s+in\s+the\s+completion\)",
|
|
45
|
-
re.IGNORECASE,
|
|
46
|
-
)
|
|
47
|
-
_MAX_CONTEXT_TOKENS_RE = re.compile(
|
|
48
|
-
r"maximum\s+context\s+length\s+is\s+([0-9,]+)\s+tokens",
|
|
49
|
-
re.IGNORECASE,
|
|
50
|
-
)
|
|
51
|
-
_CONTEXT_LENGTH_ERROR_MARKERS = (
|
|
52
|
-
"context_length_exceeded",
|
|
53
|
-
"maximum context length",
|
|
54
|
-
"exceeds the context window",
|
|
55
|
-
"exceeded the context window",
|
|
56
|
-
)
|
|
57
|
-
TERMINAL_TURN_EVENTS = {"turn_completed", "turn_failed", "turn_interrupted"}
|
|
62
|
+
EventHandler = Callable[[Event], None]
|
|
63
|
+
BASE_EVENT_HANDLER: "EventHandler" = lambda _event: None
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
class TurnInterrupted(RuntimeError):
|
|
61
|
-
|
|
67
|
+
def __init__(self):
|
|
68
|
+
super().__init__("turn interrupted")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class _TurnState:
|
|
73
|
+
turn_id: "str"
|
|
74
|
+
iteration: "int" = 0
|
|
75
|
+
output_text: "typing.Union[str, None]" = None
|
|
62
76
|
|
|
63
77
|
|
|
64
78
|
class Agent:
|
|
@@ -72,357 +86,392 @@ class Agent:
|
|
|
72
86
|
|
|
73
87
|
def __init__(
|
|
74
88
|
self,
|
|
75
|
-
model_client:
|
|
76
|
-
tool_registry:
|
|
77
|
-
|
|
78
|
-
parallel_tool_calls:
|
|
79
|
-
event_handler:
|
|
80
|
-
initial_history:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
) ->
|
|
84
|
-
self.
|
|
85
|
-
self.
|
|
86
|
-
self.
|
|
87
|
-
|
|
88
|
-
self._event_handler = event_handler
|
|
89
|
-
self._history: 'typing.List[ConversationItem]' = list(initial_history)
|
|
90
|
-
self._rollout_recorder = rollout_recorder
|
|
91
|
-
self._auto_compact_token_limit = (
|
|
92
|
-
self._context_manager.resolve_auto_compact_token_limit()
|
|
89
|
+
model_client: "ModelClient",
|
|
90
|
+
tool_registry: "ToolRegistry",
|
|
91
|
+
context_config: "ContextConfig",
|
|
92
|
+
parallel_tool_calls: "bool" = True,
|
|
93
|
+
event_handler: "EventHandler" = BASE_EVENT_HANDLER,
|
|
94
|
+
initial_history: "typing.Tuple[ConversationItem, ...]" = (),
|
|
95
|
+
session_file_path: "typing.Union[str, Path, None]" = None,
|
|
96
|
+
session_id: "typing.Union[str, None]" = None,
|
|
97
|
+
) -> "None":
|
|
98
|
+
self.model_client = model_client
|
|
99
|
+
self.tool_registry = tool_registry
|
|
100
|
+
self.context_manager = ContextManager(
|
|
101
|
+
replace(context_config, model=model_client.model)
|
|
93
102
|
)
|
|
94
|
-
self.
|
|
95
|
-
self.
|
|
96
|
-
self.
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
self._exec_manager.set_notify_hook(self.maybe_invoke)
|
|
106
|
-
clock_tool = self._tool_registry.get_tool("clock")
|
|
107
|
-
self._clock_manager: 'typing.Union[ClockManager, None]' = (
|
|
108
|
-
clock_tool._manager
|
|
109
|
-
if isinstance(clock_tool, ClockTool)
|
|
110
|
-
else None
|
|
111
|
-
)
|
|
112
|
-
if self._clock_manager is not None:
|
|
113
|
-
self._clock_manager.set_notify_hook(self.maybe_invoke)
|
|
103
|
+
self._parallel_tool_calls = parallel_tool_calls
|
|
104
|
+
self.event_handler = event_handler
|
|
105
|
+
self._history: "typing.List[ConversationItem]" = list(initial_history)
|
|
106
|
+
self._configure_recording(session_id or uuid7_string(), session_file_path)
|
|
107
|
+
self._last_total_usage_tokens: "typing.Union[int, None]" = None
|
|
108
|
+
self._idle: "typing.Union[asyncio.Event, None]" = None
|
|
109
|
+
self.is_shutdown = False
|
|
110
|
+
self.accepts_input = True
|
|
111
|
+
self._stop_requested = False
|
|
112
|
+
for tool in self.tool_registry.tools():
|
|
113
|
+
tool.bind_agent(self)
|
|
114
114
|
|
|
115
115
|
@property
|
|
116
|
-
def history(self) ->
|
|
116
|
+
def history(self) -> "typing.Tuple[ConversationItem, ...]":
|
|
117
117
|
return tuple(self._history)
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
@property
|
|
120
|
+
def session_file_path(self) -> "typing.Union[Path, None]":
|
|
121
|
+
recorder = self._rollout_recorder
|
|
122
|
+
return recorder.rollout_path if recorder is not None else None
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def recorded_session_file_path(self) -> "typing.Union[Path, None]":
|
|
126
|
+
recorder = self._rollout_recorder
|
|
127
|
+
if recorder is None or recorder._session_meta is not None:
|
|
128
|
+
return None
|
|
129
|
+
return recorder.rollout_path
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
) -> 'None':
|
|
128
|
-
self._history = list(history)
|
|
131
|
+
@property
|
|
132
|
+
def is_running(self) -> "bool":
|
|
133
|
+
return self._idle is not None
|
|
129
134
|
|
|
130
|
-
def
|
|
131
|
-
self
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
async def wait_until_idle(self) -> "None":
|
|
136
|
+
while self.is_running:
|
|
137
|
+
await self._idle.wait()
|
|
138
|
+
|
|
139
|
+
def stop_asap(self) -> "None":
|
|
140
|
+
if self.is_running:
|
|
141
|
+
self._stop_requested = True
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def model_name(self) -> "str":
|
|
145
|
+
return self.model_client.model
|
|
146
|
+
|
|
147
|
+
def set_model(self, model: "str") -> "None":
|
|
148
|
+
if self.is_running:
|
|
149
|
+
raise RuntimeError("cannot change model while agent is running")
|
|
150
|
+
client = typing.cast(ModelControl, self.model_client)
|
|
151
|
+
client.model = model
|
|
152
|
+
self.context_manager.set_model(model)
|
|
153
|
+
self._last_total_usage_tokens = None
|
|
135
154
|
|
|
136
|
-
def
|
|
155
|
+
def resume(
|
|
156
|
+
self, session_file_path: "typing.Union[str, Path, None]" = None
|
|
157
|
+
) -> "typing.Union[typing.Dict[str, object], None]":
|
|
158
|
+
if self.is_running:
|
|
159
|
+
raise RuntimeError("cannot restore session while agent is running")
|
|
160
|
+
resumed = None
|
|
161
|
+
if session_file_path is not None:
|
|
162
|
+
resumed = load_resumed_session_path(session_file_path)
|
|
163
|
+
session_id = str(resumed["session_id"])
|
|
164
|
+
if not session_id:
|
|
165
|
+
raise ValueError("session file has no session ID")
|
|
166
|
+
restored_history = list(resumed["history"])
|
|
167
|
+
self._configure_recording(session_id, resumed["rollout_path"], resume=True)
|
|
168
|
+
self._history = restored_history
|
|
169
|
+
self._last_total_usage_tokens = None
|
|
170
|
+
self.is_shutdown = False
|
|
171
|
+
self.accepts_input = True
|
|
172
|
+
for tool in self.tool_registry.tools():
|
|
173
|
+
tool.bind_agent(self)
|
|
174
|
+
return resumed
|
|
175
|
+
|
|
176
|
+
def ask(self, text: "str") -> "TurnResult":
|
|
137
177
|
from .utils.async_bridge import run_async
|
|
138
178
|
|
|
139
179
|
return run_async(self.run_turn([text]))
|
|
140
180
|
|
|
141
|
-
def
|
|
142
|
-
self
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
self._emit("turn_interrupted", turn_id, **payload)
|
|
153
|
-
raise TurnInterrupted("turn interrupted")
|
|
154
|
-
|
|
155
|
-
async def run_turn(
|
|
156
|
-
self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
|
|
157
|
-
) -> 'TurnResult':
|
|
158
|
-
self._turn_running = True
|
|
159
|
-
if self._clock_manager is not None:
|
|
160
|
-
self._clock_manager.turn_started()
|
|
161
|
-
turn_id = turn_id or uuid7_string()
|
|
162
|
-
self.interrupt_asap = False
|
|
163
|
-
new_user_messages = [UserMessage(text=text) for text in texts]
|
|
181
|
+
def fork(self) -> "None":
|
|
182
|
+
if self.is_running:
|
|
183
|
+
raise RuntimeError("cannot fork session while agent is running")
|
|
184
|
+
session_id = uuid7_string()
|
|
185
|
+
path = None
|
|
186
|
+
if self._rollout_recorder is not None:
|
|
187
|
+
path = rollout_path_for_session(
|
|
188
|
+
self.context_manager._config.codex_home or resolve_codex_home(),
|
|
189
|
+
session_id,
|
|
190
|
+
)
|
|
191
|
+
self._configure_recording(session_id, path)
|
|
164
192
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
def _configure_recording(
|
|
194
|
+
self,
|
|
195
|
+
session_id: "str",
|
|
196
|
+
session_file_path: "typing.Union[str, Path, None]" = None,
|
|
197
|
+
resume: "bool" = False,
|
|
198
|
+
) -> "None":
|
|
199
|
+
if session_file_path is None:
|
|
200
|
+
recorder = None
|
|
201
|
+
elif resume:
|
|
202
|
+
recorder = SessionRolloutRecorder.resume(session_file_path)
|
|
203
|
+
else:
|
|
204
|
+
recorder = SessionRolloutRecorder.create(
|
|
205
|
+
self.context_manager._config.codex_home or resolve_codex_home(),
|
|
206
|
+
session_id,
|
|
207
|
+
self.context_manager.cwd,
|
|
208
|
+
getattr(self.model_client, "_originator", DEFAULT_ORIGINATOR),
|
|
209
|
+
getattr(
|
|
210
|
+
getattr(self.model_client, "_config", None), "provider_name", None
|
|
211
|
+
),
|
|
212
|
+
self.context_manager.resolve_base_instructions(),
|
|
213
|
+
session_file_path,
|
|
214
|
+
)
|
|
215
|
+
if hasattr(self.model_client, "_session_id"):
|
|
216
|
+
self.model_client._session_id = session_id
|
|
217
|
+
self.session_id = session_id
|
|
218
|
+
self._rollout_recorder = recorder
|
|
174
219
|
|
|
175
|
-
|
|
176
|
-
|
|
220
|
+
async def compact(
|
|
221
|
+
self,
|
|
222
|
+
prune_tool_results_on_context_error: "bool" = True,
|
|
223
|
+
) -> "typing.Union[CompactResult, None]":
|
|
224
|
+
if self.is_shutdown:
|
|
225
|
+
raise RuntimeError("agent is shutdown")
|
|
226
|
+
if self.is_running:
|
|
227
|
+
raise RuntimeError("cannot compact while agent is running")
|
|
228
|
+
self._idle = asyncio.Event()
|
|
229
|
+
try:
|
|
230
|
+
return await self._compact_history(
|
|
231
|
+
uuid7_string(),
|
|
232
|
+
"manual",
|
|
233
|
+
None,
|
|
234
|
+
None,
|
|
235
|
+
prune_tool_results_on_context_error,
|
|
236
|
+
)
|
|
237
|
+
finally:
|
|
238
|
+
self._idle.set()
|
|
239
|
+
self._idle = None
|
|
177
240
|
|
|
178
|
-
|
|
241
|
+
async def run_turn(
|
|
242
|
+
self, texts: "typing.List[str]", turn_id: "typing.Union[str, None]" = None
|
|
243
|
+
) -> "TurnResult":
|
|
244
|
+
if self.is_shutdown:
|
|
245
|
+
raise RuntimeError("agent is shutdown")
|
|
246
|
+
if self.is_running:
|
|
247
|
+
raise RuntimeError("agent already has an active turn")
|
|
248
|
+
self._stop_requested = False
|
|
249
|
+
self._idle = asyncio.Event()
|
|
250
|
+
turn = _TurnState(turn_id or uuid7_string())
|
|
179
251
|
try:
|
|
252
|
+
self._emit(TurnStartedEvent(turn.turn_id, tuple(texts)))
|
|
180
253
|
while True:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
await self._maybe_auto_compact(turn_id, phase="mid_turn")
|
|
187
|
-
iteration += 1
|
|
188
|
-
response = await self._complete_model_request(
|
|
189
|
-
turn_id,
|
|
190
|
-
iteration,
|
|
191
|
-
)
|
|
192
|
-
final_response_items = tuple(response.items)
|
|
254
|
+
phase = "pre_turn" if turn.iteration == 0 else "mid_turn"
|
|
255
|
+
await self._maybe_auto_compact(turn.turn_id, phase)
|
|
256
|
+
if turn.iteration == 0:
|
|
257
|
+
self._append_history(UserMessage(text=text) for text in texts)
|
|
258
|
+
response = await self._sample(turn)
|
|
193
259
|
self._emit(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
item_count=len(response.items),
|
|
260
|
+
ModelCompletedEvent(
|
|
261
|
+
turn.turn_id, turn.iteration, len(response.items)
|
|
262
|
+
)
|
|
198
263
|
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
264
|
+
self._append_history(response.items)
|
|
265
|
+
tool_calls = []
|
|
266
|
+
for item in response.items:
|
|
267
|
+
if isinstance(item, AssistantMessage):
|
|
268
|
+
turn.output_text = item.text
|
|
269
|
+
elif isinstance(item, ToolCall):
|
|
270
|
+
tool_calls.append(item)
|
|
271
|
+
|
|
272
|
+
if tool_calls:
|
|
273
|
+
await self._execute_tool_batch(turn.turn_id, tool_calls)
|
|
274
|
+
if self._stop_requested:
|
|
275
|
+
raise TurnInterrupted()
|
|
205
276
|
if not tool_calls:
|
|
206
|
-
|
|
207
|
-
turn_id,
|
|
208
|
-
iteration,
|
|
209
|
-
output_text=last_assistant_message,
|
|
210
|
-
)
|
|
211
|
-
self._emit(
|
|
212
|
-
"turn_completed",
|
|
213
|
-
turn_id,
|
|
214
|
-
iteration=iteration,
|
|
215
|
-
output_text=last_assistant_message,
|
|
216
|
-
)
|
|
217
|
-
self._turn_running = False
|
|
218
|
-
if self._clock_manager is not None:
|
|
219
|
-
self._clock_manager.arm_after_reply()
|
|
220
|
-
return TurnResult(
|
|
221
|
-
turn_id=turn_id,
|
|
222
|
-
output_text=last_assistant_message,
|
|
223
|
-
iterations=iteration,
|
|
224
|
-
response_items=final_response_items,
|
|
225
|
-
history=tuple(self._history),
|
|
226
|
-
)
|
|
277
|
+
break
|
|
227
278
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
self._persist_history_items(follow_up_messages)
|
|
235
|
-
self._raise_if_interrupt_requested(
|
|
236
|
-
turn_id,
|
|
237
|
-
iteration,
|
|
238
|
-
output_text=last_assistant_message,
|
|
279
|
+
self._emit(
|
|
280
|
+
TurnCompletedEvent(
|
|
281
|
+
turn.turn_id,
|
|
282
|
+
turn.iteration,
|
|
283
|
+
turn.output_text,
|
|
284
|
+
self._background_work_count(TurnCompletedEvent),
|
|
239
285
|
)
|
|
286
|
+
)
|
|
287
|
+
result = TurnResult(
|
|
288
|
+
turn_id=turn.turn_id,
|
|
289
|
+
output_text=turn.output_text,
|
|
290
|
+
iterations=turn.iteration,
|
|
291
|
+
response_items=tuple(response.items),
|
|
292
|
+
history=self.history,
|
|
293
|
+
)
|
|
294
|
+
return result
|
|
240
295
|
except TurnInterrupted:
|
|
241
|
-
self.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
296
|
+
self._emit(
|
|
297
|
+
TurnInterruptedEvent(
|
|
298
|
+
turn.turn_id,
|
|
299
|
+
turn.iteration,
|
|
300
|
+
turn.output_text,
|
|
301
|
+
self._background_work_count(TurnInterruptedEvent),
|
|
302
|
+
)
|
|
303
|
+
)
|
|
245
304
|
raise
|
|
246
305
|
except Exception as exc:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
self.
|
|
250
|
-
self._emit("token_count", turn_id, usage=context_usage)
|
|
306
|
+
if isinstance(exc, ContextLengthExceeded) and exc.usage is not None:
|
|
307
|
+
self._remember_token_usage(exc.usage)
|
|
308
|
+
self._emit(TokenCountEvent(exc.usage, turn.turn_id))
|
|
251
309
|
self._emit(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
310
|
+
TurnFailedEvent(
|
|
311
|
+
turn.turn_id,
|
|
312
|
+
turn.iteration,
|
|
313
|
+
str(exc),
|
|
314
|
+
type(exc).__name__,
|
|
315
|
+
self._background_work_count(TurnFailedEvent),
|
|
316
|
+
)
|
|
257
317
|
)
|
|
258
|
-
self._turn_running = False
|
|
259
318
|
raise
|
|
319
|
+
finally:
|
|
320
|
+
self._stop_requested = False
|
|
321
|
+
self._idle.set()
|
|
322
|
+
self._idle = None
|
|
323
|
+
|
|
324
|
+
async def _sample(self, turn: "_TurnState") -> "ModelResponse":
|
|
325
|
+
for attempt in range(2):
|
|
326
|
+
if self._stop_requested:
|
|
327
|
+
raise TurnInterrupted()
|
|
328
|
+
if attempt == 0:
|
|
329
|
+
turn.iteration += 1
|
|
330
|
+
try:
|
|
331
|
+
return await self._complete_model_request(turn.turn_id, turn.iteration)
|
|
332
|
+
except ContextLengthExceeded as exc:
|
|
333
|
+
if attempt == 1:
|
|
334
|
+
raise
|
|
335
|
+
if exc.usage is not None:
|
|
336
|
+
self._remember_token_usage(exc.usage)
|
|
337
|
+
self._emit(TokenCountEvent(exc.usage, turn.turn_id))
|
|
338
|
+
await self._compact_history(
|
|
339
|
+
turn.turn_id,
|
|
340
|
+
phase="context_length_exceeded",
|
|
341
|
+
total_tokens=(
|
|
342
|
+
exc.usage.get("total_tokens") if exc.usage is not None else None
|
|
343
|
+
),
|
|
344
|
+
token_limit=exc.token_limit,
|
|
345
|
+
prune_tool_results_on_context_error=True,
|
|
346
|
+
)
|
|
260
347
|
|
|
261
|
-
async def maybe_invoke(self, event:
|
|
262
|
-
if self.
|
|
263
|
-
return False
|
|
264
|
-
event_type = event.get("type")
|
|
265
|
-
if event_type == "exec_command_completed":
|
|
266
|
-
payload = {
|
|
267
|
-
"session_id": event.get("session_id"),
|
|
268
|
-
"exit_code": event.get("exit_code"),
|
|
269
|
-
"command": event.get("command"),
|
|
270
|
-
}
|
|
271
|
-
tag = "exec_command_completed"
|
|
272
|
-
elif event_type == "clock_tick":
|
|
273
|
-
payload = {
|
|
274
|
-
"period_m": event.get("period_m"),
|
|
275
|
-
"current_time": event.get("current_time"),
|
|
276
|
-
}
|
|
277
|
-
tag = "clock_tick"
|
|
278
|
-
else:
|
|
348
|
+
async def maybe_invoke(self, event: "typing.Dict[str, object]") -> "bool":
|
|
349
|
+
if self.is_running or not self.accepts_input:
|
|
279
350
|
return False
|
|
351
|
+
tag = event["type"]
|
|
352
|
+
if not isinstance(tag, str) or not tag.isidentifier():
|
|
353
|
+
raise ValueError("invoke event type must be an identifier")
|
|
354
|
+
payload = {key: value for key, value in event.items() if key != "type"}
|
|
280
355
|
text = (
|
|
281
356
|
f"<{tag}>\n"
|
|
282
357
|
f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
|
|
283
358
|
f"</{tag}>"
|
|
284
359
|
)
|
|
285
|
-
self.
|
|
286
|
-
task = asyncio.create_task(self.run_turn([text]))
|
|
287
|
-
task.add_done_callback(
|
|
288
|
-
lambda task: None if task.cancelled() else task.exception()
|
|
289
|
-
)
|
|
360
|
+
await self.run_turn([text])
|
|
290
361
|
return True
|
|
291
362
|
|
|
292
|
-
def shutdown(self) ->
|
|
293
|
-
if self.
|
|
294
|
-
|
|
363
|
+
def shutdown(self) -> "None":
|
|
364
|
+
if self.is_shutdown:
|
|
365
|
+
return
|
|
366
|
+
self.accepts_input = False
|
|
367
|
+
self.is_shutdown = True
|
|
368
|
+
for tool in self.tool_registry.tools():
|
|
369
|
+
tool.shutdown()
|
|
295
370
|
|
|
296
371
|
async def _execute_tool_batch(
|
|
297
372
|
self,
|
|
298
|
-
turn_id:
|
|
299
|
-
tool_calls:
|
|
300
|
-
) ->
|
|
301
|
-
|
|
302
|
-
parallel_batch:
|
|
303
|
-
|
|
373
|
+
turn_id: "str",
|
|
374
|
+
tool_calls: "typing.List[ToolCall]",
|
|
375
|
+
) -> "None":
|
|
376
|
+
batches: "typing.List[typing.List[ToolCall]]" = []
|
|
377
|
+
parallel_batch: "typing.List[ToolCall]" = []
|
|
304
378
|
for call in tool_calls:
|
|
305
379
|
can_run_parallel = (
|
|
306
380
|
self._parallel_tool_calls
|
|
307
|
-
and self.
|
|
381
|
+
and self.tool_registry.supports_parallel(call.name)
|
|
308
382
|
)
|
|
309
383
|
if can_run_parallel:
|
|
310
384
|
parallel_batch.append(call)
|
|
311
385
|
continue
|
|
312
386
|
|
|
313
387
|
if parallel_batch:
|
|
314
|
-
|
|
315
|
-
results.extend(
|
|
316
|
-
await asyncio.gather(
|
|
317
|
-
*(
|
|
318
|
-
self._run_single_tool(turn_id, batched_call, prior_results)
|
|
319
|
-
for batched_call in parallel_batch
|
|
320
|
-
)
|
|
321
|
-
)
|
|
322
|
-
)
|
|
388
|
+
batches.append(parallel_batch)
|
|
323
389
|
parallel_batch = []
|
|
324
|
-
|
|
325
|
-
|
|
390
|
+
batches.append([call])
|
|
326
391
|
if parallel_batch:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
392
|
+
batches.append(parallel_batch)
|
|
393
|
+
|
|
394
|
+
results: "typing.List[ToolResult]" = []
|
|
395
|
+
for batch in batches:
|
|
396
|
+
context = ToolContext(
|
|
397
|
+
turn_id=turn_id,
|
|
398
|
+
history=self.history,
|
|
399
|
+
)
|
|
400
|
+
if len(batch) == 1:
|
|
401
|
+
results.append(await self._run_single_tool(turn_id, batch[0], context))
|
|
402
|
+
continue
|
|
403
|
+
outcomes = await asyncio.gather(
|
|
404
|
+
*(self._run_single_tool(turn_id, call, context) for call in batch),
|
|
405
|
+
return_exceptions=True,
|
|
335
406
|
)
|
|
336
|
-
|
|
407
|
+
for outcome in outcomes:
|
|
408
|
+
if isinstance(outcome, BaseException):
|
|
409
|
+
raise outcome
|
|
410
|
+
results.append(outcome)
|
|
411
|
+
self._append_history(self.tool_registry.follow_up_messages(results))
|
|
337
412
|
|
|
338
413
|
async def _run_single_tool(
|
|
339
414
|
self,
|
|
340
|
-
turn_id:
|
|
341
|
-
call:
|
|
342
|
-
|
|
343
|
-
) ->
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
self._emit("tool_started", turn_id, **payload)
|
|
350
|
-
result = await self._tool_registry.execute(
|
|
351
|
-
call,
|
|
352
|
-
ToolContext(
|
|
353
|
-
turn_id=turn_id,
|
|
354
|
-
history=tuple(self._history) + prior_results,
|
|
355
|
-
collaboration_mode=self._context_manager.collaboration_mode,
|
|
356
|
-
),
|
|
357
|
-
)
|
|
358
|
-
payload["result"] = result
|
|
359
|
-
payload["is_error"] = result.is_error
|
|
360
|
-
self._emit("tool_completed", turn_id, **payload)
|
|
415
|
+
turn_id: "str",
|
|
416
|
+
call: "ToolCall",
|
|
417
|
+
context: "ToolContext",
|
|
418
|
+
) -> "ToolResult":
|
|
419
|
+
self._emit(ToolStartedEvent(turn_id, call))
|
|
420
|
+
result = await self.tool_registry.execute(call, context)
|
|
421
|
+
self._append_history([truncate_tool_result_for_history(result)])
|
|
422
|
+
self._emit(ToolCompletedEvent(turn_id, call, result))
|
|
361
423
|
return result
|
|
362
424
|
|
|
363
|
-
def _emit(self,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
425
|
+
def _emit(self, event: "TurnEvent") -> "None":
|
|
426
|
+
handlers = [self.event_handler]
|
|
427
|
+
if isinstance(event, (TerminalEvent, TurnStartedEvent, CompactStartedEvent)):
|
|
428
|
+
handlers = [
|
|
429
|
+
tool.handle_agent_event for tool in self.tool_registry.tools()
|
|
430
|
+
] + handlers
|
|
431
|
+
for handler in handlers:
|
|
432
|
+
try:
|
|
433
|
+
handler(event)
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
436
|
+
{
|
|
437
|
+
"message": "Agent event observer failed: " + event.kind,
|
|
438
|
+
"exception": exc,
|
|
439
|
+
}
|
|
440
|
+
)
|
|
369
441
|
|
|
370
|
-
def _background_work_count(self,
|
|
371
|
-
manager: 'typing.Union[UnifiedExecManager, None]' = self._exec_manager
|
|
372
|
-
count = 0 if manager is None else manager.running_session_count()
|
|
373
|
-
clock_manager = self._clock_manager
|
|
374
|
-
if (
|
|
375
|
-
terminal_event == "turn_completed"
|
|
376
|
-
and clock_manager is not None
|
|
377
|
-
and clock_manager.enabled
|
|
378
|
-
):
|
|
379
|
-
count += 1
|
|
380
|
-
return count
|
|
381
|
-
|
|
382
|
-
def _persist_history_items(
|
|
383
|
-
self,
|
|
384
|
-
items: 'typing.Iterable[ConversationItem]',
|
|
385
|
-
) -> 'None':
|
|
386
|
-
recorder = self._rollout_recorder
|
|
387
|
-
if recorder is None:
|
|
388
|
-
return
|
|
442
|
+
def _background_work_count(self, event_type) -> "typing.Union[int, None]":
|
|
389
443
|
try:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
444
|
+
return sum(
|
|
445
|
+
tool.background_work_count(
|
|
446
|
+
event_type in (TurnCompletedEvent, CompactCompletedEvent)
|
|
447
|
+
)
|
|
448
|
+
for tool in self.tool_registry.tools()
|
|
449
|
+
)
|
|
450
|
+
except Exception as exc:
|
|
451
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
452
|
+
{
|
|
453
|
+
"message": "Agent background-work observer failed: "
|
|
454
|
+
+ event_type.kind,
|
|
455
|
+
"exception": exc,
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
return None
|
|
393
459
|
|
|
394
|
-
def
|
|
460
|
+
def _append_history(
|
|
395
461
|
self,
|
|
396
|
-
items:
|
|
397
|
-
) ->
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
tool_calls.append(item)
|
|
410
|
-
self._persist_history_items(persisted_response_items)
|
|
411
|
-
return tuple(persisted_response_items), tool_calls, last_assistant_message
|
|
412
|
-
|
|
413
|
-
def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
|
|
414
|
-
if event.kind == "token_count":
|
|
415
|
-
self._remember_token_usage(event.payload.get("usage"))
|
|
416
|
-
if event.kind == "assistant_delta":
|
|
417
|
-
self._emit("assistant_delta", turn_id, **event.payload)
|
|
418
|
-
elif event.kind == "tool_call":
|
|
419
|
-
self._emit("tool_called", turn_id, **event.payload)
|
|
420
|
-
elif event.kind == "token_count":
|
|
421
|
-
self._emit("token_count", turn_id, **event.payload)
|
|
422
|
-
elif event.kind == "stream_error":
|
|
423
|
-
self._emit("stream_error", turn_id, **event.payload)
|
|
424
|
-
|
|
425
|
-
def _remember_token_usage(self, usage: 'object') -> 'None':
|
|
462
|
+
items: "typing.Iterable[ConversationItem]",
|
|
463
|
+
) -> "None":
|
|
464
|
+
items = tuple(items)
|
|
465
|
+
if self._rollout_recorder is not None:
|
|
466
|
+
self._rollout_recorder.append_history_items(items, self._history)
|
|
467
|
+
self._history.extend(items)
|
|
468
|
+
|
|
469
|
+
def _handle_model_stream_event(self, turn_id: "str", event: "ModelEvent") -> "None":
|
|
470
|
+
if isinstance(event, TokenCountEvent):
|
|
471
|
+
self._remember_token_usage(event.usage)
|
|
472
|
+
self._emit(replace(event, turn_id=turn_id))
|
|
473
|
+
|
|
474
|
+
def _remember_token_usage(self, usage: "object") -> "None":
|
|
426
475
|
if not isinstance(usage, dict):
|
|
427
476
|
return
|
|
428
477
|
try:
|
|
@@ -432,67 +481,45 @@ class Agent:
|
|
|
432
481
|
|
|
433
482
|
async def _complete_model_request(
|
|
434
483
|
self,
|
|
435
|
-
turn_id:
|
|
436
|
-
iteration:
|
|
437
|
-
) ->
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
tool_count=len(prompt.tools),
|
|
484
|
+
turn_id: "str",
|
|
485
|
+
iteration: "int",
|
|
486
|
+
) -> "ModelResponse":
|
|
487
|
+
prompt = self.context_manager.build_prompt(
|
|
488
|
+
self._history,
|
|
489
|
+
self.tool_registry.model_visible_specs(),
|
|
490
|
+
self._parallel_tool_calls,
|
|
491
|
+
turn_id=turn_id,
|
|
492
|
+
)
|
|
493
|
+
self._emit(
|
|
494
|
+
ModelCalledEvent(turn_id, iteration, len(prompt.input), len(prompt.tools))
|
|
495
|
+
)
|
|
496
|
+
try:
|
|
497
|
+
return await self.model_client.complete(
|
|
498
|
+
prompt,
|
|
499
|
+
lambda event: self._handle_model_stream_event(turn_id, event),
|
|
452
500
|
)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
501
|
+
except ResponsesIncompleteError as exc:
|
|
502
|
+
if exc.reason == "max_output_tokens":
|
|
503
|
+
self._append_history(
|
|
504
|
+
item
|
|
505
|
+
for item in exc.partial_items
|
|
506
|
+
if isinstance(item, (AssistantMessage, ReasoningItem))
|
|
457
507
|
)
|
|
458
|
-
|
|
459
|
-
error_message = str(exc)
|
|
460
|
-
if (
|
|
461
|
-
not _is_context_length_error_message(error_message)
|
|
462
|
-
or attempted_context_compact
|
|
463
|
-
):
|
|
464
|
-
raise
|
|
465
|
-
attempted_context_compact = True
|
|
466
|
-
context_usage = _usage_from_context_length_error(error_message)
|
|
467
|
-
if context_usage is not None:
|
|
468
|
-
self._remember_token_usage(context_usage)
|
|
469
|
-
self._emit("token_count", turn_id, usage=context_usage)
|
|
470
|
-
await self._run_auto_compact(
|
|
471
|
-
turn_id,
|
|
472
|
-
phase="context_length_exceeded",
|
|
473
|
-
total_tokens=(
|
|
474
|
-
context_usage.get("total_tokens")
|
|
475
|
-
if context_usage is not None
|
|
476
|
-
else None
|
|
477
|
-
),
|
|
478
|
-
token_limit=_context_length_error_token_limit(error_message),
|
|
479
|
-
prune_tool_results_on_context_error=True,
|
|
480
|
-
)
|
|
481
|
-
self._raise_if_interrupt_requested(turn_id, iteration)
|
|
508
|
+
raise
|
|
482
509
|
|
|
483
510
|
async def _maybe_auto_compact(
|
|
484
511
|
self,
|
|
485
|
-
turn_id:
|
|
486
|
-
phase:
|
|
487
|
-
) ->
|
|
488
|
-
limit = self.
|
|
512
|
+
turn_id: "str",
|
|
513
|
+
phase: "str",
|
|
514
|
+
) -> "None":
|
|
515
|
+
limit = self.context_manager.resolve_auto_compact_token_limit()
|
|
489
516
|
total_tokens = self._last_total_usage_tokens
|
|
490
517
|
if limit is None or total_tokens is None:
|
|
491
518
|
return
|
|
492
519
|
if total_tokens < limit or not self._history:
|
|
493
520
|
return
|
|
494
521
|
|
|
495
|
-
await self.
|
|
522
|
+
await self._compact_history(
|
|
496
523
|
turn_id,
|
|
497
524
|
phase=phase,
|
|
498
525
|
total_tokens=total_tokens,
|
|
@@ -500,134 +527,73 @@ class Agent:
|
|
|
500
527
|
prune_tool_results_on_context_error=True,
|
|
501
528
|
)
|
|
502
529
|
|
|
503
|
-
async def
|
|
530
|
+
async def _compact_history(
|
|
504
531
|
self,
|
|
505
|
-
turn_id:
|
|
506
|
-
phase:
|
|
507
|
-
total_tokens:
|
|
508
|
-
token_limit:
|
|
509
|
-
prune_tool_results_on_context_error:
|
|
510
|
-
) ->
|
|
511
|
-
from .utils.compactor import
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
self._emit(
|
|
519
|
-
"auto_compact_started",
|
|
520
|
-
turn_id,
|
|
521
|
-
**payload,
|
|
532
|
+
turn_id: "str",
|
|
533
|
+
phase: "str",
|
|
534
|
+
total_tokens: "typing.Union[int, None]" = None,
|
|
535
|
+
token_limit: "typing.Union[int, None]" = None,
|
|
536
|
+
prune_tool_results_on_context_error: "bool" = False,
|
|
537
|
+
) -> "typing.Union[CompactResult, None]":
|
|
538
|
+
from .utils.compactor import compact_history
|
|
539
|
+
|
|
540
|
+
if not self._history:
|
|
541
|
+
return None
|
|
542
|
+
details = (turn_id, phase, total_tokens, token_limit)
|
|
543
|
+
started_type = (
|
|
544
|
+
CompactStartedEvent if phase == "manual" else AutoCompactStartedEvent
|
|
522
545
|
)
|
|
546
|
+
self._emit(started_type(*details))
|
|
523
547
|
|
|
524
|
-
def handle_compact_stream_event(event:
|
|
525
|
-
if event
|
|
526
|
-
|
|
548
|
+
def handle_compact_stream_event(event: "ModelEvent") -> "None":
|
|
549
|
+
if isinstance(event, StreamErrorEvent) or (
|
|
550
|
+
phase == "manual" and isinstance(event, TokenCountEvent)
|
|
551
|
+
):
|
|
552
|
+
self._emit(replace(event, turn_id=turn_id))
|
|
527
553
|
|
|
528
554
|
try:
|
|
529
|
-
|
|
530
|
-
|
|
555
|
+
recorder = self._rollout_recorder
|
|
556
|
+
compact_result = await compact_history(
|
|
557
|
+
self.history,
|
|
558
|
+
self.model_client,
|
|
559
|
+
self.context_manager,
|
|
531
560
|
handle_compact_stream_event,
|
|
532
561
|
prune_tool_results_on_context_error,
|
|
533
|
-
|
|
534
|
-
except Exception as exc:
|
|
535
|
-
failed_payload = dict(payload)
|
|
536
|
-
failed_payload.update(
|
|
537
|
-
{
|
|
538
|
-
"error": str(exc),
|
|
539
|
-
"error_type": type(exc).__name__,
|
|
540
|
-
}
|
|
541
|
-
)
|
|
542
|
-
self._emit(
|
|
543
|
-
"auto_compact_failed",
|
|
562
|
+
str(recorder.rollout_path) if recorder is not None else None,
|
|
544
563
|
turn_id,
|
|
545
|
-
**failed_payload,
|
|
546
564
|
)
|
|
565
|
+
if recorder is not None:
|
|
566
|
+
recorder.append_compacted_history(compact_result.history, self._history)
|
|
567
|
+
self._history = list(compact_result.history)
|
|
568
|
+
self._last_total_usage_tokens = None
|
|
569
|
+
except Exception as exc:
|
|
570
|
+
if phase == "manual":
|
|
571
|
+
self._emit(
|
|
572
|
+
CompactFailedEvent(
|
|
573
|
+
*details,
|
|
574
|
+
str(exc),
|
|
575
|
+
type(exc).__name__,
|
|
576
|
+
self._background_work_count(CompactFailedEvent),
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
self._emit(
|
|
581
|
+
AutoCompactFailedEvent(*details, str(exc), type(exc).__name__)
|
|
582
|
+
)
|
|
547
583
|
raise
|
|
548
584
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
completed_payload.update(
|
|
554
|
-
{
|
|
555
|
-
"original_item_count": compact_result.original_item_count,
|
|
556
|
-
"retained_item_count": compact_result.retained_item_count,
|
|
557
|
-
"summary": compact_result.display_text(),
|
|
558
|
-
}
|
|
585
|
+
completed = details + (
|
|
586
|
+
compact_result.original_item_count,
|
|
587
|
+
compact_result.retained_item_count,
|
|
588
|
+
compact_result.pruned_tool_results,
|
|
559
589
|
)
|
|
560
|
-
if
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
tool_results: 'typing.List[ToolResult]',
|
|
571
|
-
) -> 'typing.List[UserMessage]':
|
|
572
|
-
follow_ups: 'typing.List[UserMessage]' = []
|
|
573
|
-
for result in tool_results:
|
|
574
|
-
statuses = None
|
|
575
|
-
if (
|
|
576
|
-
result.name == "wait_agent"
|
|
577
|
-
and not result.is_error
|
|
578
|
-
and isinstance(result.output, dict)
|
|
579
|
-
):
|
|
580
|
-
statuses = result.output.get("status")
|
|
581
|
-
if isinstance(statuses, dict):
|
|
582
|
-
for agent_id, status in statuses.items():
|
|
583
|
-
if isinstance(agent_id, str) and isinstance(status, dict):
|
|
584
|
-
payload = {
|
|
585
|
-
"agent_id": agent_id,
|
|
586
|
-
"status": status,
|
|
587
|
-
}
|
|
588
|
-
follow_ups.append(
|
|
589
|
-
UserMessage(
|
|
590
|
-
text=(
|
|
591
|
-
"<subagent_notification>\n"
|
|
592
|
-
f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
|
|
593
|
-
"</subagent_notification>"
|
|
594
|
-
)
|
|
595
|
-
)
|
|
596
|
-
)
|
|
597
|
-
return follow_ups
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
def _usage_from_context_length_error(
|
|
601
|
-
message: 'str',
|
|
602
|
-
) -> 'typing.Union[typing.Dict[str, int], None]':
|
|
603
|
-
if not _is_context_length_error_message(message):
|
|
604
|
-
return None
|
|
605
|
-
|
|
606
|
-
requested_match = _REQUESTED_TOKENS_RE.search(message)
|
|
607
|
-
if requested_match is None:
|
|
608
|
-
return None
|
|
609
|
-
|
|
610
|
-
usage = {"total_tokens": _parse_token_count(requested_match.group(1))}
|
|
611
|
-
split_match = _REQUESTED_TOKEN_SPLIT_RE.search(message)
|
|
612
|
-
if split_match is not None:
|
|
613
|
-
usage["input_tokens"] = _parse_token_count(split_match.group(1))
|
|
614
|
-
usage["output_tokens"] = _parse_token_count(split_match.group(2))
|
|
615
|
-
else:
|
|
616
|
-
usage["input_tokens"] = usage["total_tokens"]
|
|
617
|
-
return usage
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
def _is_context_length_error_message(message: 'str') -> 'bool':
|
|
621
|
-
lower = message.lower()
|
|
622
|
-
return any(marker in lower for marker in _CONTEXT_LENGTH_ERROR_MARKERS)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def _context_length_error_token_limit(message: 'str') -> 'typing.Union[int, None]':
|
|
626
|
-
limit_match = _MAX_CONTEXT_TOKENS_RE.search(message)
|
|
627
|
-
if limit_match is None:
|
|
628
|
-
return None
|
|
629
|
-
return _parse_token_count(limit_match.group(1))
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
def _parse_token_count(value: 'str') -> 'int':
|
|
633
|
-
return int(value.replace(",", ""))
|
|
590
|
+
if phase == "manual":
|
|
591
|
+
self._emit(
|
|
592
|
+
CompactCompletedEvent(
|
|
593
|
+
*completed,
|
|
594
|
+
self._background_work_count(CompactCompletedEvent),
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
else:
|
|
598
|
+
self._emit(AutoCompactCompletedEvent(*completed))
|
|
599
|
+
return compact_result
|