Skip to content

feat: add agent.clone() method #1707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/google/adk/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import copy
import inspect
from typing import Any
from typing import AsyncGenerator
Expand Down Expand Up @@ -121,6 +122,33 @@ class BaseAgent(BaseModel):
response and appended to event history as agent response.
"""

def clone(self, name: Optional[str] = None) -> BaseAgent:
"""Creates a deep copy of this agent instance.

The cloned agent will have no parent and cloned sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new instance of the same agent class with identical configuration but with
no parent and cloned sub-agents.
"""
cloned_agent = copy.deepcopy(self)

# Reset parent and clone sub-agents to avoid having the same agent object in
# the tree twice
cloned_agent.parent_agent = None
cloned_agent.sub_agents = [
sub_agent.clone() for sub_agent in self.sub_agents
]

cloned_agent.name = name or f'{self.name}_clone'

return cloned_agent

@final
async def run_async(
self,
Expand Down
21 changes: 21 additions & 0 deletions src/google/adk/agents/langgraph_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from typing import AsyncGenerator
from typing import cast
from typing import Optional
from typing import Union

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

instruction: str = ''

@override
def clone(self, name: Optional[str] = None) -> 'LangGraphAgent':
"""Creates a deep copy of this LangGraphAgent instance.

The cloned agent will have no parent and cloned sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new LangGraphAgent instance with identical configuration but with
no parent and cloned sub-agents.
"""
return cast(LangGraphAgent, super().clone(name))

@override
async def _run_async_impl(
self,
Expand Down
17 changes: 17 additions & 0 deletions src/google/adk/agents/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from typing import AsyncGenerator
from typing import Awaitable
from typing import Callable
from typing import cast
from typing import Literal
from typing import Optional
from typing import Union
Expand Down Expand Up @@ -516,5 +517,21 @@ def __validate_generate_content_config(
)
return generate_content_config

@override
def clone(self, name: Optional[str] = None) -> LlmAgent:
"""Creates a deep copy of this LlmAgent instance.

The cloned agent will have no parent and no sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new LlmAgent instance with identical configuration but no parent or sub-agents.
"""
return cast(LlmAgent, super().clone(name))


Agent: TypeAlias = LlmAgent
18 changes: 18 additions & 0 deletions src/google/adk/agents/loop_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from __future__ import annotations

from typing import AsyncGenerator
from typing import cast
from typing import Optional

from typing_extensions import override
Expand All @@ -40,6 +41,23 @@ class LoopAgent(BaseAgent):
escalates.
"""

@override
def clone(self, name: Optional[str] = None) -> LoopAgent:
"""Creates a deep copy of this LoopAgent instance.

The cloned agent will have no parent and cloned sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new LoopAgent instance with identical configuration but with
no parent and cloned sub-agents.
"""
return cast(LoopAgent, super().clone(name))

@override
async def _run_async_impl(
self, ctx: InvocationContext
Expand Down
19 changes: 19 additions & 0 deletions src/google/adk/agents/parallel_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import asyncio
from typing import AsyncGenerator
from typing import cast
from typing import Optional

from typing_extensions import override

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

@override
def clone(self, name: Optional[str] = None) -> ParallelAgent:
"""Creates a deep copy of this ParallelAgent instance.

The cloned agent will have no parent and cloned sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new ParallelAgent instance with identical configuration but with
no parent and cloned sub-agents.
"""
return cast(ParallelAgent, super().clone(name))

@override
async def _run_async_impl(
self, ctx: InvocationContext
Expand Down
26 changes: 26 additions & 0 deletions src/google/adk/agents/remote_a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@

from __future__ import annotations

import asyncio
import copy
import json
import logging
import os
from pathlib import Path
import time
from typing import Any
from typing import AsyncGenerator
from typing import Awaitable
from typing import Callable
from typing import cast
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from urllib.parse import urlparse
import uuid
Expand Down Expand Up @@ -48,6 +56,7 @@

from google.genai import types as genai_types
import httpx
from typing_extensions import override

from ..a2a.converters.event_converter import convert_a2a_message_to_event
from ..a2a.converters.event_converter import convert_a2a_task_to_event
Expand Down Expand Up @@ -151,6 +160,23 @@ def __init__(
f"got {type(agent_card)}"
)

@override
def clone(self, name: Optional[str] = None) -> "RemoteA2aAgent":
"""Creates a deep copy of this RemoteA2aAgent instance.

The cloned agent will have no parent and cloned sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new RemoteA2aAgent instance with identical configuration but with
no parent and cloned sub-agents.
"""
return cast(RemoteA2aAgent, super().clone(name))

async def _ensure_httpx_client(self) -> httpx.AsyncClient:
"""Ensure HTTP client is available and properly configured."""
if not self._httpx_client:
Expand Down
19 changes: 19 additions & 0 deletions src/google/adk/agents/sequential_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from __future__ import annotations

from typing import AsyncGenerator
from typing import cast
from typing import Optional

from typing_extensions import override

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

@override
def clone(self, name: Optional[str] = None) -> SequentialAgent:
"""Creates a deep copy of this SequentialAgent instance.

The cloned agent will have no parent and cloned sub-agents to avoid the restriction
where an agent can only be a sub-agent once.

Args:
name: Optional new name for the cloned agent. If not provided, the original
name will be used with a suffix to ensure uniqueness.

Returns:
A new SequentialAgent instance with identical configuration but with
no parent and cloned sub-agents.
"""
return cast(SequentialAgent, super().clone(name))

@override
async def _run_async_impl(
self, ctx: InvocationContext
Expand Down
Loading
Loading