Explain a search
Ask Memory.search why it kept, cut or left out each turn, with explain=True.
An example
Four turns, a question, a budget of 60 tokens and a time window. The explanation covers every stored turn of the scope, oldest first, including the one the window left out.
from datetime import datetime, timezone
from llmslim.memory import Memory
memory = Memory()
day = lambda d: datetime(2026, 10, d, 9, 0, tzinfo=timezone.utc)
memory.add({"role": "user", "content": "I live in Delhi, near Lodhi Garden."}, user_id="asha", timestamp=day(1))
memory.add({"role": "user", "content": "I moved to Pune last month, to Koregaon Park."}, user_id="asha", timestamp=day(20))
memory.add({"role": "assistant", "content": "Noted: you live in Pune now, in Koregaon Park."}, user_id="asha", timestamp=day(20))
memory.add({"role": "user", "content": "Book me a window seat whenever you can."}, user_id="asha", timestamp=day(21))
context = memory.search("Where did I move to?", user_id="asha", budget=60, since="2026-10-10", explain=True)
print(context.text, end="\n\n")
for turn in context.explanation.turns:
print(f"{turn.role:9} {turn.status:8} {str(turn.reason):15} {turn.tokens:3} tokens {turn.share:.0%}")[2026-10-20 (Tue) 09:00] user: I moved to Pune last month, to Koregaon Park.
assistant: Noted: you live in Pune now, in Koregaon Park.
user dropped outside_window 0 tokens 0%
user kept None 30 tokens 50%
assistant kept None 17 tokens 28%
user dropped over_budget 0 tokens 0%Each turn's tokens is what its block takes in the returned text, and share is that over the budget. The kept reply shows assistant: without a date because it is in the same session as the turn above it.
What an explanation holds
context.explanation is a SearchExplanation:
- the call:
mode(whole_history,turnsordigest),route(the kind of question:lookup,advice,overvieworother),budget,tokens,room(what the sections left for turns),user_room(the part reserved for user turns),user_turn_capandclipped; turns: every stored turn of the scope, with itssession, itspool(userorreply),status(kept,clippedordropped),reason,tokensandshare;- each turn's
candidates: a user turn is one, a reply is cut into windows that are each one, with itsrankandscorein its pool, its size and what became of it; sections: the standing requests, preferences and conflicts sections, whether each wasshownand why not, itscap, and every statement it weighed, best first.
The numbers add up: with tiktoken, the turns' and sections' tokens sum to tokens exactly. With the 4-characters-per-token estimate, counting block by block can round a few tokens either way, and the rest is unattributed_tokens.
Why a turn was left out
When more than one reason applies, the explanation gives the one the search checked first.
| Reason | Meaning |
|---|---|
outside_window | Stored before since, or at or after until. |
over_budget | It didn't fit in the room left in the budget. |
over_share | It didn't fit in the share its pool or section may take (user_room, a section's cap). |
budget_full | Never tried: fewer than 8 tokens of room were left when its turn came. |
share_full | Never tried: the share of a summary for whole reply passages was full. |
duplicate | Its words are almost all in text already kept. |
dropped_at_fit | Selected, then dropped because the exact count of the rendered text ran over the budget. |
detail_kept | In a summary, a passage of the turn was kept whole, so its opening wasn't used. |
no_candidate | Nothing of the turn could be selected, such as a system or tool turn in a summary. |
clipped_at_end | The last resort cut the text to whole words, and this block was after the cut. |
Why a turn was cut
A clipped turn was kept in part:
| Reason | Meaning |
|---|---|
user_turn_cap | A user turn longer than user_turn_cap: its most relevant sentences were kept. |
windows | Some of the reply's windows were kept and others weren't. |
opening | In a summary, only the turn's opening (and a reply's outline) is shown. |
last_resort | Nothing fit whole, so the best candidate was cut to fit. |
cut_at_fit | The last candidate left was cut short to fit the exact count. |
clipped_at_end | The last-resort cut went through its block. |
Sections and the statements they weigh have reasons of their own, such as route (this kind of question doesn't get the section) or no_disagreement. The full list is in the reference.
Over HTTP
llmslim serve takes "explain": true on a search and adds explanation, as to_dict() gives it. The rest of the response is the same. The TypeScript client doesn't pass the flag yet, and the recall tool of llmslim mcp returns text for a model and doesn't explain.
curl -s localhost:8080/v1/memories/search -H 'content-type: application/json' \
-d '{"user_id": "asha", "query": "Where did I move to?", "explain": true}'Limits
When the text is clipped as a last resort, context.turns lists a turn only when a whole sentence of it is left, while the explanation counts every block the cut text reaches. A time window is read twice: to name the turns it left out, the scope is read again without it.
Questions
Does explain change the context?
No. The text, the turns and the token counts are the same as without it. With explain off, the default, no extra work is done.
Can I log explanations?
Yes. They name turns by id and never repeat their text, so an explanation can be stored without the conversation in it. to_dict() gives JSON and SearchExplanation.from_dict reads it back.
Can I compare scores across turns?
Only within one pool. Scores are the ranking's own numbers: they don't compare across pools, calls or relevance settings.