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)→ returnsArtifactRefget(ref)→ returnsArtifactdelete(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:
- Converts input to bytes via
_to_bytes(data) - Generates a unique ID using
new_id("artifact") - Creates the subdirectory
<root>/<artifact_id>/ - Writes the binary payload to a file named
data - 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 theartifact://prefix_to_bytes: Coerces payloads (strings or bytes) to binary format_artifact_metadata: Computes SHA-256 hashes of the payload and validates JSON-serializability usingensure_json_serializablefromaisuite/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
ArtifactStoreprotocol for persistent, filesystem-based artifact storage in aisuite - Artifacts reside in dedicated subdirectories under
.aisuite/artifacts, each containing adatafile andmetadata.jsonwith SHA-256 hashes - The
putmethod generates deterministic unique IDs, computes integrity hashes, and returns serializableArtifactRefobjects withartifact://URIs - The
getmethod reconstructsArtifactinstances from disk, whiledeleteperforms safe, idempotent cleanup - Integration with tool execution automatically offloads large outputs to prevent trace bloat, with
InMemoryArtifactStoreavailable 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →