Codumentor logo Codumentor

Attachment Staging — Giving the Agent a File

When your plugin gets hold of bytes the agent should be able to keep — a
mail attachment, an issue attachment, an upload, a downloaded PDF — putting
them in the tool result is not an option: the model cannot round-trip binary,
and a 4 MB document has no business in the context window.

Stage it instead. One call writes the bytes into the conversation's staging
area and returns the path the agent can reach them at, read-only:

from codumentor.agent.attachment_stager import stage_attachment

staged = stage_attachment(
    ctx,                       # the hook ctx, or self._current_ctx in a tool
    filename="invoice.pdf",    # sanitized for you; may be user-supplied
    data=raw_bytes,
    mime_type="application/pdf",
    producer="gmail",          # your plugin's name — see "Keying" below
)

staged is a StagedAttachment:

MemberWhat it gives you
agent_pathThe path to tell the model, e.g. /attachments/from-gmail/invoice.pdf (None when the session could not build a sandbox)
hint()Ready-made prompt lines: where the file is, that it is read-only, and a cp example. Append to any text you return
to_result_dict(){filename, size_bytes, mime_type, path, note} — the payload shape for a download tool's JSON result
host_path, rel_path, size, mime_type, nameThe underlying StagedFile details

The agent copies the file into a repo with the ordinary shell tool; there is
no dedicated "save" tool to call and none to add. Staged files sit outside the
repo namespace, so read_file / read_content will not open them — after the
copy, the repo path is readable through the normal tools.

Keying: name yourself, never a directory

Files live at <storage>/attachments/<conversation_id>/<group>/<name>, and
<group> is the one thing a producer must not invent. There are exactly two
kinds:

Pass your plugin's own name and nothing else. If you find yourself encoding a
message id or a timestamp into producer, that is the sign to stop: identical
bytes already deduplicate, and different bytes already get a -2 suffix.

Limits and failure

Where the path shows up

The staging directory is bind-mounted read-only into whatever execution view
the session uses, and the mount is requested for every turn — so a file you
stage in the middle of a turn is reachable by the next shell call without
any further plumbing. Staging also refreshes the conversation's inventory,
which core renders as an [Attached Files] note, so the model can still find
the file several turns later without you re-announcing it.

Available in tools (self._current_ctx), in hook handlers (the ctx
argument), and inside subagents (the handle is forwarded). onAtContentResolve
gets a deliberately narrow ctx, but the staging keys are in it — that is how
client_fs stages single-file uploads.

Worked examples in-tree

ProducerWhere
Gmail attachment downloadplugins/google_workspace/gmail/attachments.py
Redmine attachment download (with size preflight)RedmineGetAttachmentTool in plugins/redmine/tools.py
Browser single-file upload (bonus bytes on top of text)ClientFsPlugin._stage_upload

Design notes and the reasoning behind the split live in
doc/core-dev/design/attachment-staging.md.