@@ -1322,10 +1322,10 @@ async def consume(session: testing_utils.Session):
13221322 return collected
13231323
13241324
1325- def test_input_streaming_tool_stream_is_none_before_model_calls ():
1326- """Test that input-streaming tools have stream=None until the model calls them ."""
1327- # Add a text response before the function call so we can observe stream
1328- # state between registration and tool invocation .
1325+ def test_input_streaming_tool_registered_lazily_with_stream ():
1326+ """Test that input-streaming tools are registered lazily when called and receive a stream ."""
1327+ # A text response before the function call lets us observe that the
1328+ # tool is NOT registered before the model calls it .
13291329 text_response = LlmResponse (
13301330 content = types .Content (
13311331 role = 'model' ,
@@ -1362,7 +1362,7 @@ async def monitor_video_stream(input_stream: LiveRequestQueue):
13621362
13631363 runner = _LiveTestRunner (root_agent = root_agent )
13641364
1365- # Capture the invocation context to inspect stream state.
1365+ # Capture the invocation context to inspect registration state.
13661366 captured_context = None
13671367 original_method = runner .runner ._new_invocation_context_for_live
13681368
@@ -1379,39 +1379,39 @@ def capturing_method(*args, **kwargs):
13791379 blob = types .Blob (data = b'test_data' , mime_type = 'audio/pcm' )
13801380 )
13811381
1382- # Collect events and capture stream state before the tool is called.
1382+ # Collect events and check that the tool is NOT registered before
1383+ # the model calls it.
13831384 collected = []
1384- stream_states_before_call = []
1385+ not_registered_before_call = None
13851386
13861387 async def consume (session : testing_utils .Session ):
1388+ nonlocal not_registered_before_call
13871389 async for response in runner .runner .run_live (
13881390 session = session ,
13891391 live_request_queue = live_request_queue ,
13901392 ):
13911393 collected .append (response )
1392- # On a non-function-call event, the tool is registered but not
1393- # yet invoked — capture the stream value at that point .
1394+ # On the first non-function-call event, verify the tool is not
1395+ # yet registered (lazy registration) .
13941396 active = (
1395- captured_context .active_streaming_tools if captured_context else {}
1397+ captured_context .active_streaming_tools if captured_context else None
13961398 )
13971399 if (
1398- not stream_states_before_call
1400+ not_registered_before_call is None
13991401 and not response .get_function_calls ()
1400- and 'monitor_video_stream' in active
14011402 ):
1402- stream_states_before_call .append (active ['monitor_video_stream' ].stream )
1403+ not_registered_before_call = (
1404+ active is None or 'monitor_video_stream' not in active
1405+ )
14031406 if len (collected ) >= 4 :
14041407 return
14051408
14061409 runner ._run_with_loop (asyncio .wait_for (consume (runner .session ), timeout = 5.0 ))
14071410
1408- # Before the model calls the tool, stream should be None.
1409- assert (
1410- stream_states_before_call
1411- ), 'Stream state was never observed before the tool call'
1411+ # Tool should not be registered before the model calls it.
14121412 assert (
1413- stream_states_before_call [ 0 ] is None
1414- ), 'Expected stream to be None before the model calls the tool '
1413+ not_registered_before_call is True
1414+ ), 'Expected tool to NOT be registered before the model calls it '
14151415 # When the model calls the tool, input_stream should be provided.
14161416 assert (
14171417 stream_state_during_call is True
@@ -1458,17 +1458,20 @@ def stop_streaming(function_name: str):
14581458
14591459 runner = _LiveTestRunner (root_agent = root_agent )
14601460
1461- # Capture invocation context to verify stream is reset.
1462- captured_context = None
1463- original_method = runner .runner ._new_invocation_context_for_live
1464-
1465- def capturing_method (* args , ** kwargs ):
1466- nonlocal captured_context
1467- ctx = original_method (* args , ** kwargs )
1468- captured_context = ctx
1461+ # Capture the child invocation context (created by _create_invocation_context
1462+ # inside base_agent.run_live) to inspect active_streaming_tools.
1463+ # We cannot use the parent context from _new_invocation_context_for_live
1464+ # because model_copy creates a separate child object.
1465+ captured_child_context = None
1466+ original_create = root_agent ._create_invocation_context
1467+
1468+ def capturing_create (* args , ** kwargs ):
1469+ nonlocal captured_child_context
1470+ ctx = original_create (* args , ** kwargs )
1471+ captured_child_context = ctx
14691472 return ctx
14701473
1471- runner . runner . _new_invocation_context_for_live = capturing_method
1474+ root_agent . _create_invocation_context = capturing_create
14721475
14731476 live_request_queue = LiveRequestQueue ()
14741477 live_request_queue .send_realtime (
@@ -1488,9 +1491,9 @@ def capturing_method(*args, **kwargs):
14881491
14891492 # Verify that stop_streaming reset the stream to None.
14901493 assert (
1491- captured_context is not None
1492- ), 'Expected invocation context to be captured'
1493- active_tools = captured_context .active_streaming_tools or {}
1494+ captured_child_context is not None
1495+ ), 'Expected child invocation context to be captured'
1496+ active_tools = captured_child_context .active_streaming_tools or {}
14941497 assert (
14951498 'monitor_stock_price' in active_tools
14961499 ), 'Expected monitor_stock_price in active_streaming_tools'
@@ -1499,11 +1502,18 @@ def capturing_method(*args, **kwargs):
14991502 ), 'Expected stream to be reset to None after stop_streaming'
15001503
15011504
1502- def test_output_streaming_tool_registered_at_startup ():
1503- """Test that output-streaming tools (async generators without LiveRequestQueue) are registered at startup."""
1504- response1 = LlmResponse (turn_complete = True )
1505+ def test_output_streaming_tool_registered_lazily_without_stream ():
1506+ """Test that output-streaming tools are registered lazily when called, with stream=None."""
1507+ function_call = types .Part .from_function_call (
1508+ name = 'monitor_stock_price' , args = {'stock_symbol' : 'GOOG' }
1509+ )
1510+ response1 = LlmResponse (
1511+ content = types .Content (role = 'model' , parts = [function_call ]),
1512+ turn_complete = False ,
1513+ )
1514+ response2 = LlmResponse (turn_complete = True )
15051515
1506- mock_model = testing_utils .MockModel .create ([response1 ])
1516+ mock_model = testing_utils .MockModel .create ([response1 , response2 ])
15071517
15081518 async def monitor_stock_price (stock_symbol : str ):
15091519 """Yield periodic price updates."""
@@ -1517,31 +1527,33 @@ async def monitor_stock_price(stock_symbol: str):
15171527
15181528 runner = _LiveTestRunner (root_agent = root_agent )
15191529
1520- # Capture invocation context to verify registration.
1521- captured_context = None
1522- original_method = runner .runner ._new_invocation_context_for_live
1530+ # Capture the child invocation context (created by _create_invocation_context
1531+ # inside base_agent.run_live) to inspect active_streaming_tools.
1532+ captured_child_context = None
1533+ original_create = root_agent ._create_invocation_context
15231534
1524- def capturing_method (* args , ** kwargs ):
1525- nonlocal captured_context
1526- ctx = original_method (* args , ** kwargs )
1527- captured_context = ctx
1535+ def capturing_create (* args , ** kwargs ):
1536+ nonlocal captured_child_context
1537+ ctx = original_create (* args , ** kwargs )
1538+ captured_child_context = ctx
15281539 return ctx
15291540
1530- runner . runner . _new_invocation_context_for_live = capturing_method
1541+ root_agent . _create_invocation_context = capturing_create
15311542
15321543 live_request_queue = LiveRequestQueue ()
15331544 live_request_queue .send_realtime (
15341545 blob = types .Blob (data = b'test' , mime_type = 'audio/pcm' )
15351546 )
15361547
1537- runner .run_live (live_request_queue , max_responses = 1 )
1548+ runner .run_live (live_request_queue , max_responses = 3 )
15381549
1539- # Output-streaming tool should be registered with stream=None.
1540- assert captured_context is not None
1541- active_tools = captured_context .active_streaming_tools or {}
1550+ # After the model calls the tool, it should be registered with
1551+ # stream=None (output-streaming tools don't consume the live stream).
1552+ assert captured_child_context is not None
1553+ active_tools = captured_child_context .active_streaming_tools or {}
15421554 assert (
15431555 'monitor_stock_price' in active_tools
1544- ), 'Expected output-streaming tool to be registered at startup '
1556+ ), 'Expected output-streaming tool to be registered when called '
15451557 assert (
15461558 active_tools ['monitor_stock_price' ].stream is None
15471559 ), 'Expected stream to be None for output-streaming tool'
0 commit comments