An artifact is a persistent, addressable object—such as a dataset, file, report, or HTML output—stored outside the LLM’s conversation context and referenced by an ID.
When creating agents for extensive data-heavy tasks, it quickly becomes apparent that the large amounts of data displayed in the conversation field are unwieldy and unnecessary. Users only need to see a partial summary, as populating everything is data-consuming and overwhelming. However, we do require the full-size data or alternative formats, such as HTML reports, to ensure that the objects can be stored in the server’s backend and, if needed, made available for download such as at https://factset-index-plugin.factset.io/download/170781d2-d120-4a42-96d1-45b0b56975b7.
Herein are the codes for the realization of this artifact and for the management of its lifespan.
First, the codes are reflected in artifacts.py:
Two registries
- Data artifacts — tool result payloads (entity lists, constituents, classifications) stored as JSON. Created by
put(), read byget()/resolve()/inspect(). - File artifacts — xlsx/csv exports. Metadata stored, file content either in Redis (small) or
/tmp(large). Created byput_file()orput_file_content(), read byget_file()/get_file_content().
Three storage tiers (in priority order)
| Tier | Data artifacts | File artifacts | Cross-worker? |
|---|---|---|---|
| In-memory cache | _data_cache (line 83) | _file_cache (line 84) | No — same-worker fast path |
| Redis | mcp:artifact: prefix, setex with TTL (line 169) | mcp:filemeta: / mcp:filecontent: prefixes (lines 254, 358) | Yes — primary |
/tmp fallback | /tmp/artifacts/{aid}.json (line 133) | /tmp/artifacts/{aid}.fmeta + file path | No — same-worker only |
Redis is the primary store when REDIS_URL is set. The /tmp fallback kicks in when Redis is unavailable (lines 123-145, 172-176, 257-262). Connection is lazy and one-shot (lines 90-120).
Second, lifespan management.
Creation
put()(line 150) — generates UUID, computesexpires_at = now + ttl_hours*3600, sanitizes payload, writes to cache + Redis//tmpput_file()(line 231) — same pattern, stores file metadata (not content)put_file_content()(line 319) — stores content inline in Redis if <5MB (line 316), else falls back to/tmpfile
Default TTL
_TTL_HOURS_DEFAULT = 2(line 78)
Expiry — three mechanisms
- Redis TTL —
setex()auto-expires keys (lines 169, 254, 358). No sweep needed. - Lazy expiry on read —
get()/get_file()checkexpires_at < nowand delete on access (lines 185-192, 270-277, 220-222, 304-307). sweep()(line 403) — for/tmpfallback only. Iterates in-memory caches +/tmp/artifacts/directory, removes expired files. No-op for Redis (line 407-414).
Cleanup
_safe_remove()(line 140) — silentos.unlink, ignoresFileNotFoundError- On expiry, both the metadata file and the actual data file are removed (lines 419-426, 439-441)
HTTP route — server.py
server.py:350-393
GET /download/{artifact_id} — serves file artifacts for download:
- Try
get_file_content()first (Redis-stored, cross-worker safe) — decodes xlsx base64 if needed - Fall back to
get_file()+ read from/tmp(same-worker only) - Returns 404 if metadata missing, 410 if file missing from disk
MCP tool — inspect_artifact
server.py:321-342
Wraps artifacts.inspect() (line 462 of artifacts.py) — returns keys, list counts, and a sample without re-emitting the full payload. Lets Claude recall what an artifact contains.
Tool integration — 8 files in tools/
Tools call artifacts.put() to store results and artifacts.resolve() to consume prior artifacts.