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.
Files changed (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -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