# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from typing import Any from typing import Callable from google.adk.agents.llm_agent import Agent from google.adk.events.event import Event from google.adk.flows.llm_flows.functions import find_matching_function_call from google.adk.tools.function_tool import FunctionTool from google.adk.tools.tool_context import ToolContext from google.genai import types import pytest from ... import testing_utils def test_simple_function(): function_call_1 = types.Part.from_function_call( name='increase_by_one', args={'x': 1} ) function_respones_2 = types.Part.from_function_response( name='increase_by_one', response={'result': 2} ) responses: list[types.Content] = [ function_call_1, 'response1', 'response2', 'response3', 'response4', ] function_called = 0 mock_model = testing_utils.MockModel.create(responses=responses) def increase_by_one(x: int) -> int: nonlocal function_called function_called += 1 return x + 1 agent = Agent(name='root_agent', model=mock_model, tools=[increase_by_one]) runner = testing_utils.InMemoryRunner(agent) assert testing_utils.simplify_events(runner.run('test')) == [ ('root_agent', function_call_1), ('root_agent', function_respones_2), ('root_agent', 'response1'), ] # Asserts the requests. assert testing_utils.simplify_contents(mock_model.requests[0].contents) == [ ('user', 'test') ] assert testing_utils.simplify_contents(mock_model.requests[1].contents) == [ ('user', 'test'), ('model', function_call_1), ('user', function_respones_2), ] # Asserts the function calls. assert function_called == 1 @pytest.mark.asyncio async def test_async_function(): function_calls = [ types.Part.from_function_call(name='increase_by_one', args={'x': 1}), types.Part.from_function_call(name='multiple_by_two', args={'x': 2}), types.Part.from_function_call(name='multiple_by_two_sync', args={'x': 3}), ] function_responses = [ types.Part.from_function_response( name='increase_by_one', response={'result': 2} ), types.Part.from_function_response( name='multiple_by_two', response={'result': 4} ), types.Part.from_function_response( name='multiple_by_two_sync', response={'result': 6} ), ] responses: list[types.Content] = [ function_calls, 'response1', 'response2', 'response3', 'response4', ] function_called = 0 mock_model = testing_utils.MockModel.create(responses=responses) async def increase_by_one(x: int) -> int: nonlocal function_called function_called += 1 return x + 1 async def multiple_by_two(x: int) -> int: nonlocal function_called function_called += 1 return x * 2 def multiple_by_two_sync(x: int) -> int: nonlocal function_called function_called += 1 return x * 2 agent = Agent( name='root_agent', model=mock_model, tools=[increase_by_one, multiple_by_two, multiple_by_two_sync], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') assert testing_utils.simplify_events(events) == [ ('root_agent', function_calls), ('root_agent', function_responses), ('root_agent', 'response1'), ] # Asserts the requests. assert testing_utils.simplify_contents(mock_model.requests[0].contents) == [ ('user', 'test') ] assert testing_utils.simplify_contents(mock_model.requests[1].contents) == [ ('user', 'test'), ('model', function_calls), ('user', function_responses), ] # Asserts the function calls. assert function_called == 3 @pytest.mark.asyncio async def test_function_tool(): function_calls = [ types.Part.from_function_call(name='increase_by_one', args={'x': 1}), types.Part.from_function_call(name='multiple_by_two', args={'x': 2}), types.Part.from_function_call(name='multiple_by_two_sync', args={'x': 3}), ] function_responses = [ types.Part.from_function_response( name='increase_by_one', response={'result': 2} ), types.Part.from_function_response( name='multiple_by_two', response={'result': 4} ), types.Part.from_function_response( name='multiple_by_two_sync', response={'result': 6} ), ] responses: list[types.Content] = [ function_calls, 'response1', 'response2', 'response3', 'response4', ] function_called = 0 mock_model = testing_utils.MockModel.create(responses=responses) async def increase_by_one(x: int) -> int: nonlocal function_called function_called += 1 return x + 1 async def multiple_by_two(x: int) -> int: nonlocal function_called function_called += 1 return x * 2 def multiple_by_two_sync(x: int) -> int: nonlocal function_called function_called += 1 return x * 2 class TestTool(FunctionTool): def __init__(self, func: Callable[..., Any]): super().__init__(func=func) wrapped_increase_by_one = TestTool(func=increase_by_one) agent = Agent( name='root_agent', model=mock_model, tools=[wrapped_increase_by_one, multiple_by_two, multiple_by_two_sync], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') assert testing_utils.simplify_events(events) == [ ('root_agent', function_calls), ('root_agent', function_responses), ('root_agent', 'response1'), ] # Asserts the requests. assert testing_utils.simplify_contents(mock_model.requests[0].contents) == [ ('user', 'test') ] assert testing_utils.simplify_contents(mock_model.requests[1].contents) == [ ('user', 'test'), ('model', function_calls), ('user', function_responses), ] # Asserts the function calls. assert function_called == 3 def test_update_state(): mock_model = testing_utils.MockModel.create( responses=[ types.Part.from_function_call(name='update_state', args={}), 'response1', ] ) def update_state(tool_context: ToolContext): tool_context.state['x'] = 1 agent = Agent(name='root_agent', model=mock_model, tools=[update_state]) runner = testing_utils.InMemoryRunner(agent) runner.run('test') assert runner.session.state['x'] == 1 def test_function_call_id(): responses = [ types.Part.from_function_call(name='increase_by_one', args={'x': 1}), 'response1', ] mock_model = testing_utils.MockModel.create(responses=responses) def increase_by_one(x: int) -> int: return x + 1 agent = Agent(name='root_agent', model=mock_model, tools=[increase_by_one]) runner = testing_utils.InMemoryRunner(agent) events = runner.run('test') for request in mock_model.requests: for content in request.contents: for part in content.parts: if part.function_call: assert part.function_call.id is None if part.function_response: assert part.function_response.id is None assert events[0].content.parts[0].function_call.id.startswith('adk-') assert events[1].content.parts[0].function_response.id.startswith('adk-') def test_find_function_call_event_no_function_response_in_last_event(): """Test when last event has no function response.""" events = [ Event( invocation_id='inv1', author='user', content=types.Content(role='user', parts=[types.Part(text='Hello')]), ) ] result = find_matching_function_call(events) assert result is None def test_find_function_call_event_empty_session_events(): """Test when session has no events.""" events = [] result = find_matching_function_call(events) assert result is None def test_find_function_call_event_function_response_but_no_matching_call(): """Test when last event has function response but no matching call found.""" # Create a function response function_response = types.FunctionResponse( id='func_123', name='test_func', response={} ) events = [ Event( invocation_id='inv1', author='agent1', content=types.Content( role='model', parts=[types.Part(text='Some other response')], ), ), Event( invocation_id='inv2', author='user', content=types.Content( role='user', parts=[types.Part(function_response=function_response)], ), ), ] result = find_matching_function_call(events) assert result is None def test_find_function_call_event_function_response_with_matching_call(): """Test when last event has function response with matching function call.""" # Create a function call function_call = types.FunctionCall(id='func_123', name='test_func', args={}) # Create a function response with matching ID function_response = types.FunctionResponse( id='func_123', name='test_func', response={} ) call_event = Event( invocation_id='inv1', author='agent1', content=types.Content( role='model', parts=[types.Part(function_call=function_call)] ), ) response_event = Event( invocation_id='inv2', author='user', content=types.Content( role='user', parts=[types.Part(function_response=function_response)] ), ) events = [call_event, response_event] result = find_matching_function_call(events) assert result == call_event def test_find_function_call_event_multiple_function_responses(): """Test when last event has multiple function responses.""" # Create function calls function_call1 = types.FunctionCall(id='func_123', name='test_func1', args={}) function_call2 = types.FunctionCall(id='func_456', name='test_func2', args={}) # Create function responses function_response1 = types.FunctionResponse( id='func_123', name='test_func1', response={} ) function_response2 = types.FunctionResponse( id='func_456', name='test_func2', response={} ) call_event1 = Event( invocation_id='inv1', author='agent1', content=types.Content( role='model', parts=[types.Part(function_call=function_call1)] ), ) call_event2 = Event( invocation_id='inv2', author='agent2', content=types.Content( role='model', parts=[types.Part(function_call=function_call2)] ), ) response_event = Event( invocation_id='inv3', author='user', content=types.Content( role='user', parts=[ types.Part(function_response=function_response1), types.Part(function_response=function_response2), ], ), ) events = [call_event1, call_event2, response_event] # Should return the first matching function call event found result = find_matching_function_call(events) assert result == call_event1 # First match (func_123) @pytest.mark.asyncio async def test_function_call_args_not_modified(): """Test that function_call.args is not modified when making a copy.""" from google.adk.flows.llm_flows.functions import handle_function_calls_async from google.adk.flows.llm_flows.functions import handle_function_calls_live def simple_fn(**kwargs) -> dict: return {'result': 'test'} tool = FunctionTool(simple_fn) model = testing_utils.MockModel.create(responses=[]) agent = Agent( name='test_agent', model=model, tools=[tool], ) invocation_context = await testing_utils.create_invocation_context( agent=agent, user_content='' ) # Create original args that we want to ensure are not modified original_args = {'param1': 'value1', 'param2': 42} function_call = types.FunctionCall(name=tool.name, args=original_args) content = types.Content(parts=[types.Part(function_call=function_call)]) event = Event( invocation_id=invocation_context.invocation_id, author=agent.name, content=content, ) tools_dict = {tool.name: tool} # Test handle_function_calls_async result_async = await handle_function_calls_async( invocation_context, event, tools_dict, ) # Verify original args are not modified assert function_call.args == original_args assert function_call.args is not original_args # Should be a copy # Test handle_function_calls_live result_live = await handle_function_calls_live( invocation_context, event, tools_dict, ) # Verify original args are still not modified assert function_call.args == original_args assert function_call.args is not original_args # Should be a copy # Both should return valid results assert result_async is not None assert result_live is not None @pytest.mark.asyncio async def test_function_call_args_none_handling(): """Test that function_call.args=None is handled correctly.""" from google.adk.flows.llm_flows.functions import handle_function_calls_async from google.adk.flows.llm_flows.functions import handle_function_calls_live def simple_fn(**kwargs) -> dict: return {'result': 'test'} tool = FunctionTool(simple_fn) model = testing_utils.MockModel.create(responses=[]) agent = Agent( name='test_agent', model=model, tools=[tool], ) invocation_context = await testing_utils.create_invocation_context( agent=agent, user_content='' ) # Create function call with None args function_call = types.FunctionCall(name=tool.name, args=None) content = types.Content(parts=[types.Part(function_call=function_call)]) event = Event( invocation_id=invocation_context.invocation_id, author=agent.name, content=content, ) tools_dict = {tool.name: tool} # Test handle_function_calls_async result_async = await handle_function_calls_async( invocation_context, event, tools_dict, ) # Test handle_function_calls_live result_live = await handle_function_calls_live( invocation_context, event, tools_dict, ) # Both should return valid results even with None args assert result_async is not None assert result_live is not None @pytest.mark.asyncio async def test_function_call_args_copy_behavior(): """Test that modifying the copied args doesn't affect the original.""" from google.adk.flows.llm_flows.functions import handle_function_calls_async from google.adk.flows.llm_flows.functions import handle_function_calls_live def simple_fn(test_param: str, other_param: int) -> dict: # Modify the args to test that the copy prevents affecting the original return { 'result': 'test', 'received_args': {'test_param': test_param, 'other_param': other_param}, } tool = FunctionTool(simple_fn) model = testing_utils.MockModel.create(responses=[]) agent = Agent( name='test_agent', model=model, tools=[tool], ) invocation_context = await testing_utils.create_invocation_context( agent=agent, user_content='' ) # Create original args original_args = {'test_param': 'original_value', 'other_param': 123} function_call = types.FunctionCall(name=tool.name, args=original_args) content = types.Content(parts=[types.Part(function_call=function_call)]) event = Event( invocation_id=invocation_context.invocation_id, author=agent.name, content=content, ) tools_dict = {tool.name: tool} # Test handle_function_calls_async result_async = await handle_function_calls_async( invocation_context, event, tools_dict, ) # Verify original args are unchanged assert function_call.args == original_args assert function_call.args['test_param'] == 'original_value' # Verify the tool received the args correctly assert result_async is not None response = result_async.content.parts[0].function_response.response # Check if the response has the expected structure assert 'received_args' in response received_args = response['received_args'] assert 'test_param' in received_args assert received_args['test_param'] == 'original_value' assert received_args['other_param'] == 123 assert ( function_call.args['test_param'] == 'original_value' ) # Original unchanged @pytest.mark.asyncio async def test_function_call_args_deep_copy_behavior(): """Test that deep copy behavior works correctly with nested structures.""" from google.adk.flows.llm_flows.functions import handle_function_calls_async from google.adk.flows.llm_flows.functions import handle_function_calls_live def simple_fn(nested_dict: dict, list_param: list) -> dict: # Modify the nested structures to test deep copy nested_dict['inner']['value'] = 'modified' list_param.append('new_item') return { 'result': 'test', 'received_nested': nested_dict, 'received_list': list_param, } tool = FunctionTool(simple_fn) model = testing_utils.MockModel.create(responses=[]) agent = Agent( name='test_agent', model=model, tools=[tool], ) invocation_context = await testing_utils.create_invocation_context( agent=agent, user_content='' ) # Create original args with nested structures original_nested_dict = {'inner': {'value': 'original'}} original_list = ['item1', 'item2'] original_args = { 'nested_dict': original_nested_dict, 'list_param': original_list, } function_call = types.FunctionCall(name=tool.name, args=original_args) content = types.Content(parts=[types.Part(function_call=function_call)]) event = Event( invocation_id=invocation_context.invocation_id, author=agent.name, content=content, ) tools_dict = {tool.name: tool} # Test handle_function_calls_async result_async = await handle_function_calls_async( invocation_context, event, tools_dict, ) # Verify original args are completely unchanged assert function_call.args == original_args assert function_call.args['nested_dict']['inner']['value'] == 'original' assert function_call.args['list_param'] == ['item1', 'item2'] # Verify the tool received the modified nested structures assert result_async is not None response = result_async.content.parts[0].function_response.response # Check that the tool received modified versions assert 'received_nested' in response assert 'received_list' in response assert response['received_nested']['inner']['value'] == 'modified' assert 'new_item' in response['received_list'] # Verify original is still unchanged assert function_call.args['nested_dict']['inner']['value'] == 'original' assert function_call.args['list_param'] == ['item1', 'item2'] def test_shallow_vs_deep_copy_demonstration(): """Demonstrate why deep copy is necessary vs shallow copy.""" import copy # Original nested structure original = { 'nested_dict': {'inner': {'value': 'original'}}, 'list_param': ['item1', 'item2'], } # Shallow copy (what dict() does) shallow_copy = dict(original) # Deep copy (what copy.deepcopy() does) deep_copy = copy.deepcopy(original) # Modify the shallow copy shallow_copy['nested_dict']['inner']['value'] = 'modified' shallow_copy['list_param'].append('new_item') # Check that shallow copy affects the original assert ( original['nested_dict']['inner']['value'] == 'modified' ) # Original is affected! assert 'new_item' in original['list_param'] # Original is affected! # Reset original for deep copy test original = { 'nested_dict': {'inner': {'value': 'original'}}, 'list_param': ['item1', 'item2'], } # Modify the deep copy deep_copy['nested_dict']['inner']['value'] = 'modified' deep_copy['list_param'].append('new_item') # Check that deep copy does NOT affect the original assert ( original['nested_dict']['inner']['value'] == 'original' ) # Original unchanged assert 'new_item' not in original['list_param'] # Original unchanged assert ( deep_copy['nested_dict']['inner']['value'] == 'modified' ) # Copy is modified assert 'new_item' in deep_copy['list_param'] # Copy is modified @pytest.mark.asyncio async def test_parallel_function_execution_timing(): """Test that multiple function calls are executed in parallel, not sequentially.""" import time execution_order = [] execution_times = {} async def slow_function_1(delay: float = 0.1) -> dict: start_time = time.time() execution_order.append('start_1') await asyncio.sleep(delay) end_time = time.time() execution_times['func_1'] = (start_time, end_time) execution_order.append('end_1') return {'result': 'function_1_result'} async def slow_function_2(delay: float = 0.1) -> dict: start_time = time.time() execution_order.append('start_2') await asyncio.sleep(delay) end_time = time.time() execution_times['func_2'] = (start_time, end_time) execution_order.append('end_2') return {'result': 'function_2_result'} # Create function calls function_calls = [ types.Part.from_function_call( name='slow_function_1', args={'delay': 0.1} ), types.Part.from_function_call( name='slow_function_2', args={'delay': 0.1} ), ] function_responses = [ types.Part.from_function_response( name='slow_function_1', response={'result': 'function_1_result'} ), types.Part.from_function_response( name='slow_function_2', response={'result': 'function_2_result'} ), ] responses: list[types.Content] = [ function_calls, 'response1', ] mock_model = testing_utils.MockModel.create(responses=responses) agent = Agent( name='test_agent', model=mock_model, tools=[slow_function_1, slow_function_2], ) runner = testing_utils.TestInMemoryRunner(agent) # Measure total execution time start_time = time.time() events = await runner.run_async_with_new_session('test') total_time = time.time() - start_time # Verify parallel execution by checking execution order # In parallel execution, both functions should start before either finishes assert 'start_1' in execution_order assert 'start_2' in execution_order assert 'end_1' in execution_order assert 'end_2' in execution_order # Verify both functions started within a reasonable time window func_1_start, func_1_end = execution_times['func_1'] func_2_start, func_2_end = execution_times['func_2'] # Functions should start at approximately the same time (within 10ms) start_time_diff = abs(func_1_start - func_2_start) assert ( start_time_diff < 0.01 ), f'Functions started too far apart: {start_time_diff}s' # Total execution time should be closer to 0.1s (parallel) than 0.2s (sequential) # Allow some overhead for task creation and synchronization assert ( total_time < 0.15 ), f'Execution took too long: {total_time}s, expected < 0.15s' # Verify the results are correct assert testing_utils.simplify_events(events) == [ ('test_agent', function_calls), ('test_agent', function_responses), ('test_agent', 'response1'), ] @pytest.mark.asyncio async def test_parallel_state_modifications_thread_safety(): """Test that parallel function calls modifying state are thread-safe.""" state_modifications = [] def modify_state_1(tool_context: ToolContext) -> dict: # Track when this function modifies state current_state = dict(tool_context.state.to_dict()) state_modifications.append(('func_1_start', current_state)) tool_context.state['counter'] = tool_context.state.get('counter', 0) + 1 tool_context.state['func_1_executed'] = True final_state = dict(tool_context.state.to_dict()) state_modifications.append(('func_1_end', final_state)) return {'result': 'modified_state_1'} def modify_state_2(tool_context: ToolContext) -> dict: # Track when this function modifies state current_state = dict(tool_context.state.to_dict()) state_modifications.append(('func_2_start', current_state)) tool_context.state['counter'] = tool_context.state.get('counter', 0) + 1 tool_context.state['func_2_executed'] = True final_state = dict(tool_context.state.to_dict()) state_modifications.append(('func_2_end', final_state)) return {'result': 'modified_state_2'} # Create function calls function_calls = [ types.Part.from_function_call(name='modify_state_1', args={}), types.Part.from_function_call(name='modify_state_2', args={}), ] responses: list[types.Content] = [ function_calls, 'response1', ] mock_model = testing_utils.MockModel.create(responses=responses) agent = Agent( name='test_agent', model=mock_model, tools=[modify_state_1, modify_state_2], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') # Verify the parallel execution worked correctly by checking the events # The function response event should have the merged state_delta function_response_event = events[ 1 ] # Second event should be the function response assert function_response_event.actions.state_delta['counter'] == 2 assert function_response_event.actions.state_delta['func_1_executed'] is True assert function_response_event.actions.state_delta['func_2_executed'] is True # Verify both functions were called assert len(state_modifications) == 4 # 2 functions × 2 events each # Extract function names from modifications func_names = [mod[0] for mod in state_modifications] assert 'func_1_start' in func_names assert 'func_1_end' in func_names assert 'func_2_start' in func_names assert 'func_2_end' in func_names @pytest.mark.asyncio async def test_sync_function_blocks_async_functions(): """Test that sync functions block async functions from running concurrently.""" execution_order = [] def blocking_sync_function() -> dict: execution_order.append('sync_A') # Simulate CPU-intensive work that blocks the event loop result = 0 for i in range(1000000): # This blocks the event loop result += i execution_order.append('sync_B') return {'result': 'sync_done'} async def yielding_async_function() -> dict: execution_order.append('async_C') await asyncio.sleep( 0.001 ) # This should yield, but can't if event loop is blocked execution_order.append('async_D') return {'result': 'async_done'} # Create function calls - these should run "in parallel" function_calls = [ types.Part.from_function_call(name='blocking_sync_function', args={}), types.Part.from_function_call(name='yielding_async_function', args={}), ] responses: list[types.Content] = [function_calls, 'response1'] mock_model = testing_utils.MockModel.create(responses=responses) agent = Agent( name='test_agent', model=mock_model, tools=[blocking_sync_function, yielding_async_function], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') # With blocking sync function, execution should be sequential: A, B, C, D # The sync function blocks, preventing the async function from yielding properly assert execution_order == ['sync_A', 'sync_B', 'async_C', 'async_D'] @pytest.mark.asyncio async def test_async_function_without_yield_blocks_others(): """Test that async functions without yield statements block other functions.""" execution_order = [] async def non_yielding_async_function() -> dict: execution_order.append('non_yield_A') # CPU-intensive work without any await statements - blocks like sync function result = 0 for i in range(1000000): # No await here, so this blocks the event loop result += i execution_order.append('non_yield_B') return {'result': 'non_yielding_done'} async def yielding_async_function() -> dict: execution_order.append('yield_C') await asyncio.sleep( 0.001 ) # This should yield, but can't if event loop is blocked execution_order.append('yield_D') return {'result': 'yielding_done'} # Create function calls function_calls = [ types.Part.from_function_call( name='non_yielding_async_function', args={} ), types.Part.from_function_call(name='yielding_async_function', args={}), ] responses: list[types.Content] = [function_calls, 'response1'] mock_model = testing_utils.MockModel.create(responses=responses) agent = Agent( name='test_agent', model=mock_model, tools=[non_yielding_async_function, yielding_async_function], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') # Non-yielding async function blocks, so execution is sequential: A, B, C, D assert execution_order == ['non_yield_A', 'non_yield_B', 'yield_C', 'yield_D'] @pytest.mark.asyncio async def test_yielding_async_functions_run_concurrently(): """Test that async functions with proper yields run concurrently.""" execution_order = [] async def yielding_async_function_1() -> dict: execution_order.append('func1_A') await asyncio.sleep(0.001) # Yield control execution_order.append('func1_B') return {'result': 'func1_done'} async def yielding_async_function_2() -> dict: execution_order.append('func2_C') await asyncio.sleep(0.001) # Yield control execution_order.append('func2_D') return {'result': 'func2_done'} # Create function calls function_calls = [ types.Part.from_function_call(name='yielding_async_function_1', args={}), types.Part.from_function_call(name='yielding_async_function_2', args={}), ] responses: list[types.Content] = [function_calls, 'response1'] mock_model = testing_utils.MockModel.create(responses=responses) agent = Agent( name='test_agent', model=mock_model, tools=[yielding_async_function_1, yielding_async_function_2], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') # With proper yielding, execution should interleave: A, C, B, D # Both functions start, yield, then complete assert execution_order == ['func1_A', 'func2_C', 'func1_B', 'func2_D'] @pytest.mark.asyncio async def test_mixed_function_types_execution_order(): """Test execution order with all three types of functions.""" execution_order = [] def sync_function() -> dict: execution_order.append('sync_A') # Small amount of blocking work result = sum(range(100000)) execution_order.append('sync_B') return {'result': 'sync_done'} async def non_yielding_async() -> dict: execution_order.append('non_yield_C') # CPU work without yield result = sum(range(100000)) execution_order.append('non_yield_D') return {'result': 'non_yield_done'} async def yielding_async() -> dict: execution_order.append('yield_E') await asyncio.sleep(0.001) # Proper yield execution_order.append('yield_F') return {'result': 'yield_done'} # Create function calls function_calls = [ types.Part.from_function_call(name='sync_function', args={}), types.Part.from_function_call(name='non_yielding_async', args={}), types.Part.from_function_call(name='yielding_async', args={}), ] responses: list[types.Content] = [function_calls, 'response1'] mock_model = testing_utils.MockModel.create(responses=responses) agent = Agent( name='test_agent', model=mock_model, tools=[sync_function, non_yielding_async, yielding_async], ) runner = testing_utils.TestInMemoryRunner(agent) events = await runner.run_async_with_new_session('test') # All blocking functions run sequentially, then the yielding one # Expected order: sync_A, sync_B, non_yield_C, non_yield_D, yield_E, yield_F assert execution_order == [ 'sync_A', 'sync_B', 'non_yield_C', 'non_yield_D', 'yield_E', 'yield_F', ]