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,713 +0,0 @@
1
- import json
2
- import re
3
- import shlex
4
- import typing
5
-
6
- from ..protocol import ToolCall, ToolResult
7
-
8
- ANSI_RESET = "\x1b[0m"
9
- ANSI_BOLD = "\x1b[1m"
10
- ANSI_DIM = "\x1b[2m"
11
- ANSI_RED = "\x1b[31m"
12
- ANSI_GREEN = "\x1b[32m"
13
- ANSI_YELLOW = "\x1b[33m"
14
- ANSI_BLUE = "\x1b[34m"
15
- ANSI_MAGENTA = "\x1b[35m"
16
- ANSI_CYAN = "\x1b[36m"
17
- TOOL_ARGUMENT_PREVIEW_LIMIT = 200
18
- RUNNING_SESSION_MARKER = "Process running with session ID "
19
- PYTHON_HEREDOC_RE = re.compile(
20
- r"\bpython(?:\d+(?:\.\d+)?)?\s+-\s+<<[-]?\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\1"
21
- )
22
- TOOL_COLOR_MAP = {
23
- "update_plan": ANSI_CYAN,
24
- "exec_command": ANSI_YELLOW,
25
- "write_stdin": ANSI_MAGENTA,
26
- "shell": ANSI_YELLOW,
27
- "shell_command": ANSI_YELLOW,
28
- "exec": ANSI_YELLOW,
29
- "wait": ANSI_MAGENTA,
30
- "web_search": ANSI_MAGENTA,
31
- "spawn_agent": ANSI_BLUE,
32
- "send_input": ANSI_BLUE,
33
- "wait_agent": ANSI_BLUE,
34
- "resume_agent": ANSI_BLUE,
35
- "close_agent": ANSI_BLUE,
36
- "read_file": ANSI_DIM,
37
- "list_dir": ANSI_DIM,
38
- "grep_files": ANSI_DIM,
39
- "view_image": ANSI_DIM,
40
- }
41
-
42
-
43
- def colorize_cli_message(text: "str", kind: "str", enabled: "bool") -> "str":
44
- if not enabled:
45
- return text
46
- palette = {
47
- "assistant": ANSI_GREEN,
48
- "status": ANSI_CYAN,
49
- "error": ANSI_RED,
50
- }
51
- color = palette.get(kind)
52
- return _colorize_with(text, color, enabled)
53
-
54
-
55
- def colorize_tool_message(
56
- message: "str",
57
- enabled: "bool",
58
- tool_name: "typing.Union[str, None]" = None,
59
- ) -> "str":
60
- if message.startswith("[error]"):
61
- return colorize_cli_message(message, "error", enabled)
62
- if message.startswith((" [x]", " [>]", " [ ]")):
63
- return _colorize_with(message, TOOL_COLOR_MAP.get("update_plan"), enabled)
64
- resolved_tool_name = _tool_name_from_message(message) or (tool_name or "")
65
- if resolved_tool_name == "exec_command" and "session_id=" in message:
66
- return _colorize_with(message, ANSI_DIM, enabled)
67
- if resolved_tool_name == "exec_command" and not message.startswith("["):
68
- return _colorize_with(message, "", enabled)
69
- return _colorize_with(
70
- message, TOOL_COLOR_MAP.get(resolved_tool_name, ANSI_DIM), enabled
71
- )
72
-
73
-
74
- def _colorize_with(
75
- text: "str", color: "typing.Union[str, None]", enabled: "bool"
76
- ) -> "str":
77
- if not enabled or color is None:
78
- return text
79
- return f"{ANSI_BOLD}{color}{text}{ANSI_RESET}"
80
-
81
-
82
- def _tool_name_from_message(message: "str") -> "str":
83
- if not message.startswith("["):
84
- return ""
85
- end = message.find("]")
86
- if end <= 1:
87
- return ""
88
- return message[1:end]
89
-
90
-
91
- def tool_summary(payload: "typing.Dict[str, object]") -> "str":
92
- tool_name = str(payload.get("tool_name", "")).strip()
93
- handler = _TOOL_MESSAGE_HANDLERS.get(tool_name, _generic_tool_message)
94
- return handler(payload)
95
-
96
-
97
- def _paired_payload(
98
- payload: "typing.Dict[str, object]",
99
- ) -> "typing.Tuple[typing.Union[ToolCall, None], typing.Union[ToolResult, None]]":
100
- call = payload.get("call")
101
- result = payload.get("result")
102
- if isinstance(call, ToolCall) and isinstance(result, ToolResult):
103
- return call, result
104
- return None, None
105
-
106
-
107
- def _legacy_summary(payload: "typing.Dict[str, object]") -> "str":
108
- return str(payload.get("summary", "") or "").strip()
109
-
110
-
111
- def _combine_call_and_result(
112
- call_summary: "typing.Union[str, None]",
113
- result_summary: "typing.Union[str, None]",
114
- ) -> "str":
115
- if call_summary and result_summary:
116
- return f"{call_summary} -> {result_summary}"
117
- if call_summary:
118
- return call_summary
119
- return result_summary or ""
120
-
121
-
122
- def _agent_status_summary(status: "object") -> "str":
123
- if isinstance(status, str):
124
- return status
125
- if isinstance(status, dict):
126
- if "completed" in status:
127
- completed = status.get("completed")
128
- if completed is None:
129
- return "completed"
130
- return f"completed: {_truncate_text(str(completed), limit=48)}"
131
- if "errored" in status:
132
- return f"errored: {_truncate_text(str(status.get('errored', '')), limit=48)}"
133
- return _truncate_text(json.dumps(status, ensure_ascii=False, separators=(",", ":")))
134
-
135
-
136
- def _result_output_summary(result: "ToolResult") -> "typing.Union[str, None]":
137
- if isinstance(result.output, (dict, list)):
138
- return _truncate_text(
139
- json.dumps(result.output, ensure_ascii=False, separators=(",", ":"))
140
- )
141
- return _output_text_summary(result.output_text())
142
-
143
-
144
- def _output_text_summary(text: "str") -> "typing.Union[str, None]":
145
- lines = [line.strip() for line in text.splitlines()]
146
- if "Output:" in lines:
147
- output_index = lines.index("Output:")
148
- for line in lines[output_index + 1 :]:
149
- if line:
150
- return _truncate_text(line)
151
- for line in lines:
152
- if not line:
153
- continue
154
- if line.startswith(("Exit code:", "Wall time:", "Command:")):
155
- continue
156
- return _truncate_text(line)
157
- return None
158
-
159
-
160
- def _running_exec_session_id(result: "ToolResult") -> "typing.Union[str, None]":
161
- for line in result.output_text().splitlines():
162
- line = line.strip()
163
- if line.startswith(RUNNING_SESSION_MARKER):
164
- session_id = line[len(RUNNING_SESSION_MARKER) :].strip()
165
- if session_id:
166
- return session_id
167
- return None
168
-
169
-
170
- def _exec_command_preview(command: "str") -> "str":
171
- heredoc = _python_heredoc_preview(command)
172
- if heredoc:
173
- return heredoc
174
- return _truncate_text(command, limit=TOOL_ARGUMENT_PREVIEW_LIMIT)
175
-
176
-
177
- def _python_heredoc_preview(command: "str") -> "typing.Union[str, None]":
178
- lines = command.splitlines()
179
- if not lines:
180
- return None
181
-
182
- match = PYTHON_HEREDOC_RE.search(lines[0])
183
- if match is None:
184
- return None
185
- delimiter = match.group(2)
186
-
187
- end_index = None
188
- for index, line in enumerate(lines[1:], start=1):
189
- if line.strip() == delimiter:
190
- end_index = index
191
- break
192
- if end_index is None:
193
- return None
194
-
195
- return "\n".join(lines[: end_index + 1])
196
-
197
-
198
- def _plan_progress_summary(plan: "typing.List[object]") -> "str":
199
- total = len(plan)
200
- completed = 0
201
- in_progress = 0
202
- for item in plan:
203
- if not isinstance(item, dict):
204
- continue
205
- status = str(item.get("status", "")).strip()
206
- if status == "completed":
207
- completed += 1
208
- elif status == "in_progress":
209
- in_progress += 1
210
- if total == 0:
211
- return "0 steps"
212
- if completed >= total:
213
- return f"Done {completed}/{total}"
214
- if in_progress:
215
- return f"Working on {completed + in_progress}/{total}"
216
- return f"Planned {completed}/{total}"
217
-
218
-
219
- def _exec_command_summary(payload: "typing.Dict[str, object]") -> "str":
220
- call, result = _paired_payload(payload)
221
- if call is None or result is None:
222
- return _legacy_summary(payload)
223
-
224
- command = None
225
- if isinstance(call.arguments, dict):
226
- cmd = call.arguments.get("cmd")
227
- if cmd not in (None, ""):
228
- command = _exec_command_preview(str(cmd))
229
- session_id = _running_exec_session_id(result)
230
- if session_id:
231
- return _combine_call_and_result(command, f"session_id={session_id}")
232
- return _combine_call_and_result(command, _result_output_summary(result))
233
-
234
-
235
- def _shell_command_summary(payload: "typing.Dict[str, object]") -> "str":
236
- call, result = _paired_payload(payload)
237
- if call is None or result is None:
238
- return _legacy_summary(payload)
239
-
240
- command = None
241
- if isinstance(call.arguments, dict):
242
- raw_command = call.arguments.get("command")
243
- if raw_command not in (None, ""):
244
- command = _truncate_text(
245
- str(raw_command), limit=TOOL_ARGUMENT_PREVIEW_LIMIT
246
- )
247
- return _combine_call_and_result(command, _result_output_summary(result))
248
-
249
-
250
- def _shell_summary(payload: "typing.Dict[str, object]") -> "str":
251
- call, result = _paired_payload(payload)
252
- if call is None or result is None:
253
- return _legacy_summary(payload)
254
-
255
- command = None
256
- if isinstance(call.arguments, dict):
257
- raw_command = call.arguments.get("command")
258
- if isinstance(raw_command, list) and raw_command:
259
- rendered = " ".join(shlex.quote(str(part)) for part in raw_command)
260
- command = _truncate_text(rendered, limit=TOOL_ARGUMENT_PREVIEW_LIMIT)
261
- return _combine_call_and_result(command, _result_output_summary(result))
262
-
263
-
264
- def _write_stdin_summary(payload: "typing.Dict[str, object]") -> "str":
265
- call, result = _paired_payload(payload)
266
- if call is None or result is None:
267
- return _legacy_summary(payload)
268
-
269
- command = None
270
- if isinstance(call.arguments, dict):
271
- session_id_value = call.arguments.get("session_id")
272
- if session_id_value not in (None, ""):
273
- session_id = int(session_id_value)
274
- chars = call.arguments.get("chars") or ""
275
- if not chars:
276
- command = f"poll session {session_id}"
277
- else:
278
- command = (
279
- f"session {session_id} <- {_truncate_text(str(chars), limit=32)}"
280
- )
281
- return _combine_call_and_result(command, _result_output_summary(result))
282
-
283
-
284
- def _exec_code_summary(payload: "typing.Dict[str, object]") -> "str":
285
- call, result = _paired_payload(payload)
286
- if call is None or result is None:
287
- return _legacy_summary(payload)
288
- return _result_output_summary(result) or ""
289
-
290
-
291
- def _wait_summary(payload: "typing.Dict[str, object]") -> "str":
292
- call, result = _paired_payload(payload)
293
- if call is None or result is None:
294
- return _legacy_summary(payload)
295
- return _result_output_summary(result) or ""
296
-
297
-
298
- def _read_file_summary(payload: "typing.Dict[str, object]") -> "str":
299
- call, result = _paired_payload(payload)
300
- if call is None or result is None:
301
- return _legacy_summary(payload)
302
-
303
- path = None
304
- if isinstance(call.arguments, dict):
305
- raw_path = call.arguments.get("file_path")
306
- if raw_path not in (None, ""):
307
- path = _truncate_text(str(raw_path), limit=TOOL_ARGUMENT_PREVIEW_LIMIT)
308
- return _combine_call_and_result(path, _result_output_summary(result))
309
-
310
-
311
- def _list_dir_summary(payload: "typing.Dict[str, object]") -> "str":
312
- call, result = _paired_payload(payload)
313
- if call is None or result is None:
314
- return _legacy_summary(payload)
315
-
316
- path = None
317
- if isinstance(call.arguments, dict):
318
- raw_path = call.arguments.get("dir_path")
319
- if raw_path not in (None, ""):
320
- path = _truncate_text(str(raw_path), limit=TOOL_ARGUMENT_PREVIEW_LIMIT)
321
- return _combine_call_and_result(path, _result_output_summary(result))
322
-
323
-
324
- def _grep_files_summary(payload: "typing.Dict[str, object]") -> "str":
325
- call, result = _paired_payload(payload)
326
- if call is None or result is None:
327
- return _legacy_summary(payload)
328
-
329
- query = None
330
- if isinstance(call.arguments, dict):
331
- pattern = call.arguments.get("pattern")
332
- path = call.arguments.get("path")
333
- if pattern not in (None, "") and path not in (None, ""):
334
- query = _truncate_text(
335
- f"{pattern} @ {path}", limit=TOOL_ARGUMENT_PREVIEW_LIMIT
336
- )
337
- elif pattern not in (None, ""):
338
- query = _truncate_text(str(pattern), limit=TOOL_ARGUMENT_PREVIEW_LIMIT)
339
- return _combine_call_and_result(query, _result_output_summary(result))
340
-
341
-
342
- def _view_image_summary(payload: "typing.Dict[str, object]") -> "str":
343
- call, result = _paired_payload(payload)
344
- if call is None or result is None:
345
- return _legacy_summary(payload)
346
-
347
- path = None
348
- if isinstance(call.arguments, dict):
349
- raw_path = call.arguments.get("path")
350
- if raw_path not in (None, ""):
351
- path = _truncate_text(str(raw_path), limit=TOOL_ARGUMENT_PREVIEW_LIMIT)
352
-
353
- result_summary = None
354
- if isinstance(result.output, list):
355
- result_summary = f"{len(result.output)} image item(s)"
356
- if result_summary is None:
357
- result_summary = _result_output_summary(result)
358
- return _combine_call_and_result(path, result_summary)
359
-
360
-
361
- def _update_plan_summary(payload: "typing.Dict[str, object]") -> "str":
362
- call, result = _paired_payload(payload)
363
- if call is None or result is None:
364
- return _legacy_summary(payload)
365
-
366
- if isinstance(call.arguments, dict):
367
- plan = call.arguments.get("plan")
368
- if isinstance(plan, list):
369
- return _plan_progress_summary(plan)
370
-
371
- if isinstance(result.output, dict):
372
- plan = result.output.get("plan")
373
- if isinstance(plan, list):
374
- return f"{len(plan)} steps"
375
- return _result_output_summary(result) or ""
376
-
377
-
378
- def _spawn_agent_summary(payload: "typing.Dict[str, object]") -> "str":
379
- call, result = _paired_payload(payload)
380
- if call is None or result is None:
381
- return _legacy_summary(payload)
382
-
383
- if isinstance(result.output, dict):
384
- agent_id = str(result.output.get("agent_id", "")).strip()
385
- nickname = str(result.output.get("nickname", "")).strip()
386
- if nickname and agent_id:
387
- return f"{nickname} ({_short_id(agent_id)})"
388
- return _result_output_summary(result) or ""
389
-
390
-
391
- def _send_input_summary(payload: "typing.Dict[str, object]") -> "str":
392
- call, result = _paired_payload(payload)
393
- if call is None or result is None:
394
- return _legacy_summary(payload)
395
-
396
- command = None
397
- if isinstance(call.arguments, dict):
398
- agent_id = call.arguments.get("id")
399
- message = call.arguments.get("message")
400
- prefix = f"{_short_id(str(agent_id))} <- " if agent_id else ""
401
- if message not in (None, ""):
402
- command = f"{prefix}{_truncate_text(str(message), limit=40)}"
403
- elif prefix:
404
- command = prefix.rstrip()
405
-
406
- result_summary = None
407
- if isinstance(result.output, dict):
408
- submission_id = str(result.output.get("submission_id", "")).strip()
409
- if submission_id:
410
- result_summary = f"queued {_short_id(submission_id)}"
411
- if result_summary is None:
412
- result_summary = _result_output_summary(result)
413
- return _combine_call_and_result(command, result_summary)
414
-
415
-
416
- def _wait_agent_summary(payload: "typing.Dict[str, object]") -> "str":
417
- call, result = _paired_payload(payload)
418
- if call is None or result is None:
419
- return _legacy_summary(payload)
420
-
421
- if isinstance(result.output, dict):
422
- if result.output.get("timed_out") is True:
423
- return "timed out"
424
- status = result.output.get("status")
425
- if isinstance(status, dict):
426
- parts = []
427
- for agent_id, agent_status in status.items():
428
- if not isinstance(agent_id, str):
429
- continue
430
- parts.append(
431
- f"{_short_id(agent_id)}={_agent_status_summary(agent_status)}"
432
- )
433
- if parts:
434
- return _truncate_text(", ".join(parts), limit=96)
435
- return _result_output_summary(result) or ""
436
-
437
-
438
- def _resume_agent_summary(payload: "typing.Dict[str, object]") -> "str":
439
- call, result = _paired_payload(payload)
440
- if call is None or result is None:
441
- return _legacy_summary(payload)
442
-
443
- agent_id_summary = None
444
- if isinstance(call.arguments, dict):
445
- agent_id = call.arguments.get("id")
446
- if agent_id not in (None, ""):
447
- agent_id_summary = _short_id(str(agent_id))
448
-
449
- result_summary = None
450
- if isinstance(result.output, dict):
451
- result_summary = _agent_status_summary(result.output.get("status"))
452
- if result_summary is None:
453
- result_summary = _result_output_summary(result)
454
- return _combine_call_and_result(agent_id_summary, result_summary)
455
-
456
-
457
- def _close_agent_summary(payload: "typing.Dict[str, object]") -> "str":
458
- call, result = _paired_payload(payload)
459
- if call is None or result is None:
460
- return _legacy_summary(payload)
461
-
462
- agent_id_summary = None
463
- if isinstance(call.arguments, dict):
464
- agent_id = call.arguments.get("id")
465
- if agent_id not in (None, ""):
466
- agent_id_summary = _short_id(str(agent_id))
467
-
468
- result_summary = None
469
- if isinstance(result.output, dict):
470
- result_summary = _agent_status_summary(result.output.get("status"))
471
- if result_summary is None:
472
- result_summary = _result_output_summary(result)
473
- return _combine_call_and_result(agent_id_summary, result_summary)
474
-
475
-
476
- def _generic_payload_summary(payload: "typing.Dict[str, object]") -> "str":
477
- call, result = _paired_payload(payload)
478
- if call is None or result is None:
479
- return _legacy_summary(payload)
480
- return _result_output_summary(result) or ""
481
-
482
-
483
- def _update_plan_message(payload: "typing.Dict[str, object]") -> "str":
484
- summary = _update_plan_summary(payload)
485
- if bool(payload.get("is_error")):
486
- return (
487
- f"[error] update_plan failed: {summary}"
488
- if summary
489
- else "[error] update_plan failed"
490
- )
491
-
492
- lines = [f"[update_plan] {summary}" if summary else "[update_plan] Plan updated"]
493
- plan_items = []
494
- call = payload.get("call")
495
- if isinstance(call, ToolCall) and isinstance(call.arguments, dict):
496
- raw_plan = call.arguments.get("plan")
497
- if isinstance(raw_plan, list):
498
- plan_items = raw_plan
499
- else:
500
- raw_plan_items = payload.get("plan_items")
501
- if isinstance(raw_plan_items, list):
502
- plan_items = raw_plan_items
503
- for item in plan_items:
504
- if not isinstance(item, dict):
505
- continue
506
- step = str(item.get("step", "")).strip()
507
- status = str(item.get("status", "")).strip()
508
- if not step:
509
- continue
510
- marker = {
511
- "completed": "[x]",
512
- "in_progress": "[>]",
513
- "pending": "[ ]",
514
- }.get(status, "[ ]")
515
- lines.append(f" {marker} {step}")
516
- return "\n".join(lines)
517
-
518
-
519
- def _exec_command_message(payload: "typing.Dict[str, object]") -> "str":
520
- summary = _exec_command_summary(payload)
521
- if bool(payload.get("is_error")):
522
- return (
523
- f"[error] exec_command failed: {summary}"
524
- if summary
525
- else "[error] exec_command failed"
526
- )
527
- return f"[exec_command] {summary}" if summary else "[exec_command]"
528
-
529
-
530
- def _write_stdin_message(payload: "typing.Dict[str, object]") -> "str":
531
- summary = _write_stdin_summary(payload)
532
- if bool(payload.get("is_error")):
533
- return (
534
- f"[error] write_stdin failed: {summary}"
535
- if summary
536
- else "[error] write_stdin failed"
537
- )
538
- return f"[write_stdin] {summary}" if summary else "[write_stdin]"
539
-
540
-
541
- def _shell_message(payload: "typing.Dict[str, object]") -> "str":
542
- summary = _shell_summary(payload)
543
- if bool(payload.get("is_error")):
544
- return f"[error] shell failed: {summary}" if summary else "[error] shell failed"
545
- return f"[shell] {summary}" if summary else "[shell]"
546
-
547
-
548
- def _shell_command_message(payload: "typing.Dict[str, object]") -> "str":
549
- summary = _shell_command_summary(payload)
550
- if bool(payload.get("is_error")):
551
- return (
552
- f"[error] shell_command failed: {summary}"
553
- if summary
554
- else "[error] shell_command failed"
555
- )
556
- return f"[shell_command] {summary}" if summary else "[shell_command]"
557
-
558
-
559
- def _exec_message(payload: "typing.Dict[str, object]") -> "str":
560
- summary = _exec_code_summary(payload)
561
- if bool(payload.get("is_error")):
562
- return f"[error] exec failed: {summary}" if summary else "[error] exec failed"
563
- return f"[exec] {summary}" if summary else "[exec]"
564
-
565
-
566
- def _wait_message(payload: "typing.Dict[str, object]") -> "str":
567
- summary = _wait_summary(payload)
568
- if bool(payload.get("is_error")):
569
- return f"[error] wait failed: {summary}" if summary else "[error] wait failed"
570
- return f"[wait] {summary}" if summary else "[wait]"
571
-
572
-
573
- def _read_file_message(payload: "typing.Dict[str, object]") -> "str":
574
- summary = _read_file_summary(payload)
575
- if bool(payload.get("is_error")):
576
- return (
577
- f"[error] read_file failed: {summary}"
578
- if summary
579
- else "[error] read_file failed"
580
- )
581
- return f"[read_file] {summary}" if summary else "[read_file]"
582
-
583
-
584
- def _list_dir_message(payload: "typing.Dict[str, object]") -> "str":
585
- summary = _list_dir_summary(payload)
586
- if bool(payload.get("is_error")):
587
- return (
588
- f"[error] list_dir failed: {summary}"
589
- if summary
590
- else "[error] list_dir failed"
591
- )
592
- return f"[list_dir] {summary}" if summary else "[list_dir]"
593
-
594
-
595
- def _grep_files_message(payload: "typing.Dict[str, object]") -> "str":
596
- summary = _grep_files_summary(payload)
597
- if bool(payload.get("is_error")):
598
- return (
599
- f"[error] grep_files failed: {summary}"
600
- if summary
601
- else "[error] grep_files failed"
602
- )
603
- return f"[grep_files] {summary}" if summary else "[grep_files]"
604
-
605
-
606
- def _view_image_message(payload: "typing.Dict[str, object]") -> "str":
607
- summary = _view_image_summary(payload)
608
- if bool(payload.get("is_error")):
609
- return (
610
- f"[error] view_image failed: {summary}"
611
- if summary
612
- else "[error] view_image failed"
613
- )
614
- return f"[view_image] {summary}" if summary else "[view_image]"
615
-
616
-
617
- def _spawn_agent_message(payload: "typing.Dict[str, object]") -> "str":
618
- summary = _spawn_agent_summary(payload)
619
- if bool(payload.get("is_error")):
620
- return (
621
- f"[error] spawn_agent failed: {summary}"
622
- if summary
623
- else "[error] spawn_agent failed"
624
- )
625
- return f"[spawn_agent] spawned {summary}" if summary else "[spawn_agent] spawned"
626
-
627
-
628
- def _send_input_message(payload: "typing.Dict[str, object]") -> "str":
629
- summary = _send_input_summary(payload)
630
- if bool(payload.get("is_error")):
631
- return (
632
- f"[error] send_input failed: {summary}"
633
- if summary
634
- else "[error] send_input failed"
635
- )
636
- return f"[send_input] {summary}" if summary else "[send_input]"
637
-
638
-
639
- def _wait_agent_message(payload: "typing.Dict[str, object]") -> "str":
640
- summary = _wait_agent_summary(payload)
641
- if bool(payload.get("is_error")):
642
- return (
643
- f"[error] wait_agent failed: {summary}"
644
- if summary
645
- else "[error] wait_agent failed"
646
- )
647
- return f"[wait_agent] {summary}" if summary else "[wait_agent]"
648
-
649
-
650
- def _resume_agent_message(payload: "typing.Dict[str, object]") -> "str":
651
- summary = _resume_agent_summary(payload)
652
- if bool(payload.get("is_error")):
653
- return (
654
- f"[error] resume_agent failed: {summary}"
655
- if summary
656
- else "[error] resume_agent failed"
657
- )
658
- return f"[resume_agent] {summary}" if summary else "[resume_agent]"
659
-
660
-
661
- def _close_agent_message(payload: "typing.Dict[str, object]") -> "str":
662
- summary = _close_agent_summary(payload)
663
- if bool(payload.get("is_error")):
664
- return (
665
- f"[error] close_agent failed: {summary}"
666
- if summary
667
- else "[error] close_agent failed"
668
- )
669
- return f"[close_agent] {summary}" if summary else "[close_agent]"
670
-
671
-
672
- def _generic_tool_message(payload: "typing.Dict[str, object]") -> "str":
673
- tool_name = str(payload.get("tool_name", "")).strip()
674
- summary = _generic_payload_summary(payload)
675
- if bool(payload.get("is_error")):
676
- if summary:
677
- return f"[error] {tool_name} failed: {summary}"
678
- return f"[error] {tool_name} failed"
679
- return f"[{tool_name}] {summary}" if summary else f"[{tool_name}]"
680
-
681
-
682
- _TOOL_MESSAGE_HANDLERS = {
683
- "update_plan": _update_plan_message,
684
- "exec_command": _exec_command_message,
685
- "write_stdin": _write_stdin_message,
686
- "shell": _shell_message,
687
- "shell_command": _shell_command_message,
688
- "exec": _exec_message,
689
- "wait": _wait_message,
690
- "read_file": _read_file_message,
691
- "list_dir": _list_dir_message,
692
- "grep_files": _grep_files_message,
693
- "view_image": _view_image_message,
694
- "spawn_agent": _spawn_agent_message,
695
- "send_input": _send_input_message,
696
- "wait_agent": _wait_agent_message,
697
- "resume_agent": _resume_agent_message,
698
- "close_agent": _close_agent_message,
699
- }
700
-
701
-
702
- def _short_id(value: "str", limit: "int" = 8) -> "str":
703
- compact = value.strip()
704
- if len(compact) <= limit + 4:
705
- return compact
706
- return f"{compact[:limit]}...{compact[-4:]}"
707
-
708
-
709
- def _truncate_text(text: "str", limit: "int" = 96) -> "str":
710
- compact = " ".join(text.split())
711
- if len(compact) <= limit:
712
- return compact
713
- return compact[: limit - 3].rstrip() + "..."