Skip to content

Commit 2649e1e

Browse files
committed
Hide reasoning content after streaming done.
1 parent 9a68fbf commit 2649e1e

5 files changed

Lines changed: 143 additions & 3 deletions

File tree

python_agent_harness/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def chat(
203203
reasoning_effort=reasoning_effort,
204204
)
205205
content_parts: list[str] = []
206+
reasoning_parts: list[str] = []
206207
tc_index: dict[int, dict[str, Any]] = {}
207208
usage = Usage()
208209

@@ -238,6 +239,7 @@ def chat(
238239
on_delta(delta["content"])
239240
if delta.get("reasoning_content"):
240241
content_parts.append(delta["reasoning_content"])
242+
reasoning_parts.append(delta["reasoning_content"])
241243
if on_delta:
242244
on_delta(delta["reasoning_content"])
243245
for tc in delta.get("tool_calls") or []:
@@ -268,10 +270,12 @@ def chat(
268270
for i in sorted(tc_index)
269271
]
270272
if content or tool_calls:
271-
msg = Message(role="assistant", content=content, tool_calls=tool_calls)
273+
msg = Message(role="assistant", content=content, tool_calls=tool_calls,
274+
reasoning="".join(reasoning_parts) or None)
272275
_log_llm_interaction(self.log_path, payload, msg, usage)
273276
return msg, usage
274-
msg = Message(role="assistant", content="")
277+
msg = Message(role="assistant", content="",
278+
reasoning="".join(reasoning_parts) or None)
275279
_log_llm_interaction(self.log_path, payload, msg, usage)
276280
return msg, usage
277281

python_agent_harness/tui.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ def _strip_final_check(text: str) -> str:
150150
return "\n".join(lines[:first]).rstrip()
151151

152152

153+
def _strip_reasoning(text: str, reasoning: str) -> str:
154+
"""Remove the leading REASONING block from TEXT, or TEXT unchanged.
155+
156+
Reasoning content is streamed before the answer, so it forms the
157+
leading part of the stored message content. The TUI collapses it
158+
to a marker once the stream is done, so it stops eating the
159+
visible-row budget; the stored message is never modified.
160+
"""
161+
if not reasoning:
162+
return text
163+
if text.startswith(reasoning):
164+
return text[len(reasoning):]
165+
stripped = text.lstrip()
166+
if stripped.startswith(reasoning):
167+
return stripped[len(reasoning):]
168+
return text
169+
170+
153171
def _history_path() -> str:
154172
d = config.SESSION_DIR / "python-agent-harness"
155173
d.mkdir(parents=True, exist_ok=True)
@@ -497,7 +515,14 @@ def _build_history_rows(self) -> list[Any]:
497515
if body.strip():
498516
rows.append(Markdown(f"**user:** {body}"))
499517
elif m.role == "assistant":
500-
body = _tail_lines(_strip_final_check(m.text()), 12)
518+
body = m.text()
519+
collapsed_reasoning = False
520+
if m.reasoning:
521+
stripped = _strip_reasoning(body, m.reasoning)
522+
if stripped != body:
523+
body = stripped
524+
collapsed_reasoning = True
525+
body = _tail_lines(_strip_final_check(body), 12)
501526
if m.tool_calls:
502527
for tc in m.tool_calls:
503528
args = tc.arguments
@@ -513,6 +538,11 @@ def _build_history_rows(self) -> list[Any]:
513538
params = ""
514539
label = f"🤖 {tc.name}({params})" if params else f"🤖 {tc.name}"
515540
rows.append(Text(label, style="cyan"))
541+
if collapsed_reasoning:
542+
# the reasoning streamed live while it was being
543+
# produced; once it is done it collapses to a marker
544+
# so it doesn't eat the visible-row budget
545+
rows.append(Text("💭 ...", style="dim"))
516546
if body.strip():
517547
rows.append(Markdown(f"**assistant:** {body}"))
518548
elif m.role == "tool":

tests/fake_openai_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def do_POST(self):
1212
stream = body.get("stream", False)
1313
if stream:
1414
chunks = [
15+
{"choices": [{"delta": {"role": "assistant", "reasoning_content": "thinking"}}]},
16+
{"choices": [{"delta": {"reasoning_content": " hard"}}]},
1517
{"choices": [{"delta": {"role": "assistant", "content": "Hello"}}]},
1618
{"choices": [{"delta": {"content": " world"}}]},
1719
{"choices": [{"delta": {"tool_calls": [

tests/test_client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Client streaming tests against the in-process fake OpenAI server."""
2+
3+
import unittest
4+
5+
from python_agent_harness.client import Client
6+
7+
from fake_openai_server import serve
8+
9+
10+
def make_client() -> Client:
11+
srv = serve()
12+
host, port = srv.server_address
13+
c = Client(base_url=f"http://{host}:{port}/v1", api_key="test", model="fake")
14+
c._server = srv # keep the server alive for the test
15+
return c
16+
17+
18+
class TestClientStreaming(unittest.TestCase):
19+
def test_reasoning_content_streamed_and_captured(self):
20+
"""reasoning_content deltas stream normally (on_delta) AND are
21+
captured on the message so the TUI can collapse them later."""
22+
c = make_client()
23+
deltas: list[str] = []
24+
msg, usage = c.chat(
25+
[__import__("python_agent_harness.models", fromlist=["Message"]).Message(
26+
role="user", content="hi"
27+
)],
28+
on_delta=deltas.append,
29+
)
30+
self.assertEqual("".join(deltas), "thinking hardHello world")
31+
self.assertEqual(msg.content, "thinking hardHello world")
32+
self.assertEqual(msg.reasoning, "thinking hard")
33+
self.assertIn("Hello world", msg.content)
34+
self.assertEqual(usage.input_tokens, 12)
35+
c.close()
36+
37+
38+
if __name__ == "__main__":
39+
unittest.main()

tests/test_tui.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,71 @@ def test_stream_final_check_hidden(self):
190190
self.assertIn("Working...", row.plain)
191191
self.assertNotIn("Status:", row.plain)
192192

193+
def test_reasoning_streams_normally(self):
194+
"""While streaming, reasoning content shows up live like any
195+
other text — the collapse only happens in the final history."""
196+
tui, _ = make_tui()
197+
tui.stream_text = "thinking hard...\n\nanswer here"
198+
row = tui._stream_row()
199+
self.assertIsNotNone(row)
200+
self.assertIn("thinking hard...", row.plain)
201+
self.assertIn("answer here", row.plain)
202+
203+
def test_reasoning_collapsed_in_history(self):
204+
"""Once the stream is done, the stored reasoning collapses to a
205+
'...' marker in the history so it stops eating TUI space; the
206+
answer stays fully visible. The stored message is untouched."""
207+
tui, buf = make_tui()
208+
reasoning = "Let me think: Rayleigh scattering dominates..."
209+
tui.session.last_messages = [
210+
Message(role="user", content="why is the sky blue?"),
211+
Message(
212+
role="assistant",
213+
content=reasoning + "\n\nThe sky is blue due to Rayleigh scattering.",
214+
reasoning=reasoning,
215+
),
216+
]
217+
tui.console.print(tui._render_conversation())
218+
out = buf.getvalue()
219+
self.assertIn("💭 ...", out)
220+
self.assertIn("Rayleigh scattering.", out)
221+
self.assertNotIn("Let me think", out)
222+
# the stored message keeps its reasoning — only the display hides it
223+
self.assertEqual(
224+
tui.session.last_messages[1].reasoning, reasoning
225+
)
226+
self.assertIn("Let me think", tui.session.last_messages[1].text())
227+
228+
def test_reasoning_collapsed_marker_shows_even_without_answer(self):
229+
"""A reasoning-only assistant message (no answer content) still
230+
shows the collapse marker instead of vanishing entirely."""
231+
tui, buf = make_tui()
232+
tui.session.last_messages = [
233+
Message(role="user", content="go"),
234+
Message(
235+
role="assistant", content="pensive thoughts here",
236+
reasoning="pensive thoughts here",
237+
),
238+
]
239+
tui.console.print(tui._render_conversation())
240+
out = buf.getvalue()
241+
self.assertIn("💭 ...", out)
242+
self.assertNotIn("pensive thoughts", out)
243+
244+
def test_strip_reasoning(self):
245+
"""_strip_reasoning removes the leading reasoning prefix and
246+
leaves non-matching text untouched."""
247+
from python_agent_harness.tui import _strip_reasoning
248+
249+
self.assertEqual(
250+
_strip_reasoning("ABCanswer", "ABC"), "answer"
251+
)
252+
self.assertEqual(
253+
_strip_reasoning(" ABCanswer", "ABC"), "answer"
254+
)
255+
self.assertEqual(_strip_reasoning("answer", "ABC"), "answer")
256+
self.assertEqual(_strip_reasoning("x", ""), "x")
257+
193258
def test_long_conversation_bounded(self):
194259
"""A long conversation is capped so rendering stays fast."""
195260
tui, buf = make_tui()

0 commit comments

Comments
 (0)