Skip to content

Commit ce0f41e

Browse files
fix: extend store_media fix to team run paths and add tests
Apply the same store_media decoupling to Team that PR #6793 applied to Agent: remove if-guards on store_media_util() in all 6 team run paths and add save/restore of media fields around cleanup_and_store so callers still see generated media when store_media=False. Add unit tests verifying sync and async agent.run() returns images in RunOutput even with store_media=False. Closes #5101
1 parent b2d325c commit ce0f41e

File tree

2 files changed

+184
-17
lines changed

2 files changed

+184
-17
lines changed

libs/agno/agno/team/_run.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@ def _run_tasks(
367367

368368
# === Post-loop ===
369369

370-
# Store media if enabled
371-
if team.store_media and model_response is not None:
370+
# Always add media to run_response for caller availability
371+
if model_response is not None:
372372
store_media_util(run_response, model_response)
373373

374374
# Convert response to structured format
@@ -1136,9 +1136,8 @@ def _run(
11361136
team, run_response=run_response, session=session, run_context=run_context
11371137
)
11381138

1139-
# 8. Store media if enabled
1140-
if team.store_media:
1141-
store_media_util(run_response, model_response)
1139+
# 8. Always add media to run_response for caller availability
1140+
store_media_util(run_response, model_response)
11421141

11431142
# 9. Convert response to structured format
11441143
_convert_response_to_structured_format(team, run_response=run_response, run_context=run_context)
@@ -2127,8 +2126,8 @@ async def _arun_tasks(
21272126

21282127
# === Post-loop ===
21292128

2130-
# Store media if enabled
2131-
if team.store_media and model_response is not None:
2129+
# Always add media to run_response for caller availability
2130+
if model_response is not None:
21322131
store_media_util(run_response, model_response)
21332132

21342133
# Convert response to structured format
@@ -2955,9 +2954,8 @@ async def _arun(
29552954
team, run_response=run_response, session=team_session, run_context=run_context
29562955
)
29572956

2958-
# 8. Store media if enabled
2959-
if team.store_media:
2960-
store_media_util(run_response, model_response)
2957+
# 8. Always add media to run_response for caller availability
2958+
store_media_util(run_response, model_response)
29612959

29622960
# 9. Convert response to structured format
29632961
_convert_response_to_structured_format(team, run_response=run_response, run_context=run_context)
@@ -3885,11 +3883,24 @@ def _cleanup_and_store(
38853883
session: TeamSession,
38863884
run_context: Optional[RunContext] = None,
38873885
) -> None:
3888-
# Scrub the stored run based on storage flags
38893886
from agno.team._session import update_session_metrics
38903887

3888+
# Save output media before scrubbing so they remain available to the caller
3889+
saved_images = run_response.images
3890+
saved_videos = run_response.videos
3891+
saved_audio = run_response.audio
3892+
saved_files = run_response.files
3893+
3894+
# Scrub the stored run based on storage flags
38913895
scrub_run_output_for_storage(team, run_response)
38923896

3897+
# Also scrub output media artifacts when store_media is disabled
3898+
if not team.store_media:
3899+
run_response.images = None
3900+
run_response.videos = None
3901+
run_response.audio = None
3902+
run_response.files = None
3903+
38933904
# Stop the timer for the Run duration
38943905
if run_response.metrics:
38953906
run_response.metrics.stop_timer()
@@ -3914,18 +3925,37 @@ def _cleanup_and_store(
39143925
# Save session to memory
39153926
team.save_session(session=session)
39163927

3928+
# Restore output media so the caller can access them
3929+
run_response.images = saved_images
3930+
run_response.videos = saved_videos
3931+
run_response.audio = saved_audio
3932+
run_response.files = saved_files
3933+
39173934

39183935
async def _acleanup_and_store(
39193936
team: "Team",
39203937
run_response: TeamRunOutput,
39213938
session: TeamSession,
39223939
run_context: Optional[RunContext] = None,
39233940
) -> None:
3924-
# Scrub the stored run based on storage flags
39253941
from agno.team._session import update_session_metrics
39263942

3943+
# Save output media before scrubbing so they remain available to the caller
3944+
saved_images = run_response.images
3945+
saved_videos = run_response.videos
3946+
saved_audio = run_response.audio
3947+
saved_files = run_response.files
3948+
3949+
# Scrub the stored run based on storage flags
39273950
scrub_run_output_for_storage(team, run_response)
39283951

3952+
# Also scrub output media artifacts when store_media is disabled
3953+
if not team.store_media:
3954+
run_response.images = None
3955+
run_response.videos = None
3956+
run_response.audio = None
3957+
run_response.files = None
3958+
39293959
# Stop the timer for the Run duration
39303960
if run_response.metrics:
39313961
run_response.metrics.stop_timer()
@@ -3950,6 +3980,12 @@ async def _acleanup_and_store(
39503980
# Save session to memory
39513981
await team.asave_session(session=session)
39523982

3983+
# Restore output media so the caller can access them
3984+
run_response.images = saved_images
3985+
run_response.videos = saved_videos
3986+
run_response.audio = saved_audio
3987+
run_response.files = saved_files
3988+
39533989

39543990
def scrub_run_output_for_storage(team: "Team", run_response: TeamRunOutput) -> bool:
39553991
"""
@@ -4924,9 +4960,8 @@ def _continue_run(
49244960
# Convert to structured format
49254961
_convert_response_to_structured_format(team, run_response=run_response, run_context=run_context)
49264962

4927-
# Store media
4928-
if team.store_media:
4929-
store_media_util(run_response, model_response)
4963+
# Always add media to run_response for caller availability
4964+
store_media_util(run_response, model_response)
49304965

49314966
# Execute post-hooks
49324967
if team.post_hooks is not None:
@@ -5577,8 +5612,7 @@ async def _acontinue_run(
55775612

55785613
_convert_response_to_structured_format(team, run_response=run_response, run_context=run_context)
55795614

5580-
if team.store_media:
5581-
store_media_util(run_response, model_response)
5615+
store_media_util(run_response, model_response)
55825616

55835617
elif member_results:
55845618
# Member-only: re-run team with results
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Tests for store_media=False still returning media in RunOutput to caller.
2+
3+
Verifies fix for https://github.com/agno-agi/agno/issues/5101
4+
"""
5+
6+
from typing import Any, AsyncIterator, Iterator
7+
from unittest.mock import AsyncMock, Mock
8+
9+
import pytest
10+
11+
from agno.agent.agent import Agent
12+
from agno.media import Image
13+
from agno.models.base import Model
14+
from agno.models.message import MessageMetrics
15+
from agno.models.response import ModelResponse
16+
17+
18+
class MockModelWithImage(Model):
19+
def __init__(self):
20+
super().__init__(id="test-model", name="test-model", provider="test")
21+
self.instructions = None
22+
23+
self._mock_response = Mock()
24+
self._mock_response.content = "Here is your generated image"
25+
self._mock_response.role = "assistant"
26+
self._mock_response.reasoning_content = None
27+
self._mock_response.redacted_reasoning_content = None
28+
self._mock_response.tool_calls = None
29+
self._mock_response.tool_executions = None
30+
self._mock_response.images = [Image(url="https://example.com/generated.png", id="img-1")]
31+
self._mock_response.videos = None
32+
self._mock_response.audios = None
33+
self._mock_response.audio = None
34+
self._mock_response.files = None
35+
self._mock_response.citations = None
36+
self._mock_response.references = None
37+
self._mock_response.metadata = None
38+
self._mock_response.provider_data = None
39+
self._mock_response.extra = None
40+
self._mock_response.response_usage = MessageMetrics()
41+
42+
self.response = Mock(return_value=self._mock_response)
43+
self.aresponse = AsyncMock(return_value=self._mock_response)
44+
45+
def get_instructions_for_model(self, *args, **kwargs):
46+
return None
47+
48+
def get_system_message_for_model(self, *args, **kwargs):
49+
return None
50+
51+
async def aget_instructions_for_model(self, *args, **kwargs):
52+
return None
53+
54+
async def aget_system_message_for_model(self, *args, **kwargs):
55+
return None
56+
57+
def parse_args(self, *args, **kwargs):
58+
return {}
59+
60+
def invoke(self, *args, **kwargs) -> ModelResponse:
61+
return self._mock_response
62+
63+
async def ainvoke(self, *args, **kwargs) -> ModelResponse:
64+
return await self.aresponse(*args, **kwargs)
65+
66+
def invoke_stream(self, *args, **kwargs) -> Iterator[ModelResponse]:
67+
yield self._mock_response
68+
69+
async def ainvoke_stream(self, *args, **kwargs) -> AsyncIterator[ModelResponse]:
70+
yield self._mock_response
71+
return
72+
73+
def _parse_provider_response(self, response: Any, **kwargs) -> ModelResponse:
74+
return self._mock_response
75+
76+
def _parse_provider_response_delta(self, response: Any) -> ModelResponse:
77+
return self._mock_response
78+
79+
80+
def test_store_media_false_returns_images_to_caller():
81+
"""Returned RunOutput should have images even with store_media=False."""
82+
agent = Agent(
83+
model=MockModelWithImage(),
84+
store_media=False,
85+
)
86+
87+
result = agent.run("Generate an image")
88+
89+
assert result.images is not None
90+
assert len(result.images) == 1
91+
assert result.images[0].url == "https://example.com/generated.png"
92+
93+
94+
@pytest.mark.asyncio
95+
async def test_store_media_false_returns_images_to_caller_async():
96+
"""Async: returned RunOutput should have images even with store_media=False."""
97+
agent = Agent(
98+
model=MockModelWithImage(),
99+
store_media=False,
100+
)
101+
102+
result = await agent.arun("Generate an image")
103+
104+
assert result.images is not None
105+
assert len(result.images) == 1
106+
assert result.images[0].url == "https://example.com/generated.png"
107+
108+
109+
def test_store_media_true_returns_images_to_caller():
110+
"""With store_media=True (default), caller should still see images."""
111+
agent = Agent(
112+
model=MockModelWithImage(),
113+
store_media=True,
114+
)
115+
116+
result = agent.run("Generate an image")
117+
118+
assert result.images is not None
119+
assert len(result.images) == 1
120+
121+
122+
def test_store_media_false_without_db():
123+
"""store_media=False works correctly without any DB configured."""
124+
agent = Agent(
125+
model=MockModelWithImage(),
126+
store_media=False,
127+
)
128+
129+
result = agent.run("Generate an image")
130+
131+
assert result.images is not None
132+
assert len(result.images) == 1
133+
assert result.images[0].id == "img-1"

0 commit comments

Comments
 (0)