From dd6e2b814e3335ab1e93aa2ef590e091b4473ce6 Mon Sep 17 00:00:00 2001 From: Yatindra Rai Date: Wed, 17 Dec 2025 10:45:23 +0530 Subject: [PATCH 1/2] fix: prevent infinite loop in code_executor by preserving code execution events --- contributing/samples/hello_world/agent.py | 3 +- src/google/adk/flows/llm_flows/contents.py | 5 +- test_code_executor_fix.py | 75 +++++++++++++++++ .../flows/llm_flows/test_contents.py | 80 +++++++++++++++++++ 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 test_code_executor_fix.py diff --git a/contributing/samples/hello_world/agent.py b/contributing/samples/hello_world/agent.py index 95d8b989e7..e463547ae4 100755 --- a/contributing/samples/hello_world/agent.py +++ b/contributing/samples/hello_world/agent.py @@ -65,7 +65,7 @@ async def check_prime(nums: list[int]) -> str: root_agent = Agent( - model='gemini-2.0-flash', + model='gemini-pro', name='hello_world_agent', description=( 'hello world agent that can roll a dice of 8 sides and check prime' @@ -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, diff --git a/src/google/adk/flows/llm_flows/contents.py b/src/google/adk/flows/llm_flows/contents.py index fefa014c45..7c0c89008d 100644 --- a/src/google/adk/flows/llm_flows/contents.py +++ b/src/google/adk/flows/llm_flows/contents.py @@ -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. @@ -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) diff --git a/test_code_executor_fix.py b/test_code_executor_fix.py new file mode 100644 index 0000000000..c302fa3ab4 --- /dev/null +++ b/test_code_executor_fix.py @@ -0,0 +1,75 @@ +"""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 + sys.path.insert(0, 'c:\\Users\\Asus\\Downloads\\adk-python\\src') + + 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) diff --git a/tests/unittests/flows/llm_flows/test_contents.py b/tests/unittests/flows/llm_flows/test_contents.py index b2aa91dbee..abde90ba89 100644 --- a/tests/unittests/flows/llm_flows/test_contents.py +++ b/tests/unittests/flows/llm_flows/test_contents.py @@ -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") From dd66503a0f3bb32cd9d0d913c2f60748806cdbac Mon Sep 17 00:00:00 2001 From: Yatindra Rai Date: Wed, 17 Dec 2025 11:20:54 +0530 Subject: [PATCH 2/2] refactor: move infinite loop test to integration tests and fix path --- README.md | 2 ++ .../integration/test_code_executor_fix.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) rename test_code_executor_fix.py => tests/integration/test_code_executor_fix.py (97%) diff --git a/README.md b/README.md index 9046cc49a3..9e1022e353 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ + + 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 diff --git a/test_code_executor_fix.py b/tests/integration/test_code_executor_fix.py similarity index 97% rename from test_code_executor_fix.py rename to tests/integration/test_code_executor_fix.py index c302fa3ab4..0b433e7723 100644 --- a/test_code_executor_fix.py +++ b/tests/integration/test_code_executor_fix.py @@ -65,8 +65,6 @@ async def test_code_executor(): if __name__ == '__main__': import sys - sys.path.insert(0, 'c:\\Users\\Asus\\Downloads\\adk-python\\src') - success = asyncio.run(test_code_executor()) if success: print('\n✓ Test PASSED - No infinite loop detected')