Skip to content

Commit d1f1e57

Browse files
committed
fix(core): keep encrypted reasoning on summary-less responses
Address CodeRabbit review findings on the branch: - core: a non-streaming OpenAI Responses reasoning item can return encrypted_content with an empty summary; the conversion loop dropped it entirely, diverging from the streaming output_item.done path and making the reasoning boundary non-replayable. Emit an encrypted ThinkPart in that case, mirroring the streaming behavior, with focused coverage. - tui: render "1 agent" (singular) instead of "1 agents" in the grouped agents summary, and make the uniform-status check explicitly boolean. The two other flagged reasoning findings (summary-index preservation and cross-output merge collisions) were verified against the code and tests and are non-issues: real deltas always carry valid indices, and encrypted boundaries already block adjacent cross-output merges.
1 parent 70afc6a commit d1f1e57

4 files changed

Lines changed: 38 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ GitHub Releases page; `0.8.0` is the new starting line.
1616
## Unreleased
1717

1818
- Fixed reasoning summaries exposing Markdown delimiters and duplicate terminal rows after refocus, and redesigned agent progress (single `Agent` calls and parallel `RunAgents` fan-outs) as a compact, payload-free agent activity tree.
19+
- Retain a non-streaming OpenAI Responses reasoning item's encrypted content when it carries no summary, matching the streaming path so the reasoning boundary stays replayable.
1920
- Reduce the shell prompt session to a compatibility façade over deep `prompting/` modules (config, keybindings, completion menus, narrow shell-facing methods), with Unicode cell-width coverage and documented module ownership in the architecture guide.
2021
- Render image/audio/video and unknown content parts as payload-free labels in the live view, roll nested subagent activity (up to 16 levels) under the correct root tool card, and stop provider-remapped tool results from starting replay user turns.
2122
- **Behavior change:** `!` shell commands now run through the detected configured shell (`<shell> -c`, PowerShell `-command` on Windows) instead of the implicit `/bin/sh`/`cmd.exe`, with separate 1 MiB stdout/stderr caps and cancellation cleanup; existing `cmd.exe`-syntax commands on Windows may need updating.

packages/pythinker-core/src/pythinker_core/contrib/chat_provider/openai_responses.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,12 +548,23 @@ async def _convert_non_stream_response(
548548
)
549549
elif item.type == "reasoning":
550550
encrypted_content = getattr(item, "encrypted_content", None)
551+
emitted_summary = False
551552
for summary_index, summary in enumerate(getattr(item, "summary", ())):
553+
emitted_summary = True
552554
yield ThinkPart(
553555
think=summary.text,
554556
encrypted=encrypted_content,
555557
summary_index=summary_index,
556558
)
559+
if not emitted_summary and encrypted_content is not None:
560+
# Mirror the streaming `output_item.done` path: a reasoning item
561+
# can carry encrypted_content with no summary parts, and dropping
562+
# it here would make the reasoning boundary non-replayable.
563+
yield ThinkPart(
564+
think="",
565+
encrypted=encrypted_content,
566+
summary_index=None,
567+
)
557568

558569
async def _convert_stream_response(
559570
self, response: AsyncStream[ResponseStreamEvent]

packages/pythinker-core/tests/test_stream_tool_call_metadata.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,29 @@ async def test_openai_responses_completed_reasoning_summaries_keep_order_and_enc
658658
]
659659

660660

661+
async def test_openai_responses_completed_reasoning_without_summary_keeps_encryption() -> None:
662+
# A non-streaming reasoning item can return encrypted_content with an empty
663+
# summary; it must still emit an encrypted ThinkPart so the boundary is
664+
# replayable, matching the streaming `output_item.done` behavior.
665+
response = _response(
666+
output=[
667+
ResponseReasoningItem.model_validate(
668+
{
669+
"type": "reasoning",
670+
"id": "reasoning_1",
671+
"summary": [],
672+
"encrypted_content": "enc_orphan",
673+
}
674+
)
675+
]
676+
)
677+
stream = OpenAIResponsesStreamedMessage(response)
678+
679+
parts = [part for part in await _collect_parts(stream) if isinstance(part, ThinkPart)]
680+
681+
assert parts == [ThinkPart(think="", encrypted="enc_orphan", summary_index=None)]
682+
683+
661684
async def test_openai_responses_empty_streamed_call_id_is_deterministic(
662685
monkeypatch: pytest.MonkeyPatch,
663686
) -> None:

src/pythinker_code/ui/shell/tool_renderers/agent.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,11 +449,12 @@ def _run_agents_status_style(status: str) -> RichStyle:
449449

450450

451451
def _render_grouped_agents_summary(count: int, statuses: list[str]) -> Text:
452-
uniform = statuses and all(status == statuses[0] for status in statuses)
452+
uniform = bool(statuses) and all(status == statuses[0] for status in statuses)
453453
label = statuses[0] if uniform else "mixed"
454454
text = Text()
455455
text.append(str(count), style=RichStyle(bold=True))
456-
text.append(f" agents {label}", style=tui_rich_style("dim"))
456+
word = "agent" if count == 1 else "agents"
457+
text.append(f" {word} {label}", style=tui_rich_style("dim"))
457458
return text
458459

459460

0 commit comments

Comments
 (0)