zjcode 0.0.1__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 (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,367 @@
1
+ """Formatting utilities for tool call display in the app.
2
+
3
+ This module handles rendering tool calls and tool messages for the TUI.
4
+
5
+ Imported at module level by `textual_adapter` (itself deferred from the startup
6
+ path). Heavy SDK dependencies (e.g., `backends`) are deferred to function bodies.
7
+ """
8
+
9
+ import json
10
+ from collections.abc import Callable
11
+ from contextlib import suppress
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from deepagents_code.config import MAX_ARG_LENGTH, get_glyphs
16
+ from deepagents_code.unicode_security import strip_dangerous_unicode
17
+
18
+ _HIDDEN_CHAR_MARKER = " [hidden chars removed]"
19
+ """Marker appended to display values that had dangerous Unicode stripped, so
20
+ users know the value was modified for safety."""
21
+
22
+ JS_EVAL_HEADER_MAX_LENGTH = 120
23
+ """Width at which the `js_eval` header truncates the first code line.
24
+
25
+ Shared with `messages.py` so the "header truncates the first line" cutoff and
26
+ the "offer a collapsible code block" threshold stay in lock-step from a single
27
+ source of truth.
28
+ """
29
+
30
+ EXECUTE_HEADER_MAX_LENGTH = 120
31
+ """Width at which the `execute` header truncates the shell command.
32
+
33
+ Shared with `messages.py` so the header cutoff and the "offer a collapsible
34
+ command block" threshold stay in lock-step from a single source of truth.
35
+ """
36
+
37
+
38
+ def _format_timeout(seconds: int) -> str:
39
+ """Format timeout in human-readable units (e.g., 300 -> '5m', 3600 -> '1h').
40
+
41
+ Args:
42
+ seconds: The timeout value in seconds to format.
43
+
44
+ Returns:
45
+ Human-readable timeout string (e.g., '5m', '1h', '300s').
46
+ """
47
+ if seconds < 60: # noqa: PLR2004 # Time unit boundary
48
+ return f"{seconds}s"
49
+ if seconds < 3600 and seconds % 60 == 0: # noqa: PLR2004 # Time unit boundaries
50
+ return f"{seconds // 60}m"
51
+ if seconds % 3600 == 0:
52
+ return f"{seconds // 3600}h"
53
+ # For odd values, just show seconds
54
+ return f"{seconds}s"
55
+
56
+
57
+ def _coerce_timeout_seconds(timeout: int | str | None) -> int | None:
58
+ """Normalize timeout values to seconds for display.
59
+
60
+ Accepts integer values and numeric strings. Returns `None` for invalid
61
+ values so display formatting never raises.
62
+
63
+ Args:
64
+ timeout: Raw timeout value from tool arguments.
65
+
66
+ Returns:
67
+ Integer timeout in seconds, or `None` if unavailable/invalid.
68
+ """
69
+ if type(timeout) is int:
70
+ return timeout
71
+ if isinstance(timeout, str):
72
+ stripped = timeout.strip()
73
+ if not stripped:
74
+ return None
75
+ try:
76
+ return int(stripped)
77
+ except ValueError:
78
+ return None
79
+ return None
80
+
81
+
82
+ def truncate_value(value: str, max_length: int = MAX_ARG_LENGTH) -> str:
83
+ """Truncate a string value if it exceeds max_length.
84
+
85
+ Returns:
86
+ Truncated string with ellipsis suffix if exceeded, otherwise original.
87
+ """
88
+ if len(value) > max_length:
89
+ return value[:max_length] + get_glyphs().ellipsis
90
+ return value
91
+
92
+
93
+ def _sanitize_display_value(value: object, *, max_length: int = MAX_ARG_LENGTH) -> str:
94
+ """Sanitize a value for safe, compact terminal display.
95
+
96
+ Hidden/deceptive Unicode controls are stripped. When stripping occurs, a
97
+ marker is appended so users know the value changed for display safety.
98
+
99
+ Args:
100
+ value: Any value to display.
101
+ max_length: Maximum display length before truncation.
102
+
103
+ Returns:
104
+ Sanitized display string.
105
+ """
106
+ raw = str(value)
107
+ sanitized = strip_dangerous_unicode(raw)
108
+ display = truncate_value(sanitized, max_length)
109
+ if sanitized != raw:
110
+ return display + _HIDDEN_CHAR_MARKER
111
+ return display
112
+
113
+
114
+ def _format_scope_path(
115
+ path_value: object,
116
+ abbreviate: Callable[[str], str],
117
+ ) -> str:
118
+ """Format a glob/grep `path` argument as a display suffix.
119
+
120
+ The glob tool defaults `path` to the backend root (`"/"`); the grep tool
121
+ defaults it to `None` (the backend's working directory). In either case the
122
+ default scope adds no information and is omitted. Only an explicit, non-root
123
+ path is rendered, so that two otherwise-identical calls scoped to different
124
+ directories are distinguishable in the UI. The rendered path is shortened
125
+ via the supplied `abbreviate` helper.
126
+
127
+ Args:
128
+ path_value: The raw `path` argument, or `None` when not supplied.
129
+ abbreviate: Path-shortening helper from the calling scope.
130
+
131
+ Returns:
132
+ A suffix like ` in langchain`, or an empty string for the default scope.
133
+ """
134
+ if path_value is None:
135
+ return ""
136
+ raw = str(path_value)
137
+ if raw in {"", "/"}:
138
+ return ""
139
+ sanitized = strip_dangerous_unicode(raw)
140
+ display = abbreviate(sanitized)
141
+ if sanitized != raw:
142
+ display += _HIDDEN_CHAR_MARKER
143
+ return f" in {display}"
144
+
145
+
146
+ def format_tool_display(tool_name: str, tool_args: dict) -> str:
147
+ """Format tool calls for display with tool-specific smart formatting.
148
+
149
+ Shows the most relevant information for each tool type rather than all arguments.
150
+
151
+ Args:
152
+ tool_name: Name of the tool being called
153
+ tool_args: Dictionary of tool arguments
154
+
155
+ Returns:
156
+ Formatted string for display (e.g., "(*) read_file(config.py)" in ASCII mode)
157
+
158
+ Examples:
159
+ read_file(file_path="/long/path/file.py") → "<prefix> read_file(file.py)"
160
+ web_search(query="how to code") → '<prefix> web_search("how to code")'
161
+ execute(command="pip install foo") → '<prefix> execute("pip install foo")'
162
+ """
163
+ prefix = get_glyphs().tool_prefix
164
+
165
+ def abbreviate_path(path_str: str, max_length: int = 60) -> str:
166
+ """Abbreviate a file path intelligently - show basename or relative path.
167
+
168
+ Returns:
169
+ Shortened path string suitable for display.
170
+ """
171
+ try:
172
+ path = Path(path_str)
173
+
174
+ # If it's just a filename (no directory parts), return as-is
175
+ if len(path.parts) == 1:
176
+ return path_str
177
+
178
+ # Try to get relative path from current working directory
179
+ with suppress(
180
+ ValueError, # ValueError: path is not relative to cwd
181
+ OSError, # OSError: filesystem errors when resolving paths
182
+ ):
183
+ rel_path = path.relative_to(Path.cwd())
184
+ rel_str = str(rel_path)
185
+ # Use relative if it's shorter and not too long
186
+ if len(rel_str) < len(path_str) and len(rel_str) <= max_length:
187
+ return rel_str
188
+
189
+ # If absolute path is reasonable length, use it
190
+ if len(path_str) <= max_length:
191
+ return path_str
192
+ except Exception: # noqa: BLE001 # Fallback to original string on any path resolution error
193
+ return truncate_value(path_str, max_length)
194
+ else:
195
+ # Otherwise, just show basename (filename only)
196
+ return path.name
197
+
198
+ # Tool-specific formatting - show the most important argument(s)
199
+ if tool_name in {"read_file", "write_file", "edit_file", "delete"}:
200
+ # File operations: show the primary file path argument (file_path or path)
201
+ path_value = tool_args.get("file_path")
202
+ if path_value is None:
203
+ path_value = tool_args.get("path")
204
+ if path_value is not None:
205
+ path_raw = strip_dangerous_unicode(str(path_value))
206
+ path = abbreviate_path(path_raw)
207
+ if path_raw != str(path_value):
208
+ path += _HIDDEN_CHAR_MARKER
209
+ return f"{prefix} {tool_name}({path})"
210
+
211
+ elif tool_name == "web_search":
212
+ # Web search: show the query string
213
+ if "query" in tool_args:
214
+ query = _sanitize_display_value(tool_args["query"], max_length=100)
215
+ return f'{prefix} {tool_name}("{query}")'
216
+
217
+ elif tool_name == "grep":
218
+ # Grep: show the search pattern, and the scoped path when non-default
219
+ if "pattern" in tool_args:
220
+ pattern = _sanitize_display_value(tool_args["pattern"], max_length=70)
221
+ scope = _format_scope_path(tool_args.get("path"), abbreviate_path)
222
+ return f'{prefix} {tool_name}("{pattern}"{scope})'
223
+
224
+ elif tool_name == "execute":
225
+ # Execute: show the command, and timeout only if non-default
226
+ if "command" in tool_args:
227
+ command = _sanitize_display_value(
228
+ tool_args["command"], max_length=EXECUTE_HEADER_MAX_LENGTH
229
+ )
230
+ timeout = _coerce_timeout_seconds(tool_args.get("timeout"))
231
+ from deepagents.backends import DEFAULT_EXECUTE_TIMEOUT
232
+
233
+ if timeout is not None and timeout != DEFAULT_EXECUTE_TIMEOUT:
234
+ timeout_str = _format_timeout(timeout)
235
+ return f'{prefix} {tool_name}("{command}", timeout={timeout_str})'
236
+ return f'{prefix} {tool_name}("{command}")'
237
+
238
+ elif tool_name == "js_eval":
239
+ # JS interpreter: show only the first non-blank line of the snippet so a
240
+ # multi-line program collapses to a single, scannable header line. The
241
+ # full code is available via the collapsible args block.
242
+ code = tool_args.get("code")
243
+ if isinstance(code, str) and code.strip():
244
+ first_line = next(
245
+ (line for line in code.splitlines() if line.strip()), ""
246
+ ).strip()
247
+ multiline = sum(1 for line in code.splitlines() if line.strip()) > 1
248
+ snippet = _sanitize_display_value(
249
+ first_line, max_length=JS_EVAL_HEADER_MAX_LENGTH
250
+ )
251
+ ellipsis = get_glyphs().ellipsis if multiline else ""
252
+ return f'{prefix} {tool_name}("{snippet}{ellipsis}")'
253
+ return f"{prefix} {tool_name}()"
254
+
255
+ elif tool_name == "ls":
256
+ # ls: show directory, or empty if current directory
257
+ if tool_args.get("path"):
258
+ path_raw = strip_dangerous_unicode(str(tool_args["path"]))
259
+ path = abbreviate_path(path_raw)
260
+ if path_raw != str(tool_args["path"]):
261
+ path += _HIDDEN_CHAR_MARKER
262
+ return f"{prefix} {tool_name}({path})"
263
+ return f"{prefix} {tool_name}()"
264
+
265
+ elif tool_name == "glob":
266
+ # Glob: show the pattern, and the scoped path when non-default
267
+ if "pattern" in tool_args:
268
+ pattern = _sanitize_display_value(tool_args["pattern"], max_length=80)
269
+ scope = _format_scope_path(tool_args.get("path"), abbreviate_path)
270
+ return f'{prefix} {tool_name}("{pattern}"{scope})'
271
+
272
+ elif tool_name == "fetch_url":
273
+ # Fetch URL: show the URL being fetched
274
+ if "url" in tool_args:
275
+ url = _sanitize_display_value(tool_args["url"], max_length=80)
276
+ return f'{prefix} {tool_name}("{url}")'
277
+
278
+ elif tool_name == "task":
279
+ # Task: show subagent type badge
280
+ agent_type = tool_args.get("subagent_type", "")
281
+ if agent_type:
282
+ agent_type = _sanitize_display_value(agent_type, max_length=40)
283
+ return f"{prefix} {tool_name} [{agent_type}]"
284
+ return f"{prefix} {tool_name}"
285
+
286
+ elif tool_name == "ask_user":
287
+ if "questions" in tool_args and isinstance(tool_args["questions"], list):
288
+ count = len(tool_args["questions"])
289
+ label = "question" if count == 1 else "questions"
290
+ return f"{prefix} {tool_name}({count} {label})"
291
+
292
+ elif tool_name == "compact_conversation":
293
+ return f"{prefix} {tool_name}()"
294
+
295
+ elif tool_name == "write_todos":
296
+ if "todos" in tool_args and isinstance(tool_args["todos"], list):
297
+ count = len(tool_args["todos"])
298
+ return f"{prefix} {tool_name}({count} items)"
299
+
300
+ # Fallback: generic formatting for unknown tools
301
+ # Show all arguments in key=value format
302
+ args_str = ", ".join(
303
+ f"{_sanitize_display_value(k, max_length=30)}="
304
+ f"{_sanitize_display_value(v, max_length=50)}"
305
+ for k, v in tool_args.items()
306
+ )
307
+ return f"{prefix} {tool_name}({args_str})"
308
+
309
+
310
+ def _format_content_block(block: dict) -> str:
311
+ """Format a single content block dict for display.
312
+
313
+ Replaces large binary payloads (e.g. base64 image/video data) with a
314
+ human-readable placeholder so they don't flood the terminal.
315
+
316
+ Args:
317
+ block: An `ImageContentBlock`, `VideoContentBlock`, or `FileContentBlock`
318
+ dictionary.
319
+
320
+ Returns:
321
+ A display-friendly string for the block.
322
+ """
323
+ if block.get("type") == "image" and isinstance(block.get("base64"), str):
324
+ b64 = block["base64"]
325
+ size_kb = len(b64) * 3 // 4 // 1024 # approximate decoded size
326
+ mime = block.get("mime_type", "image")
327
+ return f"[Image: {mime}, ~{size_kb}KB]"
328
+ if block.get("type") == "video" and isinstance(block.get("base64"), str):
329
+ b64 = block["base64"]
330
+ size_kb = len(b64) * 3 // 4 // 1024 # approximate decoded size
331
+ mime = block.get("mime_type", "video")
332
+ return f"[Video: {mime}, ~{size_kb}KB]"
333
+ if block.get("type") == "file" and isinstance(block.get("base64"), str):
334
+ b64 = block["base64"]
335
+ size_kb = len(b64) * 3 // 4 // 1024 # approximate decoded size
336
+ mime = block.get("mime_type", "file")
337
+ return f"[File: {mime}, ~{size_kb}KB]"
338
+ try:
339
+ # Preserve non-ASCII characters (CJK, emoji, etc.) instead of \uXXXX escapes
340
+ return json.dumps(block, ensure_ascii=False)
341
+ except (TypeError, ValueError):
342
+ return str(block)
343
+
344
+
345
+ def format_tool_message_content(content: Any) -> str: # noqa: ANN401 # Content can be str, list, or dict
346
+ """Convert `ToolMessage` content into a printable string.
347
+
348
+ Returns:
349
+ Formatted string representation of the tool message content.
350
+ """
351
+ if content is None:
352
+ return ""
353
+ if isinstance(content, list):
354
+ parts = []
355
+ for item in content:
356
+ if isinstance(item, str):
357
+ parts.append(item)
358
+ elif isinstance(item, dict):
359
+ parts.append(_format_content_block(item))
360
+ else:
361
+ try:
362
+ # Preserve non-ASCII characters (CJK, emoji, etc.)
363
+ parts.append(json.dumps(item, ensure_ascii=False))
364
+ except (TypeError, ValueError):
365
+ parts.append(str(item))
366
+ return "\n".join(parts)
367
+ return str(content)