Skip to content

Commit bc352cc

Browse files
Fix agent memory leak with weakref
1 parent f91b38f commit bc352cc

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

src/agents/items.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

33
import abc
4-
from dataclasses import dataclass
4+
import weakref
5+
from dataclasses import dataclass, field
56
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, Union
67

78
import pydantic
@@ -84,6 +85,22 @@ class RunItemBase(Generic[T], abc.ABC):
8485
(i.e. `openai.types.responses.ResponseInputItemParam`).
8586
"""
8687

88+
_agent_ref: weakref.ReferenceType[Agent[Any]] | None = field(
89+
init=False,
90+
repr=False,
91+
default=None,
92+
)
93+
94+
def __post_init__(self) -> None:
95+
# Store the producing agent weakly to avoid keeping it alive after the run.
96+
self._agent_ref = weakref.ref(self.agent)
97+
object.__delattr__(self, "agent")
98+
99+
def __getattr__(self, name: str) -> Any:
100+
if name == "agent":
101+
return self._agent_ref() if self._agent_ref else None
102+
raise AttributeError(name)
103+
87104
def to_input_item(self) -> TResponseInputItem:
88105
"""Converts this item into an input item suitable for passing to the model."""
89106
if isinstance(self.raw_item, dict):
@@ -131,6 +148,32 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]):
131148

132149
type: Literal["handoff_output_item"] = "handoff_output_item"
133150

151+
_source_agent_ref: weakref.ReferenceType[Agent[Any]] | None = field(
152+
init=False,
153+
repr=False,
154+
default=None,
155+
)
156+
_target_agent_ref: weakref.ReferenceType[Agent[Any]] | None = field(
157+
init=False,
158+
repr=False,
159+
default=None,
160+
)
161+
162+
def __post_init__(self) -> None:
163+
super().__post_init__()
164+
# Handoff metadata should not hold strong references to the agents either.
165+
self._source_agent_ref = weakref.ref(self.source_agent)
166+
self._target_agent_ref = weakref.ref(self.target_agent)
167+
object.__delattr__(self, "source_agent")
168+
object.__delattr__(self, "target_agent")
169+
170+
def __getattr__(self, name: str) -> Any:
171+
if name == "source_agent":
172+
return self._source_agent_ref() if self._source_agent_ref else None
173+
if name == "target_agent":
174+
return self._target_agent_ref() if self._target_agent_ref else None
175+
return super().__getattr__(name)
176+
134177

135178
ToolCallItemTypes: TypeAlias = Union[
136179
ResponseFunctionToolCall,

tests/test_agent_memory_leak.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
import gc
4+
import weakref
5+
6+
import pytest
7+
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
8+
9+
from agents import Agent, Runner
10+
from tests.fake_model import FakeModel
11+
12+
13+
def _make_message(text: str) -> ResponseOutputMessage:
14+
return ResponseOutputMessage(
15+
id="msg-1",
16+
content=[ResponseOutputText(annotations=[], text=text, type="output_text")],
17+
role="assistant",
18+
status="completed",
19+
type="message",
20+
)
21+
22+
23+
@pytest.mark.asyncio
24+
async def test_agent_is_released_after_run() -> None:
25+
fake_model = FakeModel(initial_output=[_make_message("Paris")])
26+
agent = Agent(name="leaker", instructions="Answer questions.", model=fake_model)
27+
agent_ref = weakref.ref(agent)
28+
29+
await Runner.run(agent, "What is the capital of France?")
30+
31+
del agent
32+
gc.collect()
33+
34+
assert agent_ref() is None

0 commit comments

Comments
 (0)