Skip to content

Commit b58a57e

Browse files
fix(streaming): keep reasoning out of final text snapshot (#302)
1 parent 1abdfc9 commit b58a57e

4 files changed

Lines changed: 76 additions & 6 deletions

File tree

src/opencode_a2a/parts/text.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ def extract_text_from_parts(parts: Any) -> str:
1515
part_text = part.get("text")
1616
if isinstance(part_text, str):
1717
texts.append(part_text)
18-
elif part_type == "reasoning":
19-
part_text = part.get("text")
20-
if isinstance(part_text, str):
21-
texts.append(part_text)
2218
if texts:
2319
return "".join(texts).strip()
2420
return ""

tests/execution/test_streaming_output_contract_core.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,47 @@ async def test_streaming_emits_final_snapshot_only_when_stream_has_no_final_answ
345345
assert final_event.last_chunk is True
346346

347347

348+
@pytest.mark.asyncio
349+
async def test_streaming_final_snapshot_does_not_repeat_reasoning_content() -> None:
350+
client = DummyStreamingClient(
351+
stream_events_payload=[
352+
_event(session_id="ses-1", role="assistant", part_type="reasoning", delta="draft plan"),
353+
],
354+
response_text="final answer from send_message",
355+
response_raw={
356+
"parts": [
357+
{"type": "reasoning", "text": "draft plan"},
358+
{"type": "text", "text": "final answer from send_message"},
359+
]
360+
},
361+
)
362+
executor = OpencodeAgentExecutor(client, streaming_enabled=True)
363+
executor._should_stream = lambda context: True # type: ignore[method-assign]
364+
queue = DummyEventQueue()
365+
366+
await executor.execute(
367+
make_request_context(task_id="task-3b", context_id="ctx-3b", text="hello"), queue
368+
)
369+
370+
reasoning_updates = [
371+
event
372+
for event in _artifact_updates(queue)
373+
if _artifact_stream_meta(event)["block_type"] == "reasoning"
374+
]
375+
assert len(reasoning_updates) == 1
376+
assert _part_text(reasoning_updates[0]) == "draft plan"
377+
378+
text_updates = [
379+
event
380+
for event in _artifact_updates(queue)
381+
if _artifact_stream_meta(event)["block_type"] == "text"
382+
]
383+
assert len(text_updates) == 1
384+
assert _part_text(text_updates[0]) == "final answer from send_message"
385+
assert "draft plan" not in _part_text(text_updates[0])
386+
assert _artifact_stream_meta(text_updates[0])["source"] == "final_snapshot"
387+
388+
348389
@pytest.mark.asyncio
349390
async def test_execute_serializes_send_message_per_session() -> None:
350391
client = DummyStreamingClient(

tests/parts/test_parts_text.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_extract_text_from_parts_returns_text_parts_only() -> None:
3535
assert extract_text_from_parts(parts) == "final answer"
3636

3737

38-
def test_extract_text_from_parts_merges_text_and_reasoning_parts() -> None:
38+
def test_extract_text_from_parts_ignores_reasoning_parts() -> None:
3939
parts = [
4040
"skip-me",
4141
{
@@ -52,4 +52,4 @@ def test_extract_text_from_parts_merges_text_and_reasoning_parts() -> None:
5252
},
5353
]
5454

55-
assert extract_text_from_parts(parts) == "draft answer"
55+
assert extract_text_from_parts(parts) == "answer"

tests/upstream/test_opencode_upstream_client_params.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,39 @@ async def fake_post(path: str, *, params=None, json=None, timeout=_UNSET, **_kwa
807807
await client.close()
808808

809809

810+
@pytest.mark.asyncio
811+
async def test_send_message_response_text_ignores_reasoning_parts(monkeypatch) -> None:
812+
client = OpencodeUpstreamClient(
813+
make_settings(
814+
a2a_bearer_token="t-1",
815+
opencode_timeout=1.0,
816+
a2a_log_level="DEBUG",
817+
a2a_log_payloads=False,
818+
)
819+
)
820+
821+
async def fake_post(path: str, *, params=None, json=None, timeout=_UNSET, **_kwargs):
822+
del path, params, json, timeout
823+
return _DummyResponse(
824+
{
825+
"info": {"id": "m-2"},
826+
"parts": [
827+
{"type": "reasoning", "text": "draft plan"},
828+
{"type": "text", "text": "final answer"},
829+
],
830+
}
831+
)
832+
833+
monkeypatch.setattr(client._client, "post", fake_post)
834+
835+
message = await client.send_message("ses-1", "hello")
836+
837+
assert message.message_id == "m-2"
838+
assert message.text == "final answer"
839+
840+
await client.close()
841+
842+
810843
@pytest.mark.asyncio
811844
async def test_interrupt_request_helpers_ignore_invalid_and_trim_values() -> None:
812845
client = OpencodeUpstreamClient(

0 commit comments

Comments
 (0)