python-codex 0.2.6__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 +18 -14
- pycodex/agent.py +468 -462
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -436
- 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 +329 -252
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +90 -52
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +235 -803
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -174
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +4 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +201 -0
- 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 +13 -13
- 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 +50 -66
- 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/utils/image_utils.py +76 -0
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +263 -161
- 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 +25 -22
- responses_server/messages_api.py +96 -49
- responses_server/payload_processors.py +25 -19
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +196 -107
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +51 -13
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +470 -384
- workspace_server/workspace.html +859 -232
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +168 -100
- 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 -553
- python_codex-0.2.6.dist-info/METADATA +0 -441
- python_codex-0.2.6.dist-info/RECORD +0 -91
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/agent.py
CHANGED
|
@@ -1,57 +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
|
-
from .utils.truncation import truncate_tool_results_for_history
|
|
48
|
+
from .tools import ToolContext, ToolRegistry
|
|
22
49
|
from .utils import uuid7_string
|
|
23
|
-
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
|
|
24
57
|
|
|
25
58
|
if typing.TYPE_CHECKING:
|
|
26
|
-
from .utils.
|
|
27
|
-
from .runtime_services import AgentRuntimeEnvironment
|
|
59
|
+
from .utils.compactor import CompactResult
|
|
28
60
|
|
|
29
61
|
|
|
30
|
-
EventHandler = Callable[[
|
|
31
|
-
BASE_EVENT_HANDLER:
|
|
32
|
-
_REQUESTED_TOKENS_RE = re.compile(
|
|
33
|
-
r"requested\s+([0-9,]+)\s+tokens",
|
|
34
|
-
re.IGNORECASE,
|
|
35
|
-
)
|
|
36
|
-
_REQUESTED_TOKEN_SPLIT_RE = re.compile(
|
|
37
|
-
r"\(([0-9,]+)\s+in\s+the\s+messages,\s+([0-9,]+)\s+in\s+the\s+completion\)",
|
|
38
|
-
re.IGNORECASE,
|
|
39
|
-
)
|
|
40
|
-
_MAX_CONTEXT_TOKENS_RE = re.compile(
|
|
41
|
-
r"maximum\s+context\s+length\s+is\s+([0-9,]+)\s+tokens",
|
|
42
|
-
re.IGNORECASE,
|
|
43
|
-
)
|
|
44
|
-
_CONTEXT_LENGTH_ERROR_MARKERS = (
|
|
45
|
-
"context_length_exceeded",
|
|
46
|
-
"maximum context length",
|
|
47
|
-
"exceeds the context window",
|
|
48
|
-
"exceeded the context window",
|
|
49
|
-
)
|
|
50
|
-
TERMINAL_TURN_EVENTS = {"turn_completed", "turn_failed", "turn_interrupted"}
|
|
62
|
+
EventHandler = Callable[[Event], None]
|
|
63
|
+
BASE_EVENT_HANDLER: "EventHandler" = lambda _event: None
|
|
51
64
|
|
|
52
65
|
|
|
53
66
|
class TurnInterrupted(RuntimeError):
|
|
54
|
-
|
|
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
|
|
55
76
|
|
|
56
77
|
|
|
57
78
|
class Agent:
|
|
@@ -65,324 +86,392 @@ class Agent:
|
|
|
65
86
|
|
|
66
87
|
def __init__(
|
|
67
88
|
self,
|
|
68
|
-
model_client:
|
|
69
|
-
tool_registry:
|
|
70
|
-
|
|
71
|
-
parallel_tool_calls:
|
|
72
|
-
event_handler:
|
|
73
|
-
initial_history:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
) ->
|
|
77
|
-
self.
|
|
78
|
-
self.
|
|
79
|
-
self.
|
|
80
|
-
|
|
81
|
-
self._event_handler = event_handler
|
|
82
|
-
self._history: 'typing.List[ConversationItem]' = list(initial_history)
|
|
83
|
-
self._rollout_recorder = rollout_recorder
|
|
84
|
-
self._auto_compact_token_limit = (
|
|
85
|
-
self._context_manager.resolve_auto_compact_token_limit()
|
|
86
|
-
)
|
|
87
|
-
self._last_total_usage_tokens: 'typing.Union[int, None]' = None
|
|
88
|
-
self.runtime_environment = runtime_environment
|
|
89
|
-
self.interrupt_asap = False
|
|
90
|
-
self._turn_running = False
|
|
91
|
-
exec_command_tool = self._tool_registry.get_tool("exec_command")
|
|
92
|
-
self._exec_manager = (
|
|
93
|
-
exec_command_tool._manager
|
|
94
|
-
if isinstance(exec_command_tool, ExecCommandTool)
|
|
95
|
-
else None
|
|
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)
|
|
96
102
|
)
|
|
97
|
-
|
|
98
|
-
|
|
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)
|
|
99
114
|
|
|
100
115
|
@property
|
|
101
|
-
def history(self) ->
|
|
116
|
+
def history(self) -> "typing.Tuple[ConversationItem, ...]":
|
|
102
117
|
return tuple(self._history)
|
|
103
118
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
@property
|
|
132
|
+
def is_running(self) -> "bool":
|
|
133
|
+
return self._idle is not None
|
|
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
|
|
120
154
|
|
|
121
|
-
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":
|
|
122
177
|
from .utils.async_bridge import run_async
|
|
123
178
|
|
|
124
179
|
return run_async(self.run_turn([text]))
|
|
125
180
|
|
|
126
|
-
def
|
|
127
|
-
self
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
self._emit("turn_interrupted", turn_id, **payload)
|
|
138
|
-
raise TurnInterrupted("turn interrupted")
|
|
139
|
-
|
|
140
|
-
async def run_turn(
|
|
141
|
-
self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
|
|
142
|
-
) -> 'TurnResult':
|
|
143
|
-
self._turn_running = True
|
|
144
|
-
turn_id = turn_id or uuid7_string()
|
|
145
|
-
self.interrupt_asap = False
|
|
146
|
-
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)
|
|
147
192
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
157
219
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
160
240
|
|
|
161
|
-
|
|
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())
|
|
162
251
|
try:
|
|
252
|
+
self._emit(TurnStartedEvent(turn.turn_id, tuple(texts)))
|
|
163
253
|
while True:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
await self._maybe_auto_compact(turn_id, phase="mid_turn")
|
|
170
|
-
iteration += 1
|
|
171
|
-
response = await self._complete_model_request(
|
|
172
|
-
turn_id,
|
|
173
|
-
iteration,
|
|
174
|
-
)
|
|
175
|
-
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)
|
|
176
259
|
self._emit(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
item_count=len(response.items),
|
|
260
|
+
ModelCompletedEvent(
|
|
261
|
+
turn.turn_id, turn.iteration, len(response.items)
|
|
262
|
+
)
|
|
181
263
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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()
|
|
188
276
|
if not tool_calls:
|
|
189
|
-
|
|
190
|
-
turn_id,
|
|
191
|
-
iteration,
|
|
192
|
-
output_text=last_assistant_message,
|
|
193
|
-
)
|
|
194
|
-
self._emit(
|
|
195
|
-
"turn_completed",
|
|
196
|
-
turn_id,
|
|
197
|
-
iteration=iteration,
|
|
198
|
-
output_text=last_assistant_message,
|
|
199
|
-
)
|
|
200
|
-
self._turn_running = False
|
|
201
|
-
return TurnResult(
|
|
202
|
-
turn_id=turn_id,
|
|
203
|
-
output_text=last_assistant_message,
|
|
204
|
-
iterations=iteration,
|
|
205
|
-
response_items=final_response_items,
|
|
206
|
-
history=tuple(self._history),
|
|
207
|
-
)
|
|
277
|
+
break
|
|
208
278
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
self._persist_history_items(follow_up_messages)
|
|
216
|
-
self._raise_if_interrupt_requested(
|
|
217
|
-
turn_id,
|
|
218
|
-
iteration,
|
|
219
|
-
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),
|
|
220
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
|
|
221
295
|
except TurnInterrupted:
|
|
222
|
-
self.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
296
|
+
self._emit(
|
|
297
|
+
TurnInterruptedEvent(
|
|
298
|
+
turn.turn_id,
|
|
299
|
+
turn.iteration,
|
|
300
|
+
turn.output_text,
|
|
301
|
+
self._background_work_count(TurnInterruptedEvent),
|
|
302
|
+
)
|
|
303
|
+
)
|
|
226
304
|
raise
|
|
227
305
|
except Exception as exc:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
self.
|
|
231
|
-
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))
|
|
232
309
|
self._emit(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
310
|
+
TurnFailedEvent(
|
|
311
|
+
turn.turn_id,
|
|
312
|
+
turn.iteration,
|
|
313
|
+
str(exc),
|
|
314
|
+
type(exc).__name__,
|
|
315
|
+
self._background_work_count(TurnFailedEvent),
|
|
316
|
+
)
|
|
238
317
|
)
|
|
239
|
-
self._turn_running = False
|
|
240
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
|
+
)
|
|
241
347
|
|
|
242
|
-
async def maybe_invoke(self, event:
|
|
243
|
-
if self.
|
|
348
|
+
async def maybe_invoke(self, event: "typing.Dict[str, object]") -> "bool":
|
|
349
|
+
if self.is_running or not self.accepts_input:
|
|
244
350
|
return False
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"
|
|
248
|
-
|
|
249
|
-
}
|
|
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"}
|
|
250
355
|
text = (
|
|
251
|
-
"<
|
|
356
|
+
f"<{tag}>\n"
|
|
252
357
|
f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
|
|
253
|
-
"</
|
|
254
|
-
)
|
|
255
|
-
self._turn_running = True
|
|
256
|
-
task = asyncio.create_task(self.run_turn([text]))
|
|
257
|
-
task.add_done_callback(
|
|
258
|
-
lambda task: None if task.cancelled() else task.exception()
|
|
358
|
+
f"</{tag}>"
|
|
259
359
|
)
|
|
360
|
+
await self.run_turn([text])
|
|
260
361
|
return True
|
|
261
362
|
|
|
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()
|
|
370
|
+
|
|
262
371
|
async def _execute_tool_batch(
|
|
263
372
|
self,
|
|
264
|
-
turn_id:
|
|
265
|
-
tool_calls:
|
|
266
|
-
) ->
|
|
267
|
-
|
|
268
|
-
parallel_batch:
|
|
269
|
-
|
|
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]" = []
|
|
270
378
|
for call in tool_calls:
|
|
271
379
|
can_run_parallel = (
|
|
272
380
|
self._parallel_tool_calls
|
|
273
|
-
and self.
|
|
381
|
+
and self.tool_registry.supports_parallel(call.name)
|
|
274
382
|
)
|
|
275
383
|
if can_run_parallel:
|
|
276
384
|
parallel_batch.append(call)
|
|
277
385
|
continue
|
|
278
386
|
|
|
279
387
|
if parallel_batch:
|
|
280
|
-
|
|
281
|
-
results.extend(
|
|
282
|
-
await asyncio.gather(
|
|
283
|
-
*(
|
|
284
|
-
self._run_single_tool(turn_id, batched_call, prior_results)
|
|
285
|
-
for batched_call in parallel_batch
|
|
286
|
-
)
|
|
287
|
-
)
|
|
288
|
-
)
|
|
388
|
+
batches.append(parallel_batch)
|
|
289
389
|
parallel_batch = []
|
|
290
|
-
|
|
291
|
-
|
|
390
|
+
batches.append([call])
|
|
292
391
|
if parallel_batch:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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,
|
|
301
406
|
)
|
|
302
|
-
|
|
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))
|
|
303
412
|
|
|
304
413
|
async def _run_single_tool(
|
|
305
414
|
self,
|
|
306
|
-
turn_id:
|
|
307
|
-
call:
|
|
308
|
-
|
|
309
|
-
) ->
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
self._emit("tool_started", turn_id, **payload)
|
|
316
|
-
result = await self._tool_registry.execute(
|
|
317
|
-
call,
|
|
318
|
-
ToolContext(
|
|
319
|
-
turn_id=turn_id,
|
|
320
|
-
history=tuple(self._history) + prior_results,
|
|
321
|
-
collaboration_mode=self._context_manager.collaboration_mode,
|
|
322
|
-
),
|
|
323
|
-
)
|
|
324
|
-
payload["result"] = result
|
|
325
|
-
payload["is_error"] = result.is_error
|
|
326
|
-
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))
|
|
327
423
|
return result
|
|
328
424
|
|
|
329
|
-
def _emit(self,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
)
|
|
341
441
|
|
|
342
|
-
def
|
|
343
|
-
self,
|
|
344
|
-
items: 'typing.Iterable[ConversationItem]',
|
|
345
|
-
) -> 'None':
|
|
346
|
-
recorder = self._rollout_recorder
|
|
347
|
-
if recorder is None:
|
|
348
|
-
return
|
|
442
|
+
def _background_work_count(self, event_type) -> "typing.Union[int, None]":
|
|
349
443
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
353
459
|
|
|
354
|
-
def
|
|
460
|
+
def _append_history(
|
|
355
461
|
self,
|
|
356
|
-
items:
|
|
357
|
-
) ->
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
tool_calls.append(item)
|
|
370
|
-
self._persist_history_items(persisted_response_items)
|
|
371
|
-
return tuple(persisted_response_items), tool_calls, last_assistant_message
|
|
372
|
-
|
|
373
|
-
def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
|
|
374
|
-
if event.kind == "token_count":
|
|
375
|
-
self._remember_token_usage(event.payload.get("usage"))
|
|
376
|
-
if event.kind == "assistant_delta":
|
|
377
|
-
self._emit("assistant_delta", turn_id, **event.payload)
|
|
378
|
-
elif event.kind == "tool_call":
|
|
379
|
-
self._emit("tool_called", turn_id, **event.payload)
|
|
380
|
-
elif event.kind == "token_count":
|
|
381
|
-
self._emit("token_count", turn_id, **event.payload)
|
|
382
|
-
elif event.kind == "stream_error":
|
|
383
|
-
self._emit("stream_error", turn_id, **event.payload)
|
|
384
|
-
|
|
385
|
-
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":
|
|
386
475
|
if not isinstance(usage, dict):
|
|
387
476
|
return
|
|
388
477
|
try:
|
|
@@ -392,67 +481,45 @@ class Agent:
|
|
|
392
481
|
|
|
393
482
|
async def _complete_model_request(
|
|
394
483
|
self,
|
|
395
|
-
turn_id:
|
|
396
|
-
iteration:
|
|
397
|
-
) ->
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
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),
|
|
412
500
|
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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))
|
|
417
507
|
)
|
|
418
|
-
|
|
419
|
-
error_message = str(exc)
|
|
420
|
-
if (
|
|
421
|
-
not _is_context_length_error_message(error_message)
|
|
422
|
-
or attempted_context_compact
|
|
423
|
-
):
|
|
424
|
-
raise
|
|
425
|
-
attempted_context_compact = True
|
|
426
|
-
context_usage = _usage_from_context_length_error(error_message)
|
|
427
|
-
if context_usage is not None:
|
|
428
|
-
self._remember_token_usage(context_usage)
|
|
429
|
-
self._emit("token_count", turn_id, usage=context_usage)
|
|
430
|
-
await self._run_auto_compact(
|
|
431
|
-
turn_id,
|
|
432
|
-
phase="context_length_exceeded",
|
|
433
|
-
total_tokens=(
|
|
434
|
-
context_usage.get("total_tokens")
|
|
435
|
-
if context_usage is not None
|
|
436
|
-
else None
|
|
437
|
-
),
|
|
438
|
-
token_limit=_context_length_error_token_limit(error_message),
|
|
439
|
-
prune_tool_results_on_context_error=True,
|
|
440
|
-
)
|
|
441
|
-
self._raise_if_interrupt_requested(turn_id, iteration)
|
|
508
|
+
raise
|
|
442
509
|
|
|
443
510
|
async def _maybe_auto_compact(
|
|
444
511
|
self,
|
|
445
|
-
turn_id:
|
|
446
|
-
phase:
|
|
447
|
-
) ->
|
|
448
|
-
limit = self.
|
|
512
|
+
turn_id: "str",
|
|
513
|
+
phase: "str",
|
|
514
|
+
) -> "None":
|
|
515
|
+
limit = self.context_manager.resolve_auto_compact_token_limit()
|
|
449
516
|
total_tokens = self._last_total_usage_tokens
|
|
450
517
|
if limit is None or total_tokens is None:
|
|
451
518
|
return
|
|
452
519
|
if total_tokens < limit or not self._history:
|
|
453
520
|
return
|
|
454
521
|
|
|
455
|
-
await self.
|
|
522
|
+
await self._compact_history(
|
|
456
523
|
turn_id,
|
|
457
524
|
phase=phase,
|
|
458
525
|
total_tokens=total_tokens,
|
|
@@ -460,134 +527,73 @@ class Agent:
|
|
|
460
527
|
prune_tool_results_on_context_error=True,
|
|
461
528
|
)
|
|
462
529
|
|
|
463
|
-
async def
|
|
530
|
+
async def _compact_history(
|
|
464
531
|
self,
|
|
465
|
-
turn_id:
|
|
466
|
-
phase:
|
|
467
|
-
total_tokens:
|
|
468
|
-
token_limit:
|
|
469
|
-
prune_tool_results_on_context_error:
|
|
470
|
-
) ->
|
|
471
|
-
from .utils.compactor import
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
self._emit(
|
|
479
|
-
"auto_compact_started",
|
|
480
|
-
turn_id,
|
|
481
|
-
**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
|
|
482
545
|
)
|
|
546
|
+
self._emit(started_type(*details))
|
|
483
547
|
|
|
484
|
-
def handle_compact_stream_event(event:
|
|
485
|
-
if event
|
|
486
|
-
|
|
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))
|
|
487
553
|
|
|
488
554
|
try:
|
|
489
|
-
|
|
490
|
-
|
|
555
|
+
recorder = self._rollout_recorder
|
|
556
|
+
compact_result = await compact_history(
|
|
557
|
+
self.history,
|
|
558
|
+
self.model_client,
|
|
559
|
+
self.context_manager,
|
|
491
560
|
handle_compact_stream_event,
|
|
492
561
|
prune_tool_results_on_context_error,
|
|
493
|
-
|
|
494
|
-
except Exception as exc:
|
|
495
|
-
failed_payload = dict(payload)
|
|
496
|
-
failed_payload.update(
|
|
497
|
-
{
|
|
498
|
-
"error": str(exc),
|
|
499
|
-
"error_type": type(exc).__name__,
|
|
500
|
-
}
|
|
501
|
-
)
|
|
502
|
-
self._emit(
|
|
503
|
-
"auto_compact_failed",
|
|
562
|
+
str(recorder.rollout_path) if recorder is not None else None,
|
|
504
563
|
turn_id,
|
|
505
|
-
**failed_payload,
|
|
506
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
|
+
)
|
|
507
583
|
raise
|
|
508
584
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
completed_payload.update(
|
|
514
|
-
{
|
|
515
|
-
"original_item_count": compact_result.original_item_count,
|
|
516
|
-
"retained_item_count": compact_result.retained_item_count,
|
|
517
|
-
"summary": compact_result.display_text(),
|
|
518
|
-
}
|
|
585
|
+
completed = details + (
|
|
586
|
+
compact_result.original_item_count,
|
|
587
|
+
compact_result.retained_item_count,
|
|
588
|
+
compact_result.pruned_tool_results,
|
|
519
589
|
)
|
|
520
|
-
if
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
tool_results: 'typing.List[ToolResult]',
|
|
531
|
-
) -> 'typing.List[UserMessage]':
|
|
532
|
-
follow_ups: 'typing.List[UserMessage]' = []
|
|
533
|
-
for result in tool_results:
|
|
534
|
-
statuses = None
|
|
535
|
-
if (
|
|
536
|
-
result.name == "wait_agent"
|
|
537
|
-
and not result.is_error
|
|
538
|
-
and isinstance(result.output, dict)
|
|
539
|
-
):
|
|
540
|
-
statuses = result.output.get("status")
|
|
541
|
-
if isinstance(statuses, dict):
|
|
542
|
-
for agent_id, status in statuses.items():
|
|
543
|
-
if isinstance(agent_id, str) and isinstance(status, dict):
|
|
544
|
-
payload = {
|
|
545
|
-
"agent_id": agent_id,
|
|
546
|
-
"status": status,
|
|
547
|
-
}
|
|
548
|
-
follow_ups.append(
|
|
549
|
-
UserMessage(
|
|
550
|
-
text=(
|
|
551
|
-
"<subagent_notification>\n"
|
|
552
|
-
f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
|
|
553
|
-
"</subagent_notification>"
|
|
554
|
-
)
|
|
555
|
-
)
|
|
556
|
-
)
|
|
557
|
-
return follow_ups
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def _usage_from_context_length_error(
|
|
561
|
-
message: 'str',
|
|
562
|
-
) -> 'typing.Union[typing.Dict[str, int], None]':
|
|
563
|
-
if not _is_context_length_error_message(message):
|
|
564
|
-
return None
|
|
565
|
-
|
|
566
|
-
requested_match = _REQUESTED_TOKENS_RE.search(message)
|
|
567
|
-
if requested_match is None:
|
|
568
|
-
return None
|
|
569
|
-
|
|
570
|
-
usage = {"total_tokens": _parse_token_count(requested_match.group(1))}
|
|
571
|
-
split_match = _REQUESTED_TOKEN_SPLIT_RE.search(message)
|
|
572
|
-
if split_match is not None:
|
|
573
|
-
usage["input_tokens"] = _parse_token_count(split_match.group(1))
|
|
574
|
-
usage["output_tokens"] = _parse_token_count(split_match.group(2))
|
|
575
|
-
else:
|
|
576
|
-
usage["input_tokens"] = usage["total_tokens"]
|
|
577
|
-
return usage
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
def _is_context_length_error_message(message: 'str') -> 'bool':
|
|
581
|
-
lower = message.lower()
|
|
582
|
-
return any(marker in lower for marker in _CONTEXT_LENGTH_ERROR_MARKERS)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def _context_length_error_token_limit(message: 'str') -> 'typing.Union[int, None]':
|
|
586
|
-
limit_match = _MAX_CONTEXT_TOKENS_RE.search(message)
|
|
587
|
-
if limit_match is None:
|
|
588
|
-
return None
|
|
589
|
-
return _parse_token_count(limit_match.group(1))
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def _parse_token_count(value: 'str') -> 'int':
|
|
593
|
-
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
|