Skip to content

fix: include generated media in run output regardless of store_media setting#6793

Open
Br1an67 wants to merge 12 commits intoagno-agi:mainfrom
Br1an67:fix/media-in-run-output
Open

fix: include generated media in run output regardless of store_media setting#6793
Br1an67 wants to merge 12 commits intoagno-agi:mainfrom
Br1an67:fix/media-in-run-output

Conversation

@Br1an67
Copy link
Contributor

@Br1an67 Br1an67 commented Feb 28, 2026

Summary

Decouples store_media from in-run media availability. Previously, store_media=False prevented media from appearing in both the DB and the returned RunOutput, making it impossible to forward generated images/videos/audio to external services (e.g., WhatsApp, Slack) immediately after a run.

Now store_media controls only DB persistence. Generated media is always available on the returned RunOutput for the caller to act on.

Closes #5101

Problem

When store_media=False, generated media (images, videos, audio, files) was stripped before reaching the caller:

  1. store_media_util() was gatedif agent.store_media: prevented media from being copied from ModelResponseRunOutput in all 4 agent run paths and 6 team run paths
  2. cleanup_and_store() scrubbed without restoring — media was removed for DB persistence but never restored on the returned object
  3. Streaming had the same gate_response.py guarded image collection during chunk processing

This meant WhatsApp/Slack interfaces couldn't forward generated images back to users when store_media=False, even though the media existed in-memory during the run.

Before vs After

BEFORE (store_media=False):
═══════════════════════════════════════════════════════════════

  Model generates image
        │
        ▼
  store_media_util()  ──── if store_media: ──── SKIPPED ✗
        │                                         │
        ▼                                         ▼
  RunOutput.images = []          Media never reaches RunOutput
        │
        ▼
  cleanup_and_store()
        │
        ├── scrub media ──► DB gets no media ✓
        │
        └── RunOutput returned to caller
                │
                ▼
          .images = None  ◄── caller can't forward to WhatsApp/Slack ✗


AFTER (store_media=False):
═══════════════════════════════════════════════════════════════

  Model generates image
        │
        ▼
  store_media_util()  ──── ALWAYS runs ──── RunOutput.images = [img] ✓
        │
        ▼
  cleanup_and_store()
        │
        ├── save media refs     saved = run_response.images
        │
        ├── scrub + null        run_response.images = None
        │
        ├── copy.copy()         session cache holds scrubbed snapshot
        │
        ├── persist to DB ───►  DB gets no media ✓
        │
        └── finally: restore    run_response.images = saved
                │
                ▼
          RunOutput returned to caller
                │
                ▼
          .images = [img]  ◄── caller forwards to WhatsApp/Slack ✓

Solution

Three-layer fix across Agent, Team, and streaming:

1. Remove store_media gates on store_media_util() calls

Media is always copied from ModelResponseRunOutput regardless of store_media. Affected paths:

  • _run() / _arun() / _continue_run() / _acontinue_run() (agent)
  • _run() / _arun() / _run_tasks() / _arun_tasks() / _continue_run() / _acontinue_run() (team)

2. Save/scrub/persist/restore pattern in cleanup_and_store

save media fields → scrub for storage → null media if !store_media
→ shallow copy for session cache → persist → restore media in finally
  • try/finally guarantees media is restored even if DB write fails
  • copy.copy() breaks reference aliasing between session cache and returned RunOutput — without this, save_session() on subsequent runs would re-persist the old run with restored media, leaking it to DB

3. Remove streaming gate in _response.py

handle_model_response_chunk now always collects images into run_response during streaming, not just when store_media=True.

Files Changed

