Retrieval
Grounded retrieval + answers over your own corpus.
An index holds your tenant's documents — versioned, embedded, and queryable. The SDK ingests them, queries them, and an agent answers grounded in the hits with citations. This page walks the four steps: ingest, query, citations, and the eval gate.
This assumes you have an index created (the dashboard's retrieval page, or the management API).
Ingest documents
upsertDocuments adds a batch to an index. A RetrievalDocument is { text, source?, kind?, heading?, metadata? } — the source is what a citation links
back to, so set it.
Ingest is a management-tier call — a secret key, server-side (never the
browser). The same discipline as mintSession.
// Ingest is a management-tier call — a secret key, server-side (never the
// browser). The same discipline as mintSession. The API origin defaults to
// production.
const retrieval = createRetrieval({ session: process.env.INLAY_SECRET_KEY! });
async function ingestDocs() {
const docs: RetrievalDocument[] = [
{ text: "…", source: "https://example.com/docs/a", heading: "Getting started" },
{ text: "…", source: "https://example.com/docs/b", heading: "The profile" },
];
return retrieval.upsertDocuments("docs", docs);
}Query the index
Headless, query returns the top-k hits. Each RetrievalHit carries the
source, the text, the distance, and the scoreKind — "l2" (the vector
distance), "rrf" (the fused hybrid), or "rerank" (the reranker pass).
async function searchDocs(text: string) {
const hits = await retrieval.query("docs", text, { k: 8, include: ["highlights"] });
return hits.map((hit) => ({
source: hit.source,
heading: hit.heading,
text: hit.text,
// The score kind — "l2" (the vector distance), "rrf" (the fused hybrid), or
// "rerank" (the reranker pass).
scoreKind: hit.scoreKind,
}));
}From a component, the useRetrieval hook owns the query state — a search box
in a few lines:
"use client";
function SearchBox({ session }: { session: SessionSource }) {
const { query, hits, isFetching } = useRetrieval({
session,
index: "docs",
});
return (
<>
<input onChange={(e) => void query(e.target.value)} />
{isFetching ? <p>Searching…</p> : hits.map((hit) => <p key={hit.id}>{hit.text}</p>)}
</>
);
}Grounded answers + citations
An agent with the retrieval tool answers grounded in the hits — the answer's
citations ride the message.end payload onto the assistant row. Render them
with <Citations>: each marker is a <sup>[n]</sup> linking the claim to its
source.
A citation is a point-in-time snapshot — a re-published corpus can drift
it. Pass the index's CURRENT generation (each RetrievalHit carries its
generation) so a stale citation gets the drift marker.
function GroundedAnswer({ row, hits }: { row: AssistantRow; hits: RetrievalHit[] }) {
return (
<>
<p>{row.content}</p>
<Citations citations={row.citations ?? []} currentGeneration={hits[0]?.generation} />
</>
);
}The eval pack (the publish gate)
A golden set, when installed, gates publication of that index's corpus.
It is not an automatic evaluation of every agent publish, and publishing an
agent that uses retrieval is not evidence that its answers were evaluated.
putEvalPack installs the pack: cases (golden queries + expectSources /
expectNone) + minRecall + maxFalseHitRate. evaluate runs it on demand
and returns the same report the gate produces (recall, falseHitRate,
gatePassed, per-case results).
async function installGoldenSet() {
const pack: EvalPack = {
cases: [
{ question: "How do I ingest documents?", expectSources: ["https://example.com/docs/a"] },
{ question: "What's the refund policy?", expectNone: true },
],
minRecall: 0.9,
maxFalseHitRate: 0.05,
};
// The golden set gates this index's corpus publication, not agent publishing.
await retrieval.putEvalPack("docs", pack);
// Run it on demand (the same report the gate produces).
return retrieval.evaluate("docs");
}Tuning: the profile
The index's declared profile drives how it retrieves. getProfile reads the
effective profile; putProfile applies an evolution; previewProfile shows
the evolution a put would produce (the rebuild classification) without
applying it — a "rebuild" re-embeds the corpus, so preview first.
The retrieval field holds the query-time knobs: bm25, fusion (the hybrid
weights), rerank (a reranker pass over the fused candidates), diversify (an
MMR-style spread over a declared metadata field).
async function enableRerank() {
const { profile: current } = await retrieval.getProfile("docs");
const candidate: RetrievalProfile = {
...current,
retrieval: { ...current.retrieval, rerank: { kind: "api", model: "rerank-v3.5" } },
};
// previewProfile shows the evolution a put WOULD produce (the rebuild
// classification) without applying it — a "rebuild" re-embeds the corpus.
const preview = await retrieval.previewProfile("docs", candidate);
if (preview.evolution !== "rebuild") {
await retrieval.putProfile("docs", candidate);
}
}Reference
The full surface — the RetrievalFacade's methods, the RetrievalProfile's
closed schema, the EvalPack / EvalReport shapes — is under
@inlayai/sdk and
@inlayai/react.
Next
Typed clients — turning agent names into compile errors.