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/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/tests/integration/test_code_executor_fix.py b/tests/integration/test_code_executor_fix.py new file mode 100644 index 0000000000..0b433e7723 --- /dev/null +++ b/tests/integration/test_code_executor_fix.py @@ -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) 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")