Skip to content

Commit 29bb75f

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: updating BaseAgent.clone() and LlmAgent.clone() to properly clone fields that are lists
PiperOrigin-RevId: 797855214
1 parent 167182b commit 29bb75f

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

src/google/adk/agents/base_agent.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ def clone(
181181

182182
cloned_agent = self.model_copy(update=update)
183183

184+
# If any field is stored as list and not provided in the update, need to
185+
# shallow copy it for the cloned agent to avoid sharing the same list object
186+
# with the original agent.
187+
for field_name in cloned_agent.__class__.model_fields:
188+
if field_name == 'sub_agents':
189+
continue
190+
if update is not None and field_name in update:
191+
continue
192+
field = getattr(cloned_agent, field_name)
193+
if isinstance(field, list):
194+
setattr(cloned_agent, field_name, field.copy())
195+
184196
if update is None or 'sub_agents' not in update:
185197
# If `sub_agents` is not provided in the update, need to recursively clone
186198
# the sub-agents to avoid sharing the sub-agents with the original agent.

tests/unittests/agents/test_agent_clone.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
"""Testings for the clone functionality of agents."""
1616

17+
from typing import Any
18+
from typing import cast
19+
from typing import Iterable
20+
1721
from google.adk.agents.llm_agent import LlmAgent
1822
from google.adk.agents.loop_agent import LoopAgent
1923
from google.adk.agents.parallel_agent import ParallelAgent
@@ -374,6 +378,189 @@ def test_clone_with_sub_agents_update():
374378
assert original.sub_agents[1].name == "original_sub2"
375379

376380

381+
def _check_lists_contain_same_contents(*lists: Iterable[list[Any]]) -> None:
382+
"""Assert that all provided lists contain the same elements."""
383+
if lists:
384+
first_list = lists[0]
385+
assert all(len(lst) == len(first_list) for lst in lists)
386+
for idx, elem in enumerate(first_list):
387+
assert all(lst[idx] is elem for lst in lists)
388+
389+
390+
def test_clone_shallow_copies_lists():
391+
"""Test that cloning shallow copies fields stored as lists."""
392+
# Define the list fields
393+
before_agent_callback = [lambda *args, **kwargs: None]
394+
after_agent_callback = [lambda *args, **kwargs: None]
395+
before_model_callback = [lambda *args, **kwargs: None]
396+
after_model_callback = [lambda *args, **kwargs: None]
397+
before_tool_callback = [lambda *args, **kwargs: None]
398+
after_tool_callback = [lambda *args, **kwargs: None]
399+
tools = [lambda *args, **kwargs: None]
400+
401+
# Create the original agent with list fields
402+
original = LlmAgent(
403+
name="original_agent",
404+
description="Original agent",
405+
before_agent_callback=before_agent_callback,
406+
after_agent_callback=after_agent_callback,
407+
before_model_callback=before_model_callback,
408+
after_model_callback=after_model_callback,
409+
before_tool_callback=before_tool_callback,
410+
after_tool_callback=after_tool_callback,
411+
tools=tools,
412+
)
413+
414+
# Clone the agent
415+
cloned = original.clone()
416+
417+
# Verify the lists are copied
418+
assert original.before_agent_callback is not cloned.before_agent_callback
419+
assert original.after_agent_callback is not cloned.after_agent_callback
420+
assert original.before_model_callback is not cloned.before_model_callback
421+
assert original.after_model_callback is not cloned.after_model_callback
422+
assert original.before_tool_callback is not cloned.before_tool_callback
423+
assert original.after_tool_callback is not cloned.after_tool_callback
424+
assert original.tools is not cloned.tools
425+
426+
# Verify the list copies are shallow
427+
_check_lists_contain_same_contents(
428+
before_agent_callback,
429+
original.before_agent_callback,
430+
cloned.before_agent_callback,
431+
)
432+
_check_lists_contain_same_contents(
433+
after_agent_callback,
434+
original.after_agent_callback,
435+
cloned.after_agent_callback,
436+
)
437+
_check_lists_contain_same_contents(
438+
before_model_callback,
439+
original.before_model_callback,
440+
cloned.before_model_callback,
441+
)
442+
_check_lists_contain_same_contents(
443+
after_model_callback,
444+
original.after_model_callback,
445+
cloned.after_model_callback,
446+
)
447+
_check_lists_contain_same_contents(
448+
before_tool_callback,
449+
original.before_tool_callback,
450+
cloned.before_tool_callback,
451+
)
452+
_check_lists_contain_same_contents(
453+
after_tool_callback,
454+
original.after_tool_callback,
455+
cloned.after_tool_callback,
456+
)
457+
_check_lists_contain_same_contents(tools, original.tools, cloned.tools)
458+
459+
460+
def test_clone_shallow_copies_lists_with_sub_agents():
461+
"""Test that cloning recursively shallow copies fields stored as lists."""
462+
# Define the list fields for the sub-agent
463+
before_agent_callback = [lambda *args, **kwargs: None]
464+
after_agent_callback = [lambda *args, **kwargs: None]
465+
before_model_callback = [lambda *args, **kwargs: None]
466+
after_model_callback = [lambda *args, **kwargs: None]
467+
before_tool_callback = [lambda *args, **kwargs: None]
468+
after_tool_callback = [lambda *args, **kwargs: None]
469+
tools = [lambda *args, **kwargs: None]
470+
471+
# Create the original sub-agent with list fields and the top-level agent
472+
sub_agents = [
473+
LlmAgent(
474+
name="sub_agent",
475+
description="Sub agent",
476+
before_agent_callback=before_agent_callback,
477+
after_agent_callback=after_agent_callback,
478+
before_model_callback=before_model_callback,
479+
after_model_callback=after_model_callback,
480+
before_tool_callback=before_tool_callback,
481+
after_tool_callback=after_tool_callback,
482+
tools=tools,
483+
)
484+
]
485+
original = LlmAgent(
486+
name="original_agent",
487+
description="Original agent",
488+
sub_agents=sub_agents,
489+
)
490+
491+
# Clone the top-level agent
492+
cloned = original.clone()
493+
494+
# Verify the sub_agents list is copied for the top-level agent
495+
assert original.sub_agents is not cloned.sub_agents
496+
497+
# Retrieve the sub-agent for the original and cloned top-level agent
498+
original_sub_agent = cast(LlmAgent, original.sub_agents[0])
499+
cloned_sub_agent = cast(LlmAgent, cloned.sub_agents[0])
500+
501+
# Verify the lists are copied for the sub-agent
502+
assert (
503+
original_sub_agent.before_agent_callback
504+
is not cloned_sub_agent.before_agent_callback
505+
)
506+
assert (
507+
original_sub_agent.after_agent_callback
508+
is not cloned_sub_agent.after_agent_callback
509+
)
510+
assert (
511+
original_sub_agent.before_model_callback
512+
is not cloned_sub_agent.before_model_callback
513+
)
514+
assert (
515+
original_sub_agent.after_model_callback
516+
is not cloned_sub_agent.after_model_callback
517+
)
518+
assert (
519+
original_sub_agent.before_tool_callback
520+
is not cloned_sub_agent.before_tool_callback
521+
)
522+
assert (
523+
original_sub_agent.after_tool_callback
524+
is not cloned_sub_agent.after_tool_callback
525+
)
526+
assert original_sub_agent.tools is not cloned_sub_agent.tools
527+
528+
# Verify the list copies are shallow for the sub-agent
529+
_check_lists_contain_same_contents(
530+
before_agent_callback,
531+
original_sub_agent.before_agent_callback,
532+
cloned_sub_agent.before_agent_callback,
533+
)
534+
_check_lists_contain_same_contents(
535+
after_agent_callback,
536+
original_sub_agent.after_agent_callback,
537+
cloned_sub_agent.after_agent_callback,
538+
)
539+
_check_lists_contain_same_contents(
540+
before_model_callback,
541+
original_sub_agent.before_model_callback,
542+
cloned_sub_agent.before_model_callback,
543+
)
544+
_check_lists_contain_same_contents(
545+
after_model_callback,
546+
original_sub_agent.after_model_callback,
547+
cloned_sub_agent.after_model_callback,
548+
)
549+
_check_lists_contain_same_contents(
550+
before_tool_callback,
551+
original_sub_agent.before_tool_callback,
552+
cloned_sub_agent.before_tool_callback,
553+
)
554+
_check_lists_contain_same_contents(
555+
after_tool_callback,
556+
original_sub_agent.after_tool_callback,
557+
cloned_sub_agent.after_tool_callback,
558+
)
559+
_check_lists_contain_same_contents(
560+
tools, original_sub_agent.tools, cloned_sub_agent.tools
561+
)
562+
563+
377564
if __name__ == "__main__":
378565
# Run a specific test for debugging
379566
test_three_level_nested_agent()

0 commit comments

Comments
 (0)