diff --git a/src/plyngent/cli/transcript.py b/src/plyngent/cli/transcript.py index be6acdc..b60dd00 100644 --- a/src/plyngent/cli/transcript.py +++ b/src/plyngent/cli/transcript.py @@ -243,23 +243,36 @@ def echo_row(row: Row, *, detail: Detail) -> None: click.echo() +def _echo_reasoning(message: AssistantChatMessage) -> None: + """Print the model's reasoning trace for *message*, when it has one.""" + shown = message.reasoning_content + if not isinstance(shown, str) or not shown.strip(): + return + click.secho("reasoning:", fg="bright_black") + click.echo(shown) + click.echo() + + def _echo_assistant(row: Row, *, reasoning: bool) -> None: + """Print an assistant row: the thinking first, then the answer it produced. + + A transcript reads in the order the model produced it, and the live stream + prints the reasoning before the answer; printing it after the body made the + answer look like it had come before the thinking behind it. + """ from plyngent.cli.display import markdown_render_available, print_markdown message = row.message assert isinstance(message, AssistantChatMessage) click.echo(row.prefix()) + if reasoning: + _echo_reasoning(message) content = message.content if isinstance(content, str) and content.strip(): if markdown_render_available(): print_markdown(content, label="") else: click.echo(content) - if reasoning: - shown = message.reasoning_content - if isinstance(shown, str) and shown.strip(): - click.secho("reasoning:", fg="bright_black") - click.echo(shown) names = _tool_call_names(message) if names: click.secho(f" tool_calls=[{', '.join(names)}]", fg="yellow") diff --git a/tests/test_cli/test_history.py b/tests/test_cli/test_history.py index 68aeb79..081f50e 100644 --- a/tests/test_cli/test_history.py +++ b/tests/test_cli/test_history.py @@ -140,6 +140,44 @@ async def test_double_verbose_prints_full_bodies( assert "You are plyngent." in out +async def test_reasoning_prints_before_the_answer( + state: ReplState, + capsys: pytest.CaptureFixture[str], +) -> None: + """A reasoning trace leads the answer it produced, as it does live.""" + state.agent.messages = [ + UserChatMessage(content="why is the sky blue?"), + AssistantChatMessage(content="Rayleigh scattering.", reasoning_content="Light scatters off air."), + ] + assert await handle_slash(state, "/history") is True + out = capsys.readouterr().out + assert "2. assistant:" in out + assert "reasoning:\nLight scatters off air." in out + assert out.index("reasoning:") < out.index("Rayleigh scattering.") + assert out.index("2. assistant:") < out.index("reasoning:") + + +async def test_reasoning_is_absent_without_one( + state: ReplState, + capsys: pytest.CaptureFixture[str], +) -> None: + state.agent.messages = [UserChatMessage(content="hi"), AssistantChatMessage(content="hello")] + assert await handle_slash(state, "/history -vv") is True + assert "reasoning:" not in capsys.readouterr().out + + +async def test_preview_never_prints_reasoning( + state: ReplState, + capsys: pytest.CaptureFixture[str], +) -> None: + state.agent.messages = [ + UserChatMessage(content="why?"), + AssistantChatMessage(content="Because.", reasoning_content="Because the docs say so."), + ] + assert await handle_slash(state, "/history --preview") is True + assert "Because the docs say so." not in capsys.readouterr().out + + async def test_verbose_keeps_local_rows_out_of_the_way( state: ReplState, capsys: pytest.CaptureFixture[str],