Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
</h3>
</html>


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove empty lines


Agent Development Kit (ADK) is a flexible and modular framework that applies
software development principles to AI agent creation. It is designed to
simplify building, deploying, and orchestrating agent workflows, from simple
Expand Down
3 changes: 2 additions & 1 deletion contributing/samples/hello_world/agent.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is the changes in this file related to the fix ?

Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async def check_prime(nums: list[int]) -> str:


root_agent = Agent(
model='gemini-2.0-flash',
model='gemini-pro',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it a valid model ?

name='hello_world_agent',
description=(
'hello world agent that can roll a dice of 8 sides and check prime'
Expand Down Expand Up @@ -98,6 +98,7 @@ async def check_prime(nums: list[int]) -> str:
# ),
# ),
generate_content_config=types.GenerateContentConfig(
max_output_tokens=100,
safety_settings=[
types.SafetySetting( # avoid false alarm about rolling dice.
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
Expand Down
5 changes: 4 additions & 1 deletion src/google/adk/flows/llm_flows/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ def _contains_empty_content(event: Event) -> bool:
This can happen to the events that only changed session state.
When both content and transcriptions are empty, the event will be considered
as empty. The content is considered empty if none of its parts contain text,
inline data, file data, function call, or function response.
inline data, file data, function call, function response, executable code,
or code execution result.

Args:
event: The event to check.
Expand All @@ -246,6 +247,8 @@ def _contains_empty_content(event: Event) -> bool:
and not p.file_data
and not p.function_call
and not p.function_response
and not p.executable_code
and not p.code_execution_result
for p in [event.content.parts[0]]
)
) and (not event.output_transcription and not event.input_transcription)
Expand Down
73 changes: 73 additions & 0 deletions tests/integration/test_code_executor_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Test to verify the code executor infinite loop fix."""
import asyncio
from google.adk import Agent
from google.adk.code_executors import UnsafeLocalCodeExecutor
from google.adk.runners import InMemoryRunner
from google.genai import types


async def test_code_executor():
"""Test that code executor doesn't cause infinite loop."""
root_agent = Agent(
model='gemini-pro',
name='hello_world_agent',
description='hello world agent.',
instruction="""
you are an agent that knows how to produce python code.
""",
code_executor=UnsafeLocalCodeExecutor(),
)

app_name = 'test_app'
user_id = 'test_user'
runner = InMemoryRunner(
agent=root_agent,
app_name=app_name,
)

session = await runner.session_service.create_session(
app_name=app_name, user_id=user_id
)

content = types.Content(
role='user',
parts=[types.Part.from_text(text='write python code that computes 2+2.')]
)

print('Sending request to agent...')
event_count = 0
max_events = 20 # Prevent true infinite loop in test

async for event in runner.run_async(
user_id=user_id,
session_id=session.id,
new_message=content,
):
event_count += 1
print(f'Event {event_count}: {event.author} - {type(event.content).__name__ if event.content else "no content"}')

if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
print(f' Text: {part.text[:100]}...' if len(part.text) > 100 else f' Text: {part.text}')
if part.executable_code:
print(f' Code: {part.executable_code.code[:100]}...')
if part.code_execution_result:
print(f' Result: {part.code_execution_result.output[:100] if part.code_execution_result.output else "empty"}')

if event_count >= max_events:
print(f'ERROR: Reached {max_events} events - likely infinite loop!')
return False

print(f'\nTest completed successfully with {event_count} events')
return event_count < max_events


if __name__ == '__main__':
import sys
success = asyncio.run(test_code_executor())
if success:
print('\n✓ Test PASSED - No infinite loop detected')
else:
print('\n✗ Test FAILED - Infinite loop detected')
sys.exit(1)
80 changes: 80 additions & 0 deletions tests/unittests/flows/llm_flows/test_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,83 @@ async def test_events_with_empty_content_are_skipped():
role="user",
),
]


@pytest.mark.asyncio
async def test_code_execution_events_are_not_skipped():
"""Test that events with executable_code or code_execution_result are included.

This test ensures that code execution events are properly included in the
LLM context and not filtered out as empty content, which would cause an
infinite loop (issue #3921).
"""
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 compute 2+2"),
),
# Event with executable code (should NOT be skipped)
Event(
invocation_id="inv2",
author="test_agent",
content=types.Content(
parts=[
types.Part(
executable_code=types.ExecutableCode(
code="print(2+2)",
language="PYTHON",
)
)
],
role="model",
),
),
# Event with code execution result (should NOT be skipped)
Event(
invocation_id="inv3",
author="test_agent",
content=types.Content(
parts=[
types.Part(
code_execution_result=types.CodeExecutionResult(
output="4",
outcome=types.Outcome.OUTCOME_OK,
)
)
],
role="model",
),
),
Event(
invocation_id="inv4",
author="test_agent",
content=types.ModelContent("The result is 4"),
),
]
invocation_context.session.events = events

# Process the request
async for _ in contents.request_processor.run_async(
invocation_context, llm_request
):
pass

# Verify code execution events are included
assert len(llm_request.contents) == 4
assert llm_request.contents[0] == types.UserContent(
"Write code to compute 2+2"
)
# Check that executable_code is present
assert llm_request.contents[1].parts[0].executable_code is not None
assert llm_request.contents[1].parts[0].executable_code.code == "print(2+2)"
# Check that code_execution_result is present
assert llm_request.contents[2].parts[0].code_execution_result is not None
assert llm_request.contents[2].parts[0].code_execution_result.output == "4"
assert llm_request.contents[3] == types.ModelContent("The result is 4")