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