fix(run_agent): gate reasoning.available on reasoning_content, not content - #24566
Conversation
…ntent
The reasoning.available event was sourced from assistant_message.content
(the visible reply text) instead of assistant_message.reasoning_content
(the actual model reasoning block). External UIs that render both a
thinking pane and a message bubble received identical text in both, making
the reasoning panel useless and the UX confusing.
Root cause: the callback block at run_agent.py:14582 read:
_think_text = assistant_message.content.strip()
...
elif _think_text:
tool_progress_callback("reasoning.available", "_thinking", _think_text[:500], None)
Fix: decouple the two emitters:
1. Subagent delegation (_thinking) — unchanged: still relays first line
of assistant_message.content to the parent for progress display.
2. reasoning.available — now gates on reasoning_content:
_reasoning_text = (getattr(assistant_message, "reasoning_content", None) or "").strip()
if _reasoning_text:
tool_progress_callback("reasoning.available", "_thinking", _reasoning_text[:500], None)
For models without a separate reasoning_content field the event is
simply not emitted, which is correct — the reply text is not reasoning.
Closes NousResearch#24518
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes incorrect reasoning.available event payloads so downstream UIs don’t show the final assistant reply duplicated in both the “thinking” pane and the normal message bubble. The change aligns the event to exclusively use the model-provided structured reasoning_content field (when present), while keeping the subagent _thinking relay behavior separate.
Changes:
- Update
run_agent.pyto emitreasoning.availableonly whenassistant_message.reasoning_contentis non-empty, instead of sourcing fromassistant_message.content. - Preserve subagent delegation “thinking relay” behavior by continuing to use visible
contentfor_thinkingwhendelegate_depth > 0. - Add a regression test suite covering emission/non-emission, payload correctness, and truncation behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
run_agent.py |
Decouples subagent _thinking relay from reasoning.available, and gates reasoning.available on reasoning_content only. |
tests/run_agent/test_reasoning_available_event_24518.py |
Adds regression tests for correct reasoning.available sourcing, truncation, and non-emission when reasoning is absent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from types import SimpleNamespace |
| # Production code block (verbatim copy for isolation): | ||
| if agent.tool_progress_callback: | ||
| if msg.content and getattr(agent, '_delegate_depth', 0) > 0: | ||
| _think_text = msg.content.strip() | ||
| _think_text = re.sub( | ||
| r'</?(?:REASONING_SCRATCHPAD|think|reasoning)>', '', _think_text | ||
| ).strip() | ||
| first_line = _think_text.split('\n')[0][:80] if _think_text else "" | ||
| if first_line: | ||
| try: | ||
| agent.tool_progress_callback("_thinking", first_line) | ||
| except Exception: | ||
| pass | ||
| _reasoning_text = (getattr(msg, "reasoning_content", None) or "").strip() | ||
| if _reasoning_text: | ||
| try: | ||
| agent.tool_progress_callback("reasoning.available", "_thinking", _reasoning_text[:500], None) | ||
| except Exception: | ||
| pass |
|
Closing — PR has merge conflicts that can't be auto-resolved. The codebase has evolved past this fix. Re-opening with a fresh rebase welcome if the issue is still open. |
What does this PR do?
reasoning.availablewas emitting the final assistant reply text instead of the model's actual reasoning/chain-of-thought block. External UIs that display a separate "thinking" pane and a message bubble received identical text in both.Root cause
reasoning.availablewas emitting the final assistant reply text instead of the model's actual reasoning/chain-of-thought block. External UIs that display a separate "thinking" pane and a message bubble received identical text in both.Reported in #24518.
Fix
Decoupled the two code paths completely:
_thinkingrelay — still readscontent(correct: relays what subagents say to parent), gated on_delegate_depth > 0(unchanged behaviour).reasoning.available— now readsgetattr(msg, "reasoning_content", None)exclusively and fires only when that field is non-empty.NormalizedResponse.reasoning_contentis already a property backed byprovider_data["reasoning_content"]— no changes needed inagent/transports/types.py.Why this shape
This shape mirrors #29640 so reviewers can quickly compare scope, root cause, fix, tests, and related context without having to decode a custom PR description.
Tests
Original body
Related PRs / issues
Closes #24518
Original body
Summary
reasoning.availablewas emitting the final assistant reply text instead of the model's actual reasoning/chain-of-thought block. External UIs that display a separate "thinking" pane and a message bubble received identical text in both.What Changed
Fluxo
A mudança continua seguindo o fluxo original descrito na seção preservada abaixo, sem ampliar o escopo funcional deste PR.
Visão
A padronização melhora a revisão, reduz ruído e evita deriva de formatação entre PRs abertos.
Test Plan
Original body
What does this PR do?
Problem
reasoning.availablewas emitting the final assistant reply text instead of the model's actual reasoning/chain-of-thought block. External UIs that display a separate "thinking" pane and a message bubble received identical text in both.Reported in #24518.
Root cause
The callback block in
run_agent.pyshared_think_text(read fromassistant_message.content) across two branches via an implicit coupling:reasoning_content(the actual model reasoning block stored inprovider_data) was never consulted. When a model returnedcontent = "final reply"with no separatereasoning_content, the event fired carrying"final reply"— duplicating the message bubble text in the reasoning pane.Fix
Decoupled the two code paths completely:
_thinkingrelay — still readscontent(correct: relays what subagents say to parent), gated on_delegate_depth > 0(unchanged behaviour).reasoning.available— now readsgetattr(msg, "reasoning_content", None)exclusively and fires only when that field is non-empty.NormalizedResponse.reasoning_contentis already a property backed byprovider_data["reasoning_content"]— no changes needed inagent/transports/types.py.Tests
New regression suite
tests/run_agent/test_reasoning_available_event_24518.py(5 tests):test_event_not_emitted_when_reasoning_content_absentreasoning_contentisNonetest_event_emitted_with_reasoning_content_when_presentreasoning_content, notcontenttest_event_payload_truncated_at_500_charsreasoning_contentis truncated to 500 charstest_subagent_thinking_relay_unaffected_thinkingrelay for subagents (delegate_depth > 0) still usescontenttest_normalized_response_reasoning_content_propertyNormalizedResponse.reasoning_contentproperty contractAll 5 pass. Full reasoning test suite (54 tests) green.
Visual
flowchart TD CB[tool_progress_callback exists?] CB -->|yes| DA{delegate_depth > 0 AND content non-empty?} DA -->|yes| TH[emit _thinking first line of content] DA -->|no| RC{reasoning_content non-empty?} TH --> RC RC -->|yes| RA[emit reasoning.available with reasoning_content:500] RC -->|no| END[no event] CB -->|no| ENDCloses #24518
Solution Sketch
Related Issue
Closes #24518
Type of Change
Changes Made
.github/PULL_REQUEST_TEMPLATE.mdHow to Test
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Generated by Hermes Turbo
Generated by Hermes Turbo