Skip to content

Commit 3494d9f

Browse files
committed
feat: add agent.clone() method
1 parent ffa9b36 commit 3494d9f

File tree

8 files changed

+304
-0
lines changed

8 files changed

+304
-0
lines changed

src/google/adk/agents/base_agent.py

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

1515
from __future__ import annotations
1616

17+
import copy
1718
import inspect
1819
from typing import Any
1920
from typing import AsyncGenerator
@@ -121,6 +122,33 @@ class BaseAgent(BaseModel):
121122
response and appended to event history as agent response.
122123
"""
123124

125+
def clone(self, name: Optional[str] = None) -> BaseAgent:
126+
"""Creates a deep copy of this agent instance.
127+
128+
The cloned agent will have no parent and cloned sub-agents to avoid the restriction
129+
where an agent can only be a sub-agent once.
130+
131+
Args:
132+
name: Optional new name for the cloned agent. If not provided, the original
133+
name will be used with a suffix to ensure uniqueness.
134+
135+
Returns:
136+
A new instance of the same agent class with identical configuration but with
137+
no parent and cloned sub-agents.
138+
"""
139+
cloned_agent = copy.deepcopy(self)
140+
141+
# Reset parent and clone sub-agents to avoid having the same agent object in
142+
# the tree twice
143+
cloned_agent.parent_agent = None
144+
cloned_agent.sub_agents = [
145+
sub_agent.clone() for sub_agent in self.sub_agents
146+
]
147+
148+
cloned_agent.name = name or f'{self.name}_clone'
149+
150+
return cloned_agent
151+
124152
@final
125153
async def run_async(
126154
self,

src/google/adk/agents/langgraph_agent.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
from typing import AsyncGenerator
18+
from typing import cast
19+
from typing import Optional
1620
from typing import Union
1721

1822
from google.genai import types
@@ -59,6 +63,23 @@ class LangGraphAgent(BaseAgent):
5963

6064
instruction: str = ''
6165

66+
@override
67+
def clone(self, name: Optional[str] = None) -> 'LangGraphAgent':
68+
"""Creates a deep copy of this LangGraphAgent instance.
69+
70+
The cloned agent will have no parent and cloned sub-agents to avoid the restriction
71+
where an agent can only be a sub-agent once.
72+
73+
Args:
74+
name: Optional new name for the cloned agent. If not provided, the original
75+
name will be used with a suffix to ensure uniqueness.
76+
77+
Returns:
78+
A new LangGraphAgent instance with identical configuration but with
79+
no parent and cloned sub-agents.
80+
"""
81+
return cast(LangGraphAgent, super().clone(name))
82+
6283
@override
6384
async def _run_async_impl(
6485
self,

src/google/adk/agents/llm_agent.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import AsyncGenerator
2121
from typing import Awaitable
2222
from typing import Callable
23+
from typing import cast
2324
from typing import Literal
2425
from typing import Optional
2526
from typing import Union
@@ -511,5 +512,21 @@ def __validate_generate_content_config(
511512
)
512513
return generate_content_config
513514

515+
@override
516+
def clone(self, name: Optional[str] = None) -> LlmAgent:
517+
"""Creates a deep copy of this LlmAgent instance.
518+
519+
The cloned agent will have no parent and no sub-agents to avoid the restriction
520+
where an agent can only be a sub-agent once.
521+
522+
Args:
523+
name: Optional new name for the cloned agent. If not provided, the original
524+
name will be used with a suffix to ensure uniqueness.
525+
526+
Returns:
527+
A new LlmAgent instance with identical configuration but no parent or sub-agents.
528+
"""
529+
return cast(LlmAgent, super().clone(name))
530+
514531

515532
Agent: TypeAlias = LlmAgent

src/google/adk/agents/loop_agent.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from __future__ import annotations
1818

1919
from typing import AsyncGenerator
20+
from typing import cast
2021
from typing import Optional
2122

2223
from typing_extensions import override
@@ -40,6 +41,23 @@ class LoopAgent(BaseAgent):
4041
escalates.
4142
"""
4243

