fix(cli): re-emit assistant text after tool output prevents scroll-away - #68576
fix(cli): re-emit assistant text after tool output prevents scroll-away#68576WangYeYi wants to merge 1 commit into
Conversation
68fef27 to
21953da
Compare
Main added call between and in #69559. Merged both: activity touch runs first, then re-emit block. Also discovered during local testing: the save in verify-on-stop paths was unconditional — each verification retry overwrites the pending response. Fixed in local #68586 integration by wrapping with so only the first (real) answer is preserved for restore merging. |
Root cause found & fixed locallyReproductionNon-verbose CLI mode (default). Agent responds with text + tool_calls in the same turn. The streamed text scrolls out of the viewport behind tool output. Short answers become invisible. Root cause (2 issues)1.
2. OpenAI-compatible providers (DeepSeek etc.) may return Fix (2 changes in
|
Follow-up: fix is incomplete — two root causes foundAfter local testing with DeepSeek v4 Pro, the original fix has two issues: 1.
|
| Issue | Status | Location |
|---|---|---|
| quiet_mode gate | Fixed | conversation_loop.py |
| streamed text capture | Needs transport fix | run_agent.py / transports |
Final root cause and fix (verified working)3 root causes found through runtime tracing:1. 2. 3. Fix (3 changes, conversation_loop.py only):
VerifiedWorking in default CLI mode with DeepSeek v4 Pro. |
Root cause analysis (verified through runtime tracing)ProblemCLI default mode (non-verbose): agent outputs text + tool_calls in the same turn. Streamed text scrolls out of the viewport behind tool output. Short answers become invisible — user only sees tool progress. Root cause chain (3 independent issues)1.
2. OpenAI-compatible providers (DeepSeek etc.) may return Text IS streamed and rendered to terminal via delta chunks — the user saw it scroll by — but 3.
This is the core timing issue: text accumulation and re-emit reading happen in DIFFERENT lifecycle phases, and the reset fires between them. Fix (3 changes)
# _reset_stream_delivery_tracking(), after flushing scrubbers, before clearing:
self._saved_streamed_text = getattr(self, "_current_streamed_assistant_text", "") or ""
self._current_streamed_assistant_text = ""
self._current_streamed_reasoning_text = ""
# After assistant_message = normalized, before streaming clears it:
agent._saved_streamed_text = getattr(agent, "_current_streamed_assistant_text", "") or ""
# Both re-emit sites, fallback chain:
_streamed_text = getattr(agent, "_saved_streamed_text", "") or \
getattr(agent, "_current_streamed_assistant_text", "") or ""
_effective_content = turn_content or _streamed_textThe fallback chain
Verified
Timeline of discovery
|
|
Thanks for tracing the CLI display path. The current branch needs substantial re-scoping before the proposed fix can be evaluated. Problems
Suggested changes
Automated hermes-sweeper review. |
When providers return content=null with tool_calls (DeepSeek, OpenAI-compatible), turn_content becomes empty and the existing re-emit at line 5977 is skipped because evaluates to False. The text WAS streamed and accumulated in _current_streamed_assistant_text, but the re-emit block never reads it. Fix: introduce _effective = turn_content or streamed_text, and use it throughout the re-emit block (condition, _last_content_with_tools storage, _strip_think_blocks input). Tested: 3 scenarios verified — normal providers (re-emit unchanged), DeepSeek content=null (previously broken, now fixed), backward-compatible (pure no-op when streamed_text is also empty).
e13c833 to
43ab97c
Compare
Updated: addressing hermes-sweeper reviewThanks @teknium1. Re-scoped the PR to the specific gap your review identified. What changedThe original PR tried to add a second re-emit site — redundant since main already covers the default CLI case via Actual fix: content=null fallbackWhen providers return Fix (3 lines, # Before:
if turn_content and agent._has_content_after_think_block(turn_content):
agent._last_content_with_tools = turn_content
...
clean = agent._strip_think_blocks(turn_content).strip()
# After:
_effective = turn_content or getattr(agent, "_current_streamed_assistant_text", "")
if _effective and agent._has_content_after_think_block(_effective):
agent._last_content_with_tools = _effective
...
clean = agent._strip_think_blocks(_effective).strip()Verified3 scenarios tested against live code:
Branch statusForce-pushed clean: 1 commit, 1 file, +4/−3. No merge noise. |
…lit, re-emit, verify-on-stop, skill normalize, HERMES_PLATFORM Restored from fork/backup-patches-20260730: - output-guard: L1+L2+L3 coverage check + JSONL logging - semantic split: _split_user_items comma-question detection - holographic dimension guards - fact-check MiniLM dispatch Fixed nudge leak: output-guard nudge now injected as hidden conversation history message instead of appended to user-visible final_response. New fixes carried forward: - NousResearch#68576: re-emit content=null fallback - NousResearch#68586: verify-on-stop answer restoration - NousResearch#48333: skill normalize multiline block scalar - NousResearch#50521: HERMES_PLATFORM explicit platform param
What does this PR do?
When the assistant outputs text followed by tool calls (e.g. answering
a multi-question turn with "Q1 answer... Q2 needs tools"), the streamed
assistant text is scrolled out of the CLI viewport by the subsequent tool
output. Short answers placed before tool calls become invisible.
Fix: after all tool calls complete and before
continueto the nextiteration, re-emit the assistant text content via
_safe_print.Root cause (3 independent issues, found through runtime tracing)
quiet_modegate:quiet_mode = not self.verbosemeans default CLIis
quiet_mode=True. The conditionnot agent.quiet_modesilentlyskips re-emit in the default CLI mode.
content: nullwithtool_calls: DeepSeek/OpenAI-compatibleproviders return
assistant_message.content = nullwhentool_callsare present -- valid API behavior.
turn_contentbecomes empty._current_streamed_assistant_textcleared too early:_reset_stream_delivery_tracking()atrun_agent.py:5190clearsthe accumulated text BEFORE
conversation_loop.pygets to read itfor re-emit. Text IS streamed and visible to the user, but the buffer
is already empty when re-emit checks.
Fix (3 changes)
run_agent.py-- save streamed text before clearing:agent/conversation_loop.py-- capture at response time + use fallback:agent/conversation_loop.py-- removenot agent.quiet_modefrom re-emit condition.Related Issues
Type of Change
Changes Made
agent/conversation_loop.py-- save_saved_streamed_text, use as re-emit fallbackrun_agent.py-- save_saved_streamed_textin_reset_stream_delivery_tracking()How to Test
agent.quiet_mode) suppresses the re-emit.Verified
verify-patches.sh: all local patches passTimeline of discovery
quiet_mode=Trueblockingquiet=Truequiet_mode = not verbosecli_agent_setup_mixin.py:375content: nullfrom API_reset_stream_delivery_trackingclears itrun_agent.py:5190, called atchat_completion_helpers.py:3473