Skip to content

Commit 43eec82

Browse files
GWealecopybara-github
authored andcommitted
fix: Add a NOTE to agent transfer instructions listing available agents
The system instructions for agent transfer now include a NOTE section that lists all agents available for the `transfer_to_agent` function. This also has the target agents and, if there is one that applies, the parent agent. New unit tests are added to verify the correct generation of this NOTE. PiperOrigin-RevId: 804569691
1 parent 5b465fd commit 43eec82

File tree

2 files changed

+310
-3
lines changed

2 files changed

+310
-3
lines changed

src/google/adk/flows/llm_flows/agent_transfer.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ def _build_target_agents_info(target_agent: BaseAgent) -> str:
8282
def _build_target_agents_instructions(
8383
agent: LlmAgent, target_agents: list[BaseAgent]
8484
) -> str:
85+
# Build list of available agent names for the NOTE
86+
# target_agents already includes parent agent if applicable, so no need to add it again
87+
available_agent_names = [target_agent.name for target_agent in target_agents]
88+
89+
# Sort for consistency
90+
available_agent_names.sort()
91+
92+
# Format agent names with backticks for clarity
93+
formatted_agent_names = ', '.join(
94+
f'`{name}`' for name in available_agent_names
95+
)
96+
8597
si = f"""
8698
You have a list of other agents to transfer to:
8799
@@ -96,13 +108,13 @@ def _build_target_agents_instructions(
96108
description, call `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function to transfer the
97109
question to that agent. When transferring, do not generate any text other than
98110
the function call.
111+
112+
**NOTE**: the only available agents for `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function are {formatted_agent_names}.
99113
"""
100114