44+
@override
45+
def clone(self, name: Optional[str] = None) -> LoopAgent:
46+
"""Creates a deep copy of this LoopAgent instance.
47+
48+
The cloned agent will have no parent and cloned sub-agents to avoid the restriction
49+
where an agent can only be a sub-agent once.
50+
51+
Args:
52+
name: Optional new name for the cloned agent. If not provided, the original
53+
name will be used with a suffix to ensure uniqueness.
54+
55+
Returns:
56+
A new LoopAgent instance with identical configuration but with
57+
no parent and cloned sub-agents.
58+
"""
59+
return cast(LoopAgent, super().clone(name))
60+
4361
@override
4462
async def _run_async_impl(
4563
self, ctx: InvocationContext

src/google/adk/agents/parallel_agent.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import asyncio
2020
from typing import AsyncGenerator
21+
from typing import cast
22+
from typing import Optional
2123

2224
from typing_extensions import override
2325

@@ -92,6 +94,23 @@ class ParallelAgent(BaseAgent):
9294
- Generating multiple responses for review by a subsequent evaluation agent.
9395
"""
9496

97+
@override
98+
def clone(self, name: Optional[str] = None) -> ParallelAgent:
99+
"""Creates a deep copy of this ParallelAgent instance.
100+
101+
The cloned agent will have no parent and cloned sub-agents to avoid the restriction
102+
where an agent can only be a sub-agent once.
103+
104+
Args:
105+
name: Optional new name for the cloned agent. If not provided, the original
106+
name will be used with a suffix to ensure uniqueness.
107+
108+
Returns:
109+
A new ParallelAgent instance with identical configuration but with
110+
no parent and cloned sub-agents.
111+
"""
112+
return cast(ParallelAgent, super().clone(name))
113+
95114
@override
96115
async def _run_async_impl(
97116
self, ctx: InvocationContext

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@
1414

1515
from __future__ import annotations
1616

17+
import asyncio
18+
import copy
1719
import json
1820
import logging
21+
import os
1922
from pathlib import Path
23+
import time
2024
from typing import Any
2125
from typing import AsyncGenerator
26+
from typing import Awaitable
27+
from typing import Callable
28+
from typing import cast
2229
from typing import Optional
30+
from typing import TYPE_CHECKING
2331
from typing import Union
2432
from urllib.parse import urlparse
2533
import uuid
@@ -48,6 +56,7 @@
4856

4957
from google.genai import types as genai_types
5058
import httpx
59+
from typing_extensions import override
5160

5261
from ..a2a.converters.event_converter import convert_a2a_message_to_event
5362
from ..a2a.converters.event_converter import convert_a2a_task_to_event
@@ -151,6 +160,23 @@ def __init__(
151160
f"got {type(agent_card)}"
152161
)
153162

163+
@override
164+
def clone(self, name: Optional[str] = None) -> "RemoteA2aAgent":
165+
"""Creates a deep copy of this RemoteA2aAgent instance.
166+
167+
The cloned agent will have no parent and cloned sub-agents to avoid the restriction
168+
where an agent can only be a sub-agent once.
169+
170+
Args:
171+
name: Optional new name for the cloned agent. If not provided, the original
172+
name will be used with a suffix to ensure uniqueness.
173+
174+
Returns:
175+
A new RemoteA2aAgent instance with identical configuration but with
176+
no parent and cloned sub-agents.
177+
"""
178+
return cast(RemoteA2aAgent, super().clone(name))
179+
154180
async def _ensure_httpx_client(self) -> httpx.AsyncClient:
155181
"""Ensure HTTP client is available and properly configured."""
156182
if not self._httpx_client:

src/google/adk/agents/sequential_agent.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from __future__ import annotations
1818

1919
from typing import AsyncGenerator
20+
from typing import cast
21+
from typing import Optional
2022

2123
from typing_extensions import override
2224

@@ -29,6 +31,23 @@
2931
class SequentialAgent(BaseAgent):
3032
"""A shell agent that runs its sub-agents in sequence."""
3133

34+
@override
35+
def clone(self, name: Optional[str] = None) -> SequentialAgent:
36+
"""Creates a deep copy of this SequentialAgent instance.
37+
38+
The cloned agent will have no parent and cloned sub-agents to avoid the restriction
39+
where an agent can only be a sub-agent once.
40+
41+
Args:
42+
name: Optional new name for the cloned agent. If not provided, the original
43+
name will be used with a suffix to ensure uniqueness.
44+
45+
Returns:
46+
A new SequentialAgent instance with identical configuration but with
47+
no parent and cloned sub-agents.
48+
"""
49+
return cast(SequentialAgent, super().clone(name))
50+
3251
@override
3352
async def _run_async_impl(
3453
self, ctx: InvocationContext

0 commit comments

Comments
 (0)