Skip to main content
Agents can now return images, charts, and files that are automatically delivered through the bot adapter’s native upload primitive — Telegram send_photo, Slack files_upload_v2, Discord file send, and more.
The user messages the bot; generated images and files upload through the channel adapter automatically.
Outbound media now honours threaded targets: when the delivery route names a thread — Slack thread_ts, Telegram forum topic, or Discord thread — the attachment lands in the same thread as the text reply. Adapters that don’t support threads are completely unaffected.
Outbound media uploads share the same retry/backoff path as text replies — DeliveryRouter.send_media wraps the upload in deliver_with_retry, reusing the adapter’s configured BackoffPolicy (or a sensible default). See Retry & backoff on transient failures below and Outbound Resilience for the underlying policy config.

Quick Start

1

Simple Usage

2

With Configuration

Start the bot:
Users receive text plus the chart image attached automatically.

How It Works

When an agent returns a media file path, the gateway’s outbound binding validates it through the path safety guard and delivers it via the adapter’s native primitive:

Path safety guard

validate_media_delivery_path() runs two checks before any file is sent: System directory denylist (always blocked): /etc, /proc, /sys, ~/.ssh, ~/.aws, ~/.gnupg, gateway secret/pairing directories. Credential basename denylist (always blocked): .env, id_rsa, id_ecdsa, id_ed25519, .npmrc, .netrc, and similar credential filenames — matched from any directory.

Remote-sandbox media (Modal, E2B, Daytona, Fly.io, SSH, …)

When an agent runs off-box in a remote sandbox and generates a file, the gateway fetches the artifact back and delivers it through the same MEDIA path — the user gets the file automatically.
The user sends a message. The agent runs remotely, writes the chart on the sandbox filesystem, and returns MEDIA:/workspace/chart.png. The gateway detects the remote path, fetches it back, re-screens it, and delivers it — no extra wiring.

End-to-end flow

Behaviour

Unlike the old silent drop, the operator now always gets a logger.warning(...) when a MEDIA path is undeliverable, so the failure is visible in logs.

The RemoteMediaResolver protocol

Exported from praisonaiagents.gateway:
Each remote sandbox backend already has a download_file / read_file primitive (Modal read_file, E2B read_file, Daytona fs.download_file, Fly.io compute.download_file, SSH read_file, Docker download_file, Novita read_file, Subprocess read_file). The active sandbox backend for the turn is the resolver — application code does not implement this protocol itself.

Compatibility & safety

  • Byte-for-byte backward compatible. With no resolver in play, split_media_from_output_async() returns the same dict shape and values as the existing split_media_from_output() — local paths and URLs are untouched.
  • No new required parameters on Agent. No new dependencies added. No per-turn plumbing surfaced to user code.
  • The exfiltration guard still runs. Fetched artifacts are re-screened by validate_media_delivery_path() before delivery, so the system-directory / credential-basename denylist, size cap, strict-mode allowlist, and recent_mtime_seconds window all still apply. See Path safety guard.
  • Fail-open on error. A resolver failure (missing/wrong backend, download error, guard rejection) degrades to the historical silent drop plus an operator-visible warning — never a crash on the reply path.
  • The sync split_media_from_output() is unchanged. Only the new async variant split_media_from_output_async() is remote-aware.

When does this fire?


Configuration (media_delivery YAML block)

OutboundMediaPolicy fields


Platform Support

PlatformCapabilities.supports_media is checked per adapter — platforms that don’t support file uploads receive text-only replies and the attachment is skipped with a log entry.

Threaded media delivery

Attachments follow the same platform:channel_id:thread_id grammar the text path uses. When a route names a thread, both the text reply and any uploaded file land in that thread.
DeliveryRouter.send_media resolves the thread from the target and forwards it to deliver_media_to_adapter, which passes it through each transport’s native keyword only when the primitive accepts it — so non-threaded adapters never raise.

Retry & backoff on transient failures

Media uploads go through the same deliver_with_retry wrapper as text replies. A transient transport failure — HTTP 5xx, rate limit, connection reset — is retried with bounded exponential backoff (honouring a server-mandated Retry-After hint) instead of silently dropping the attachment on the first blip.

Which backoff policy is used?

DeliveryRouter.send_media reuses the adapter’s already-configured _outbound_backoff BackoffPolicy if one is set (matching the text path). If the adapter exposes no policy, a sensible default is used: To customise, set outbound_resilience on the channel in praisonai.yml (the same block that tunes text retries — see Outbound Resilience). The media upload picks up the same values automatically.

Non-retryable outcomes are preserved

Prior to this fix the Discord native upload branch caught every exception locally and returned False, which bypassed the retry wrapper entirely — a transient blip on Discord silently dropped the file. That branch now only catches the missing optional discord dependency; every other transport error propagates so deliver_with_retry can apply the same backoff Slack and Telegram already got.

Common Patterns

Chart generation bot

Multi-tenant / hosted gateway (strict mode)

For hosted environments where multiple users share the same gateway, enable strict mode with an explicit output directory:

Disable media delivery

Or with quoted string (both are equivalent):

Summary Messages

When media delivery completes, BotOutboundMessenger.send appends a summary:
Skipped files (denied by path guard, over size cap, etc.) are logged at debug level and never cause the message send to fail.

Best Practices

Configure your agent to save all generated files to a single directory like /tmp/agent-media/ and set that as the allowlist_roots entry. This keeps the path guard simple and predictable.
In production or multi-tenant gateways, set strict: true with explicit allowlist_roots. This prevents agents from accidentally delivering files from unexpected locations.
Default is 25 MB. Telegram and Slack have their own size limits — set max_size_mb below those limits to fail fast at the gateway rather than at the adapter.
The basename denylist blocks common credential filenames, but keep your output directory completely separate from config directories. Symlink attacks are also blocked (symlinks are resolved before path checks).
Use platform:channel_id:thread_id — not just platform:channel_id — for reports and charts that belong to an ongoing thread. Text and attachment now land together in the same thread; the :channel_id form still posts to the parent chat.
When you run agents in a remote sandbox, instruct them to save generated files under a stable directory (e.g. /workspace/) and reference that exact path in MEDIA: lines. Every shipped remote backend’s owns_path recognises its sandbox filesystem root, and the fetched copy is validated by the same validate_media_delivery_path() guard as local files — so the same allowlist_roots / strict / max_size_mb rules apply after fetch.

Messaging Bots

Full bot setup and configuration guide

Channels Gateway

Gateway channel routing and configuration

Gateway

Gateway architecture and YAML reference

Bot Inbound Media

Handle images and files sent by users

Sandbox Backends

Where the agent’s code actually runs