File What changed
agno/agent/_response.py Removed store_media guard on streaming image collection
agno/agent/_run.py Removed store_media guard on store_media_util() in 4 paths; save/scrub/restore in cleanup_and_store + acleanup_and_store
agno/team/_run.py Same decoupling across 6 team run paths + save/scrub/restore in _cleanup_and_store + _acleanup_and_store
tests/unit/agent/test_store_media_run_output.py New — 8 tests: sync/async × non-streaming/streaming × yield_run_output/stream_events
tests/unit/os/routers/test_slack_store_media.py New — 8 tests: Slack integration covering non-streaming + streaming with mock and real Agent, plus store_media=True regression

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Improvement
  • Model update
  • Other:

Checklist

  • Code complies with style guidelines
  • Ran format/validation scripts (./scripts/format.sh and ./scripts/validate.sh)
  • Self-review completed
  • Documentation updated (comments, docstrings)
  • Examples and guides: Relevant cookbook examples have been included or updated (if applicable)
  • Tested in clean environment
  • Tests added/updated (if applicable)

Design Notes for Reviewers

Why copy.copy() instead of just restoring fields?
session.upsert_run() stores a reference to the RunOutput in session.runs. Without the copy, the finally-block restore would rehydrate media on the same object held in the session's run list. On the next save_session(), the previously-scrubbed run would be re-persisted with media restored — defeating store_media=False.

Why try/finally?
If save_session() or upsert_run() raises between scrub and restore, the returned RunOutput would have media permanently stripped. finally guarantees the caller always sees media.

Interaction with scrub_media_from_run_output():
That utility scrubs media from input, messages, and reasoning_messages (for DB cleanliness). The explicit nulling in this PR handles the output media fields (images, videos, audio, files). These are complementary, not redundant.

Related Issues

This fix addresses a pattern reported across multiple issues where store_media conflated DB persistence with in-run availability:

Issue Problem How this PR helps
#5101 (direct fix) store_media=False strips media from RunOutput — can't forward images to WhatsApp Media now stays on RunOutput for immediate forwarding
#6529 DB bloat from inline base64 media (165MB sessions, 4GB+ tables) Users who set store_media=False to avoid bloat can now still act on media in the current run
#5960 Expired media URLs in history break subsequent LLM calls (404 errors) Reinforces the "act now, don't persist" pattern — forward immediately, don't rely on stored URLs
#5838 Session storage growth "extremely exaggerated" in Team mode Users already combining store_media=False + store_tool_messages=False — this fix ensures media is still usable before being discarded

Key takeaway: Users who disable media persistence do so deliberately (privacy, compliance, ephemeral URLs, DB size). They still need in-run access to forward media to external services. This PR serves that use case without changing behavior for the default store_media=True path.

@Br1an67 Br1an67 requested a review from a team as a code owner February 28, 2026 13:23
Mustafa-Esoofally added a commit that referenced this pull request Mar 2, 2026
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
@Mustafa-Esoofally Mustafa-Esoofally self-assigned this Mar 3, 2026
Br1an67 and others added 5 commits March 3, 2026 19:10
Apply the same store_media decoupling to Team that PR agno-agi#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 agno-agi#5101
If persistence fails between scrub and restore, media would be
left scrubbed on the returned RunOutput. Wrapping in try/finally
guarantees the caller always sees media regardless of DB errors.
Tests cover both non-streaming and streaming Slack paths with
store_media=False, including real Agent end-to-end scenarios.
Verifies media reaches upload_response_media_async in both paths.
Cover sync/async streaming with both stream_events=True and
yield_run_output=True to verify images survive cleanup_and_store.
@Mustafa-Esoofally Mustafa-Esoofally force-pushed the fix/media-in-run-output branch from b2d325c to 16cefe1 Compare March 4, 2026 00:20
Mustafa-Esoofally and others added 7 commits March 3, 2026 19:37
When cache_session=True and store_media=False, the finally-block
media restore in cleanup_and_store rehydrated media on the same
RunOutput object stored in session.runs (reference aliasing).
On the next run, save_session would re-persist the old run with
restored media, leaking it to DB despite store_media=False.

Shallow-copy run_response before upsert_run so session.runs holds
an independent snapshot of the scrubbed state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Generated media not added to run_output

3 participants