fix: eliminate execute_code progress spam on gateway platforms
Root cause: two issues combined to create visual spam on Telegram/Discord:
1. build_tool_preview() preserved newlines from tool arguments. A preview
like 'import os\nprint("...")' rendered as 2+ visual lines per
progress entry on messaging platforms. This affected execute_code most
(code always has newlines), but could also hit terminal, memory,
send_message, session_search, and process tools.
2. No deduplication of identical progress messages. When models iterate
with execute_code using the same boilerplate code (common pattern),
each call produced an identical progress line. 9 calls x 2 visual
lines = 18 lines of identical spam in one message bubble.
Fixes:
- Added _oneline() helper to collapse all whitespace (newlines, tabs) to
single spaces. Applied to ALL code paths in build_tool_preview() —
both the generic path and every early-return path that touches user
content (memory, session_search, send_message, process).
- Added dedup in gateway progress_callback: consecutive identical messages
are collapsed with a repeat counter, e.g. 'execute_code: ... (x9)'
instead of 9 identical lines. The send_progress_messages async loop
handles dedup tuples by updating the last progress_line in-place.
This commit is contained in:
parent
400b8d92b7
commit
8121aef83c
2 changed files with 44 additions and 11 deletions
|
|
@ -63,6 +63,11 @@ def get_skin_tool_prefix() -> str:
|
||||||
# Tool preview (one-line summary of a tool call's primary argument)
|
# Tool preview (one-line summary of a tool call's primary argument)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
|
def _oneline(text: str) -> str:
|
||||||
|
"""Collapse whitespace (including newlines) to single spaces."""
|
||||||
|
return " ".join(text.split())
|
||||||
|
|
||||||
|
|
||||||
def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str:
|
def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str:
|
||||||
"""Build a short preview of a tool call's primary argument for display."""
|
"""Build a short preview of a tool call's primary argument for display."""
|
||||||
if not args:
|
if not args:
|
||||||
|
|
@ -89,7 +94,7 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str:
|
||||||
if sid:
|
if sid:
|
||||||
parts.append(sid[:16])
|
parts.append(sid[:16])
|
||||||
if data:
|
if data:
|
||||||
parts.append(f'"{data[:20]}"')
|
parts.append(f'"{_oneline(data[:20])}"')
|
||||||
if timeout_val and action == "wait":
|
if timeout_val and action == "wait":
|
||||||
parts.append(f"{timeout_val}s")
|
parts.append(f"{timeout_val}s")
|
||||||
return " ".join(parts) if parts else None
|
return " ".join(parts) if parts else None
|
||||||
|
|
@ -105,24 +110,24 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str:
|
||||||
return f"planning {len(todos_arg)} task(s)"
|
return f"planning {len(todos_arg)} task(s)"
|
||||||
|
|
||||||
if tool_name == "session_search":
|
if tool_name == "session_search":
|
||||||
query = args.get("query", "")
|
query = _oneline(args.get("query", ""))
|
||||||
return f"recall: \"{query[:25]}{'...' if len(query) > 25 else ''}\""
|
return f"recall: \"{query[:25]}{'...' if len(query) > 25 else ''}\""
|
||||||
|
|
||||||
if tool_name == "memory":
|
if tool_name == "memory":
|
||||||
action = args.get("action", "")
|
action = args.get("action", "")
|
||||||
target = args.get("target", "")
|
target = args.get("target", "")
|
||||||
if action == "add":
|
if action == "add":
|
||||||
content = args.get("content", "")
|
content = _oneline(args.get("content", ""))
|
||||||
return f"+{target}: \"{content[:25]}{'...' if len(content) > 25 else ''}\""
|
return f"+{target}: \"{content[:25]}{'...' if len(content) > 25 else ''}\""
|
||||||
elif action == "replace":
|
elif action == "replace":
|
||||||
return f"~{target}: \"{args.get('old_text', '')[:20]}\""
|
return f"~{target}: \"{_oneline(args.get('old_text', '')[:20])}\""
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
return f"-{target}: \"{args.get('old_text', '')[:20]}\""
|
return f"-{target}: \"{_oneline(args.get('old_text', '')[:20])}\""
|
||||||
return action
|
return action
|
||||||
|
|
||||||
if tool_name == "send_message":
|
if tool_name == "send_message":
|
||||||
target = args.get("target", "?")
|
target = args.get("target", "?")
|
||||||
msg = args.get("message", "")
|
msg = _oneline(args.get("message", ""))
|
||||||
if len(msg) > 20:
|
if len(msg) > 20:
|
||||||
msg = msg[:17] + "..."
|
msg = msg[:17] + "..."
|
||||||
return f"to {target}: \"{msg}\""
|
return f"to {target}: \"{msg}\""
|
||||||
|
|
@ -156,7 +161,7 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str:
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
value = value[0] if value else ""
|
value = value[0] if value else ""
|
||||||
|
|
||||||
preview = str(value).strip()
|
preview = _oneline(str(value))
|
||||||
if not preview:
|
if not preview:
|
||||||
return None
|
return None
|
||||||
if len(preview) > max_len:
|
if len(preview) > max_len:
|
||||||
|
|
|
||||||
|
|
@ -2916,6 +2916,8 @@ class GatewayRunner:
|
||||||
# Queue for progress messages (thread-safe)
|
# Queue for progress messages (thread-safe)
|
||||||
progress_queue = queue.Queue() if tool_progress_enabled else None
|
progress_queue = queue.Queue() if tool_progress_enabled else None
|
||||||
last_tool = [None] # Mutable container for tracking in closure
|
last_tool = [None] # Mutable container for tracking in closure
|
||||||
|
last_progress_msg = [None] # Track last message for dedup
|
||||||
|
repeat_count = [0] # How many times the same message repeated
|
||||||
|
|
||||||
def progress_callback(tool_name: str, preview: str = None, args: dict = None):
|
def progress_callback(tool_name: str, preview: str = None, args: dict = None):
|
||||||
"""Callback invoked by agent when a tool is called."""
|
"""Callback invoked by agent when a tool is called."""
|
||||||
|
|
@ -2988,6 +2990,18 @@ class GatewayRunner:
|
||||||
else:
|
else:
|
||||||
msg = f"{emoji} {tool_name}..."
|
msg = f"{emoji} {tool_name}..."
|
||||||
|
|
||||||
|
# Dedup: collapse consecutive identical progress messages.
|
||||||
|
# Common with execute_code where models iterate with the same
|
||||||
|
# code (same boilerplate imports → identical previews).
|
||||||
|
if msg == last_progress_msg[0]:
|
||||||
|
repeat_count[0] += 1
|
||||||
|
# Update the last line in progress_lines with a counter
|
||||||
|
# via a special "dedup" queue message.
|
||||||
|
progress_queue.put(("__dedup__", msg, repeat_count[0]))
|
||||||
|
return
|
||||||
|
last_progress_msg[0] = msg
|
||||||
|
repeat_count[0] = 0
|
||||||
|
|
||||||
progress_queue.put(msg)
|
progress_queue.put(msg)
|
||||||
|
|
||||||
# Background task to send progress messages
|
# Background task to send progress messages
|
||||||
|
|
@ -3008,8 +3022,17 @@ class GatewayRunner:
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
msg = progress_queue.get_nowait()
|
raw = progress_queue.get_nowait()
|
||||||
progress_lines.append(msg)
|
|
||||||
|
# Handle dedup messages: update last line with repeat counter
|
||||||
|
if isinstance(raw, tuple) and len(raw) == 3 and raw[0] == "__dedup__":
|
||||||
|
_, base_msg, count = raw
|
||||||
|
if progress_lines:
|
||||||
|
progress_lines[-1] = f"{base_msg} (×{count + 1})"
|
||||||
|
msg = progress_lines[-1] if progress_lines else base_msg
|
||||||
|
else:
|
||||||
|
msg = raw
|
||||||
|
progress_lines.append(msg)
|
||||||
|
|
||||||
if can_edit and progress_msg_id is not None:
|
if can_edit and progress_msg_id is not None:
|
||||||
# Try to edit the existing progress message
|
# Try to edit the existing progress message
|
||||||
|
|
@ -3045,8 +3068,13 @@ class GatewayRunner:
|
||||||
# Drain remaining queued messages
|
# Drain remaining queued messages
|
||||||
while not progress_queue.empty():
|
while not progress_queue.empty():
|
||||||
try:
|
try:
|
||||||
msg = progress_queue.get_nowait()
|
raw = progress_queue.get_nowait()
|
||||||
progress_lines.append(msg)
|
if isinstance(raw, tuple) and len(raw) == 3 and raw[0] == "__dedup__":
|
||||||
|
_, base_msg, count = raw
|
||||||
|
if progress_lines:
|
||||||
|
progress_lines[-1] = f"{base_msg} (×{count + 1})"
|
||||||
|
else:
|
||||||
|
progress_lines.append(raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
break
|
break
|
||||||
# Final edit with all remaining tools (only if editing works)
|
# Final edit with all remaining tools (only if editing works)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue