fix: Update empty event check to include executable code and execution results

Close #3859
Close #3921

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 852375447
This commit is contained in:
George Weale
2026-01-05 11:04:34 -08:00
committed by Copybara-Service
parent 6f259f08b3
commit 688f48fffb
2 changed files with 196 additions and 8 deletions
@@ -572,6 +572,38 @@ async def test_events_with_empty_content_are_skipped():
role="user",
),
),
# Event with content that has executable code part
Event(
invocation_id="inv10",
author="test_agent",
content=types.Content(
parts=[
types.Part(
executable_code=types.ExecutableCode(
code="print('hello')",
language="PYTHON",
)
)
],
role="model",
),
),
# Event with content that has code execution result part
Event(
invocation_id="inv11",
author="test_agent",
content=types.Content(
parts=[
types.Part(
code_execution_result=types.CodeExecutionResult(
outcome="OUTCOME_OK",
output="hello",
)
)
],
role="model",
),
),
]
invocation_context.session.events = events
@@ -608,4 +640,153 @@ async def test_events_with_empty_content_are_skipped():
parts=[types.Part(text=""), types.Part(text="Mixed content")],
role="user",
),
types.Content(
parts=[
types.Part(
executable_code=types.ExecutableCode(
code="print('hello')",
language="PYTHON",
)
)
],
role="model",
),
types.Content(
parts=[
types.Part(
code_execution_result=types.CodeExecutionResult(
outcome="OUTCOME_OK",
output="hello",
)
)
],
role="model",
),
]
@pytest.mark.asyncio
async def test_code_execution_result_events_are_not_skipped():
"""Test that events with code execution result are not skipped.
This is a regression test for the endless loop bug where code executor
outputs were not passed to the LLM because the events were incorrectly
filtered as empty.
"""
agent = Agent(model="gemini-2.5-flash", name="test_agent")
llm_request = LlmRequest(model="gemini-2.5-flash")
invocation_context = await testing_utils.create_invocation_context(
agent=agent
)
events = [
Event(
invocation_id="inv1",
author="user",
content=types.UserContent("Write code to calculate factorial"),
),
# Model generates code
Event(
invocation_id="inv2",
author="test_agent",
content=types.Content(
parts=[
types.Part(text="Here's the code:"),
types.Part(
executable_code=types.ExecutableCode(
code=(
"def factorial(n):\n return 1 if n <= 1 else n *"
" factorial(n-1)\nprint(factorial(5))"
),
language="PYTHON",
)
),
],
role="model",
),
),
# Code execution result
Event(
invocation_id="inv3",
author="test_agent",
content=types.Content(
parts=[
types.Part(
code_execution_result=types.CodeExecutionResult(
outcome="OUTCOME_OK",
output="120",
)
)
],
role="model",
),
),
]
invocation_context.session.events = events
# Process the request
async for _ in contents.request_processor.run_async(
invocation_context, llm_request
):
pass
# Verify all three events are included, especially the code execution result
assert len(llm_request.contents) == 3
assert llm_request.contents[0] == types.UserContent(
"Write code to calculate factorial"
)
# Second event has executable code
assert llm_request.contents[1].parts[1].executable_code is not None
# Third event has code execution result - this was the bug!
assert llm_request.contents[2].parts[0].code_execution_result is not None
assert llm_request.contents[2].parts[0].code_execution_result.output == "120"
@pytest.mark.asyncio
async def test_code_execution_result_not_in_first_part_is_not_skipped():
"""Test that code execution results aren't skipped.
This covers results that appear in a non-first part.
"""
agent = Agent(model="gemini-2.5-flash", name="test_agent")
llm_request = LlmRequest(model="gemini-2.5-flash")
invocation_context = await testing_utils.create_invocation_context(
agent=agent
)
events = [
Event(
invocation_id="inv1",
author="user",
content=types.UserContent("Run some code."),
),
Event(
invocation_id="inv2",
author="test_agent",
content=types.Content(
parts=[
types.Part(text=""),
types.Part(
code_execution_result=types.CodeExecutionResult(
outcome="OUTCOME_OK",
output="42",
)
),
],
role="model",
),
),
]
invocation_context.session.events = events
async for _ in contents.request_processor.run_async(
invocation_context, llm_request
):
pass
assert len(llm_request.contents) == 2
assert any(
part.code_execution_result is not None
and part.code_execution_result.output == "42"
for part in llm_request.contents[1].parts
)