The shared wrapper over ALLM.Pipeline.Artifacts — stores and retrieves
pipeline artifacts (HTML, JSON, extracted text) through the configured
adapter, and returns a URL reference for the PostgreSQL step_log.
What this module owns, and what the adapter owns
Here: compression, checksum and size accounting. store/4 gzips (unless
compress: false), SHA-256s the ORIGINAL bytes, records the ORIGINAL size,
and fetch/1 gunzips on the way back out. An adapter receives an opaque,
already-encoded payload plus a ALLM.Pipeline.Artifacts.meta/0 and stores
both verbatim — so a new backend never re-derives any of it.
There: capacity. "Does this fit me?" is the adapter's question. An
adapter that cannot take a payload returns {:error, :too_large} and this
module routes to the next tier. DynamoDB's answer is the interesting one: its
400KB ceiling constrains the item as stored, i.e. gzipped and then
base64-encoded, so Dynamo.fits_item?/1 measures that and not the caller's
original byte count — gating on the original routed a 600KB HTML artifact
that gzips to 40KB down the (unimplemented) S3 path, where it was discarded.
size_bytes in the return tuple and in the stored item is still the ORIGINAL
uncompressed size; only the routing decision changed.
URL Format
- DynamoDB:
dynamo://table_name/artifact_id - Filesystem:
file:///path/to/artifact - Memory:
memory://artifact_id - S3:
s3://bucket_name/key
Tiering is an adapter choice (Phase 7)
This module no longer routes by size or special-cases an s3:// URL. Every
URL is handed to Artifacts.impl/0, which for the production wiring is
ALLM.Pipeline.Artifacts.Tiered — it measures the post-encode size and routes
small→DynamoDB / large→S3 on put/4, and dispatches fetch/delete/exists?
by URL scheme. An oversize artifact now has a real home, which is why
ALLM.Pipeline.Executor.build_envelope/3 no longer truncates its LLM envelope
in two rounds.
The gunzip is bounded (memory-safety)
fetch/1 gunzips a stored payload with an EXPLICIT decompressed-size ceiling
(max_decompressed_bytes/0, default 64 MB), returning
{:error, :artifact_too_large} rather than inflating an unbounded amount into
memory. The ceiling is a fixed memory-safety budget — what one fetch may
safely hold — chosen INDEPENDENTLY of any store's capacity: Tiered/S3
removed the 400 KB DynamoDB bound, so "the max stored size" is undefined, and a
cap generous enough to inflate a legitimate huge artifact whole would still
admit a decompression bomb of that size. A genuinely huge artifact simply
cannot be fetched whole into memory — the correct posture.
Summary
Types
@type store_result() :: {:ok, url :: String.t(), size :: non_neg_integer(), checksum :: String.t()}
Functions
Delete artifact by URL.
Check if artifact exists.
@spec fetch(String.t()) :: fetch_result()
Retrieve artifact content by URL, decompressed.
The adapter returns the payload as stored (see
ALLM.Pipeline.Artifacts.stored/0); the gunzip happens here, because
compression is this layer's concern.
@spec max_decompressed_bytes() :: pos_integer()
The decompressed-size ceiling fetch/1 enforces, in bytes (default 64 MB).
A memory-safety budget, overridable per environment via
config :allm_pipeline, ALLM.Pipeline.ArtifactStore, max_decompressed_bytes: N.
Store artifact and return URL.
Artifacts whose stored payload fits a DynamoDB item go to DynamoDB, larger ones to S3. "Stored payload" means post-compression and post-base64 — see the moduledoc.
Options
:compress- Whether to gzip compress the content (default: true)
Returns
{:ok, url, size_bytes, checksum} where:
urlisdynamo://table/idors3://bucket/keysize_bytesis the original uncompressed sizechecksumis the SHA-256 hash of the original content