The artifact store
Compression is only defensible once it is reversible.
Milestone A piece of information missing from the digest is retrieved via expand() without the 38,000 tokens ever touching the main context.
Chapter 06 ends with an unsolved problem, and it is the most serious one in the whole system.
The tiny agent read 38,000 tokens and returned 118. So it decided what matters. Except it does not know the overall task - it sees a brief, not the conversation. Perhaps the caller wanted to know afterwards which countries were involved. The agent threw that paragraph away, because the brief only asked about start and end.
Compression without a way back is data loss with better PR.
The goal
Compression is only defensible once it is reversible. The caller gets the compact answer and keeps access to the original. That is the difference between a summary and a loss.
The idea in one sentence
Throw nothing away, just do not send it along.
tool output (38k tokens)
├─> full text to the file system -> artifact_id
├─> cut into chunks (~512 tokens, 64 overlap)
├─> embed the chunks -> in-memory matrix, as in chapter 03
└─> handle to the caller -> "art_7f3a1c"
And then expand(handle, question): a vector search inside this one artifact
that returns the three most relevant chunks. Typically 400 to 1,500 tokens
instead of 38,000.
Step 1: the store
# tiny/artifacts.py
from __future__ import annotations
import hashlib
import json
import time
from dataclasses import dataclass
from pathlib import Path
import numpy as np
from tiny.llm import embed
STORE = Path(".artifacts")
CHUNK = 2000 # characters, roughly 500 tokens
OVERLAP = 250 # so a sentence on the seam is not lost
TTL_HOURS = 24
@dataclass
class Artifact:
handle: str
source: str
tokens_estimated: int
preview: str
def _chunks(text: str) -> list[str]:
step = CHUNK - OVERLAP
return [text[i : i + CHUNK] for i in range(0, len(text), step)] or [""]
def store(text: str, source: str) -> Artifact:
STORE.mkdir(exist_ok=True)
handle = "art_" + hashlib.sha256(f"{source}{text[:400]}".encode()).hexdigest()[:8]
folder = STORE / handle
folder.mkdir(exist_ok=True)
(folder / "fulltext.txt").write_text(text, encoding="utf-8")
chunks = _chunks(text)
(folder / "chunks.json").write_text(json.dumps(chunks), encoding="utf-8")
vectors = np.array(embed(chunks), dtype=np.float32)
vectors /= np.maximum(np.linalg.norm(vectors, axis=1, keepdims=True), 1e-12)
np.save(folder / "vectors.npy", vectors)
(folder / "meta.json").write_text(
json.dumps({"source": source, "created": time.time()}), encoding="utf-8"
)
return Artifact(handle, source, len(text) // 4, text[:200].replace("\n", " "))
def expand(handle: str, question: str, k: int = 3) -> str:
"""Vector search INSIDE one artifact. The rest stays where it is."""
folder = STORE / handle
if not folder.exists():
return f"Artifact {handle} does not exist (any more)."
chunks = json.loads((folder / "chunks.json").read_text(encoding="utf-8"))
vectors = np.load(folder / "vectors.npy")
q = np.array(embed([question])[0], dtype=np.float32)
q /= max(float(np.linalg.norm(q)), 1e-12)
best = np.argsort(-(vectors @ q))[:k]
return "\n\n---\n\n".join(chunks[i] for i in sorted(best))
def cleanup() -> int:
"""Artifacts are a session cache, not an archive."""
limit = time.time() - TTL_HOURS * 3600
removed = 0
for folder in STORE.glob("art_*"):
meta = json.loads((folder / "meta.json").read_text(encoding="utf-8"))
if meta["created"] < limit:
for file in folder.iterdir():
file.unlink()
folder.rmdir()
removed += 1
return removed
Two decisions in there are deliberate, not convenient.
The overlap. Without it a sentence sitting exactly on the cut line ends up half in each chunk - and is found in neither. 250 characters are roughly one or two sentences and cost 12 percent more storage. A good trade.
The expiry. Artifacts are a session cache. Keep them and after two weeks you
have an unintended archive of scraped third-party content on disk - with
everything that means legally and practically. cleanup() belongs in a cron job,
at the latest the day after the prototype.
Step 2: wire it into the runner
Remember the ugly 12,000-character emergency brake from chapter 05? It gets replaced now:
# tiny/runner.py, inside the tool loop
from tiny.artifacts import store
ARTIFACT_THRESHOLD = 16000 # characters, roughly 4000 tokens
...
else:
function = REGISTRY[name][0]
content = str(function(**arguments))
if len(content) > ARTIFACT_THRESHOLD:
artifact = store(content, source=f"{name}:{arguments}")
collected_artifacts.append(artifact)
content = (
content[:ARTIFACT_THRESHOLD]
+ f"\n\n[truncated. Full text under handle={artifact.handle}]"
)
This first of all protects the tiny agent itself - otherwise a single scrape eats its own context before it gets round to condensing anything. And it produces the handle that travels on.
In the digest the handle comes up with it:
return Result(
...,
artifacts=[
{
"handle": a.handle,
"source": a.source,
"tokens": a.tokens_estimated,
"preview": a.preview,
}
for a in collected_artifacts
],
)
Those four fields cost around 40 tokens together. In exchange the caller knows that 38,000 more are lying around, where they came from and roughly how they start. That is enough to decide whether following up is worth it.
Step 3: the caller's third tool
The calling agent now has exactly three tools, no matter how many capabilities are registered:
# tiny/host.py
from tiny.artifacts import expand
from tiny.concierge import load_concierge
from tiny.runner import delegate
_concierge = load_concierge()
def find_capability(goal: str) -> dict:
"""Can anybody here do this? No LLM call, calibrated answer."""
d = _concierge.route(goal)
if d.kind == "abstain":
return {"can_do": False, "confidence": round(d.score, 3)}
card = _concierge.cards[d.agent_id]
return {
"can_do": True,
"agent": card.id,
"summary": card.summary,
"limitations": card.does_not,
"confidence": round(d.score, 3),
"ambiguous": d.kind == "ambiguous",
"alternatives": [a for a, _ in d.alternatives],
}
def host_delegate(task: str) -> dict:
return dict(delegate(_concierge, task))
def host_expand(handle: str, question: str) -> str:
return expand(handle, question)
Three functions, three tool definitions in the main context. By the arithmetic from chapter 01 that is around 600 tokens - constant, whether three capabilities are registered or five hundred. That is the target figure of the whole architecture, and now it is there.
find_capability is the declarative answer to the question you first want to
solve dialogically: just ask the agent whether it can do this. That would be an
LLM call, a round of latency and an uncalibrated answer. Here it is a vector
search with a number behind it.
The milestone
The test consists of two calls, and the second one is the point.
# scripts/07_expand_test.py
from tiny.host import host_delegate, host_expand
result = host_delegate(
"read https://en.wikipedia.org/wiki/World_War_II and tell me "
"when the war began and when it ended"
)
print("Digest :", result["summary"][:120])
print("Fields :", result["fields"])
for a in result["artifacts"]:
print(f"Artifact: {a['handle']} ~{a['tokens']} tokens {a['source'][:40]}")
# This information was NOT in the digest - nobody asked for it.
handle = result["artifacts"][0]["handle"]
follow_up = host_expand(handle, "which countries took part in the wannsee conference")
print(f"\nFollow-up: ~{len(follow_up) // 4} tokens")
print(follow_up[:400])
Digest : The Second World War began on 1 September 1939 with the invasion of Poland ...
Fields : {'start': '1 September 1939', 'end': '2 September 1945'}
Artifact: art_7f3a1c ~38412 tokens http_get:{'url': 'https://en.wikipe
Follow-up: ~610 tokens
... the Wannsee Conference on 20 January 1942 ...
Now the bookkeeping, because only that proves anything:
| What | Tokens in the main context |
|---|---|
| Digest | 118 |
| Artifact reference | ~40 |
Follow-up via expand | ~610 |
| Total | ~770 |
| Without the artifact store it would be | 38,412 |
The state is reached when you can retrieve a piece of information that did not appear in the digest - while the 38,000 tokens never show up in your main context. They are on disk. Reachable, but not present.
That is the difference this was all about.