101115
if agent.parent_agent and not agent.disallow_transfer_to_parent:
102116
si += f"""
103-
Your parent agent is {agent.parent_agent.name}. If neither the other agents nor
104-
you are best for answering the question according to the descriptions, transfer
105-
to your parent agent.
117+
If neither you nor the other agents are best for the question, transfer to your parent agent {agent.parent_agent.name}.
106118
"""
107119
return si
108120

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Behavioral tests for agent transfer system instructions.
16+
17+
These tests verify the behavior of the agent transfer system by calling
18+
the request processor and checking the resulting system instructions not just
19+
implementation.
20+
"""
21+
22+
from google.adk.agents.invocation_context import InvocationContext
23+
from google.adk.agents.llm_agent import Agent
24+
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
25+
from google.adk.flows.llm_flows import agent_transfer
26+
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
27+
from google.adk.models.llm_request import LlmRequest
28+
from google.adk.plugins.plugin_manager import PluginManager
29+
from google.adk.runners import RunConfig
30+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
31+
from google.genai import types
32+
import pytest
33+
34+
from ... import testing_utils
35+
36+
37+
async def create_test_invocation_context(agent: Agent) -> InvocationContext:
38+
"""Helper to create constructed InvocationContext."""
39+
session_service = InMemorySessionService()
40+
memory_service = InMemoryMemoryService()
41+
session = await session_service.create_session(
42+
app_name='test_app', user_id='test_user'
43+
)
44+
45+
return InvocationContext(
46+
artifact_service=InMemoryArtifactService(),
47+
session_service=session_service,
48+
memory_service=memory_service,
49+
plugin_manager=PluginManager(plugins=[]),
50+
invocation_id='test_invocation_id',
51+
agent=agent,
52+
session=session,
53+
user_content=types.Content(
54+
role='user', parts=[types.Part.from_text(text='test')]
55+
),
56+
run_config=RunConfig(),
57+
)
58+
59+
60+
@pytest.mark.asyncio
61+
async def test_agent_transfer_includes_sorted_agent_names_in_system_instructions():
62+
"""Test that agent transfer adds NOTE with sorted agent names to system instructions."""
63+
mockModel = testing_utils.MockModel.create(responses=[])
64+
65+
# Create agents with names that will test alphabetical sorting
66+
z_agent = Agent(name='z_agent', model=mockModel, description='Last agent')
67+
a_agent = Agent(name='a_agent', model=mockModel, description='First agent')
68+
m_agent = Agent(name='m_agent', model=mockModel, description='Middle agent')
69+
peer_agent = Agent(
70+
name='peer_agent', model=mockModel, description='Peer agent'
71+
)
72+
73+
# Create parent agent with a peer agent
74+
parent_agent = Agent(
75+
name='parent_agent',
76+
model=mockModel,
77+
sub_agents=[peer_agent],
78+
description='Parent agent',
79+
)
80+
81+
# Create main agent with sub-agents and parent (intentionally unsorted order)
82+
main_agent = Agent(
83+
name='main_agent',
84+
model=mockModel,
85+
sub_agents=[z_agent, a_agent, m_agent], # Unsorted input
86+
parent_agent=parent_agent,
87+
description='Main coordinating agent',
88+
)
89+
90+
# Create test context and LLM request
91+
invocation_context = await create_test_invocation_context(main_agent)
92+
llm_request = LlmRequest()
93+
94+
# Call the actual agent transfer request processor (this behavior we're testing)
95+
async for _ in agent_transfer.request_processor.run_async(
96+
invocation_context, llm_request
97+
):
98+
pass
99+
100+
# Check on the behavior: verify system instructions contain sorted agent names
101+
instructions = llm_request.config.system_instruction
102+
103+
# The NOTE should contain agents in alphabetical order: sub-agents + parent + peers
104+
expected_content = """\
105+
106+
You have a list of other agents to transfer to:
107+
108+
109+
Agent name: z_agent
110+
Agent description: Last agent
111+
112+
113+
Agent name: a_agent
114+
Agent description: First agent
115+
116+
117+
Agent name: m_agent
118+
Agent description: Middle agent
119+
120+
121+
Agent name: parent_agent
122+
Agent description: Parent agent
123+
124+
125+
Agent name: peer_agent
126+
Agent description: Peer agent
127+
128+
129+
If you are the best to answer the question according to your description, you
130+
can answer it.
131+
132+
If another agent is better for answering the question according to its
133+
description, call `transfer_to_agent` function to transfer the
134+
question to that agent. When transferring, do not generate any text other than
135+
the function call.
136+
137+
**NOTE**: the only available agents for `transfer_to_agent` function are `a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`.
138+
139+
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
140+
141+
assert expected_content in instructions
142+
143+
144+
@pytest.mark.asyncio
145+
async def test_agent_transfer_system_instructions_without_parent():
146+
"""Test system instructions when agent has no parent."""
147+
mockModel = testing_utils.MockModel.create(responses=[])
148+
149+
# Create agents without parent
150+
sub_agent_1 = Agent(
151+
name='agent1', model=mockModel, description='First sub-agent'
152+
)
153+
sub_agent_2 = Agent(
154+
name='agent2', model=mockModel, description='Second sub-agent'
155+
)
156+
157+
main_agent = Agent(
158+
name='main_agent',
159+
model=mockModel,
160+
sub_agents=[sub_agent_1, sub_agent_2],
161+
# No parent_agent
162+
description='Main agent without parent',
163+
)
164+
165+
# Create test context and LLM request
166+
invocation_context = await create_test_invocation_context(main_agent)
167+
llm_request = LlmRequest()
168+
169+
# Call the agent transfer request processor
170+
async for _ in agent_transfer.request_processor.run_async(
171+
invocation_context, llm_request
172+
):
173+
pass
174+
175+
# Assert behavior: should only include sub-agents in NOTE, no parent
176+
instructions = llm_request.config.system_instruction
177+
178+
# Direct multiline string assertion showing the exact expected content
179+
expected_content = """\
180+
181+
You have a list of other agents to transfer to:
182+
183+
184+
Agent name: agent1
185+
Agent description: First sub-agent
186+
187+
188+
Agent name: agent2
189+
Agent description: Second sub-agent
190+
191+
192+
If you are the best to answer the question according to your description, you
193+
can answer it.
194+
195+
If another agent is better for answering the question according to its
196+
description, call `transfer_to_agent` function to transfer the
197+
question to that agent. When transferring, do not generate any text other than
198+
the function call.
199+
200+
**NOTE**: the only available agents for `transfer_to_agent` function are `agent1`, `agent2`."""
201+
202+
assert expected_content in instructions
203+
204+
205+
@pytest.mark.asyncio
206+
async def test_agent_transfer_simplified_parent_instructions():
207+
"""Test that parent agent instructions are simplified and not verbose."""
208+
mockModel = testing_utils.MockModel.create(responses=[])
209+
210+
# Create agent with parent
211+
sub_agent = Agent(name='sub_agent', model=mockModel, description='Sub agent')
212+
parent_agent = Agent(
213+
name='parent_agent', model=mockModel, description='Parent agent'
214+
)
215+
216+
main_agent = Agent(
217+
name='main_agent',
218+
model=mockModel,
219+
sub_agents=[sub_agent],
220+
parent_agent=parent_agent,
221+
description='Main agent with parent',
222+
)
223+
224+
# Create test context and LLM request
225+
invocation_context = await create_test_invocation_context(main_agent)
226+
llm_request = LlmRequest()
227+
228+
# Call the agent transfer request processor
229+
async for _ in agent_transfer.request_processor.run_async(
230+
invocation_context, llm_request
231+
):
232+
pass
233+
234+
# Assert behavior: parent instructions should be simplified
235+
instructions = llm_request.config.system_instruction
236+
237+
# Direct multiline string assertion showing the exact expected content
238+
expected_content = """\
239+
240+
You have a list of other agents to transfer to:
241+
242+
243+
Agent name: sub_agent
244+
Agent description: Sub agent
245+
246+
247+
Agent name: parent_agent
248+
Agent description: Parent agent
249+
250+
251+
If you are the best to answer the question according to your description, you
252+
can answer it.
253+
254+
If another agent is better for answering the question according to its
255+
description, call `transfer_to_agent` function to transfer the
256+
question to that agent. When transferring, do not generate any text other than
257+
the function call.
258+
259+
**NOTE**: the only available agents for `transfer_to_agent` function are `parent_agent`, `sub_agent`.
260+
261+
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
262+
263+
assert expected_content in instructions
264+
265+
266+
@pytest.mark.asyncio
267+
async def test_agent_transfer_no_instructions_when_no_transfer_targets():
268+
"""Test that no instructions are added when there are no transfer targets."""
269+
mockModel = testing_utils.MockModel.create(responses=[])
270+
271+
# Create agent with no sub-agents and no parent
272+
main_agent = Agent(
273+
name='main_agent',
274+
model=mockModel,
275+
# No sub_agents, no parent_agent
276+
description='Isolated agent',
277+
)
278+
279+
# Create test context and LLM request
280+
invocation_context = await create_test_invocation_context(main_agent)
281+
llm_request = LlmRequest()
282+
original_system_instruction = llm_request.config.system_instruction
283+
284+
# Call the agent transfer request processor
285+
async for _ in agent_transfer.request_processor.run_async(
286+
invocation_context, llm_request
287+
):
288+
pass
289+
290+
# Assert behavior: no instructions should be added
291+
assert llm_request.config.system_instruction == original_system_instruction
292+
293+
instructions = llm_request.config.system_instruction or ''
294+
assert '**NOTE**:' not in instructions
295+
assert 'transfer_to_agent' not in instructions

0 commit comments

Comments
 (0)