Skip to content

Commit 4d88a52

Browse files
GWealecopybara-github
authored andcommitted
fix: avoid yielding a None function-response event in live mode
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 941267314
1 parent 48e6e50 commit 4d88a52

2 files changed

Lines changed: 61 additions & 14 deletions

File tree

src/google/adk/flows/llm_flows/base_llm_flow.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,23 +1221,28 @@ async def _postprocess_live(
12211221

12221222
# Handles function calls.
12231223
if model_response_event.get_function_calls():
1224-
function_response_event = await functions.handle_function_calls_live(
1224+
# handle_function_calls_live returns None when every call is deferred
1225+
# (e.g. all long-running), so guard before yielding to avoid emitting a
1226+
# None event into the live stream.
1227+
if function_response_event := await functions.handle_function_calls_live(
12251228
invocation_context, model_response_event, llm_request.tools_dict
1226-
)
1227-
# Always yield the function response event first
1228-
yield function_response_event
1229-
1230-
# Check if this is a set_model_response function response
1231-
if json_response := _output_schema_processor.get_structured_model_response(
1232-
function_response_event
12331229
):
1234-
# Create and yield a final model response event
1235-
final_event = (
1236-
_output_schema_processor.create_final_model_response_event(
1237-
invocation_context, json_response
1230+
# Always yield the function response event first
1231+
yield function_response_event
1232+
1233+
# Check if this is a set_model_response function response
1234+
if json_response := (
1235+
_output_schema_processor.get_structured_model_response(
1236+
function_response_event
12381237
)
1239-
)
1240-
yield final_event
1238+
):
1239+
# Create and yield a final model response event
1240+
final_event = (
1241+
_output_schema_processor.create_final_model_response_event(
1242+
invocation_context, json_response
1243+
)
1244+
)
1245+
yield final_event
12411246

12421247
async def _postprocess_run_processors_async(
12431248
self, invocation_context: InvocationContext, llm_response: LlmResponse

tests/unittests/flows/llm_flows/test_base_llm_flow.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,3 +1784,45 @@ async def test_transfer_to_sibling_from_non_llm_agent_allowed():
17841784
# Assert
17851785
assert agent is not None
17861786
assert agent.name == 'child2'
1787+
1788+
1789+
@pytest.mark.asyncio
1790+
async def test_postprocess_live_skips_none_function_response_event():
1791+
"""When every live function call defers, no None event must be yielded.
1792+
1793+
handle_function_calls_live returns None if all calls are long-running, and
1794+
yielding that None downstream crashes the live receive loop.
1795+
"""
1796+
from google.adk.flows.llm_flows import base_llm_flow as blf
1797+
1798+
agent = Agent(name='test_agent', model='gemini-2.0-flash')
1799+
invocation_context = await testing_utils.create_invocation_context(
1800+
agent=agent
1801+
)
1802+
flow = BaseLlmFlowForTesting()
1803+
1804+
fc_part = types.Part(
1805+
function_call=types.FunctionCall(name='lro', id='1', args={})
1806+
)
1807+
content = types.Content(role='model', parts=[fc_part])
1808+
model_response_event = Event(
1809+
invocation_id=invocation_context.invocation_id,
1810+
author=agent.name,
1811+
content=content,
1812+
)
1813+
llm_request = LlmRequest(model='gemini-2.0-flash')
1814+
llm_response = LlmResponse(content=content)
1815+
1816+
with mock.patch.object(
1817+
blf.functions,
1818+
'handle_function_calls_live',
1819+
new=AsyncMock(return_value=None),
1820+
):
1821+
events = [
1822+
event
1823+
async for event in flow._postprocess_live(
1824+
invocation_context, llm_request, llm_response, model_response_event
1825+
)
1826+
]
1827+
1828+
assert all(event is not None for event in events)

0 commit comments

Comments
 (0)