Skip to main content

Memory Pointer Pattern

The problem

When a tool returns 200KB of logs, or 5,000 database rows, or a full PDF, passing the raw output back into the LLM context causes three failures:
  1. Token blowup. A 200KB log dump is ~50,000 tokens. At GPT-4o’s 2.50/1Minputrate,thats2.50 / 1M input rate, that's 0.13 per turn just to feed the model context it doesn’t need.
  2. Silent truncation. Most providers cap at 128K–200K tokens. A few big tool calls can break the conversation entirely.
  3. Worse answers. Models get distracted by huge irrelevant blobs (the “needle in a haystack” problem).
IBM reported reducing 20,000,000 tokens to 1,234 tokens in production by adopting this pattern. The principle is simple: store the big value outside the LLM’s view, give the LLM a short pointer instead.

How it works

The agent sees only the short JSON pointer envelope. If it needs the full value later, it calls the auto-injected getArtifact(pointer) tool.

Enabling on an Agent

ArtifactsConfig fields

When enabled: true, three tools are auto-injected into the agent’s tool list:
  • storeArtifact(name, value, contentType?)
  • getArtifact(pointerOrName)
  • listArtifacts()

The auto-converted result

When a tool exceeds the threshold, its result is replaced with a JSON string of this shape:
The LLM sees ~250 tokens instead of ~25,000. The preview field is critical — it lets the model decide whether the artifact is interesting before fetching the full value.

Manual artifact storage

Tools can opt into the pattern explicitly:
Now the LLM can refer to the artifact by name:
The agent calls getArtifact("report-2024-q4") (by name, not pointer) and gets the full text.

API reference

storeArtifact(ctx, value, opts?)

Stores a value and returns a pointer.
value can be any JSON-serializable object or a string. Objects are JSON.stringify’d for preview and size computation; the raw value is preserved for retrieval.

getArtifact(ctx, pointerOrName)

Looks up an artifact by either its art: pointer or its name. Returns null for missing.

listArtifacts(ctx)

Returns every artifact stored in the current RunContext, deduplicated (name aliases don’t double-count).

isPointer(value)

Helper for runtime checks:

approxByteSize(value)

Quick UTF-8 size estimate used by the executor:
Falls back to 0 for circular objects.

Auto-injected tools (when artifacts.enabled)

These tools intentionally bypass the size threshold themselves — otherwise storeArtifact calls would recursively wrap their own output.

Lifecycle and scope

Artifacts live on RunContext.sessionState["__artifacts"] as a Map<string, StoredArtifact>. That means:
  • Per-run by default: A new RunContext starts with an empty map.
  • Per-session if you persist sessionState: Pass the same sessionState between runs (e.g. via your session manager) and artifacts carry forward.
  • Not persisted by default: The default SessionManager does write sessionState to storage, but the map serializes to [] unless you use a JSON-aware codec. For durable artifacts, persist them explicitly to your own storage and re-hydrate.

When to use

  • Database query tools that may return many rows
  • Web scraping / page fetch tools
  • Log search tools
  • File reading tools where files can be > a few hundred KB
  • Tool chains where output of step N is input to step N+1 but doesn’t need to pass through the LLM in between

When NOT to use

  • Short status checks (“is X online?”) — overhead isn’t worth it
  • Single-row lookups
  • Anything the LLM legitimately needs to reason over inline (e.g. a small JSON config)
  • Streaming output (the threshold check is one-shot on the final result)

See also

  • Tool PolishtoModelOutput is a more surgical way to shrink specific tool outputs.
  • Async HandleId Pattern — pair pointers with handles for long-running + large-output tools.