Handling Attachments and File Uploads in ChatDev Multi-Agent Workflows

ChatDev treats files as first-class assets through the AttachmentStore class, which registers local files or byte streams, deduplicates by SHA-256, and embeds references into MessageBlock objects for seamless agent-to-agent transfer.

The OpenBMB/ChatDev framework enables complex multi-agent workflows where files must persist across execution boundaries. Handling attachments and file uploads in ChatDev multi-agent workflows relies on a filesystem-backed storage layer that couples immutable metadata with MIME-type-aware message blocks. This architecture allows agents to share images, PDFs, and binaries through a consistent API that abstracts physical file paths.

Core Attachment Architecture

ChatDev implements file handling through three layered abstractions defined in utils/attachments.py and entity/messages.py.

AttachmentRef and Immutable Metadata

The AttachmentRef dataclass stores immutable properties including a unique attachment_id, MIME type, SHA-256 hash, file size, and physical location. This reference serves as the canonical identity for any file within the system. According to the source code in entity/messages.py at line 43, AttachmentRef objects are lightweight and hashable, making them suitable for indexing and comparison operations.

AttachmentRecord and Message Coupling

AttachmentRecord binds an AttachmentRef to a specific MessageBlockType (e.g., file, image) and optional user-supplied descriptions. Defined in utils/attachments.py at line 18, this record acts as the bridge between raw storage and conversational content. The method as_message_block() converts the record into a MessageBlock instance that can be inserted directly into a message's content list.

AttachmentStore and Workflow Scoping

The AttachmentStore class provides the primary interface for registration, retrieval, and persistence. Each workflow execution receives a dedicated store instance backed by a per-run directory structure (<run-root>/attachments/<attachment_id>/). The store maintains an in-memory index and writes persistent records to attachments_manifest.json via the _save_manifest method.

Registering Files and Binary Data

The store exposes three registration helpers in utils/attachments.py (lines 72-84) to accommodate different input sources:

  • register_file – Ingests existing local files from disk. Accepts file_path, kind (defaults to MessageBlockType.FILE), display_name, mime_type, and a deduplicate boolean.
  • register_bytes – Accepts in-memory binary data (e.g., generated PNGs). Requires data, mime_type, and display_name.
  • register_remote_file – References files hosted by external providers (e.g., OpenAI file IDs). Uses remote_file_id, name, mime_type, and size.

All methods compute a SHA-256 hash for deduplication, copy the file into the per-run attachments directory, and return an AttachmentRecord.

from pathlib import Path
from utils.attachments import AttachmentStore
from entity.messages import MessageBlockType

store = AttachmentStore(Path("/tmp/run/attachments"))
record = store.register_file(
    "/home/user/report.pdf",
    kind=MessageBlockType.FILE,
    display_name="Project Report.pdf",
    mime_type="application/pdf",
    deduplicate=True,
)

Embedding Attachments in Agent Messages

Once registered, attachments enter the conversation flow through MessageBlock objects. The AttachmentRecord.as_message_block() method constructs a block with the appropriate type derived from the MIME type via MessageBlockType.from_mime_type (see lines 30-40 in entity/messages.py).

from entity.messages import Message, MessageRole

msg = Message(
    role=MessageRole.ASSISTANT,
    content=[record.as_message_block()],
)

This design allows multimodal content to traverse the message history exactly like text tokens, ensuring that downstream agents receive both the metadata and the file reference.

Runtime Context and Dependency Injection

Every workflow run receives its AttachmentStore through the runtime context, eliminating the need for agents to manage filesystem paths manually.

  1. RuntimeBuilder instantiates the store during workflow initialization. Lines 31-36 in workflow/runtime/runtime_builder.py create the attachments subdirectory under code_workspace and initialize the store.
  2. RuntimeContext holds the store as a dependency injected into all nodes and tools. Line 25 in workflow/runtime/runtime_context.py exposes attachment_store as a context attribute.

