Why architecture matters here
Agent frameworks live or die on how they separate reasoning state from work product. The transcript and session state are reasoning state: compact, textual, replayed into the model every turn. Work product is different in every dimension — it is large (megabytes, not kilobytes), binary (images, spreadsheets, archives), and long-lived (the user wants the report next week, not just this turn). An architecture that conflates the two pays twice: once in tokens, because blobs smuggled into context are re-sent on every model call, and once in reliability, because session stores backed by databases have row-size ceilings that a single PDF will blow through.
The artifact service exists to make that separation structural rather than disciplinary. Because it is a distinct interface with its own scoping and versioning rules, the framework can enforce the cheap path by default: tools return small textual summaries to the model — saved sales_q3.csv, version 2, 48,112 rows — while the bytes take a side channel to durable storage. The model reasons over names and metadata; the bytes move only when something genuinely needs them. This is the same instinct that separates a message queue from a blob store in any distributed system, applied inside the agent: control plane in the transcript, data plane in the artifact service. Get it wrong and every turn drags the data plane through the control plane; get it right and a session that produced a gigabyte of artifacts still replays in a few thousand tokens.
The architecture: every piece explained
The contract is a small, deliberately storage-shaped interface: saveArtifact(appName, userId, sessionId, filename, part) returning the new version number, loadArtifact(..., filename, version) where an absent version means latest, listArtifactKeys, deleteArtifact, and listVersions. The unit stored is a Part — the same content type the model API uses — so an artifact can flow into a prompt without conversion. Saves are append-only: version numbers increment per filename, and old versions stay readable until the artifact is deleted. That single decision buys reproducibility (a tool that read v3 can be re-run against v3), safe concurrency (two writers produce two versions instead of a lost update), and an audit trail for free.
Above the interface sits the namespace resolver. A plain filename like draft_report.pdf is scoped to the session — invisible to other sessions, garbage-collected with it. A user: prefix — user:preferences.json, user:signed_contract.pdf — keys the artifact to the user across all sessions of the app, which is how an agent remembers a document the user uploaded last month. Access flows through context objects, not raw service handles: ToolContext and CallbackContext carry the app/user/session triple implicitly, so a tool author writes toolContext.saveArtifact(name, part) and cannot accidentally cross tenants. Behind it all sit the implementations: InMemoryArtifactService for unit tests and local runs, and a GCS-backed service that lays blobs out under an app/user/session/filename/version object path, one bucket per deployment.
End-to-end flow
Follow one turn of a data-analysis agent. The user asks for a revenue chart; the model calls the run_python tool. The code executor runs the generated script in its sandbox, and the script writes revenue.png. The executor does not paste bytes into the tool response — it hands the file to the framework, which calls saveArtifact("revenue.png", part) through the tool's context. The artifact service resolves the namespace (no prefix, so session scope), asks the version index for the next version — this is the first save, so v0 — writes the blob to gs://bucket/analytics-app/user-42/session-9f3/revenue.png/0, and returns 0. The tool's textual reply to the model is one line: chart saved as revenue.png (v0).
The model now wants to describe the chart, so the framework loads the artifact back and attaches it to the next model request as an inline image Part — bytes flow model-ward exactly once, when vision is actually needed. The user asks for a tweak; the tool runs again and saves v1. Nothing overwrote v0: a later show me the first version resolves cleanly. At session end, the UI lists artifact keys, finds revenue.png, and offers the latest version as a download link served straight from GCS with a signed URL — the bytes never transit the agent runtime again. Weeks later, a session-TTL sweeper deletes the session and its artifacts; user:-scoped documents survive because their lifecycle is bound to the user, not the session.