Token-budgeted injection
At the start of every run the agent injects recalled memory — working blocks, facts, episodes — as a SYSTEM block above the chat history. Historically that injection used item-count limits (5 facts / 3 episodes / all working blocks) with no token cap: one oversized episode could blow the context.
Tuning(memory_token_budget=) puts a hard token ceiling on the whole
memory block, with optional recency decay:
from loomflow import Agent, Tuning
agent = Agent(
"Answer using what you know about the user.",
model="gpt-4.1-mini",
memory="sqlite:./bot.db",
tuning=Tuning(
memory_token_budget=1500, # cap the seed memory block
memory_decay_half_life_days=30.0, # optional: prefer fresh memories
),
)| Tuning field | Type | Default | Description |
|---|---|---|---|
memory_token_budget | int | None | None | Token budget (chars/4 estimate) for the seed-time memory block. None keeps the historical item-count behaviour, byte-identical. |
memory_decay_half_life_days | float | None | None | Half-life in days for the recency decay under memory_token_budget. None disables decay. |
What the budget covers
Three memory layers feed the seed, and the budget treats them differently:
- Working blocks are pinned. They are injected first, count against the budget, but are never dropped or truncated — pinned context (a user profile block, say) is pinned. Oversized blocks shrink the recall allowance instead.
- Facts + episodes fill the remainder. They are merged into one
list, scored, and greedily packed into whatever the working blocks
left over — rendered as a single
Recalled memory (most relevant first):block. - Session history is untouched. The budget governs cross-session recall only; this conversation’s rehydrated turns are separate (see Run vs session); mid-run transcript growth is auto-compact’s job, not this budget’s.
Scoring: relevance × decay
Each candidate item is scored relevance * decay:
- Relevance comes from
recall_scored()when the backend supports it — the same BM25 + vector + RRF ranking used everywhere else. Backends without it fall back to plain recall with a neutral relevance of1.0. - Decay is exponential:
0.5 ** (age_days / half_life_days). A 30-day half-life halves an item’s score every 30 days. Facts decay onrecorded_at, episodes onoccurred_at; items without a timestamp never decay.memory_decay_half_life_days=None(the default) means no decay — pure relevance ordering.
Packing is greedy, best-score-first. The first item that doesn’t
fully fit is truncated to the remaining allowance with an
…[truncated] marker, and packing stops there. The top-scored item
is always included — truncated if even it doesn’t fit — so a
tiny budget still surfaces the single most relevant memory rather
than nothing.
# Fresh episode outranks a year-old one at equal relevance:
# score(fresh, 1 day) = 1.0 * 0.5 ** (1/30) ≈ 0.977
# score(old, 365 days) = 1.0 * 0.5 ** (365/30) ≈ 0.0002Token costs use the framework’s chars/4 heuristic — the same estimator family used for tool-result caps and tool-def budgeting.
None = legacy parity
With memory_token_budget=None (the default) the legacy path runs
unchanged: item-count limits, partitioned Known facts: /
Relevant past episodes: headers, byte-identical seed messages.
Setting a half-life without a budget also changes nothing — decay
only activates under a budget. Upgrades are zero-risk until you
opt in.
Tuning() # legacy behaviour, exactly
Tuning(memory_decay_half_life_days=30.0) # still legacy — no budget set
Tuning(memory_token_budget=1500) # budgeted, no decayChoosing a budget
| Workload | Suggested budget |
|---|---|
| Chat assistant, small model context | 500–1500 |
| Knowledge-heavy support agent | 2000–4000 |
| Long-context model, memory-central app | 4000+, or None if item counts suffice |
A useful mental model: the budget is a cap and a ranking policy, not a retrieval expansion. The fetch limits are unchanged (5 facts, 3 episodes per seed) — the budget decides how much of what was fetched actually ships, and in what order.
Bounds. Token costs are estimated (chars/4), not tokenizer-exact — budget for slack rather than treating the cap as a hard model-context guarantee. The budget applies to seed-time memory injection only; it does not govern tool results, session history, or mid-run compaction. And because working blocks are pinned, a working-block set larger than the budget ships in full and starves recall entirely — keep pinned blocks small.
For what working blocks, facts, and episodes are, see the Memory overview and Bi-temporal facts.