fix: prevent TypeError in _summarize_tool_result when tool args contain non-string values

When LLMs return non-string parameter values (e.g. bool, int) in tool call
arguments, _summarize_tool_result() crashes with TypeError because it calls
len(), .count(), or slicing directly on args.get() return values.

This causes an infinite crash loop in the TUI — context compression
triggers on session resume, which crashes, which restarts, which triggers
compression again.

Add _str_arg() helper that coerces any value to str, and use it in all 5
vulnerable call sites:
- terminal: len(cmd)
- write_file: content.count()
- delegate_task: len(goal)
- execute_code: len(code)
- vision_analyze: question[:50]
This commit is contained in:
liuwei666888 2026-06-25 16:51:38 +08:00 committed by Teknium
parent 3f0b0e20e8
commit dd923619c7

View file

@ -586,6 +586,21 @@ def _strip_historical_media(messages: List[Dict[str, Any]]) -> List[Dict[str, An
return result if changed else messages
def _str_arg(args: dict, key: str, default: str = "") -> str:
"""Safely get a string argument from parsed tool args.
LLMs sometimes return non-string parameter values (e.g. bool, int) for
tool calls. Calling ``len()`` / ``.count()`` / slicing on those causes
``TypeError`` / ``AttributeError`` which crashes context compression.
This helper coerces any value to ``str`` so downstream code can assume
a string is always returned.
"""
val = args.get(key, default)
if isinstance(val, str):
return val
return str(val) if val is not None else default
def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> str:
"""Create an informative 1-line summary of a tool call + result.
@ -609,7 +624,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
line_count = content.count("\n") + 1 if content.strip() else 0
if tool_name == "terminal":
cmd = args.get("command", "")
cmd = _str_arg(args, "command")
if len(cmd) > 80:
cmd = cmd[:77] + "..."
exit_match = re.search(r'"exit_code"\s*:\s*(-?\d+)', content)
@ -623,7 +638,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
if tool_name == "write_file":
path = args.get("path", "?")
written_lines = args.get("content", "").count("\n") + 1 if args.get("content") else "?"
written_lines = _str_arg(args, "content").count("\n") + 1 if args.get("content") else "?"
return f"[write_file] wrote to {path} ({written_lines} lines)"
if tool_name == "search_files":
@ -658,14 +673,15 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
return f"[web_extract] {url_desc} ({content_len:,} chars)"
if tool_name == "delegate_task":
goal = args.get("goal", "")
goal = _str_arg(args, "goal")
if len(goal) > 60:
goal = goal[:57] + "..."
return f"[delegate_task] '{goal}' ({content_len:,} chars result)"
if tool_name == "execute_code":
code_preview = (args.get("code") or "")[:60].replace("\n", " ")
if len(args.get("code", "")) > 60:
code_str = _str_arg(args, "code")
code_preview = code_str[:60].replace("\n", " ")
if len(code_str) > 60:
code_preview += "..."
return f"[execute_code] `{code_preview}` ({line_count} lines output)"
@ -674,7 +690,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
return f"[{tool_name}] name={name} ({content_len:,} chars)"
if tool_name == "vision_analyze":
question = args.get("question", "")[:50]
question = _str_arg(args, "question")[:50]
return f"[vision_analyze] '{question}' ({content_len:,} chars)"
if tool_name == "memory":