fix: allows current sub-agent to finish execution before exiting the loop agent due to a sub-agent's escalation. This commit also disables the summarization for exit_loop tool

Fixes #423

Related to #1670

- This avoids the `GeneratorExit` error thrown, which would crash OTel metric collection and cause `Failed to detach context` error.
- This also allows all function  calls are processed when exit_loop is called together with other tools in the same LLmResponse.

A sample agent for testing:

```
from google.adk import Agent
from google.adk.agents.loop_agent import LoopAgent
from google.adk.tools.exit_loop_tool import exit_loop

worker_1 = Agent(
    name='worker_1',
    description='Worker 1',
    instruction="""\
Just say job #1 is done.

If job #1 is said to be done. Call exit_loop tool.""",
    tools=[exit_loop],
)

worker_2 = Agent(
    name='worker_2',
    description='Worker 2',
    instruction="""\
Just say job #2 is done.

If job #2 is said to be done. Call exit_loop tool.""",
    tools=[exit_loop],
)

work_agent = LoopAgent(
    name='work_agent',
    description='Do all work.',
    sub_agents=[worker_1, worker_2],
    max_iterations=5,
)

root_agent = Agent(
    model='gemini-2.0-flash',
    name='hello_world_agent',
    description='hello world agent that can roll a  check prime',
    instruction="""Hand off works to sub agents.""",
    sub_agents=[work_agent],
)
```
PiperOrigin-RevId: 785538101
This commit is contained in:
Wei Sun (Jack)
2025-07-21 12:42:37 -07:00
committed by Copybara-Service
parent 96a0d4b02c
commit 2aab1cf98e
4 changed files with 23 additions and 6 deletions
+6 -1
View File
@@ -53,10 +53,15 @@ class LoopAgent(BaseAgent):
times_looped = 0
while not self.max_iterations or times_looped < self.max_iterations:
for sub_agent in self.sub_agents:
should_exit = False
async for event in sub_agent.run_async(ctx):
yield event
if event.actions.escalate:
return
should_exit = True
if should_exit:
return
times_looped += 1
return
+1
View File
@@ -21,3 +21,4 @@ def exit_loop(tool_context: ToolContext):
Call this function only when you are instructed to do so.
"""
tool_context.actions.escalate = True
tool_context.actions.skip_summarization = True
+15 -2
View File
@@ -68,6 +68,13 @@ class _TestingAgentWithEscalateAction(BaseAgent):
),
actions=EventActions(escalate=True),
)
yield Event(
author=self.name,
invocation_id=ctx.invocation_id,
content=types.Content(
parts=[types.Part(text=f'I have done my job after escalation!!')]
),
)
async def _create_parent_invocation_context(
@@ -115,9 +122,12 @@ async def test_run_async_with_escalate_action(request: pytest.FixtureRequest):
escalating_agent = _TestingAgentWithEscalateAction(
name=f'{request.function.__name__}_test_escalating_agent'
)
ignored_agent = _TestingAgent(
name=f'{request.function.__name__}_test_ignored_agent'
)
loop_agent = LoopAgent(
name=f'{request.function.__name__}_test_loop_agent',
sub_agents=[non_escalating_agent, escalating_agent],
sub_agents=[non_escalating_agent, escalating_agent, ignored_agent],
)
parent_ctx = await _create_parent_invocation_context(
request.function.__name__, loop_agent
@@ -125,7 +135,7 @@ async def test_run_async_with_escalate_action(request: pytest.FixtureRequest):
events = [e async for e in loop_agent.run_async(parent_ctx)]
# Only two events are generated because the sub escalating_agent escalates.
assert len(events) == 2
assert len(events) == 3
assert events[0].author == non_escalating_agent.name
assert events[1].author == escalating_agent.name
assert events[0].content.parts[0].text == (
@@ -134,3 +144,6 @@ async def test_run_async_with_escalate_action(request: pytest.FixtureRequest):
assert events[1].content.parts[0].text == (
f'Hello, async {escalating_agent.name}!'
)
assert (
events[2].content.parts[0].text == 'I have done my job after escalation!!'
)
@@ -303,11 +303,9 @@ def test_auto_to_loop():
name='exit_loop', response={'result': None}
),
),
# root_agent summarizes.
('root_agent', 'response4'),
]
# root_agent should still be the current agent because sub_agent_1 is loop.
assert testing_utils.simplify_events(runner.run('test2')) == [
('root_agent', 'response5'),
('root_agent', 'response4'),
]