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/utils/visualize.py
DELETED
|
@@ -1,560 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import inspect
|
|
3
|
-
import os
|
|
4
|
-
import threading
|
|
5
|
-
from contextlib import suppress
|
|
6
|
-
from prompt_toolkit import PromptSession
|
|
7
|
-
from prompt_toolkit.patch_stdout import patch_stdout
|
|
8
|
-
|
|
9
|
-
from ..protocol import AgentEvent, JSONDict, ToolCall
|
|
10
|
-
from .toolcall_visualize import (
|
|
11
|
-
colorize_cli_message,
|
|
12
|
-
colorize_tool_message,
|
|
13
|
-
tool_summary,
|
|
14
|
-
)
|
|
15
|
-
import typing
|
|
16
|
-
|
|
17
|
-
STATUS_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
|
18
|
-
PROMPT_CONTEXT_BASELINE_TOKENS = 12_000
|
|
19
|
-
DEFAULT_MAIN_PROMPT = "pycodex> "
|
|
20
|
-
IDLE_SLEEPING_STATUS = "idle: sleeping"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def background_work_count(payload: "typing.Mapping[str, object]") -> "int":
|
|
24
|
-
value = payload.get("background_work_count", 0)
|
|
25
|
-
try:
|
|
26
|
-
return max(int(value), 0)
|
|
27
|
-
except (TypeError, ValueError):
|
|
28
|
-
return 0
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def shorten_title(text: "str", limit: "int" = 48) -> "str":
|
|
32
|
-
compact = " ".join(text.split())
|
|
33
|
-
if len(compact) <= limit:
|
|
34
|
-
return compact
|
|
35
|
-
return compact[: limit - 3].rstrip() + "..."
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def cli_color_enabled() -> "bool":
|
|
39
|
-
return os.environ.get("PYCODEX_NO_COLOR", "").strip().lower() not in {
|
|
40
|
-
"1",
|
|
41
|
-
"true",
|
|
42
|
-
"yes",
|
|
43
|
-
"on",
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def percent_of_context_window_remaining(
|
|
48
|
-
total_tokens: "int",
|
|
49
|
-
context_window_tokens: "int",
|
|
50
|
-
) -> "int":
|
|
51
|
-
if context_window_tokens <= PROMPT_CONTEXT_BASELINE_TOKENS:
|
|
52
|
-
return 0
|
|
53
|
-
|
|
54
|
-
effective_window = context_window_tokens - PROMPT_CONTEXT_BASELINE_TOKENS
|
|
55
|
-
used = max(total_tokens - PROMPT_CONTEXT_BASELINE_TOKENS, 0)
|
|
56
|
-
remaining = max(effective_window - used, 0)
|
|
57
|
-
return int(round(max(0.0, min(100.0, (remaining / effective_window) * 100.0))))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class Prompter:
|
|
61
|
-
def __init__(self, prompt: str = DEFAULT_MAIN_PROMPT, lock=None):
|
|
62
|
-
self.lock = lock or threading.Lock()
|
|
63
|
-
self._prompt_session = PromptSession(**prompt_session_kwargs())
|
|
64
|
-
|
|
65
|
-
self.prompt = prompt
|
|
66
|
-
self._status = None
|
|
67
|
-
self._status_frame_index = 0
|
|
68
|
-
|
|
69
|
-
self._prompt_task = None
|
|
70
|
-
|
|
71
|
-
def set_prompt(self, prompt):
|
|
72
|
-
self.prompt = prompt
|
|
73
|
-
|
|
74
|
-
def set_status(self, text=None, active=True):
|
|
75
|
-
self._status = text if active else None
|
|
76
|
-
|
|
77
|
-
async def poll_input(self) -> "typing.Union[str, None]":
|
|
78
|
-
if self._prompt_task is None:
|
|
79
|
-
self._prompt_task = asyncio.create_task(self._block_prompt())
|
|
80
|
-
|
|
81
|
-
done, _pending = await asyncio.wait(
|
|
82
|
-
{self._prompt_task},
|
|
83
|
-
timeout=0.05,
|
|
84
|
-
return_when=asyncio.FIRST_COMPLETED,
|
|
85
|
-
)
|
|
86
|
-
if not done:
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
prompt_task, self._prompt_task = self._prompt_task, None
|
|
90
|
-
try:
|
|
91
|
-
return prompt_task.result()
|
|
92
|
-
except asyncio.CancelledError:
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
async def require_input(self):
|
|
96
|
-
if self._prompt_task and not self._prompt_task.done():
|
|
97
|
-
self._prompt_task.cancel()
|
|
98
|
-
with suppress(asyncio.CancelledError):
|
|
99
|
-
await self._prompt_task
|
|
100
|
-
|
|
101
|
-
return await self._block_prompt()
|
|
102
|
-
|
|
103
|
-
async def _block_prompt(self):
|
|
104
|
-
with patch_stdout(raw=True):
|
|
105
|
-
return await self._prompt_session.prompt_async(
|
|
106
|
-
lambda: self.prompt,
|
|
107
|
-
refresh_interval=0.12,
|
|
108
|
-
bottom_toolbar=self._get_status,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
def _get_status(self):
|
|
112
|
-
self._status_frame_index += 1
|
|
113
|
-
if self._status is None:
|
|
114
|
-
return None
|
|
115
|
-
else:
|
|
116
|
-
frame = STATUS_FRAMES[self._status_frame_index % len(STATUS_FRAMES)]
|
|
117
|
-
status = f"{frame} {self._status}"
|
|
118
|
-
return status
|
|
119
|
-
|
|
120
|
-
def close(self) -> "None":
|
|
121
|
-
if self._prompt_task is not None and not self._prompt_task.done():
|
|
122
|
-
self._prompt_task.cancel()
|
|
123
|
-
self._prompt_task = None
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def prompt_session_kwargs() -> "typing.Dict[str, object]":
|
|
127
|
-
kwargs = {
|
|
128
|
-
"erase_when_done": True,
|
|
129
|
-
"enable_system_prompt": True,
|
|
130
|
-
}
|
|
131
|
-
try:
|
|
132
|
-
parameters = inspect.signature(PromptSession.__init__).parameters
|
|
133
|
-
except (TypeError, ValueError):
|
|
134
|
-
return kwargs
|
|
135
|
-
|
|
136
|
-
if "show_frame" in parameters:
|
|
137
|
-
kwargs["show_frame"] = True
|
|
138
|
-
return kwargs
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def format_cli_tool_call_message(
|
|
142
|
-
tool_name: "str", payload: "JSONDict"
|
|
143
|
-
) -> "typing.Union[str, None]":
|
|
144
|
-
if tool_name != "web_search":
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
action_type = str(payload.get("action_type", "")).strip()
|
|
148
|
-
if action_type == "search":
|
|
149
|
-
query = str(payload.get("query", "")).strip()
|
|
150
|
-
if not query:
|
|
151
|
-
queries = payload.get("queries")
|
|
152
|
-
if isinstance(queries, list) and queries:
|
|
153
|
-
query = str(queries[0]).strip()
|
|
154
|
-
return f"[web_search] searched: {query}" if query else "[web_search] searched"
|
|
155
|
-
|
|
156
|
-
if action_type == "open_page":
|
|
157
|
-
url = str(payload.get("url", "")).strip()
|
|
158
|
-
return f"[web_search] opened: {url}" if url else "[web_search] opened"
|
|
159
|
-
|
|
160
|
-
if action_type == "find_in_page":
|
|
161
|
-
pattern = str(payload.get("pattern", "")).strip()
|
|
162
|
-
url = str(payload.get("url", "")).strip()
|
|
163
|
-
if pattern and url:
|
|
164
|
-
return f"[web_search] found: {pattern} @ {url}"
|
|
165
|
-
if pattern:
|
|
166
|
-
return f"[web_search] found: {pattern}"
|
|
167
|
-
return "[web_search] found in page"
|
|
168
|
-
|
|
169
|
-
return "[web_search] browsing"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def short_id(value: "str", limit: "int" = 8) -> "str":
|
|
173
|
-
compact = value.strip()
|
|
174
|
-
if len(compact) <= limit + 4:
|
|
175
|
-
return compact
|
|
176
|
-
return f"{compact[:limit]}...{compact[-4:]}"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
class CliSessionView:
|
|
180
|
-
"""Own the interactive CLI terminal surface for one session.
|
|
181
|
-
|
|
182
|
-
This class is the single place that knows how to:
|
|
183
|
-
- render `AgentEvent`s into human-facing terminal output;
|
|
184
|
-
- multiplex prompt input, streamed assistant output, and status state;
|
|
185
|
-
- keep lightweight session UI state such as title, history, and steer markers.
|
|
186
|
-
|
|
187
|
-
Public interface:
|
|
188
|
-
- `handle_event(event)`: feed runtime/agent events into the view.
|
|
189
|
-
- `write_line(text)`, `finish_stream()`, `show_error(text)`: imperative output
|
|
190
|
-
helpers for CLI-side messages that do not come from `AgentEvent`.
|
|
191
|
-
- `show_history()`, `show_title()`, `load_session_history(...)`, `show_steer_queued(...)`,
|
|
192
|
-
`schedule_steer_inserted(...)`: small session UI helpers used by the
|
|
193
|
-
interactive command loop.
|
|
194
|
-
- `close()`: release prompt resources at shutdown.
|
|
195
|
-
|
|
196
|
-
"""
|
|
197
|
-
|
|
198
|
-
def __init__(
|
|
199
|
-
self,
|
|
200
|
-
context_window_tokens: "typing.Union[int, None]" = None,
|
|
201
|
-
) -> "None":
|
|
202
|
-
import sys
|
|
203
|
-
|
|
204
|
-
self._line_output = print
|
|
205
|
-
self._terminal_lock = threading.RLock()
|
|
206
|
-
self._title: "typing.Union[str, None]" = None
|
|
207
|
-
self._pending_user_prompts: "typing.Dict[str, str]" = {}
|
|
208
|
-
self._queued_steer_prompts: "typing.Dict[str, typing.List[str]]" = {}
|
|
209
|
-
self._inserted_steer_prompts: "typing.Dict[str, typing.List[str]]" = {}
|
|
210
|
-
self._history: "typing.List[typing.Tuple[str, str]]" = []
|
|
211
|
-
self._context_window_tokens = context_window_tokens
|
|
212
|
-
self._context_remaining_percent: "typing.Union[int, None]" = (
|
|
213
|
-
100 if context_window_tokens is not None else None
|
|
214
|
-
)
|
|
215
|
-
self._color_enabled = cli_color_enabled() and sys.stdout.isatty()
|
|
216
|
-
self._agent_names: "typing.Dict[str, str]" = {}
|
|
217
|
-
|
|
218
|
-
self.prompter = Prompter(lock=self._terminal_lock)
|
|
219
|
-
|
|
220
|
-
self._stream_buffer = ""
|
|
221
|
-
|
|
222
|
-
def handle_event(self, event: "AgentEvent") -> "None":
|
|
223
|
-
if event.kind == "assistant_delta":
|
|
224
|
-
self._stream_buffer += str(event.payload.get("delta", ""))
|
|
225
|
-
self.prompter.set_status("talking")
|
|
226
|
-
return
|
|
227
|
-
if event.kind == "token_count":
|
|
228
|
-
self._update_context_window(event.payload.get("usage"))
|
|
229
|
-
self.prompter.set_prompt(self._format_main_prompt(DEFAULT_MAIN_PROMPT))
|
|
230
|
-
return
|
|
231
|
-
if event.kind == "model_completed":
|
|
232
|
-
return
|
|
233
|
-
if event.kind == "turn_completed":
|
|
234
|
-
submission_id = str(
|
|
235
|
-
event.payload.get("submission_id", event.turn_id)
|
|
236
|
-
).strip()
|
|
237
|
-
final_text = str(event.payload.get("output_text", "") or "")
|
|
238
|
-
if final_text:
|
|
239
|
-
if final_text.startswith(self._stream_buffer):
|
|
240
|
-
self._stream_buffer += final_text[len(self._stream_buffer) :]
|
|
241
|
-
else:
|
|
242
|
-
self._stream_buffer += final_text
|
|
243
|
-
self.finish_stream()
|
|
244
|
-
pending_prompt = self._pending_user_prompts.pop(submission_id, None)
|
|
245
|
-
if pending_prompt is not None:
|
|
246
|
-
self._history.append((pending_prompt, final_text))
|
|
247
|
-
self._set_idle_status(event)
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
if event.kind == "stream_error":
|
|
251
|
-
self._stream_buffer = ""
|
|
252
|
-
message = str(event.payload.get("message", "")).strip() or "Reconnecting..."
|
|
253
|
-
self._print_line(
|
|
254
|
-
colorize_cli_message(
|
|
255
|
-
f"[status] {message}",
|
|
256
|
-
"status",
|
|
257
|
-
self._color_enabled,
|
|
258
|
-
)
|
|
259
|
-
)
|
|
260
|
-
self.prompter.set_status("reconnecting")
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
self.finish_stream()
|
|
264
|
-
|
|
265
|
-
if event.kind == "turn_started":
|
|
266
|
-
self.prompter.set_status(event.kind)
|
|
267
|
-
submission_id = str(
|
|
268
|
-
event.payload.get("submission_id", event.turn_id)
|
|
269
|
-
).strip()
|
|
270
|
-
user_texts = event.payload.get("user_texts")
|
|
271
|
-
if isinstance(user_texts, list):
|
|
272
|
-
normalized_user_texts = [
|
|
273
|
-
str(text).strip() for text in user_texts if str(text).strip()
|
|
274
|
-
]
|
|
275
|
-
else:
|
|
276
|
-
normalized_user_texts = []
|
|
277
|
-
user_text = str(event.payload.get("user_text", "")).strip()
|
|
278
|
-
if not user_text and normalized_user_texts:
|
|
279
|
-
user_text = "\n".join(normalized_user_texts)
|
|
280
|
-
if self._title is None and user_text:
|
|
281
|
-
self._title = shorten_title(user_text)
|
|
282
|
-
self._print_line(f"Session: {self._title}")
|
|
283
|
-
if user_text:
|
|
284
|
-
self._pending_user_prompts[submission_id] = user_text
|
|
285
|
-
inserted_steer_prompts = self._inserted_steer_prompts.pop(submission_id, [])
|
|
286
|
-
for inserted_steer_prompt in inserted_steer_prompts:
|
|
287
|
-
self._print_line(
|
|
288
|
-
colorize_cli_message(
|
|
289
|
-
f"[steer] inserted: {inserted_steer_prompt}",
|
|
290
|
-
"status",
|
|
291
|
-
self._color_enabled,
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
queued_steer_prompts = self._queued_steer_prompts.pop(submission_id, [])
|
|
295
|
-
for queued_steer_prompt in queued_steer_prompts:
|
|
296
|
-
self._print_line(
|
|
297
|
-
colorize_cli_message(
|
|
298
|
-
f"[steer] inserted: {queued_steer_prompt}",
|
|
299
|
-
"status",
|
|
300
|
-
self._color_enabled,
|
|
301
|
-
)
|
|
302
|
-
)
|
|
303
|
-
if user_text:
|
|
304
|
-
self._print_user_turn(user_text)
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
if event.kind == "model_called":
|
|
308
|
-
# self.prompter.set_status("thinking")
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
if event.kind == "auto_compact_started":
|
|
312
|
-
total_tokens = event.payload.get("total_tokens")
|
|
313
|
-
token_limit = event.payload.get("token_limit")
|
|
314
|
-
if total_tokens is not None and token_limit is not None:
|
|
315
|
-
message = f"[status] auto-compact: {total_tokens}/{token_limit} tokens"
|
|
316
|
-
else:
|
|
317
|
-
message = "[status] auto-compact"
|
|
318
|
-
self._print_line(
|
|
319
|
-
colorize_cli_message(message, "status", self._color_enabled)
|
|
320
|
-
)
|
|
321
|
-
self.prompter.set_status("compacting")
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
if event.kind == "auto_compact_completed":
|
|
325
|
-
summary = str(event.payload.get("summary", "")).strip()
|
|
326
|
-
message = f"[status] {summary}" if summary else "[status] context compacted"
|
|
327
|
-
self._print_line(
|
|
328
|
-
colorize_cli_message(message, "status", self._color_enabled)
|
|
329
|
-
)
|
|
330
|
-
self.prompter.set_status("compacted")
|
|
331
|
-
return
|
|
332
|
-
|
|
333
|
-
if event.kind == "auto_compact_failed":
|
|
334
|
-
error = str(event.payload.get("error", "")).strip()
|
|
335
|
-
message = (
|
|
336
|
-
f"[error] auto-compact failed: {error}"
|
|
337
|
-
if error
|
|
338
|
-
else "[error] auto-compact failed"
|
|
339
|
-
)
|
|
340
|
-
self._print_line(
|
|
341
|
-
colorize_cli_message(message, "error", self._color_enabled)
|
|
342
|
-
)
|
|
343
|
-
return
|
|
344
|
-
|
|
345
|
-
if event.kind == "tool_called":
|
|
346
|
-
tool_name = str(event.payload.get("tool_name", "")).strip()
|
|
347
|
-
message = format_cli_tool_call_message(tool_name, event.payload)
|
|
348
|
-
if message is not None:
|
|
349
|
-
self._print_line(colorize_tool_message(message, self._color_enabled))
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
if event.kind == "tool_started":
|
|
353
|
-
tool_name = str(event.payload.get("tool_name", "")).strip()
|
|
354
|
-
call = event.payload.get("call")
|
|
355
|
-
args = None
|
|
356
|
-
if isinstance(call, ToolCall):
|
|
357
|
-
args = call.arguments
|
|
358
|
-
if tool_name and args is not None:
|
|
359
|
-
self.prompter.set_status(
|
|
360
|
-
shorten_title(f"calling {tool_name}({args})", limit=72)
|
|
361
|
-
)
|
|
362
|
-
elif tool_name:
|
|
363
|
-
self.prompter.set_status(f"calling {tool_name}")
|
|
364
|
-
else:
|
|
365
|
-
self.prompter.set_status("calling provider tools")
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
if event.kind == "tool_completed":
|
|
369
|
-
tool_name = str(event.payload.get("tool_name", "")).strip()
|
|
370
|
-
message = tool_summary(event.payload)
|
|
371
|
-
if tool_name:
|
|
372
|
-
self.prompter.set_status(f"called {tool_name}")
|
|
373
|
-
message = self._replace_agent_ids_with_names(tool_name, message)
|
|
374
|
-
if tool_name == "spawn_agent":
|
|
375
|
-
agent_summary = message
|
|
376
|
-
prefix = "[spawn_agent] spawned "
|
|
377
|
-
if agent_summary.startswith(prefix):
|
|
378
|
-
agent_summary = agent_summary[len(prefix) :]
|
|
379
|
-
self._remember_agent_name(tool_name, agent_summary)
|
|
380
|
-
for line in message.splitlines() or [""]:
|
|
381
|
-
self._print_line(
|
|
382
|
-
colorize_tool_message(line, self._color_enabled, tool_name)
|
|
383
|
-
)
|
|
384
|
-
return
|
|
385
|
-
|
|
386
|
-
if event.kind == "turn_failed":
|
|
387
|
-
submission_id = str(
|
|
388
|
-
event.payload.get("submission_id", event.turn_id)
|
|
389
|
-
).strip()
|
|
390
|
-
self._pending_user_prompts.pop(submission_id, None)
|
|
391
|
-
self._set_idle_status(event)
|
|
392
|
-
return
|
|
393
|
-
|
|
394
|
-
if event.kind == "turn_interrupted":
|
|
395
|
-
submission_id = str(
|
|
396
|
-
event.payload.get("submission_id", event.turn_id)
|
|
397
|
-
).strip()
|
|
398
|
-
final_text = str(event.payload.get("output_text", "") or "")
|
|
399
|
-
pending_prompt = self._pending_user_prompts.pop(submission_id, None)
|
|
400
|
-
if pending_prompt is not None and final_text:
|
|
401
|
-
self._history.append((pending_prompt, final_text))
|
|
402
|
-
self._set_idle_status(event)
|
|
403
|
-
return
|
|
404
|
-
|
|
405
|
-
def show_history(self) -> "None":
|
|
406
|
-
self.finish_stream()
|
|
407
|
-
if not self._history:
|
|
408
|
-
self._print_line("No history yet.")
|
|
409
|
-
return
|
|
410
|
-
|
|
411
|
-
self._print_line(f"Session: {self._title or 'untitled'}")
|
|
412
|
-
for index, (user_text, assistant_text) in enumerate(self._history, start=1):
|
|
413
|
-
self._print_line(f"[{index}]U> {user_text}")
|
|
414
|
-
self._print_line(f"[{index}]A> {assistant_text}")
|
|
415
|
-
|
|
416
|
-
def show_title(self) -> "None":
|
|
417
|
-
self.finish_stream()
|
|
418
|
-
self._print_line(f"Session: {self._title or 'untitled'}")
|
|
419
|
-
|
|
420
|
-
def set_session_title(self, title: "str") -> "None":
|
|
421
|
-
self.finish_stream()
|
|
422
|
-
self._title = title or None
|
|
423
|
-
self._print_line(f"Session: {self._title or 'untitled'}")
|
|
424
|
-
|
|
425
|
-
def show_resumed_session(self, title: "str") -> "None":
|
|
426
|
-
self.write_line(f"Resumed session: {title}")
|
|
427
|
-
self.show_history()
|
|
428
|
-
|
|
429
|
-
def load_session_history(
|
|
430
|
-
self,
|
|
431
|
-
title: "typing.Union[str, None]",
|
|
432
|
-
history: "typing.Tuple[typing.Tuple[str, str], ...]",
|
|
433
|
-
) -> "None":
|
|
434
|
-
self.finish_stream()
|
|
435
|
-
self._title = title or None
|
|
436
|
-
self._history = list(history)
|
|
437
|
-
self._pending_user_prompts.clear()
|
|
438
|
-
self._queued_steer_prompts.clear()
|
|
439
|
-
self._inserted_steer_prompts.clear()
|
|
440
|
-
|
|
441
|
-
def close(self):
|
|
442
|
-
self.prompter.close()
|
|
443
|
-
|
|
444
|
-
async def poll_prompt(self, prompt: "str" = None) -> "typing.Union[str, None]":
|
|
445
|
-
if prompt:
|
|
446
|
-
self.prompter.set_prompt(prompt)
|
|
447
|
-
return await self.prompter.poll_input()
|
|
448
|
-
|
|
449
|
-
async def get_prompt(self, prompt: "str" = None) -> "str":
|
|
450
|
-
if prompt:
|
|
451
|
-
self.prompter.set_prompt(prompt)
|
|
452
|
-
return await self.prompter.require_input()
|
|
453
|
-
|
|
454
|
-
def _update_context_window(self, usage: "object") -> "None":
|
|
455
|
-
if self._context_window_tokens is None:
|
|
456
|
-
return
|
|
457
|
-
if not isinstance(usage, dict):
|
|
458
|
-
self._context_remaining_percent = None
|
|
459
|
-
return
|
|
460
|
-
try:
|
|
461
|
-
total_tokens = int(usage["total_tokens"])
|
|
462
|
-
except (KeyError, TypeError, ValueError):
|
|
463
|
-
self._context_remaining_percent = None
|
|
464
|
-
return
|
|
465
|
-
self._context_remaining_percent = percent_of_context_window_remaining(
|
|
466
|
-
total_tokens,
|
|
467
|
-
self._context_window_tokens,
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
def _format_main_prompt(self, prompt: "str") -> "str":
|
|
471
|
-
if prompt != DEFAULT_MAIN_PROMPT:
|
|
472
|
-
return prompt
|
|
473
|
-
if self._context_remaining_percent is None:
|
|
474
|
-
return prompt
|
|
475
|
-
return f"pyco({self._context_remaining_percent}%)> "
|
|
476
|
-
|
|
477
|
-
def _set_idle_status(self, event: "AgentEvent") -> "None":
|
|
478
|
-
if background_work_count(event.payload) > 0:
|
|
479
|
-
self.prompter.set_status(IDLE_SLEEPING_STATUS)
|
|
480
|
-
else:
|
|
481
|
-
self.prompter.set_status(active=False)
|
|
482
|
-
|
|
483
|
-
def show_steer_queued(self, turn_id: "str", prompt: "str") -> "None":
|
|
484
|
-
preview = shorten_title(prompt, limit=72)
|
|
485
|
-
self._queued_steer_prompts.setdefault(turn_id, []).append(preview)
|
|
486
|
-
self._print_line(
|
|
487
|
-
colorize_cli_message(
|
|
488
|
-
f"[steer] queued: {preview}",
|
|
489
|
-
"status",
|
|
490
|
-
self._color_enabled,
|
|
491
|
-
)
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
def schedule_steer_inserted(self, turn_id: "str", prompt: "str") -> "None":
|
|
495
|
-
self._inserted_steer_prompts.setdefault(turn_id, []).append(
|
|
496
|
-
shorten_title(prompt, limit=72)
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
def set_context_window_tokens(
|
|
500
|
-
self,
|
|
501
|
-
context_window_tokens: "typing.Union[int, None]",
|
|
502
|
-
) -> "None":
|
|
503
|
-
self._context_window_tokens = context_window_tokens
|
|
504
|
-
self._context_remaining_percent = (
|
|
505
|
-
100 if context_window_tokens is not None else None
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
def finish_stream(self) -> "None":
|
|
509
|
-
with self._terminal_lock:
|
|
510
|
-
if self._stream_buffer:
|
|
511
|
-
self._print_line("assistant> " + self._stream_buffer)
|
|
512
|
-
self._stream_buffer = ""
|
|
513
|
-
|
|
514
|
-
def write_line(self, text: "str") -> "None":
|
|
515
|
-
self._print_line(text)
|
|
516
|
-
|
|
517
|
-
def show_error(self, text: "str") -> "None":
|
|
518
|
-
self.finish_stream()
|
|
519
|
-
lines = str(text).splitlines() or [""]
|
|
520
|
-
formatted = [f"Error: {lines[0]}"]
|
|
521
|
-
formatted.extend(f" {line}" if line else "" for line in lines[1:])
|
|
522
|
-
self._print_line(
|
|
523
|
-
colorize_cli_message(
|
|
524
|
-
"\n".join(formatted),
|
|
525
|
-
"error",
|
|
526
|
-
self._color_enabled,
|
|
527
|
-
)
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
def _print_line(self, text: "str") -> "None":
|
|
531
|
-
with self._terminal_lock:
|
|
532
|
-
self._line_output(text)
|
|
533
|
-
|
|
534
|
-
def _print_user_turn(self, text: "str") -> "None":
|
|
535
|
-
self._print_line(
|
|
536
|
-
colorize_cli_message(f"user> {text}", "assistant", self._color_enabled)
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
def _remember_agent_name(self, tool_name: "str", summary: "str") -> "None":
|
|
540
|
-
if tool_name != "spawn_agent":
|
|
541
|
-
return
|
|
542
|
-
if " (" not in summary or not summary.endswith(")"):
|
|
543
|
-
return
|
|
544
|
-
nickname, rest = summary.rsplit(" (", 1)
|
|
545
|
-
agent_short_id = rest[:-1].strip()
|
|
546
|
-
nickname = nickname.strip()
|
|
547
|
-
if not nickname or not agent_short_id:
|
|
548
|
-
return
|
|
549
|
-
self._agent_names[agent_short_id] = nickname
|
|
550
|
-
|
|
551
|
-
def _replace_agent_ids_with_names(self, tool_name: "str", message: "str") -> "str":
|
|
552
|
-
if tool_name not in {"wait_agent", "send_input", "resume_agent", "close_agent"}:
|
|
553
|
-
return message
|
|
554
|
-
for agent_short_id, nickname in sorted(
|
|
555
|
-
self._agent_names.items(),
|
|
556
|
-
key=lambda item: len(item[0]),
|
|
557
|
-
reverse=True,
|
|
558
|
-
):
|
|
559
|
-
message = message.replace(agent_short_id, nickname)
|
|
560
|
-
return message
|