internal/recall/recall.go
internal/recall · 543 lines · 19 declarations · source
This file carries the package documentation, rendered on the package page.
Declarations
const longestAnchor
const longestAnchor = 4
longestAnchor is the most words an entity name is assumed to have.
Four. Every window up to this length is looked up, so the cost of raising it is a longer candidate list on every recall and the cost of lowering it is entities whose names are simply unreachable. Names longer than this exist; they are reached by whichever shorter window matches an alias.
type Store
type Store interface {
// Anchors resolves candidate terms to entities within the authorised scopes.
AnchorsForSubject(ctx context.Context, scopes, terms []string, subject string) ([]domain.Anchor, error)
// FactsAbout returns the currently-valid facts naming any of the given entities, with their
// evidence, up to limit+1 rows so the caller can tell a full result from a truncated one.
//
// `sources` is the set of message roles whose facts may be returned, and it is an argument
// rather than a default so that no call can omit it and get everything.
//
// `at` is the pair of instants the read is taken at. Its zero value is the current read, which
// is what almost every call wants — so the historical read is available without the ordinary
// one having to say anything.
//
// `hops` is how far from the anchors the traversal may go. One is the ordinary read: facts about
// what the question named. More is a chain, and it comes back with the relations it followed.
FactsAboutForSubject(ctx context.Context, scopes, entityIDs []string, limit int, sources []string,
at domain.AsOf, hops int, subject string) ([]domain.CitedFact, error)
}
Store is the read this package needs. It is an interface here so that what recall DOES is separable from where the rows are — and so the anchoring rules can be read without reading SQL.
type Budget
type Budget struct {
// Characters bounds what comes back. Zero takes the default.
Characters int
// MaxRows bounds the query rather than the answer. It exists so an enormous budget cannot ask
// the database for an unbounded read, and it is not the unit a caller reasons in.
MaxRows int
}
Budget is how much a caller can afford to be given back.
Why characters and not tokens
A caller's constraint is tokens; ours cannot be. A token count is a property of the tokeniser the caller's model uses, which is not the one we would use, and the difference is a few percent that varies by model and by text. Reporting a token count we computed with the wrong tokeniser is a number that is wrong in a way the caller cannot correct — and it would be believed, because it is labelled tokens.
Characters are exact, stable, and convertible by whoever knows their own model. A caller with an eight-thousand-token allowance and a model averaging four characters to the token asks for thirty-two thousand characters. That is their arithmetic with their number, rather than ours with a guess.
Why not rows
A row is not a size. One fact carries a two-word quote and another carries a paragraph, so a caller converting rows to anything has to assume the worst — which means asking for far less than they can afford, every time, to avoid overrunning once.
func DefaultBudget
func DefaultBudget() Budget
DefaultBudget is deliberately modest. It is a cut nothing has ranked, so a caller who has not said what they can afford should be given a small arbitrary slice rather than a large one.
type Recaller
type Recaller struct {
store Store
budget Budget
// semantic is nil when the deployment has no embedding revision; then only the exact path runs.
semantic Semantic
// surfaces and themes are per-call selections copied in by RecallWithControls; the zero value
// selects every surface and no forced theme lookup.
surfaces map[string]bool
themes bool
}
Recaller answers questions against one instance's memory.
func NewWithBudget
func NewWithBudget(store Store, b Budget) *Recaller
NewWithBudget builds a recaller over an explicit budget.
var PrincipalSources
var PrincipalSources = []string{string(domain.RoleUser)}
Recall resolves a question to entities and returns the facts naming them.
The authorised scopes are an argument, never a default
A project is a permission inside one instance, and a principal granted several recalls across them in one bundle. So the set comes from the caller on every read. There is no "all scopes" and no empty-means-everything: an empty set returns nothing, because a read that widens when its permission list is missing is the failure that permission list exists to prevent.
No anchor means an empty bundle, not an error
A question naming nothing we know about is an ordinary outcome — it is most questions, early in a scope's life. A caller made to distinguish that from a failure will end up treating failures as ordinary. PrincipalSources is what a recall returns unless the caller asks for more: facts extracted from what the principal themselves said.
Why this is the default rather than an option
A tool result, a fetched page or a document is written by somebody who is not the person the memory is about, and text reaching extraction from those sources has been measured planting a fact that passes every structural check the product makes — an admitted relation, a verbatim quote, an exact span. Detecting that is undecidable. Knowing who said it is not.
So the ordinary bundle contains what the person said. Everything else is still stored, still citable, and still returned to a caller who asks for it — labelled, so a reader can tell a statement from something a web page claimed.
method Recaller.Recall
func (r *Recaller) Recall(ctx context.Context, scopes []string, question string) (domain.Bundle, error)
Recall answers a question from what the principal said, as things stand now, about the entities the question names.
const DirectHop
const DirectHop = 1
DirectHop is the ordinary read: facts about what the question named, and nothing reached through them.
Why one is the default rather than two
A second hop is paid for on every call by every caller, and most questions are answered by the first. It also changes what a bundle IS: at one hop every fact is about something the question named, and at two some facts are about something else entirely, reached through a relation the caller did not ask about. That is genuinely useful for "who does Marta's manager work for" and genuinely noise for "where does Marta live", and nothing here can tell which question it was given.
So the caller says. A default that quietly widened would make every bundle bigger, every read slower and every answer harder to explain, in exchange for helping the minority of questions that need a chain.
const MaxHops
const MaxHops = 2
MaxHops is as far as a traversal goes.
Two, because two is what has a reader: "my manager's employer" is a real question and a chain of four is one nobody has asked for. The cost of a third hop is not linear — each level multiplies by the fanout cap — so raising this is a decision that arrives with a measurement rather than with a larger number.
method Recaller.RecallAsOf
func (r *Recaller) RecallAsOf(ctx context.Context, scopes []string, question string,
sources []string, at domain.AsOf, hops int) (domain.Bundle, error)
RecallAsOf answers a question as it would have been answered at a moment.
Two instants, because a memory has two histories
What was true of the world, and what this system had been told. They come apart on every correction, and only both together answer "why did you tell me that last week" — which is the question an audit asks and the one a scalar timestamp cannot answer.
A past read is not a different product
It anchors the same way, bounds the same scopes, admits the same sources and is cut by the same budget. The only difference is which rows are current, so nothing about a historical bundle is weaker — and every fact in one carries the date its validity ended, which is what stops a fact that held in March from reading as a fact that holds now.
method Recaller.RecallForSubjectAsOf
func (r *Recaller) RecallForSubjectAsOf(ctx context.Context, scopes []string, question string,
sources []string, at domain.AsOf, hops int, subject string) (domain.Bundle, error)
RecallForSubjectAsOf narrows attribution inside the authorised projects. Empty subject preserves project-wide recall; a supplied subject is applied by the store to anchors, each hop and evidence.
method Recaller.composeUnanchored
func (r *Recaller) composeUnanchored(ctx context.Context, scopes []string, question string, sources []string,
subject string, bundle domain.Bundle, reach domain.Reach) (domain.Bundle, error)
composeUnanchored answers a question that anchored nothing: reports by theme, then passages, each charged to the budget. The reach still says nothing was anchored, because that is still true.
method Recaller.composeRest
func (r *Recaller) composeRest(ctx context.Context, scopes []string, question string, sources []string,
subject string, bundle domain.Bundle, spent int, unanchored bool) domain.Bundle
composeRest adds the report and passage surfaces after the facts, in that order, under what the budget has left. Reports run when nothing anchored or the caller asked for themes; passages run whenever the budget is not yet spent. A surface that is off, unavailable or refusing is named in Degraded and the bundle answers with the rest. unanchored is whether the question matched no name exactly; reports answer such a question by theme, and a semantic anchor does not change that.
method Recaller.Budget
func (r *Recaller) Budget() Budget
Terms returns the candidate entity names a question might be naming.
Why every window rather than one pass of named-entity recognition
A model call per question would put a second inference hop in front of every read, on the path a caller is waiting on, to answer something a lookup already answers: an entity is in memory or it is not, and its name is a string we already hold. Windowing the question and asking the index is exact, has no vocabulary of its own, and costs one query.
Why the windows are normalised the same way entity names are
The resolution key is `NormalizeName`, so anything that generates lookup candidates has to produce them in exactly that shape. Any other normalisation here would silently fail to match entities that are in fact stored — the worst kind of miss, because the entity is right there.
Punctuation is dropped at token boundaries and nowhere else, so `work?` reaches `work` while `o'brien` and `co-op` survive as written. Budget is what the recaller was built with, for the operations that share it.
func Terms
func Terms(question string) []string
func markCoDerived
func markCoDerived(facts []domain.CitedFact)
markCoDerived says which facts in a bundle are not independent of each other.
Two facts from the same message about the same subject are one person saying one thing once. They may be restatements at different granularities, or genuinely different claims that happen to share a sentence — this does not decide which, because nothing deterministic can.
What it prevents is the reader's error: counting them as corroboration. A model given a bundle with two rows saying nearly the same thing treats the repetition as weight, and the repetition is an artefact of how one sentence was extracted rather than evidence of anything.
Computed here rather than stored, because it is a property of a BUNDLE and not of a fact: the same two facts in a bundle that contains only one of them are independent of nothing.
func NormalizeSubject
func NormalizeSubject(s string) string
NormalizeSubject compares subjects the way entity resolution does, so two facts naming one person in different casing are recognised as being about the same subject.
func factSize
func factSize(f domain.CitedFact) int
factSize counts Unicode code points in the fact text a caller places in context. Quote and surrounding context are both charged because both are returned. Source labels and graph paths are also text; identifiers, anchors, timestamps and JSON syntax are outside this content budget.