Recall is the agent toolkit's headline capability: hand it a natural-language
query and a token budget, and it returns a ranked, budget-fitted context
pack assembled across the vector, search, and
graph planes. It is the difference between a vector store and a
brain — recall is a pipeline, and fabriq has every stage as a first-class
port.
type RecallRequest struct {
Query string // natural-language; embedded + lexical-searched
Budget int // token budget for the returned pack
Entities []string // entity types to recall from
K int // candidates per channel (default 24)
Hops int // graph expansion depth (default 1)
Filters query.Where // optional structured constraints
}
pack, err := tk.Recall(ctx, agent.RecallRequest{
Query: "overheating pumps", Budget: 8000, Entities: []string{"asset"},
})The pipeline01
Semantic — embed
Query(via the Embedder) and runVector().Similarper entity.Lexical —
Search().Searchper searchable entity (full-text).Graph — expand the top seeds one or more hops along each entity's declared edges (and, optionally, reverse edges).
Fuse — combine the channels with Reciprocal Rank Fusion.
Hydrate — load the fused ids' authoritative rows from the relational plane (the source of truth), batched per entity.
Pack — greedily include rows in fused-rank order until the next would exceed
Budget; the rest are reported asOmitted.
type ContextItem struct {
Entity, ID string
Row json.RawMessage // the hydrated, current row
Score float64 // fused rank score
Source []string // provenance: "vector","search","graph"
Tokens int
}
type ContextPack struct {
Items []ContextItem
Omitted int // candidates that didn't fit the budget
Tokens int // total estimate used
Warnings []string // degraded-channel notes — observable, not silent
}Why Reciprocal Rank Fusion
RRF combines the channels using only rank position, never raw scores —
score = Σ weight / (60 + rank) across channels. So cosine similarity, BM25
relevance, and graph hop-distance never have to be normalized against each
other, which is the single biggest source of fragility in hybrid retrieval. An
item that surfaces in multiple channels rises; per-channel weights are tunable
via Config.ChannelWeights.
Recall is lenient by default: if a channel errors (search engine
unreachable, no embedder), it is logged, dropped, and surfaced in
ContextPack.Warnings — recall returns what the surviving channels found. Set
Config.Strict = true to make any channel failure a hard error instead.
Graph expansion02
The graph channel expands from the top seeds along each seed entity's declared
edges, staying inside fabriq's confirmed openCypher subset
(MATCH (n:Label {id:$id})-[:Rel]->(m:Target) RETURN m.id). Neighbours map back
to entities through the edge's target — no label-to-entity reverse lookup
needed. Knobs:
Hops— depth;> 1emits a variable-length path ([:Rel*1..H]).Config.GraphReverse— also expand incoming edges (opt-in; off by default to bound fan-out).Config.GraphSeeds— how many top-ranked seeds to expand (default 8).
Token budgeting03
Config.Tokenizer func([]byte) int estimates a row's token cost; the default is
a dependency-free bytes/4 heuristic. Wire an exact tokenizer (tiktoken, etc.)
for precise budgets. The packer stops at the first row that would overflow,
preserving fused-rank order, and reports the remainder as Omitted.
Altitude-aware recall04
When context distillation is enabled, RecallRequest
accepts an altitude parameter that controls which layer of the digest tree
is consulted before (or instead of) raw entity rows:
type RecallRequest struct {
Query string
Budget int
Entities []string
K int
Hops int
Filters query.Where
Altitude agent.Altitude // AltAuto (default) | AltEntity | AltScope | AltTenant
}| Value | Behaviour |
AltAuto (default, zero value) | Token budget drives descent: if the candidate entity rows fit within Budget, recall descends to raw entity rows (AltEntity); otherwise it climbs to the tenant-root digest summary (AltTenant). |
AltEntity | Raw entity rows only — digest items are dropped. |
AltScope | Scope/cluster digest summaries are kept; the entity rows they cover are dropped (when at least one digest is present). |
AltTenant | The tenant-root digest only; covered entity rows are dropped (when a digest is present). |
In all non-AltAuto modes, a digest and any entity row it covers are never
both included in the same ContextPack. If no digest items are present the
behaviour gracefully falls back to returning whatever was found.
// Tight budget: AltAuto climbs to the tenant digest when entity rows don't fit.
pack, err := tk.Recall(ctx, agent.RecallRequest{
Query: "north site equipment status",
Budget: 1500,
Entities: []string{"asset"},
Altitude: agent.AltAuto,
})
// Generous budget: AltAuto descends to entity rows when they fit.
pack, err := tk.Recall(ctx, agent.RecallRequest{
Query: "north site equipment status",
Budget: 12000,
Entities: []string{"asset"},
Altitude: agent.AltAuto,
})Auto-distillation rides the same write events as auto-embedding: the
proj:distill worker consumes the same event stream as proj:embed, so a
write that triggers re-indexing also triggers re-summarisation. No extra
wiring is needed to keep the two in sync.
Read primitives05
For agents that want manual control, the same tool surface exposes the raw planes as individual tools (see MCP for the agent-facing names):
| Tool | Plane | Notes |
recall | all (fused) | the auto-context front door |
vector_similar | vector | semantic nearest-neighbour by query text |
search | search | full-text over indexed fields |
graph_traverse | graph | read-only openCypher (caller-supplied) |
get | relational | one row by id |
graph_traverse passes caller-supplied cypher straight to the graph engine. A
read-only guard rejects mutating clauses (CREATE/MERGE/DELETE/SET/…) as
defense-in-depth, but deployments exposing it to untrusted callers should also
use a read-only graph connection.