Skip to content

Commit 2aab1cf

Browse files
Jacksunweicopybara-github
authored andcommitted
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
1 parent 96a0d4b commit 2aab1cf

File tree

4 files changed

+23
-6
lines changed

4 files changed

+23
-6
lines changed

src/google/adk/agents/loop_agent.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,15 @@ async def _run_async_impl(
5353
times_looped = 0
5454
while not self.max_iterations or times_looped < self.max_iterations:
5555
for sub_agent in self.sub_agents:
56+
should_exit = False
5657
async for event in sub_agent.run_async(ctx):
5758
yield event
5859
if event.actions.escalate:
59-
return
60+
should_exit = True
61+
62+
if should_exit:
63+
return
64+
6065
times_looped += 1
6166
return
6267

src/google/adk/tools/exit_loop_tool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ def exit_loop(tool_context: ToolContext):
2121
Call this function only when you are instructed to do so.
2222
"""
2323
tool_context.actions.escalate = True
24+
tool_context.actions.skip_summarization = True

tests/unittests/agents/test_loop_agent.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ async def _run_async_impl(
6868
),
6969
actions=EventActions(escalate=True),
7070
)
71+
yield Event(
72+
author=self.name,
73+
invocation_id=ctx.invocation_id,
74+
content=types.Content(
75+
parts=[types.Part(text=f'I have done my job after escalation!!')]
76+
),
77+
)
7178

7279

7380
async def _create_parent_invocation_context(
@@ -115,17 +122,20 @@ async def test_run_async_with_escalate_action(request: pytest.FixtureRequest):
115122
escalating_agent = _TestingAgentWithEscalateAction(
116123
name=f'{request.function.__name__}_test_escalating_agent'
117124
)
125+
ignored_agent = _TestingAgent(
126+
name=f'{request.function.__name__}_test_ignored_agent'
127+
)
118128
loop_agent = LoopAgent(
119129
name=f'{request.function.__name__}_test_loop_agent',
120-
sub_agents=[non_escalating_agent, escalating_agent],
130+
sub_agents=[non_escalating_agent, escalating_agent, ignored_agent],
121131
)
122132
parent_ctx = await _create_parent_invocation_context(
123133
request.function.__name__, loop_agent
124134
)
125135
events = [e async for e in loop_agent.run_async(parent_ctx)]
126136

127137
# Only two events are generated because the sub escalating_agent escalates.
128-
assert len(events) == 2
138+
assert len(events) == 3
129139
assert events[0].author == non_escalating_agent.name
130140
assert events[1].author == escalating_agent.name
131141
assert events[0].content.parts[0].text == (
@@ -134,3 +144,6 @@ async def test_run_async_with_escalate_action(request: pytest.FixtureRequest):
134144
assert events[1].content.parts[0].text == (
135145
f'Hello, async {escalating_agent.name}!'
136146
)
147+
assert (
148+
events[2].content.parts[0].text == 'I have done my job after escalation!!'
149+
)

tests/unittests/flows/llm_flows/test_agent_transfer.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,9 @@ def test_auto_to_loop():
303303
name='exit_loop', response={'result': None}
304304
),
305305
),
306-
# root_agent summarizes.
307-
('root_agent', 'response4'),
308306
]
309307

310308
# root_agent should still be the current agent because sub_agent_1 is loop.
311309
assert testing_utils.simplify_events(runner.run('test2')) == [
312-
('root_agent', 'response5'),
310+
('root_agent', 'response4'),
313311
]

0 commit comments

Comments
 (0)