Use Artifacts in the Agents

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

  1. Data artifacts — tool result payloads (entity lists, constituents, classifications) stored as JSON. Created by put(), read by get() / resolve() / inspect().
  2. File artifacts — xlsx/csv exports. Metadata stored, file content either in Redis (small) or /tmp (large). Created by put_file() or put_file_content(), read by get_file() / get_file_content().

Three storage tiers (in priority order)

TierData artifactsFile artifactsCross-worker?
In-memory cache_data_cache (line 83)_file_cache (line 84)No — same-worker fast path
Redismcp: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 pathNo — 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, computes expires_at = now + ttl_hours*3600, sanitizes payload, writes to cache + Redis//tmp
  • put_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 /tmp file

Default TTL

  • _TTL_HOURS_DEFAULT = 2 (line 78)

Expiry — three mechanisms

  1. Redis TTL — setex() auto-expires keys (lines 169, 254, 358). No sweep needed.
  2. Lazy expiry on read — get() / get_file() check expires_at < now and delete on access (lines 185-192, 270-277, 220-222, 304-307).
  3. sweep() (line 403) — for /tmp fallback only. Iterates in-memory caches + /tmp/artifacts/ directory, removes expired files. No-op for Redis (line 407-414).

Cleanup

  • _safe_remove() (line 140) — silent os.unlink, ignores FileNotFoundError
  • 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:

  1. Try get_file_content() first (Redis-stored, cross-worker safe) — decodes xlsx base64 if needed
  2. Fall back to get_file() + read from /tmp (same-worker only)
  3. 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.

Leave a Reply