fix: keep ACP stdout protocol-clean
Route AIAgent print output to stderr via _print_fn for ACP stdio sessions. Gate quiet-mode spinner startup on _should_start_quiet_spinner() so JSON-RPC on stdout isn't corrupted. Child agents inherit the redirect. Co-authored-by: Git-on-my-level <Git-on-my-level@users.noreply.github.com>
This commit is contained in:
29
run_agent.py
29
run_agent.py
@@ -1491,6 +1491,25 @@ class AIAgent:
|
||||
return
|
||||
self._safe_print(*args, **kwargs)
|
||||
|
||||
def _should_start_quiet_spinner(self) -> bool:
|
||||
"""Return True when quiet-mode spinner output has a safe sink.
|
||||
|
||||
In headless/stdio-protocol environments, a raw spinner with no custom
|
||||
``_print_fn`` falls back to ``sys.stdout`` and can corrupt protocol
|
||||
streams such as ACP JSON-RPC. Allow quiet spinners only when either:
|
||||
- output is explicitly rerouted via ``_print_fn``; or
|
||||
- stdout is a real TTY.
|
||||
"""
|
||||
if self._print_fn is not None:
|
||||
return True
|
||||
stream = getattr(sys, "stdout", None)
|
||||
if stream is None:
|
||||
return False
|
||||
try:
|
||||
return bool(stream.isatty())
|
||||
except (AttributeError, ValueError, OSError):
|
||||
return False
|
||||
|
||||
def _emit_status(self, message: str) -> None:
|
||||
"""Emit a lifecycle status message to both CLI and gateway channels.
|
||||
|
||||
@@ -6066,7 +6085,7 @@ class AIAgent:
|
||||
|
||||
# Start spinner for CLI mode (skip when TUI handles tool progress)
|
||||
spinner = None
|
||||
if self.quiet_mode and not self.tool_progress_callback:
|
||||
if self.quiet_mode and not self.tool_progress_callback and self._should_start_quiet_spinner():
|
||||
face = random.choice(KawaiiSpinner.KAWAII_WAITING)
|
||||
spinner = KawaiiSpinner(f"{face} ⚡ running {num_tools} tools concurrently", spinner_type='dots', print_fn=self._print_fn)
|
||||
spinner.start()
|
||||
@@ -6294,7 +6313,7 @@ class AIAgent:
|
||||
goal_preview = (function_args.get("goal") or "")[:30]
|
||||
spinner_label = f"🔀 {goal_preview}" if goal_preview else "🔀 delegating"
|
||||
spinner = None
|
||||
if self.quiet_mode and not self.tool_progress_callback:
|
||||
if self.quiet_mode and not self.tool_progress_callback and self._should_start_quiet_spinner():
|
||||
face = random.choice(KawaiiSpinner.KAWAII_WAITING)
|
||||
spinner = KawaiiSpinner(f"{face} {spinner_label}", spinner_type='dots', print_fn=self._print_fn)
|
||||
spinner.start()
|
||||
@@ -7120,9 +7139,9 @@ class AIAgent:
|
||||
# CLI TUI mode: use prompt_toolkit widget instead of raw spinner
|
||||
# (works in both streaming and non-streaming modes)
|
||||
self.thinking_callback(f"{face} {verb}...")
|
||||
elif not self._has_stream_consumers():
|
||||
# Raw KawaiiSpinner only when no streaming consumers
|
||||
# (would conflict with streamed token output)
|
||||
elif not self._has_stream_consumers() and self._should_start_quiet_spinner():
|
||||
# Raw KawaiiSpinner only when no streaming consumers and the
|
||||
# spinner output has a safe sink.
|
||||
spinner_type = random.choice(['brain', 'sparkle', 'pulse', 'moon', 'star'])
|
||||
thinking_spinner = KawaiiSpinner(f"{face} {verb}...", spinner_type=spinner_type, print_fn=self._print_fn)
|
||||
thinking_spinner.start()
|
||||
|
||||
Reference in New Issue
Block a user