项目文件夹

文件
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

1867 行
59 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Copyright 2026 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 unittest import mock
from fastapi.openapi.models import HTTPBearer
from google.adk.agents.live_request_queue import LiveRequestQueue
from google.adk.agents.llm_agent import Agent
from google.adk.auth.auth_tool import AuthConfig
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.events.ui_widget import UiWidget
from google.adk.flows.llm_flows.functions import find_matching_function_call
from google.adk.flows.llm_flows.functions import handle_function_calls_async
from google.adk.flows.llm_flows.functions import handle_function_calls_live
from google.adk.flows.llm_flows.functions import merge_parallel_function_response_events
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.computer_use.computer_use_tool import ComputerUseTool
from google.adk.tools.function_tool import FunctionTool
from google.adk.tools.tool_confirmation import ToolConfirmation
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_responses_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_responses_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_responses_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."""
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."""
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."""
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."""
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."""
execution_order = []
async def slow_function_1(delay: float = 0.1) -> dict:
execution_order.append('start_1')
await asyncio.sleep(delay)
execution_order.append('end_1')
return {'result': 'function_1_result'}
async def slow_function_2(delay: float = 0.1) -> dict:
execution_order.append('start_2')
await asyncio.sleep(delay)
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)
events = await runner.run_async_with_new_session('test')
# Parallel execution means both functions start before either one finishes.
assert set(execution_order) == {'start_1', 'start_2', 'end_1', 'end_2'}
last_start = max(
execution_order.index('start_1'), execution_order.index('start_2')
)
first_end = min(
execution_order.index('end_1'), execution_order.index('end_2')
)
assert (
last_start < first_end
), f'Functions did not overlap; execution was sequential: {execution_order}'
# 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']
def test_merge_parallel_function_response_events_preserves_invocation_id():
"""Test that merge_parallel_function_response_events preserves the base event's invocation_id."""
# Create multiple function response events with different invocation IDs
invocation_id = 'base_invocation_123'
function_response1 = types.FunctionResponse(
id='func_123', name='test_function1', response={'result': 'success1'}
)
function_response2 = types.FunctionResponse(
id='func_456', name='test_function2', response={'result': 'success2'}
)
event1 = Event(
invocation_id=invocation_id,
author='test_agent',
content=types.Content(
role='user', parts=[types.Part(function_response=function_response1)]
),
)
event2 = Event(
invocation_id='different_invocation_456', # Different invocation ID
author='test_agent',
content=types.Content(
role='user', parts=[types.Part(function_response=function_response2)]
),
)
# Merge the events
merged_event = merge_parallel_function_response_events([event1, event2])
# Should preserve the base event's (first event's) invocation_id
assert merged_event.invocation_id == invocation_id
assert merged_event.invocation_id != 'different_invocation_456'
# Should contain both function responses
assert len(merged_event.content.parts) == 2
# Verify the responses are preserved
response_ids = {
part.function_response.id for part in merged_event.content.parts
}
assert 'func_123' in response_ids
assert 'func_456' in response_ids
def test_merge_parallel_function_response_events_single_event():
"""Test that merge_parallel_function_response_events returns single event unchanged."""
invocation_id = 'single_invocation_123'
function_response = types.FunctionResponse(
id='func_123', name='test_function', response={'result': 'success'}
)
event = Event(
invocation_id=invocation_id,
author='test_agent',
content=types.Content(
role='user', parts=[types.Part(function_response=function_response)]
),
)
# Merge single event
merged_event = merge_parallel_function_response_events([event])
# Should return the same event object
assert merged_event is event
assert merged_event.invocation_id == invocation_id
def test_merge_parallel_function_response_events_preserves_other_attributes():
"""Test that merge_parallel_function_response_events preserves other attributes from base event."""
invocation_id = 'base_invocation_123'
base_author = 'base_agent'
base_branch = 'main_branch'
function_response1 = types.FunctionResponse(
id='func_123', name='test_function1', response={'result': 'success1'}
)
function_response2 = types.FunctionResponse(
id='func_456', name='test_function2', response={'result': 'success2'}
)
event1 = Event(
invocation_id=invocation_id,
author=base_author,
branch=base_branch,
content=types.Content(
role='user', parts=[types.Part(function_response=function_response1)]
),
)
event2 = Event(
invocation_id='different_invocation_456',
author='different_agent', # Different author
branch='different_branch', # Different branch
content=types.Content(
role='user', parts=[types.Part(function_response=function_response2)]
),
)
# Merge the events
merged_event = merge_parallel_function_response_events([event1, event2])
# Should preserve base event's attributes
assert merged_event.invocation_id == invocation_id
assert merged_event.author == base_author
assert merged_event.branch == base_branch
# Should contain both function responses
assert len(merged_event.content.parts) == 2
@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',
]
def test_merge_parallel_function_response_events_merges_ui_widgets():
"""Test that merge_parallel_function_response_events merges render_ui_widgets."""
invocation_id = 'base_invocation_123'
widget1 = UiWidget(
id='widget_1', provider='mcp', payload={'resource_uri': 'ui://widget1'}
)
widget2 = UiWidget(
id='widget_2', provider='mcp', payload={'resource_uri': 'ui://widget2'}
)
widget3 = UiWidget(
id='widget_3', provider='mcp', payload={'resource_uri': 'ui://widget3'}
)
event1 = Event(
invocation_id=invocation_id,
author='test_agent',
actions=EventActions(render_ui_widgets=[widget1]),
)
event2 = Event(
invocation_id='different_invocation_456',
author='different_agent',
actions=EventActions(render_ui_widgets=[widget2, widget3]),
)
# Merge the events
merged_event = merge_parallel_function_response_events([event1, event2])
# Should contain all ui widgets
assert merged_event.actions.render_ui_widgets is not None
assert len(merged_event.actions.render_ui_widgets) == 3
widget_ids = {widget.id for widget in merged_event.actions.render_ui_widgets}
assert 'widget_1' in widget_ids
assert 'widget_2' in widget_ids
assert 'widget_3' in widget_ids
@pytest.mark.asyncio
@pytest.mark.parametrize(
'handle_function_calls',
[
(handle_function_calls_async),
(handle_function_calls_live),
],
)
async def test_computer_use_tool_decoding_behavior(handle_function_calls):
"""Tests that computer use tools automatically decode base64 images."""
valid_b64 = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
# make the tool return a dictionary with the image
async def mock_run(*args, **kwargs):
return {
'image': {'data': valid_b64, 'mimetype': 'image/png'},
'url': 'https://example.com',
}
# create a ComputerUseTool
tool = ComputerUseTool(func=mock_run, screen_size=(1024, 768))
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
function_call = types.FunctionCall(name=tool.name, 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}
result = await handle_function_calls(
invocation_context,
event,
tools_dict,
)
assert result is not None
response_part = result.content.parts[0].function_response
# Verify original image data is removed from the dict response
assert 'image' not in response_part.response
assert 'url' in response_part.response
# Verify the image was converted to a blob
assert len(response_part.parts) == 1
assert response_part.parts[0].inline_data is not None
@pytest.mark.asyncio
async def test_handle_function_calls_live_preserves_live_session_id():
"""Tests that handle_function_calls_live preserves live_session_id for single call."""
def simple_fn() -> dict[str, str]:
return {'result': 'test'}
tool1 = FunctionTool(simple_fn)
model = testing_utils.MockModel.create(responses=[])
agent = Agent(
name='test_agent',
model=model,
tools=[tool1],
)
invocation_context = await testing_utils.create_invocation_context(
agent=agent, user_content=''
)
function_call1 = types.FunctionCall(id='call_1', name=tool1.name, args={})
content1 = types.Content(parts=[types.Part(function_call=function_call1)])
event1 = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=content1,
live_session_id='test-live-session-id',
)
tools_dict = {tool1.name: tool1}
result_single = await handle_function_calls_live(
invocation_context,
event1,
tools_dict,
)
assert result_single is not None
assert result_single.live_session_id == 'test-live-session-id'
@pytest.mark.asyncio
async def test_handle_function_calls_live_parallel_preserves_live_session_id():
"""Tests that handle_function_calls_live preserves live_session_id for parallel calls."""
def simple_fn() -> dict[str, str]:
return {'result': 'test'}
tool1 = FunctionTool(simple_fn)
tool2 = FunctionTool(simple_fn)
model = testing_utils.MockModel.create(responses=[])
agent = Agent(
name='test_agent',
model=model,
tools=[tool1, tool2],
)
invocation_context = await testing_utils.create_invocation_context(
agent=agent, user_content=''
)
function_call1 = types.FunctionCall(id='call_1', name=tool1.name, args={})
function_call2 = types.FunctionCall(id='call_2', name=tool2.name, args={})
content2 = types.Content(
parts=[
types.Part(function_call=function_call1),
types.Part(function_call=function_call2),
]
)
event2 = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=content2,
live_session_id='test-live-session-id-parallel',
)
tools_dict = {tool1.name: tool1, tool2.name: tool2}
result_parallel = await handle_function_calls_live(
invocation_context,
event2,
tools_dict,
)
assert result_parallel is not None
assert result_parallel.live_session_id == 'test-live-session-id-parallel'
class _MockControlSignalTool(BaseTool):
"""A tool that simulates requesting confirmation or OAuth authentication."""
def __init__(self, name: str, behavior: str):
super().__init__(name=name, description='Simulated control tool')
self.behavior = behavior
async def run_async(self, *, args, tool_context):
if self.behavior == 'confirm':
tool_context.actions.requested_tool_confirmations = {
'fc_test_confirm': ToolConfirmation(hint='Authorize execution?')
}
return {'error': 'This tool requires user approval.'}
elif self.behavior == 'auth':
tool_context.actions.requested_auth_configs = {
'fc_test_auth': AuthConfig(auth_scheme=HTTPBearer())
}
return {'error': 'Please complete OAuth setup.'}
def _detect_error_in_response(self, response: Any) -> str | None:
if isinstance(response, dict) and 'error' in response:
return 'TOOL_ERROR'
return None
class _ErrorDetectingTool(BaseTool):
"""A test tool whose _detect_error_in_response raises an exception."""
async def run_async(self, *, args, tool_context):
return {'result': 'result'}
def _detect_error_in_response(self, response: Any) -> str | None:
raise RuntimeError('detection exploded')
@pytest.mark.asyncio
@pytest.mark.parametrize(
'handle_function_calls',
[
(handle_function_calls_async),
(handle_function_calls_live),
],
)
@pytest.mark.parametrize(
'mock_response,expected_error_type',
[
({'error': 'Internal component timeout'}, 'TOOL_ERROR'),
({'result': 'Execution succeeded'}, None),
],
ids=['dict_error_recorded', 'success_dict_ignored'],
)
async def test_e2e_telemetry_error_classification(
monkeypatch, handle_function_calls, mock_response, expected_error_type
):
"""E2E: asserts that tool outputs successfully translate to targeted OTel span error attributes."""
recorded_calls = []
# Intercept trace_tool_call to capture final telemetry state
monkeypatch.setattr(
'google.adk.telemetry._instrumentation.tracing.trace_tool_call',
lambda **kw: recorded_calls.append(kw),
)
tool = FunctionTool(func=lambda: mock_response)
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=''
)
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
await handle_function_calls(invocation_context, event, {tool.name: tool})
assert len(recorded_calls) == 1
assert recorded_calls[0]['error_type'] == expected_error_type
assert recorded_calls[0]['error'] is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
'handle_function_calls',
[
(handle_function_calls_async),
(handle_function_calls_live),
],
)
async def test_exception_takes_precedence_over_dict_error(
monkeypatch, handle_function_calls
):
"""End-to-end integration: exception takes strict precedence over manual dict error_type."""
recorded_calls = []
monkeypatch.setattr(
'google.adk.telemetry._instrumentation.tracing.trace_tool_call',
lambda **kw: recorded_calls.append(kw),
)
def mock_crashing_func():
raise ValueError('Fatal arithmetic error')
tool = FunctionTool(func=mock_crashing_func)
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=''
)
function_call = types.FunctionCall(
name=tool.name, args={}, id='fc_test_exception'
)
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
with pytest.raises(ValueError, match='Fatal arithmetic error'):
await handle_function_calls(invocation_context, event, {tool.name: tool})
assert len(recorded_calls) == 1
assert isinstance(recorded_calls[0]['error'], ValueError)
assert recorded_calls[0]['error_type'] is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
'handle_function_calls',
[
(handle_function_calls_async),
(handle_function_calls_live),
],
)
async def test_detection_skipped_when_confirmation_requested(
monkeypatch, handle_function_calls
):
"""E2E confirmation verification: control prompt avoids polluting telemetry with TOOL_ERROR."""
recorded_calls = []
monkeypatch.setattr(
'google.adk.telemetry._instrumentation.tracing.trace_tool_call',
lambda **kw: recorded_calls.append(kw),
)
tool = _MockControlSignalTool(name='confirm_tool', behavior='confirm')
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=''
)
function_call = types.FunctionCall(
name=tool.name, args={}, id='fc_test_confirm'
)
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
await handle_function_calls(invocation_context, event, {tool.name: tool})
assert len(recorded_calls) == 1
assert recorded_calls[0]['error_type'] is None
assert recorded_calls[0]['error'] is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
'handle_function_calls',
[
(handle_function_calls_async),
(handle_function_calls_live),
],
)
async def test_detection_skipped_when_auth_requested(
monkeypatch, handle_function_calls
):
"""E2E OAuth verification: authenticate control prompt avoids polluting telemetry with TOOL_ERROR."""
recorded_calls = []
monkeypatch.setattr(
'google.adk.telemetry._instrumentation.tracing.trace_tool_call',
lambda **kw: recorded_calls.append(kw),
)
tool = _MockControlSignalTool(name='auth_tool', behavior='auth')
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=''
)
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test_auth')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
await handle_function_calls(invocation_context, event, {tool.name: tool})
assert len(recorded_calls) == 1
assert recorded_calls[0]['error_type'] is None
assert recorded_calls[0]['error'] is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
'handle_function_calls',
[
(handle_function_calls_async),
(handle_function_calls_live),
],
)
async def test_detection_exception_does_not_break_tool_call(
monkeypatch, handle_function_calls
):
"""Safety Verification: telemetry errors during error parsing are caught cleanly, not crashing tool calls."""
recorded_calls = []
monkeypatch.setattr(
'google.adk.telemetry._instrumentation.tracing.trace_tool_call',
lambda **kw: recorded_calls.append(kw),
)
tool = _ErrorDetectingTool(
name='buggy_telemetry_tool', description='raises on tel'
)
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=''
)
function_call = types.FunctionCall(
name=tool.name, args={}, id='fc_test_buggy'
)
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
result_event = await handle_function_calls(
invocation_context, event, {tool.name: tool}
)
assert result_event is not None
assert result_event.content.parts[0].function_response.response == {
'result': 'result'
}
assert len(recorded_calls) == 1
assert recorded_calls[0]['error_type'] is None
assert recorded_calls[0]['error'] is None
@pytest.mark.asyncio
async def test_response_scheduling_applied_to_function_response():
"""response_scheduling on a tool is stamped onto the FunctionResponse part."""
def simple_fn(**kwargs) -> dict:
return {'result': 'test'}
tool = FunctionTool(simple_fn)
tool.response_scheduling = types.FunctionResponseScheduling.SILENT
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=''
)
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
result_event = await handle_function_calls_async(
invocation_context, event, {tool.name: tool}
)
assert result_event is not None
function_response = result_event.content.parts[0].function_response
assert function_response.scheduling is types.FunctionResponseScheduling.SILENT
@pytest.mark.asyncio
async def test_response_scheduling_unset_by_default():
"""Without response_scheduling, the FunctionResponse part leaves it unset."""
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=''
)
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
result_event = await handle_function_calls_async(
invocation_context, event, {tool.name: tool}
)
assert result_event is not None
function_response = result_event.content.parts[0].function_response
assert function_response.scheduling is None
async def _drain_live_function_responses(
live_request_queue: LiveRequestQueue,
count: int,
) -> list[types.Content]:
"""Drains ``count`` contents from a live request queue, ignoring acks."""
contents = []
while len(contents) < count:
request = await asyncio.wait_for(live_request_queue._queue.get(), timeout=5)
if request.content is not None:
contents.append(request.content)
return contents
@pytest.mark.asyncio
async def test_streaming_tool_with_scheduling_emits_function_response():
"""A streaming tool with response_scheduling relays yields as FunctionResponses."""
async def streaming_fn(**kwargs):
yield 'first'
yield 'second'
tool = FunctionTool(streaming_fn)
tool.response_scheduling = types.FunctionResponseScheduling.SILENT
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=''
)
invocation_context.live_request_queue = LiveRequestQueue()
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_stream')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
await handle_function_calls_live(invocation_context, event, {tool.name: tool})
contents = await _drain_live_function_responses(
invocation_context.live_request_queue, count=2
)
responses = [content.parts[0].function_response for content in contents]
assert [r.response for r in responses] == [
{'result': 'first'},
{'result': 'second'},
]
assert all(r.id == 'fc_stream' for r in responses)
assert all(
r.scheduling is types.FunctionResponseScheduling.SILENT for r in responses
)
@pytest.mark.asyncio
async def test_streaming_tool_without_scheduling_emits_function_response():
"""A streaming tool without response_scheduling still relays yields as
FunctionResponses, leaving scheduling unset."""
async def streaming_fn(**kwargs):
yield 'hello'
tool = FunctionTool(streaming_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=''
)
invocation_context.live_request_queue = LiveRequestQueue()
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_stream')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
await handle_function_calls_live(invocation_context, event, {tool.name: tool})
contents = await _drain_live_function_responses(
invocation_context.live_request_queue, count=1
)
function_response = contents[0].parts[0].function_response
assert function_response.response == {'result': 'hello'}
assert function_response.id == 'fc_stream'
assert function_response.scheduling is None
async def test_non_blocking_tool_handled_asynchronously():
"""Tests that a NON_BLOCKING tool returns None inline and pushes to live request queue."""
import asyncio
async def slow_fn() -> dict[str, str]:
await asyncio.sleep(0.1)
return {'result': 'done'}
tool = FunctionTool(slow_fn)
tool.response_scheduling = types.FunctionResponseScheduling.WHEN_IDLE
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=''
)
invocation_context.live_request_queue = LiveRequestQueue()
function_call = types.FunctionCall(
name=tool.name, args={}, id='fc_non_blocking'
)
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
# Inline call should return None, indicating it is handled asynchronously
result = await handle_function_calls_live(
invocation_context, event, {tool.name: tool}
)
assert result is None
# Verify strong reference is kept in active_non_blocking_tool_tasks while running
assert invocation_context.active_non_blocking_tool_tasks is not None
task_key = f'{tool.name}_fc_non_blocking'
assert task_key in invocation_context.active_non_blocking_tool_tasks
# Wait for the background task to complete and push to the queue
contents = await _drain_live_function_responses(
invocation_context.live_request_queue, count=1
)
function_response = contents[0].parts[0].function_response
assert function_response.response == {'result': 'done'}
assert function_response.id == 'fc_non_blocking'
assert (
function_response.scheduling == types.FunctionResponseScheduling.WHEN_IDLE
)
# Give event loop a tick for finally block to clean up the completed task
await asyncio.sleep(0)
assert task_key not in invocation_context.active_non_blocking_tool_tasks
async def test_non_blocking_tool_exception_handling_and_cleanup():
"""Tests that an exception in a NON_BLOCKING tool is logged and cleaned up."""
import asyncio
async def failing_fn() -> dict[str, str]:
await asyncio.sleep(0.05)
raise RuntimeError('Tool execution failed purposely')
tool = FunctionTool(failing_fn)
tool.response_scheduling = types.FunctionResponseScheduling.WHEN_IDLE
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=''
)
invocation_context.live_request_queue = LiveRequestQueue()
function_call = types.FunctionCall(
name=tool.name, args={}, id='fc_failing_non_blocking'
)
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(parts=[types.Part(function_call=function_call)]),
)
result = await handle_function_calls_live(
invocation_context, event, {tool.name: tool}
)
assert result is None
task_key = f'{tool.name}_fc_failing_non_blocking'
assert (
invocation_context.active_non_blocking_tool_tasks
and task_key in invocation_context.active_non_blocking_tool_tasks
)
# Allow background task to execute and raise its exception
await asyncio.sleep(0.1)
# Give event loop a tick for finally block to clean up the completed task
await asyncio.sleep(0)
assert task_key not in invocation_context.active_non_blocking_tool_tasks
async def test_parallel_non_blocking_tools():
"""Tests that multiple NON_BLOCKING tools execute in parallel and clean up independently."""
import asyncio
async def slow_fn_1() -> dict[str, str]:
await asyncio.sleep(0.05)
return {'result': 'done_1'}
async def slow_fn_2() -> dict[str, str]:
await asyncio.sleep(0.05)
return {'result': 'done_2'}
tool1 = FunctionTool(slow_fn_1)
tool1.response_scheduling = types.FunctionResponseScheduling.WHEN_IDLE
tool2 = FunctionTool(slow_fn_2)
tool2.response_scheduling = types.FunctionResponseScheduling.SILENT
model = testing_utils.MockModel.create(responses=[])
agent = Agent(name='test_agent', model=model, tools=[tool1, tool2])
invocation_context = await testing_utils.create_invocation_context(
agent=agent, user_content=''
)
invocation_context.live_request_queue = LiveRequestQueue()
fc1 = types.FunctionCall(name=tool1.name, args={}, id='fc_1')
fc2 = types.FunctionCall(name=tool2.name, args={}, id='fc_2')
event = Event(
invocation_id=invocation_context.invocation_id,
author=agent.name,
content=types.Content(
parts=[
types.Part(function_call=fc1),
types.Part(function_call=fc2),
]
),
)
result = await handle_function_calls_live(
invocation_context, event, {tool1.name: tool1, tool2.name: tool2}
)
assert result is None
assert len(invocation_context.active_non_blocking_tool_tasks) == 2
key1 = f'{tool1.name}_fc_1'
key2 = f'{tool2.name}_fc_2'
assert key1 in invocation_context.active_non_blocking_tool_tasks
assert key2 in invocation_context.active_non_blocking_tool_tasks
contents = await _drain_live_function_responses(
invocation_context.live_request_queue, count=2
)
responses = {
c.parts[0].function_response.id: c.parts[0].function_response
for c in contents
}
assert responses['fc_1'].response == {'result': 'done_1'}
assert responses['fc_2'].response == {'result': 'done_2'}
await asyncio.sleep(0)
assert len(invocation_context.active_non_blocking_tool_tasks) == 0