Skip to content

Commit 812ab7f

Browse files
fix: properly close streaming messages when finish_reason is present
Add fallback logic to detect streaming completion using finish_reason when is_final_response returns False but finish_reason is set. **Problem:** Gemini returns events with partial=True and is_final_response()=False even on the final chunk that contains finish_reason="STOP". This caused streaming messages to remain open and require force-closing, resulting in warnings. **Solution:** Enhanced should_send_end logic to check for finish_reason as a fallback: - Check if finish_reason attribute exists and is truthy - If streaming is active and finish_reason is present, emit TEXT_MESSAGE_END - Formula: should_send_end = (is_final_response and not is_partial) or (has_finish_reason and self._is_streaming) **Testing:** ✅ All 277 tests pass ✅ Added test_partial_with_finish_reason to verify the fix ✅ Eliminates "Force-closing unterminated streaming message" warnings ✅ Properly emits TEXT_MESSAGE_END for events with finish_reason 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d8fadcc commit 812ab7f

File tree

2 files changed

+75
-6
lines changed

2 files changed

+75
-6
lines changed

typescript-sdk/integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,13 @@ async def _translate_text_content(
165165
is_final_response = adk_event.is_final_response
166166

167167
# Handle None values: if is_final_response=True, it means streaming should end
168-
should_send_end = is_final_response and not is_partial
169-
168+
# Also check for finish_reason as a fallback indicator that streaming is complete
169+
has_finish_reason = bool(getattr(adk_event, 'finish_reason', None))
170+
should_send_end = (is_final_response and not is_partial) or (has_finish_reason and self._is_streaming)
171+
170172
logger.info(f"📥 Text event - partial={is_partial}, turn_complete={turn_complete}, "
171-
f"is_final_response={is_final_response}, should_send_end={should_send_end}, "
172-
f"currently_streaming={self._is_streaming}")
173+
f"is_final_response={is_final_response}, has_finish_reason={has_finish_reason}, "
174+
f"should_send_end={should_send_end}, currently_streaming={self._is_streaming}")
173175

174176
if is_final_response:
175177

typescript-sdk/integrations/adk-middleware/python/tests/test_streaming.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,72 @@ async def test_streaming_behavior():
100100
print(f" Got: {event_type_strings}")
101101
return False
102102

103+
async def test_partial_with_finish_reason():
104+
"""Test the specific scenario: partial=True, is_final_response=False, but finish_reason=STOP.
105+
106+
This is the bug we fixed - Gemini returns partial=True even on the final chunk with finish_reason.
107+
The fix checks for finish_reason as a fallback to properly close the streaming message.
108+
"""
109+
print("\n🧪 Testing Partial Event with finish_reason (Bug Fix Scenario)")
110+
print("=================================================================")
111+
112+
translator = EventTranslator()
113+
114+
# First event: start streaming
115+
first_event = MagicMock()
116+
first_event.content = MagicMock()
117+
first_event.content.parts = [MagicMock(text="Hello")]
118+
first_event.author = "assistant"
119+
first_event.partial = True
120+
first_event.turn_complete = None
121+
first_event.finish_reason = None
122+
first_event.is_final_response = lambda: False
123+
first_event.get_function_calls = lambda: []
124+
first_event.get_function_responses = lambda: []
125+
126+
# Second event: final chunk with finish_reason BUT still partial=True (the bug scenario!)
127+
final_event = MagicMock()
128+
final_event.content = MagicMock()
129+
final_event.content.parts = [MagicMock(text=" world")]
130+
final_event.author = "assistant"
131+
final_event.partial = True # Still marked as partial!
132+
final_event.turn_complete = None
133+
final_event.finish_reason = "STOP" # But has finish_reason!
134+
final_event.is_final_response = lambda: False # And is_final_response returns False!
135+
final_event.get_function_calls = lambda: []
136+
final_event.get_function_responses = lambda: []
137+
138+
print("\n📡 Event 1: partial=True, finish_reason=None, is_final_response=False")
139+
print("📡 Event 2: partial=True, finish_reason=STOP, is_final_response=False ⚠️")
140+
141+
all_events = []
142+
143+
# Process first event
144+
async for ag_ui_event in translator.translate(first_event, "test_thread", "test_run"):
145+
all_events.append(ag_ui_event)
146+
147+
# Process final event
148+
async for ag_ui_event in translator.translate(final_event, "test_thread", "test_run"):
149+
all_events.append(ag_ui_event)
150+
151+
event_types = [str(event.type).split('.')[-1] for event in all_events]
152+
153+
print(f"\n📊 Generated Events: {event_types}")
154+
155+
# Expected: START, CONTENT (Hello), CONTENT (world), END
156+
# The fix ensures that finish_reason triggers END even when partial=True and is_final_response=False
157+
expected = ["TEXT_MESSAGE_START", "TEXT_MESSAGE_CONTENT", "TEXT_MESSAGE_CONTENT", "TEXT_MESSAGE_END"]
158+
159+
if event_types == expected:
160+
print("✅ Bug fix verified! finish_reason properly triggers TEXT_MESSAGE_END")
161+
print(" Even when partial=True and is_final_response=False")
162+
return True
163+
else:
164+
print(f"❌ Bug fix failed!")
165+
print(f" Expected: {expected}")
166+
print(f" Got: {event_types}")
167+
return False
168+
103169
async def test_non_streaming():
104170
"""Test that complete messages still work."""
105171
print("\n🧪 Testing Non-Streaming (Complete Messages)")
@@ -135,9 +201,10 @@ async def test_non_streaming():
135201
if __name__ == "__main__":
136202
async def run_tests():
137203
test1 = await test_streaming_behavior()
138-
test2 = await test_non_streaming()
204+
test2 = await test_partial_with_finish_reason()
205+
test3 = await test_non_streaming()
139206

140-
if test1 and test2:
207+
if test1 and test2 and test3:
141208
print("\n🎉 All streaming tests passed!")
142209
print("💡 Ready for real ADK integration with proper streaming")
143210
else:

0 commit comments

Comments
 (0)