Skip to content

Commit 383c726

Browse files
committed
fix: retain empty thinking only for block-local opaque state
1 parent 7fc1220 commit 383c726

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

‎amplifier_module_provider_github_copilot/streaming.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ def to_chat_response(self) -> "StreamingChatResponse":
326326
elif btype == "thinking":
327327
thinking_text: str = block["text"]
328328
signature = block.get("opaque") or self.reasoning_opaque
329-
if thinking_text or signature:
329+
# Only block-local opaque state can retain an empty block.
330+
if thinking_text or block.get("opaque"):
330331
# Contract: streaming-contract:ThinkingBlock:MUST:1
331332
# Copilot opaque state is not a native Anthropic/Gemini signature.
332333
# Keep its producer namespace through context persistence.

‎tests/test_contract_streaming.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,40 @@ def test_thinking_block_handles_no_signature(self) -> None:
374374
# signature should be None (not missing, not error)
375375
assert thinking_block.signature is None
376376

377+
@pytest.mark.parametrize("empty_first", [False, True], ids=["empty-after", "empty-before"])
378+
@pytest.mark.parametrize("thinking", ["Visible reasoning", ""], ids=["visible", "opaque-only"])
379+
def test_empty_unsigned_block_does_not_inherit_retention_from_separate_block(
380+
self, empty_first: bool, thinking: str
381+
) -> None:
382+
"""Only locally captured opaque state can retain an empty thinking block."""
383+
unsigned = DomainEvent(
384+
type=DomainEventType.CONTENT_DELTA, data={"text": ""}, block_type="THINKING"
385+
)
386+
signed = DomainEvent(
387+
type=DomainEventType.CONTENT_DELTA,
388+
data={"text": thinking, "reasoning_opaque": "copilot-opaque-fixture"},
389+
block_type="THINKING",
390+
)
391+
separator = DomainEvent(type=DomainEventType.CONTENT_DELTA, data={"text": "Answer"})
392+
accumulator = StreamingAccumulator()
393+
events = [unsigned, separator, signed] if empty_first else [signed, separator, unsigned]
394+
for event in events:
395+
accumulator.add(event)
396+
397+
response = accumulator.to_chat_response()
398+
assert [block.type for block in response.content] == (
399+
["text", "thinking"] if empty_first else ["thinking", "text"]
400+
)
401+
blocks = [block for block in response.content if isinstance(block, ThinkingBlock)]
402+
assert len(blocks) == 1
403+
assert blocks[0].thinking == thinking
404+
assert blocks[0].signature == "copilot-opaque-fixture"
405+
assert response.content_blocks is not None
406+
ui_thinking = [
407+
block for block in response.content_blocks if isinstance(block, ThinkingContent)
408+
]
409+
assert [block.text for block in ui_thinking] == ([thinking] if thinking else [])
410+
377411
@pytest.mark.parametrize("object_event", [False, True], ids=["dict", "sdk-object"])
378412
@pytest.mark.parametrize("split", [False, True], ids=["single", "split"])
379413
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)