@@ -126,15 +126,24 @@ async def test_auto_send_streams_text_and_returns_final_text():
126126async def test_auto_send_posts_full_tool_messages ():
127127 streaming = _FakeStreaming ()
128128 events = [
129+ # A bare tool_request Start (no Done/Full) must NOT open a streaming
130+ # context on its own — only Full events post messages.
131+ StreamTaskMessageStart (
132+ type = "start" , index = 0 ,
133+ content = ToolRequestContent (
134+ type = "tool_request" , author = "agent" ,
135+ tool_call_id = "c0" , name = "Bash" , arguments = {},
136+ ),
137+ ),
129138 StreamTaskMessageFull (
130- type = "full" , index = 0 ,
139+ type = "full" , index = 1 ,
131140 content = ToolRequestContent (
132141 type = "tool_request" , author = "agent" ,
133142 tool_call_id = "c1" , name = "Bash" , arguments = {"cmd" : "ls" },
134143 ),
135144 ),
136145 StreamTaskMessageFull (
137- type = "full" , index = 1 ,
146+ type = "full" , index = 2 ,
138147 content = ToolResponseContent (
139148 type = "tool_response" , author = "agent" ,
140149 tool_call_id = "c1" , name = "Bash" , content = "file.py" ,
@@ -145,12 +154,12 @@ async def test_auto_send_posts_full_tool_messages():
145154
146155 assert result .final_text == ""
147156
148- # One context per Full event
157+ # The opened contexts correspond ONLY to the two Full events — the
158+ # tool_request Start did not open a context.
149159 ctx_events = [s for s in streaming .sink if s [0 ] == "ctx" ]
150160 assert len (ctx_events ) == 2
151161 content_types = [s [1 ] for s in ctx_events ]
152- assert "tool_request" in content_types
153- assert "tool_response" in content_types
162+ assert content_types == ["tool_request" , "tool_response" ]
154163
155164 # Each context is opened and closed
156165 opens = [s for s in streaming .sink if s [0 ] == "open" ]
@@ -246,3 +255,28 @@ async def test_auto_send_closes_text_context_before_full_message():
246255 text_close_idx = next (i for i , s in enumerate (event_sequence ) if s == ("close" , "text" ))
247256 tool_open_idx = next (i for i , s in enumerate (event_sequence ) if s == ("open" , "tool_request" ))
248257 assert text_open_idx < text_close_idx < tool_open_idx
258+
259+
260+ # ---------------------------------------------------------------------------
261+ # Test 5: midstream error — propagates AND the open context is closed (finally)
262+ # ---------------------------------------------------------------------------
263+
264+ @pytest .mark .asyncio
265+ async def test_open_context_closed_on_midstream_error ():
266+ streaming = _FakeStreaming ()
267+
268+ async def _exploding_gen ():
269+ yield StreamTaskMessageStart (
270+ type = "start" , index = 0 ,
271+ content = TextContent (type = "text" , author = "agent" , content = "" ),
272+ )
273+ raise RuntimeError ("boom" )
274+
275+ with pytest .raises (RuntimeError , match = "boom" ):
276+ await auto_send (
277+ _exploding_gen (), task_id = "task1" , tracer = None , streaming = streaming
278+ )
279+
280+ # The text context that was opened mid-stream was closed by the finally block.
281+ assert ("open" , "text" ) in [(s [0 ], s [1 ]) for s in streaming .sink ]
282+ assert ("close" , "text" ) in [(s [0 ], s [1 ]) for s in streaming .sink ]
0 commit comments