InlayDocs

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.

On this page