Agents and tools access the store uniformly via context["attachment_store"] or context.attachment_store, depending on the context implementation.

HTTP Upload Handling

The FastAPI server handles multipart uploads through AttachmentService.save_upload_file (lines 46-70 in server/services/attachment_service.py). The service:

  1. Streams UploadFile chunks to a temporary directory.
  2. Detects MIME types via upload.content_type or mimetypes.guess_type.
  3. Registers the temporary file with the session-specific AttachmentStore using register_file with kind=MessageBlockType.from_mime_type(...).
  4. Cleans up temporary files after registration.

This ensures that files uploaded via HTTP become immediately available to the workflow's agents through the same store interface.

Cross-Session Propagation and Deduplication

When tools or sub-graphs forward attachments between different sessions or agents, AttachmentService.build_attachment_blocks (lines 95-103 in server/services/attachment_service.py) orchestrates the transfer:

  • Retrieves the source AttachmentRecord from the origin store.
  • Copies the underlying file to a target store via ingest_record when stores reside in different workflow roots.
  • Returns a list of MessageBlock objects ready for new messages.
blocks = attachment_service.build_attachment_blocks(
    session_id="session_123",
    attachment_ids=["a1b2c3"],
    target_store=other_context.attachment_store,
)

Deduplication occurs at registration time through the _hash_index dictionary. When deduplicate=True, the store checks for existing SHA-256 hashes; if found, it returns the existing AttachmentRecord instead of copying duplicate data. Only records marked with persist=True are written to attachments_manifest.json via _save_manifest, which is reloaded on store startup via _load_manifest.

Tool Integration

File-related tools in functions/function_calling/file.py expect an AttachmentStore in their tool_context. The _require_store helper at lines 42-45 validates the store's presence before tools attempt file operations. This pattern ensures that all file-aware tools operate within the workflow's scoped storage, maintaining isolation between concurrent runs.

def generate_chart(context: ToolContext) -> List[MessageBlock]:
    img_bytes = create_plot()  # matplotlib or similar

    store = context["attachment_store"]
    record = store.register_bytes(
        img_bytes,
        mime_type="image/png",
        display_name="sales_chart.png",
        description="Sales performance chart",
    )
    return [record.as_message_block()]

Summary

  • AttachmentStore provides the central API for registering local files, byte streams, and remote references while handling SHA-256 deduplication.
  • AttachmentRef and AttachmentRecord separate immutable metadata from message-specific presentation logic.
  • The runtime builder injects a scoped store into RuntimeContext, making file operations available to all workflow nodes without hardcoded paths.
  • AttachmentService bridges HTTP uploads and cross-session transfers through save_upload_file and build_attachment_blocks.
  • Deduplication and manifest persistence optimize storage and enable workflow resumption across executions.

Frequently Asked Questions

How does ChatDev prevent duplicate file storage across workflow runs?

The AttachmentStore maintains a _hash_index mapping SHA-256 hashes to existing records. When deduplicate=True is passed to register_file or register_bytes, the store checks this index and returns the existing AttachmentRecord if the hash matches, avoiding redundant copies.

Can agents access attachments from previous workflow sessions?

Yes, through the ingest_record method. When AttachmentService.build_attachment_blocks copies an attachment to a target store in a different session, it uses ingest_record with copy_file=True to physically transfer the file while preserving metadata, enabling cross-session file sharing.

What MIME types automatically map to specific message block types?

The MessageBlockType.from_mime_type method at lines 30-40 in entity/messages.py maps common patterns (e.g., image/* to MessageBlockType.IMAGE, application/pdf to MessageBlockType.FILE). Tools can override this by explicitly specifying the kind parameter during registration.

How do I register a file that exists only in memory?

Use AttachmentStore.register_bytes, which accepts a bytes object, mime_type, and display_name. This method writes the data to the store's directory structure and returns an AttachmentRecord identical to disk-based registrations, as shown in the tool integration examples.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →