Why architecture matters here

The architecture matters because the naive alternative — putting everything in session state — fails in three compounding ways. First, prompt cost: every turn of an agent re-serializes and often re-sends context, so a megabyte of inlined data is paid for on every single model call, not once. Second, token limits: binary or large text simply will not fit in a context window, and base64-encoding it to survive JSON makes it a third larger while remaining unreadable to the model. Third, persistence cost: session stores are tuned for small, frequently-written records; forcing them to hold blobs turns cheap state writes into expensive ones and couples the lifetime of bulk data to the lifetime of a conversation.

Separating artifacts from state fixes all three at once. Because the blob lives in object storage and only a reference travels in the session, prompts stay small, token budgets are spent on reasoning rather than payload, and the session store keeps doing the one thing it is good at. The reference indirection also unlocks capabilities that inlining cannot offer: the same artifact can be shared across turns and even across sessions without copying, and it can be fetched lazily only when a step actually needs its contents.

Versioning is the second architectural win. Agents are iterative — a tool may regenerate a report three times before the user is satisfied — and an append-only version history means each regeneration produces v2, v3 under the same name rather than destroying the prior output. That gives you an audit trail (what did the agent produce at each step?), safe rollback (the previous version is still there), and a natural way to reference 'the latest' or 'a specific' output without inventing ad-hoc naming conventions per tool.

Consider the cost profile concretely. A support agent handling a session that uploads three documents and generates two reports has, without artifacts, a session record carrying five payloads — perhaps twelve megabytes — that is re-serialized on every one of dozens of turns and re-sent to the model wherever the framework inlines history. That is hundreds of megabytes of redundant serialization and, worse, tokens spent re-encoding bytes the model cannot even read. With the Artifact Service the same session carries five short references totalling a few hundred bytes; the twelve megabytes live once in object storage and are fetched only by the specific steps that parse them. The per-turn cost stops scaling with the payloads and starts scaling with the conversation's actual reasoning, which is the only thing that should drive it. For document-heavy or media-heavy agents this is not a micro-optimization but the difference between a workable cost curve and one that makes the product uneconomic after a handful of turns. The same reference indirection is also what makes an agent's output durable and inspectable after the fact: because every generated artifact is a versioned object in storage, an operator can list exactly what the agent produced across a session, diff two versions of a report, or replay a session from its event log without the payloads having been lost to a truncated context window. State that only ever lived in the prompt is gone the moment the window rolls over; an artifact persists on its own schedule, governed by retention rather than by conversation length.

Finally, the indirection is where governance lives. Because every large payload passes through one service, that service is the single place to enforce size caps, scan or type-check content, apply per-user access control, and attach retention policy. Scatter blobs directly into session records and each of those controls has to be reinvented at every call site; funnel them through an Artifact Service and you get one chokepoint where security, cost, and lifecycle policy can be applied uniformly. That consolidation is the difference between a system whose storage footprint you can reason about and one that quietly grows without bound.

Advertisement

The architecture: every piece explained

The artifact name and version are the primary key. A name is a stable string a tool chooses — report.pdf, chart.png — and each save_artifact under that name allocates the next integer version. The pair (name, version) uniquely identifies an immutable blob; there is no in-place update, only new versions, which is what makes concurrent writers safe and history auditable. Reads default to the latest version but can request any specific one.

The namespace decides visibility. A plain name is scoped to the current session: only agents within that conversation can see it, and it is cleaned up with the session. A name carrying a user-scope marker (conventionally a user: prefix) is scoped to the user across all of their sessions, so a profile photo or a standing document survives between conversations. This two-level scope — session-local versus user-global — is the single most important design decision when saving an artifact, because it fixes both who can read the data and when it is eligible for cleanup.

The storage backend sits behind a uniform interface. An in-memory backend keeps artifacts in a process-local dictionary — perfect for tests and local development, gone when the process exits. A GCS (or S3-style) backend maps each (app, user, session, name, version) tuple to an object key in a bucket and persists durably. Because both implement the same save/load/list contract, application code is written once and the backend is chosen by deployment; nothing in the agent logic knows or cares which is in use.

The reference in the event log is what ties the blob back to the conversation. When an artifact is saved, the runtime records an event carrying the name and version, not the bytes. That reference is what a downstream agent reads to know an artifact exists and how to load it, and it is what makes the whole scheme cheap: the durable, replayable event history stays compact because it holds handles, while the heavy payloads live in object storage keyed by those handles. The version index per name lets list_versions enumerate history and lets a reader resolve 'latest' to a concrete version at read time. Together these pieces mean the Artifact Service is really a small content-addressed store with a conversation-aware naming and scoping layer bolted on top — and understanding that layering is what tells you where to put a given piece of data.

Artifact Service — binary/large payloads kept out of session stateagents save and load named, versioned blobs by referenceAgent / toolsave_artifact(name, blob)Artifact Servicenamespacing + versioningStorage backendGCS bucket | in-memorySession statesmall JSON facts onlyEvent logartifact reference (name+ver)Version indexv1, v2, v3 … per nameNamespacesuser: (cross-session) vs session:Consumer agentload_artifact(name, version?)Ops — lifecycle + GC + access control + size limits + content-typewritepersistrecord refappend verscoperesolveread blobgovernenforce
Agents write large or binary payloads to the Artifact Service, which persists versioned blobs to a storage backend and records only a lightweight name+version reference in the event log; consumers resolve the reference and load the blob on demand, keeping session state small.
Advertisement

End-to-end flow

Trace a document-analysis turn. The user uploads a contract; the runtime saves it as an artifact named contract.pdf in the session namespace. The Artifact Service writes the bytes to the GCS backend under a key derived from app, user, session, name, and version 1, and returns a reference. An event is appended to the session log recording that contract.pdf@1 now exists — but the megabytes stay in the bucket, and the session state grows by only a handful of bytes.

The analysis agent runs next. It reads the event, sees the reference, and calls load_artifact('contract.pdf'); the service resolves 'latest' to version 1, fetches the object from GCS, and hands back the bytes. The agent extracts the text it needs, reasons over a summary, and produces a risk report. Crucially, it does not inline the whole PDF into the prompt — it loads the blob into tool code, extracts the relevant spans, and puts only those into the model's context. The full document never occupies token budget.

The agent then saves its output as risk_report.md, producing version 1 under that name, and records the reference. If the user asks for a revision, a second run regenerates the report and save_artifact allocates version 2 — the first report is untouched and still loadable, giving a clean before/after. Throughout, the session log is a compact sequence of references and small facts; the bulk lives entirely in the backend, fetched only when a step actually needs it.

Watch what the reference indirection buys across the flow. Because the contract is stored once and addressed by name, a third agent — say a compliance checker invoked later in the same session — loads the very same bytes without any re-upload or copy; the artifact is shared, not duplicated. And because the report is versioned, a UI can offer the user 'compare v1 and v2' for free, since both are durably present. Had the document and reports been stuffed into session state, each of these consumers would have paid to re-serialize the whole payload on every turn, and the revision would have overwritten the original with no way to recover it. The end-to-end flow, read carefully, is a small graph of references over a content store — which is exactly why it stays cheap as the conversation and the payloads both grow.