-
Notifications
You must be signed in to change notification settings - Fork 2.9k
fix: #2008 Fix agent memory leak using weakref #2014
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
base: main
Are you sure you want to change the base?
Conversation
|
Thank you for sending this! At a glance, it looks good to me. I will look into it tomorrow (sorry, it's already in the evening in my timezone) |
|
No worries, have a look when you can! |
|
@codex can you review this again? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@DanieleMorotti If you're still interested in continuing to work on this, please let me know. Since we've received similar feedback from customers, I plan to work on this issue soon. |
|
Yes, I am. Let me know which changes you’d like to move forward with, and I’ll work on them. Thanks |
|
If you have time, can you check my above comments? |
|
Yes, sorry. I thought you were waiting for Rohan's approval |
|
@codex Can you review the latest changes again? I'd like you to focus on potential impacts on the existing apps and edge case scenarios rather general reviews. Also, if you see other patterns that should be covered by the unit tests, please point those out as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
src/agents/result.py
Outdated
| def release_agents(self) -> None: | ||
| """ | ||
| Release strong references to agents held by this result. After calling this method, | ||
| accessing `item.agent` or `last_agent` may return `None` if the agent has been garbage | ||
| collected. Callers can use this when they are done inspecting the result and want to | ||
| eagerly drop any associated agent graph. | ||
| """ | ||
| for item in self.new_items: | ||
| release = getattr(item, "release_agent", None) | ||
| if callable(release): | ||
| release() | ||
| self._release_last_agent_reference() | ||
|
|
||
| def __del__(self) -> None: | ||
| try: | ||
| # Fall back to releasing agents automatically in case the caller never invoked | ||
| # `release_agents()` explicitly. This keeps the no-leak guarantee confirmed by tests. | ||
| self.release_agents() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-release discards agent info for retained RunItems
Lines 78‑95 now invoke release_agents() from RunResultBase.__del__, which calls every RunItem’s release_agent() as soon as the RunResult instance is garbage-collected. release_agent() deletes the strong agent attribute and keeps only a weakref, so if the caller created the agent inline (the common Runner.run(Agent(...), input) case) and only keeps references to the MessageOutputItems for later auditing, those items lose their agent metadata the moment the enclosing result drops out of scope—the weakref is the last reference and is collected immediately. Previously the RunItem itself owned a strong reference, so item.agent.name stayed available even when the RunResult object was discarded. This regression silently breaks any code that stores RunItems but frees the RunResult to reduce memory, because their persisted items can no longer tell which agent produced them. Consider making the auto-release opt-in, or at least documenting/providing a way to keep RunItems’ agent data alive when needed (and adding a regression test for retaining items beyond the result lifecycle).
Useful? React with 👍 / 👎.
src/agents/items.py
Outdated
| def release_agent(self) -> None: | ||
| """Release the strong reference to the agent while keeping a weak reference.""" | ||
| if "agent" not in self.__dict__: | ||
| return | ||
| agent = self.__dict__["agent"] | ||
| self._agent_ref = weakref.ref(agent) if agent is not None else None | ||
| object.__delattr__(self, "agent") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
release_agent deletes dataclass fields causing repr/asdict crashes
In RunItemBase.release_agent() (lines 103‑109) the code removes the dataclass field entirely via object.__delattr__(self, "agent") after storing the weakref. Deleting a dataclass field violates the assumptions of the generated __repr__, __eq__, dataclasses.asdict, etc., which immediately raise AttributeError once the field disappears. Because RunResult.release_agents() now calls this helper automatically (and tests encourage users to call it manually), any instrumentation that logs or serializes a RunItem after releasing agents will start crashing—e.g. repr(item) or dataclasses.asdict(item) now fail even though release was supposed to be a benign cleanup step. Setting the field to None (or keeping a lightweight struct with the metadata) would drop the strong reference without breaking dataclass behavior; please add coverage for repr/asdict after release so this regression is caught.
Useful? React with 👍 / 👎.
|
Checked the above review comments and ran a few review sessions with codex locally. Here is the code changes I got so far. diff --git a/src/agents/items.py b/src/agents/items.py
index ab6f1f7..991a7f8 100644
--- a/src/agents/items.py
+++ b/src/agents/items.py
@@ -73,6 +73,9 @@ TResponseStreamEvent = ResponseStreamEvent
T = TypeVar("T", bound=Union[TResponseOutputItem, TResponseInputItem])
+# Distinguish a missing dict entry from an explicit None value.
+_MISSING_ATTR_SENTINEL = object()
+
@dataclass
class RunItemBase(Generic[T], abc.ABC):
@@ -95,18 +98,38 @@ class RunItemBase(Generic[T], abc.ABC):
# Store a weak reference so we can release the strong reference later if desired.
self._agent_ref = weakref.ref(self.agent)
- def __getattr__(self, name: str) -> Any:
+ def __getattribute__(self, name: str) -> Any:
if name == "agent":
- return self._agent_ref() if self._agent_ref else None
- raise AttributeError(name)
+ return self._get_agent_via_weakref("agent", "_agent_ref")
+ return super().__getattribute__(name)
def release_agent(self) -> None:
"""Release the strong reference to the agent while keeping a weak reference."""
if "agent" not in self.__dict__:
return
agent = self.__dict__["agent"]
+ if agent is None:
+ return
self._agent_ref = weakref.ref(agent) if agent is not None else None
- object.__delattr__(self, "agent")
+ # Set to None instead of deleting so dataclass repr/asdict keep working.
+ self.__dict__["agent"] = None
+
+ def _get_agent_via_weakref(self, attr_name: str, ref_name: str) -> Any:
+ # Preserve the dataclass field so repr/asdict still read it, but lazily resolve the weakref
+ # when the stored value is None (meaning release_agent already dropped the strong ref).
+ # If the attribute was never overridden we fall back to the default descriptor chain.
+ data = object.__getattribute__(self, "__dict__")
+ value = data.get(attr_name, _MISSING_ATTR_SENTINEL)
+ if value is _MISSING_ATTR_SENTINEL:
+ return object.__getattribute__(self, attr_name)
+ if value is not None:
+ return value
+ ref = object.__getattribute__(self, ref_name)
+ if ref is not None:
+ agent = ref()
+ if agent is not None:
+ return agent
+ return None
def to_input_item(self) -> TResponseInputItem:
"""Converts this item into an input item suitable for passing to the model."""
@@ -172,23 +195,30 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]):
self._source_agent_ref = weakref.ref(self.source_agent)
self._target_agent_ref = weakref.ref(self.target_agent)
- def __getattr__(self, name: str) -> Any:
+ def __getattribute__(self, name: str) -> Any:
if name == "source_agent":
- return self._source_agent_ref() if self._source_agent_ref else None
+ # Provide lazy weakref access like the base `agent` field so HandoffOutputItem
+ # callers keep seeing the original agent until GC occurs.
+ return self._get_agent_via_weakref("source_agent", "_source_agent_ref")
if name == "target_agent":
- return self._target_agent_ref() if self._target_agent_ref else None
- return super().__getattr__(name)
+ # Same as above but for the target of the handoff.
+ return self._get_agent_via_weakref("target_agent", "_target_agent_ref")
+ return super().__getattribute__(name)
def release_agent(self) -> None:
super().release_agent()
if "source_agent" in self.__dict__:
source_agent = self.__dict__["source_agent"]
- self._source_agent_ref = weakref.ref(source_agent) if source_agent is not None else None
- object.__delattr__(self, "source_agent")
+ if source_agent is not None:
+ self._source_agent_ref = weakref.ref(source_agent)
+ # Preserve dataclass fields for repr/asdict while dropping strong refs.
+ self.__dict__["source_agent"] = None
if "target_agent" in self.__dict__:
target_agent = self.__dict__["target_agent"]
- self._target_agent_ref = weakref.ref(target_agent) if target_agent is not None else None
- object.__delattr__(self, "target_agent")
+ if target_agent is not None:
+ self._target_agent_ref = weakref.ref(target_agent)
+ # Preserve dataclass fields for repr/asdict while dropping strong refs.
+ self.__dict__["target_agent"] = None
ToolCallItemTypes: TypeAlias = Union[
diff --git a/src/agents/result.py b/src/agents/result.py
index 181fffc..438d53a 100644
--- a/src/agents/result.py
+++ b/src/agents/result.py
@@ -75,24 +75,27 @@ class RunResultBase(abc.ABC):
def last_agent(self) -> Agent[Any]:
"""The last agent that was run."""
- def release_agents(self) -> None:
+ def release_agents(self, *, release_new_items: bool = True) -> None:
"""
Release strong references to agents held by this result. After calling this method,
accessing `item.agent` or `last_agent` may return `None` if the agent has been garbage
collected. Callers can use this when they are done inspecting the result and want to
eagerly drop any associated agent graph.
"""
- for item in self.new_items:
- release = getattr(item, "release_agent", None)
- if callable(release):
- release()
+ if release_new_items:
+ for item in self.new_items:
+ release = getattr(item, "release_agent", None)
+ if callable(release):
+ release()
self._release_last_agent_reference()
def __del__(self) -> None:
try:
# Fall back to releasing agents automatically in case the caller never invoked
- # `release_agents()` explicitly. This keeps the no-leak guarantee confirmed by tests.
- self.release_agents()
+ # `release_agents()` explicitly so GC of the RunResult drops the last strong reference.
+ # We pass `release_new_items=False` so RunItems that the user intentionally keeps
+ # continue exposing their originating agent until that agent itself is collected.
+ self.release_agents(release_new_items=False)
except Exception:
# Avoid raising from __del__.
pass
@@ -164,7 +167,8 @@ class RunResult(RunResultBase):
if agent is None:
return
self._last_agent_ref = weakref.ref(agent)
- object.__delattr__(self, "_last_agent")
+ # Preserve dataclass field so repr/asdict continue to succeed.
+ self.__dict__["_last_agent"] = None
def __str__(self) -> str:
return pretty_print_result(self)
@@ -244,7 +248,8 @@ class RunResultStreaming(RunResultBase):
if agent is None:
return
self._current_agent_ref = weakref.ref(agent)
- object.__delattr__(self, "current_agent")
+ # Preserve dataclass field so repr/asdict continue to succeed.
+ self.__dict__["current_agent"] = None
def cancel(self, mode: Literal["immediate", "after_turn"] = "immediate") -> None:
"""Cancel the streaming run.
diff --git a/src/agents/run.py b/src/agents/run.py
index c14f13e..5ea1dba 100644
--- a/src/agents/run.py
+++ b/src/agents/run.py
@@ -648,51 +648,60 @@ class AgentRunner:
tool_input_guardrail_results.extend(turn_result.tool_input_guardrail_results)
tool_output_guardrail_results.extend(turn_result.tool_output_guardrail_results)
- if isinstance(turn_result.next_step, NextStepFinalOutput):
- output_guardrail_results = await self._run_output_guardrails(
- current_agent.output_guardrails + (run_config.output_guardrails or []),
- current_agent,
- turn_result.next_step.output,
- context_wrapper,
- )
- result = RunResult(
- input=original_input,
- new_items=generated_items,
- raw_responses=model_responses,
- final_output=turn_result.next_step.output,
- _last_agent=current_agent,
- input_guardrail_results=input_guardrail_results,
- output_guardrail_results=output_guardrail_results,
- tool_input_guardrail_results=tool_input_guardrail_results,
- tool_output_guardrail_results=tool_output_guardrail_results,
- context_wrapper=context_wrapper,
- )
- if not any(
- guardrail_result.output.tripwire_triggered
- for guardrail_result in input_guardrail_results
- ):
- await self._save_result_to_session(
- session, [], turn_result.new_step_items
+ try:
+ if isinstance(turn_result.next_step, NextStepFinalOutput):
+ output_guardrail_results = await self._run_output_guardrails(
+ current_agent.output_guardrails
+ + (run_config.output_guardrails or []),
+ current_agent,
+ turn_result.next_step.output,
+ context_wrapper,
+ )
+ result = RunResult(
+ input=original_input,
+ new_items=generated_items,
+ raw_responses=model_responses,
+ final_output=turn_result.next_step.output,
+ _last_agent=current_agent,
+ input_guardrail_results=input_guardrail_results,
+ output_guardrail_results=output_guardrail_results,
+ tool_input_guardrail_results=tool_input_guardrail_results,
+ tool_output_guardrail_results=tool_output_guardrail_results,
+ context_wrapper=context_wrapper,
)
+ if not any(
+ guardrail_result.output.tripwire_triggered
+ for guardrail_result in input_guardrail_results
+ ):
+ await self._save_result_to_session(
+ session, [], turn_result.new_step_items
+ )
- return result
- elif isinstance(turn_result.next_step, NextStepHandoff):
- current_agent = cast(Agent[TContext], turn_result.next_step.new_agent)
- current_span.finish(reset_current=True)
- current_span = None
- should_run_agent_start_hooks = True
- elif isinstance(turn_result.next_step, NextStepRunAgain):
- if not any(
- guardrail_result.output.tripwire_triggered
- for guardrail_result in input_guardrail_results
- ):
- await self._save_result_to_session(
- session, [], turn_result.new_step_items
+ return result
+ elif isinstance(turn_result.next_step, NextStepHandoff):
+ current_agent = cast(Agent[TContext], turn_result.next_step.new_agent)
+ current_span.finish(reset_current=True)
+ current_span = None
+ should_run_agent_start_hooks = True
+ elif isinstance(turn_result.next_step, NextStepRunAgain):
+ if not any(
+ guardrail_result.output.tripwire_triggered
+ for guardrail_result in input_guardrail_results
+ ):
+ await self._save_result_to_session(
+ session, [], turn_result.new_step_items
+ )
+ else:
+ raise AgentsException(
+ f"Unknown next step type: {type(turn_result.next_step)}"
)
- else:
- raise AgentsException(
- f"Unknown next step type: {type(turn_result.next_step)}"
- )
+ finally:
+ # RunImpl.execute_tools_and_side_effects returns a SingleStepResult that
+ # stores direct references to the `pre_step_items` and `new_step_items`
+ # lists it manages internally. Clear them here so the next turn does not
+ # hold on to items from previous turns and to avoid leaking agent refs.
+ turn_result.pre_step_items.clear()
+ turn_result.new_step_items.clear()
except AgentsException as exc:
exc.run_data = RunErrorDetails(
input=original_input,
diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py
index 408ba13..ad8da22 100644
--- a/tests/test_items_helpers.py
+++ b/tests/test_items_helpers.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import gc
import json
+import weakref
from openai.types.responses.response_computer_tool_call import (
ActionScreenshot,
@@ -30,6 +31,7 @@ from pydantic import TypeAdapter
from agents import (
Agent,
+ HandoffOutputItem,
ItemHelpers,
MessageOutputItem,
ModelResponse,
@@ -152,14 +154,59 @@ def test_text_message_outputs_across_list_of_runitems() -> None:
def test_message_output_item_retains_agent_until_release() -> None:
# Construct the run item with an inline agent to ensure the run item keeps a strong reference.
message = make_message([ResponseOutputText(annotations=[], text="hello", type="output_text")])
- item = MessageOutputItem(agent=Agent(name="inline"), raw_item=message)
- assert item.agent is not None
+ agent = Agent(name="inline")
+ item = MessageOutputItem(agent=agent, raw_item=message)
+ assert item.agent is agent
assert item.agent.name == "inline"
- # After explicitly releasing, the weak reference should drop once GC runs.
+ # Releasing the agent should keep the weak reference alive while strong refs remain.
item.release_agent()
+ assert item.agent is agent
+
+ agent_ref = weakref.ref(agent)
+ del agent
gc.collect()
+
+ # Once the original agent is collected, the weak reference should drop.
+ assert agent_ref() is None
+ assert item.agent is None
+
+
+def test_handoff_output_item_retains_agents_until_gc() -> None:
+ raw_item: TResponseInputItem = {
+ "call_id": "call1",
+ "output": "handoff",
+ "type": "function_call_output",
+ }
+ owner_agent = Agent(name="owner")
+ source_agent = Agent(name="source")
+ target_agent = Agent(name="target")
+ item = HandoffOutputItem(
+ agent=owner_agent,
+ raw_item=raw_item,
+ source_agent=source_agent,
+ target_agent=target_agent,
+ )
+
+ item.release_agent()
+ assert item.agent is owner_agent
+ assert item.source_agent is source_agent
+ assert item.target_agent is target_agent
+
+ owner_ref = weakref.ref(owner_agent)
+ source_ref = weakref.ref(source_agent)
+ target_ref = weakref.ref(target_agent)
+ del owner_agent
+ del source_agent
+ del target_agent
+ gc.collect()
+
+ assert owner_ref() is None
+ assert source_ref() is None
+ assert target_ref() is None
assert item.agent is None
+ assert item.source_agent is None
+ assert item.target_agent is None
def test_tool_call_output_item_constructs_function_call_output_dict():
diff --git a/tests/test_result_cast.py b/tests/test_result_cast.py
index 87f4fc2..e919171 100644
--- a/tests/test_result_cast.py
+++ b/tests/test_result_cast.py
@@ -1,3 +1,4 @@
+import dataclasses
import gc
import weakref
from typing import Any
@@ -6,7 +7,7 @@ import pytest
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
from pydantic import BaseModel
-from agents import Agent, MessageOutputItem, RunContextWrapper, RunResult
+from agents import Agent, MessageOutputItem, RunContextWrapper, RunResult, RunResultStreaming
from agents.exceptions import AgentsException
@@ -29,6 +30,16 @@ class Foo(BaseModel):
bar: int
+def _create_message(text: str) -> ResponseOutputMessage:
+ return ResponseOutputMessage(
+ id="msg",
+ content=[ResponseOutputText(annotations=[], text=text, type="output_text")],
+ role="assistant",
+ status="completed",
+ type="message",
+ )
+
+
def test_result_cast_typechecks():
"""Correct casts should work fine."""
result = create_run_result(1)
@@ -66,13 +77,7 @@ def test_bad_cast_with_param_raises():
def test_run_result_release_agents_breaks_strong_refs() -> None:
- message = ResponseOutputMessage(
- id="msg",
- content=[ResponseOutputText(annotations=[], text="hello", type="output_text")],
- role="assistant",
- status="completed",
- type="message",
- )
+ message = _create_message("hello")
agent = Agent(name="leak-test-agent")
item = MessageOutputItem(agent=agent, raw_item=message)
result = RunResult(
@@ -99,3 +104,173 @@ def test_run_result_release_agents_breaks_strong_refs() -> None:
assert item.agent is None
with pytest.raises(AgentsException):
_ = result.last_agent
+
+
+def test_run_item_retains_agent_when_result_is_garbage_collected() -> None:
+ def build_item() -> tuple[MessageOutputItem, weakref.ReferenceType[RunResult]]:
+ message = _create_message("persist")
+ agent = Agent(name="persisted-agent")
+ item = MessageOutputItem(agent=agent, raw_item=message)
+ result = RunResult(
+ input="test",
+ new_items=[item],
+ raw_responses=[],
+ final_output=None,
+ input_guardrail_results=[],
+ output_guardrail_results=[],
+ tool_input_guardrail_results=[],
+ tool_output_guardrail_results=[],
+ _last_agent=agent,
+ context_wrapper=RunContextWrapper(context=None),
+ )
+ return item, weakref.ref(result)
+
+ item, result_ref = build_item()
+ gc.collect()
+
+ assert result_ref() is None
+ assert item.agent is not None
+ assert item.agent.name == "persisted-agent"
+
+
+def test_run_item_repr_and_asdict_after_release() -> None:
+ message = _create_message("repr")
+ agent = Agent(name="repr-agent")
+ item = MessageOutputItem(agent=agent, raw_item=message)
+
+ item.release_agent()
+ assert item.agent is agent
+
+ text = repr(item)
+ assert "MessageOutputItem" in text
+
+ serialized = dataclasses.asdict(item)
+ assert isinstance(serialized["agent"], dict)
+ assert serialized["agent"]["name"] == "repr-agent"
+
+ agent_ref = weakref.ref(agent)
+ del agent
+ gc.collect()
+
+ assert agent_ref() is None
+ assert item.agent is None
+
+ serialized_after_gc = dataclasses.asdict(item)
+ assert serialized_after_gc["agent"] is None
+
+
+def test_run_result_repr_and_asdict_after_release_agents() -> None:
+ agent = Agent(name="repr-result-agent")
+ result = RunResult(
+ input="test",
+ new_items=[],
+ raw_responses=[],
+ final_output=None,
+ input_guardrail_results=[],
+ output_guardrail_results=[],
+ tool_input_guardrail_results=[],
+ tool_output_guardrail_results=[],
+ _last_agent=agent,
+ context_wrapper=RunContextWrapper(context=None),
+ )
+
+ result.release_agents()
+
+ text = repr(result)
+ assert "RunResult" in text
+
+ serialized = dataclasses.asdict(result)
+ assert serialized["_last_agent"] is None
+
+
+def test_run_result_release_agents_without_releasing_new_items() -> None:
+ message = _create_message("keep")
+ item_agent = Agent(name="item-agent")
+ last_agent = Agent(name="last-agent")
+ item = MessageOutputItem(agent=item_agent, raw_item=message)
+ result = RunResult(
+ input="test",
+ new_items=[item],
+ raw_responses=[],
+ final_output=None,
+ input_guardrail_results=[],
+ output_guardrail_results=[],
+ tool_input_guardrail_results=[],
+ tool_output_guardrail_results=[],
+ _last_agent=last_agent,
+ context_wrapper=RunContextWrapper(context=None),
+ )
+
+ result.release_agents(release_new_items=False)
+
+ assert item.agent is item_agent
+
+ last_agent_ref = weakref.ref(last_agent)
+ del last_agent
+ gc.collect()
+
+ assert last_agent_ref() is None
+ with pytest.raises(AgentsException):
+ _ = result.last_agent
+
+
+def test_run_result_release_agents_is_idempotent() -> None:
+ message = _create_message("idempotent")
+ agent = Agent(name="idempotent-agent")
+ item = MessageOutputItem(agent=agent, raw_item=message)
+ result = RunResult(
+ input="test",
+ new_items=[item],
+ raw_responses=[],
+ final_output=None,
+ input_guardrail_results=[],
+ output_guardrail_results=[],
+ tool_input_guardrail_results=[],
+ tool_output_guardrail_results=[],
+ _last_agent=agent,
+ context_wrapper=RunContextWrapper(context=None),
+ )
+
+ result.release_agents()
+ result.release_agents()
+
+ assert item.agent is agent
+
+ agent_ref = weakref.ref(agent)
+ del agent
+ gc.collect()
+
+ assert agent_ref() is None
+ assert item.agent is None
+ with pytest.raises(AgentsException):
+ _ = result.last_agent
+
+
+def test_run_result_streaming_release_agents_releases_current_agent() -> None:
+ agent = Agent(name="streaming-agent")
+ streaming_result = RunResultStreaming(
+ input="stream",
+ new_items=[],
+ raw_responses=[],
+ final_output=None,
+ input_guardrail_results=[],
+ output_guardrail_results=[],
+ tool_input_guardrail_results=[],
+ tool_output_guardrail_results=[],
+ context_wrapper=RunContextWrapper(context=None),
+ current_agent=agent,
+ current_turn=0,
+ max_turns=1,
+ _current_agent_output_schema=None,
+ trace=None,
+ )
+
+ streaming_result.release_agents(release_new_items=False)
+
+ agent_ref = weakref.ref(agent)
+ del agent
+ gc.collect()
+
+ assert agent_ref() is None
+ with pytest.raises(AgentsException):
+ _ = streaming_result.last_agent
|
|
I have integrated the corrections you got so far |
|
@codex Can you review the latest changes with fresh eyes? I'd like you to focus on potential impacts on the existing apps and edge case scenarios rather general feedback. |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@DanieleMorotti Thanks for quickly updating this PR! I will continue the final validation before merging this PR next week. |
|
You’re welcome. Happy to provide any additional updates if required |
Hi @seratch,
I tried to implement a fix for the issue #2008 .
This PR refactors the
RunItemBaseand related classes to improve memory management usingweakref.Previously, strong references to agent, source_agent, and target_agent could persist longer than intended, leading to memory leaks.
Resolves #2008
Checks
make lintandmake format