Skip to content
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
17 changes: 12 additions & 5 deletions src/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,11 +548,18 @@ def __post_init__(self):
def clone(self, **kwargs: Any) -> Agent[TContext]:
"""Make a copy of the agent, with the given arguments changed.
Notes:
- Uses `dataclasses.replace`, which performs a **shallow copy**.
- Mutable attributes like `tools` and `handoffs` are shallow-copied:
new list objects are created only if overridden, but their contents
(tool functions and handoff objects) are shared with the original.
- To modify these independently, pass new lists when calling `clone()`.
- Uses `dataclasses.replace`, which performs a **shallow copy** and never copies a
list attribute such as `tools`, `handoffs`, `mcp_servers`, `input_guardrails`, or
`output_guardrails`. Each of those attributes is whatever the merged arguments hold.
- An attribute you do not pass arrives as the original agent's own list, so both
agents hold that one list and its entries. Appending through either agent, for
example `cloned.tools.append(extra_tool)`, therefore also changes the other.
- An attribute you do pass is used exactly as given, so it shares a list or an entry
with the original agent only where you reused one. `agent.clone(tools=agent.tools)`
still shares that list, while `agent.clone(tools=[other_tool])` shares nothing.
- To give the clone a list that no other agent holds, pass a new one, for example
`agent.clone(tools=[*agent.tools, extra_tool])`. The entries copied into it remain
the same objects the original agent holds.
Example:
```python
new_agent = agent.clone(instructions="New instructions")
Expand Down
17 changes: 12 additions & 5 deletions src/agents/realtime/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,18 @@ def clone(self, **kwargs: Any) -> RealtimeAgent[TContext]:
"""Make a copy of the agent, with the given arguments changed.

Notes:
- Uses `dataclasses.replace`, which performs a **shallow copy**.
- Mutable attributes like `tools` and `handoffs` are shallow-copied:
new list objects are created only if overridden, but their contents
(tool functions and handoff objects) are shared with the original.
- To modify these independently, pass new lists when calling `clone()`.
- Uses `dataclasses.replace`, which performs a **shallow copy** and never copies a
list attribute such as `tools`, `handoffs`, `mcp_servers`, or `output_guardrails`.
Each of those attributes is whatever the merged arguments hold.
- An attribute you do not pass arrives as the original agent's own list, so both
agents hold that one list and its entries. Appending through either agent, for
example `cloned.tools.append(extra_tool)`, therefore also changes the other.
- An attribute you do pass is used exactly as given, so it shares a list or an entry
with the original agent only where you reused one. `agent.clone(tools=agent.tools)`
still shares that list, while `agent.clone(tools=[other_tool])` shares nothing.
- To give the clone a list that no other agent holds, pass a new one, for example
`agent.clone(tools=[*agent.tools, extra_tool])`. The entries copied into it remain
the same objects the original agent holds.

Example:
```python
Expand Down
49 changes: 49 additions & 0 deletions tests/test_agent_clone_shallow_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,52 @@ def test_agent_clone_shallow_copy():
assert cloned.tools[0] is original.tools[0], "Tool objects should be same instance"
assert cloned.handoffs is not original.handoffs, "Handoffs should be different list"
assert cloned.handoffs[0] is original.handoffs[0], "Handoff objects should be same instance"


def test_agent_clone_keeps_list_attributes_it_is_not_given():
"""An attribute that clone() is not given arrives as the original agent's own list."""
target_agent = Agent(name="Target")
original = Agent(name="Original", tools=[greet], handoffs=[handoff(target_agent)])

cloned = original.clone(name="Cloned")

assert cloned.tools is original.tools
assert cloned.handoffs is original.handoffs


def test_agent_clone_uses_a_given_list_as_is():
"""An attribute passed to clone() is used exactly as given, entries included."""

@function_tool
def farewell(name: str) -> str:
return f"Goodbye, {name}!"

original = Agent(name="Original", tools=[greet])
supplied = [farewell]

cloned = original.clone(name="Cloned", tools=supplied)

assert cloned.tools is supplied
assert original.tools == [greet]
# Passing a list does not by itself share entries with the original agent.
assert all(tool is not greet for tool in cloned.tools)


def test_agent_clone_still_shares_when_given_the_original_list():
"""Passing the original agent's own list keeps both agents on that one list."""
original = Agent(name="Original", tools=[greet])

cloned = original.clone(name="Cloned", tools=original.tools)

assert cloned.tools is original.tools


def test_agent_clone_shared_list_mutation_affects_both_agents():
"""Appending through either agent changes the other while they hold one list."""
original = Agent(name="Original", tools=[greet])
cloned = original.clone(name="Cloned")

cloned.tools.append(greet)

assert original.tools == cloned.tools
assert len(original.tools) == 2