fix(cli): buffer reasoning tokens to avoid one-token-per-line display

This commit is contained in:
liyazhou 2026-05-17 17:00:02 +08:00 committed by Xubin Ren
parent eb0ff3ad1d
commit b67205f5aa

View File

@ -22,6 +22,11 @@ if sys.platform == "win32":
import typer import typer
from loguru import logger from loguru import logger
# Buffered reasoning display: accumulate streaming tokens and flush
# on sentence/line boundaries so the user sees grouped text instead of
# one token per line. The empty string placeholder is the sentinel.
_reasoning_buf: str = ""
# Remove default handler and re-add with unified nanobot format # Remove default handler and re-add with unified nanobot format
logger.remove() logger.remove()
_log_handler_id = logger.add( _log_handler_id = logger.add(
@ -242,10 +247,14 @@ def _print_cli_progress_line(text: str, thinking: ThinkingSpinner | None, render
target.print(f" [dim]↳ {text}[/dim]") target.print(f" [dim]↳ {text}[/dim]")
def _print_cli_reasoning(text: str, thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None: def _flush_reasoning(thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None:
"""Print reasoning/thinking content in a distinct style.""" """Flush accumulated reasoning buffer to the display."""
if not text.strip(): global _reasoning_buf
if not _reasoning_buf or not _reasoning_buf.strip():
_reasoning_buf = ""
return return
text = _reasoning_buf.strip()
_reasoning_buf = ""
target = renderer.console if renderer else console target = renderer.console if renderer else console
pause = renderer.pause_spinner() if renderer else (thinking.pause() if thinking else nullcontext()) pause = renderer.pause_spinner() if renderer else (thinking.pause() if thinking else nullcontext())
with pause: with pause:
@ -254,6 +263,28 @@ def _print_cli_reasoning(text: str, thinking: ThinkingSpinner | None, renderer:
target.print(f"[dim italic]✻ {text}[/dim italic]") target.print(f"[dim italic]✻ {text}[/dim italic]")
def _print_cli_reasoning(text: str, thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None:
"""Accumulate reasoning tokens and flush on sentence / line boundaries.
Without buffering, each streaming delta (often a single token) would be
printed as a separate ```` line. This version groups tokens into
natural chunks visible in the terminal.
"""
global _reasoning_buf
if not text:
return
_reasoning_buf += text
# Flush on newline, sentence-ending punctuation, or when the chunk is
# long enough to wrap meaningfully at typical terminal widths.
if (
text.endswith("\n")
or any(text.rstrip().endswith(p) for p in (".", "!", "?", "", "", ""))
or len(_reasoning_buf) >= 60
):
_flush_reasoning(thinking, renderer)
async def _print_interactive_progress_line(text: str, thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None: async def _print_interactive_progress_line(text: str, thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None:
"""Print an interactive progress line, pausing the spinner if needed.""" """Print an interactive progress line, pausing the spinner if needed."""
if not text.strip(): if not text.strip():
@ -281,6 +312,11 @@ async def _maybe_print_interactive_progress(
if not metadata.get("_progress"): if not metadata.get("_progress"):
return False return False
# Flush reasoning buffer when the reasoning stream ends (bus path).
if metadata.get("_reasoning_end"):
_flush_reasoning(thinking, renderer)
return True
is_tool_hint = metadata.get("_tool_hint", False) is_tool_hint = metadata.get("_tool_hint", False)
is_reasoning = metadata.get("_reasoning", False) or metadata.get("_reasoning_delta", False) is_reasoning = metadata.get("_reasoning", False) or metadata.get("_reasoning_delta", False)
if is_reasoning: if is_reasoning:
@ -1109,6 +1145,12 @@ def agent(
def _make_progress(renderer: StreamRenderer | None = None): def _make_progress(renderer: StreamRenderer | None = None):
async def _cli_progress(content: str, *, tool_hint: bool = False, reasoning: bool = False, **_kwargs: Any) -> None: async def _cli_progress(content: str, *, tool_hint: bool = False, reasoning: bool = False, **_kwargs: Any) -> None:
ch = agent_loop.channels_config ch = agent_loop.channels_config
# Flush remaining reasoning buffer when the stream ends.
if _kwargs.get("reasoning_end"):
_flush_reasoning(_thinking, renderer)
return
if reasoning: if reasoning:
if ch and not ch.show_reasoning: if ch and not ch.show_reasoning:
return return