How FileArtifactStore Manages Artifact Storage in aisuite

FileArtifactStore persists large binary or textual data to disk under a configurable root directory (default .aisuite/artifacts), storing each artifact in its own subdirectory containing a data file and a metadata.json with SHA-256 hashes, while returning lightweight ArtifactRef objects that keep traces small.

aisuite, the framework for building LLM-powered agents, treats large data outputs—such as tool stdout or file contents—as artifacts to prevent bloating execution traces. The FileArtifactStore class in aisuite/agents/artifact_store.py provides the production-ready, filesystem-based implementation of the ArtifactStore protocol, enabling persistent artifact storage with integrity verification and seamless integration with the framework's tracing system.

Core Abstractions in artifact_store.py

The artifact system rests on three core components defined in the same source file.

ArtifactRef and Artifact

The ArtifactRef class (lines 14-31) serves as a lightweight, JSON-serializable handle containing the artifact ID, URI (artifact://... or memory://...), media type, size in bytes, and optional metadata. This reference can be safely embedded in execution traces without inflating payload size.

The Artifact class (lines 44-52) wraps the actual binary payload alongside its reference and creation timestamp. It provides a convenience text() method for decoding binary data to strings.

The ArtifactStore Protocol

Lines 54-66 define the ArtifactStore protocol, which mandates three operations that any store must implement:

  • put(data, media_type, metadata) → returns ArtifactRef
  • get(ref) → returns Artifact
  • delete(ref) → removes the artifact

FileArtifactStore satisfies this protocol using the local filesystem.

FileArtifactStore Implementation

FileArtifactStore maps the abstract protocol to concrete file operations, creating a predictable on-disk layout.

Directory Structure and Initialization

The constructor (lines 11-14) accepts a configurable root path defaulting to .aisuite/artifacts and normalizes it using pathlib.Path:

def __init__(self, root: str | Path = ".aisuite/artifacts"):
    self.root = Path(root)

Each artifact receives a unique subdirectory named after its generated ID, containing two files: data (raw binary) and metadata.json (reference and hash).

Storing Artifacts with put()

The put method (lines 15-46) handles serialization, unique ID generation, and atomic writes:

  1. Converts input to bytes via _to_bytes(data)
  2. Generates a unique ID using new_id("artifact")
  3. Creates the subdirectory <root>/<artifact_id>/
  4. Writes the binary payload to a file named data
  5. Computes a SHA-256 hash and writes the reference plus metadata to metadata.json
payload = _to_bytes(data)
artifact_id = new_id("artifact")
artifact_dir = self.root / artifact_id
artifact_dir.mkdir(parents=True, exist_ok=False)
data_path = artifact_dir / "data"
meta_path = artifact_dir / "metadata.json"
ref = ArtifactRef(
    artifact_id=artifact_id,
    uri=f"artifact://{artifact_id}",
    media_type=media_type,
    size_bytes=len(payload),
    metadata=_artifact_metadata(payload, metadata),
)
data_path.write_bytes(payload)
meta_path.write_text(json.dumps({"ref": ref.to_dict(),
                                 "created_at": now()}, sort_keys=True) + "\n",
                     encoding="utf-8")

The method returns the ArtifactRef, which contains the artifact://<uuid> URI for later retrieval.

Retrieving Artifacts with get()

The get method (lines 49-61) resolves references (either ArtifactRef objects or URI strings) and reconstructs complete Artifact instances:

artifact_id = _artifact_id(ref)
artifact_dir = self.root / artifact_id
data_path = artifact_dir / "data"
meta_path = artifact_dir / "metadata.json"
if not data_path.exists() or not meta_path.exists():
    raise KeyError(...)
metadata = json.loads(meta_path.read_text(encoding="utf-8"))
return Artifact(
    ref=ArtifactRef.from_dict(metadata["ref"]),
    data=data_path.read_bytes(),
    created_at=metadata.get("created_at", ""),
)

Deleting Artifacts with delete()

The delete method (lines 63-80) performs idempotent cleanup by attempting to remove the data file, metadata.json, and the artifact directory, silently ignoring FileNotFoundError and OSError exceptions:

artifact_id = _artifact_id(ref)
artifact_dir = self.root / artifact_id
try: (artifact_dir / "data").unlink()
except FileNotFoundError: pass
try: (artifact_dir / "metadata.json").unlink()
except FileNotFoundError: pass
try: artifact_dir.rmdir()
except (FileNotFoundError, OSError): pass

Metadata Integrity and Helper Functions

Several helper functions (lines 82-108) ensure data consistency and safety:

  • _artifact_id: Normalizes reference strings by stripping the artifact:// prefix
  • _to_bytes: Coerces payloads (strings or bytes) to binary format
  • _artifact_metadata: Computes SHA-256 hashes of the payload and validates JSON-serializability using ensure_json_serializable from aisuite/agents/types.py

These guarantees allow the framework to verify artifact integrity and safely embed metadata in JSON traces.

Integration with the aisuite Framework

When tools return large outputs—such as the stdout from a shell command—the framework automatically converts them to artifacts. The resulting ArtifactRef is stored in step.data["result_artifacts"] rather than the full payload, keeping network and storage overhead minimal. The viewer UI retrieves these via HTTP endpoints that internally call FileArtifactStore.get, while InMemoryArtifactStore provides a volatile alternative for unit testing.

Working with FileArtifactStore

The following example demonstrates persistent artifact storage across process boundaries:

from aisuite import FileArtifactStore

# Initialize store with a custom directory

store = FileArtifactStore("/tmp/aisuite_artifacts")

# Store large text output from a tool

ref = store.put(
    "large stdout content from long-running process...",
    media_type="text/plain",
    metadata={"source": "shell_command", "exit_code": 0}
)

# The reference contains a lightweight URI that can be serialized

print(ref.uri)  # artifact://<uuid>

# Retrieve later, possibly in a different process

artifact = store.get(ref)
print(artifact.text()[:50])  # First 50 characters of the stored data

# Cleanup the on-disk files

store.delete(ref)

Summary

  • FileArtifactStore implements the ArtifactStore protocol for persistent, filesystem-based artifact storage in aisuite
  • Artifacts reside in dedicated subdirectories under .aisuite/artifacts, each containing a data file and metadata.json with SHA-256 hashes
  • The put method generates deterministic unique IDs, computes integrity hashes, and returns serializable ArtifactRef objects with artifact:// URIs
  • The get method reconstructs Artifact instances from disk, while delete performs safe, idempotent cleanup
  • Integration with tool execution automatically offloads large outputs to prevent trace bloat, with InMemoryArtifactStore available for testing scenarios

Frequently Asked Questions

What is the default storage location for FileArtifactStore?

By default, FileArtifactStore writes artifacts to the .aisuite/artifacts directory relative to the execution context. You can override this by passing a custom path to the constructor: FileArtifactStore("/custom/path"). The directory structure <root>/<artifact_id>/ organizes each artifact's files independently.

How does FileArtifactStore ensure data integrity?

During the put operation, the store computes a SHA-256 hash of the binary payload via _artifact_metadata and stores it in the metadata.json file alongside the ArtifactRef. This hash persists with the artifact, allowing validation whenever the data is retrieved via get.

Can I use FileArtifactStore across different processes?

Yes. Since FileArtifactStore persists data to the filesystem using standard file operations, separate processes can initialize FileArtifactStore with the same root directory and retrieve artifacts using either the ArtifactRef object or the artifact://<uuid> URI string. This enables distributed agent workflows where producers and consumers run in different environments.

What happens if I try to delete an artifact that doesn't exist?

The delete method is idempotent. It catches FileNotFoundError when attempting to unlink the data file or metadata.json, and catches OSError if the directory is not empty or missing. All errors are silently ignored, so calling delete multiple times on the same reference does not raise exceptions.

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 →