---
url: 'https://adk.nht.io/batteries/vector/retrievable.md'
description: >-
  createVectorRetrievableCallbacks() turns any vector store into the four
  Retrievable TurnRunner callbacks.
---

# Retrievable Glue

## LLM summary — Retrievable Glue

* `createVectorRetrievableCallbacks(opts)` (from `@nhtio/adk/batteries/vector`) returns the four `Retrievable` callbacks of the `TurnRunnerConfig` retrieval surface: `fetchRetrievablesCallback`, `storeRetrievableCallback`, `mutateRetrievableCallback`, `deleteRetrievableCallback`.
* Options `VectorRetrievableGlueOptions`: `{ store: CallableVectorStore, collection: string, trustTier: RetrievableTrustTier | ((m: VectorMatch) => RetrievableTrustTier), topK?=5, filter?: VectorFilter, deriveQuery?, toRetrievable? }`.
* `trustTier` is REQUIRED and never inferred from `source` — honors the security rule that trust tiers are deployer-asserted, not derived from data.
* fetch: default `deriveQuery` = last `role:'user'` message from `await ctx.fetchMessages()`, else `undefined` → `[]`. Runs `store(collection).nearText(text)` (or `.nearVector()` for a `number[]`), applies `filter` via `.whereRaw(filter)`, `.select('id','document','metadata').limit(topK)`, maps each `VectorMatch` → `new Retrievable({ ...toRetrievable(m), trustTier })`.
* store/mutate: `Retrievable` → `vs(collection).upsert([{ id, document: await r.contentString(), metadata: { trustTier, createdAt, updatedAt, source?, kind? } }])` (vector omitted → the store encodes the document; needs an encoder or built-in encoding). mutate === store (upsert replaces).
* delete: `store(collection).whereIn('id', [id]).delete()`.
* The encoder lives on the `store` (its `encoder` option or built-in encoding); the glue carries no encoder of its own — it passes text through the builder and the store resolves it.
* default `toRetrievable` maps `VectorMatch` → `RawRetrievable` minus trustTier: id, content (document), source/kind (from metadata if string), score, createdAt/updatedAt (metadata or now). Override `deriveQuery`/`toRetrievable` to customize.
* This is the downstream seam to Bring-your-own-retrieval: the four callbacks drop straight into a `TurnRunnerConfig`.

The query builder is the standalone store. This is how that store becomes part of a turn. A `TurnRunner` doesn't know what a vector database is — it knows the four `Retrievable` callbacks of its retrieval contract (fetch, store, mutate, delete). `createVectorRetrievableCallbacks()` is the adapter between the two: hand it a store and a collection, get back the four callbacks, drop them into your `TurnRunnerConfig`. The agent now retrieves from your vector index without a line of glue code you had to write.

## One call, four callbacks

```typescript
import { createVectorRetrievableCallbacks } from '@nhtio/adk/batteries/vector'

const {
  fetchRetrievablesCallback,
  storeRetrievableCallback,
  mutateRetrievableCallback,
  deleteRetrievableCallback,
} = createVectorRetrievableCallbacks({
  store: vs,                 // a CallableVectorStore
  collection: 'docs',
  trustTier: 'first-party',  // required — see below
  topK: 5,
})

// drop straight into the TurnRunner retrieval surface
const runner = new TurnRunner({
  // …
  fetchRetrievablesCallback,
  storeRetrievableCallback,
  mutateRetrievableCallback,
  deleteRetrievableCallback,
  // …
})
```

That's the whole integration. What each callback does:

| Callback | Does |
| --- | --- |
| `fetchRetrievablesCallback` | Derives a query from the turn, searches the collection, maps hits → `Retrievable[]` |
| `storeRetrievableCallback` | `Retrievable` → upsert (the store encodes the document text) |
| `mutateRetrievableCallback` | Same as store — an upsert replaces |
| `deleteRetrievableCallback` | Deletes the record by id |

## `trustTier` is required, and never guessed

`trustTier` is the one option with no default, and that's a security decision, not an oversight. A trust tier is a **deployer assertion** about how much the runtime should trust a piece of content — it is never inferred from the data itself, because inferring "this is first-party because its `source` looks internal" is exactly the kind of guess an attacker games. So you state it:

```typescript
// a flat tier for the whole collection
createVectorRetrievableCallbacks({ store: vs, collection: 'docs', trustTier: 'first-party' })

// or a function, if the collection mixes provenance
createVectorRetrievableCallbacks({
  store: vs,
  collection: 'mixed',
  trustTier: (m) => (m.metadata?.source === 'internal' ? 'first-party' : 'third-party'),
})
```

Even the function form is you writing the rule, in code you can read and test — not the battery sniffing a field and hoping. This mirrors the rule documented in [Bring your own retrieval](../../assembly/byo-retrieval): the kit will not assign trust on your behalf.

## How fetch builds its query

By default the glue retrieves against the **last user message** in the turn. It calls `ctx.fetchMessages()`, walks back to the most recent `role: 'user'` message, and uses its content as the query text; if there's no user message, it returns `[]` (nothing to search for, so nothing retrieved). Then it runs the builder you already know:

```typescript
store(collection)
  .nearText(queryText)              // or .nearVector(vec) if deriveQuery returns a number[]
  .whereRaw(filter)                 // if you passed a `filter` option
  .select('id', 'document', 'metadata')
  .limit(topK)                      // default 5
```

…and maps each `VectorMatch` into a `new Retrievable({ ...toRetrievable(m), trustTier })`. Two hooks let you change the behaviour without rewriting the callback:

* **`deriveQuery(ctx)`** — return a `string` (text, encoded by the store), a `number[]` (a precomputed query vector), or `undefined` (skip retrieval this turn). Override it to query from something other than the last user message — a summarized conversation, a rewritten query, a HyDE passage.
* **`toRetrievable(match)`** — map a `VectorMatch` to a `RawRetrievable` (minus `trustTier`, which the glue applies). The default pulls `id`, `content` from `document`, `source`/`kind` from metadata when present, `score`, and `createdAt`/`updatedAt` (from metadata or now). Override it when your metadata shape differs.

## How store/mutate/delete work

Writing is the inverse, and it leans on the store's encoder:

```typescript
// store / mutate: a Retrievable becomes an upsert with the document text and no vector —
// the store encodes the document (so the store needs an encoder or built-in encoding)
await vs(collection).upsert([{
  id: r.id,
  document: await r.contentString(),
  metadata: { trustTier: r.trustTier, createdAt, updatedAt, source, kind },
}])

// delete: by id
await vs(collection).whereIn('id', [id]).delete()
```

The glue carries **no encoder of its own.** It passes the document text through the builder and lets the store resolve it — via the store's `encoder` option or its built-in encoding. If the store has neither, a store/mutate call surfaces `E_VECTOR_STORE_ENCODER_REQUIRED`, same as a text upsert anywhere else (see [Encoders](./encoders)). `mutate` is literally `store` — an upsert is a replace — so updating a `Retrievable` re-encodes and overwrites it.

## Where this sits

This is the downstream seam named on the [hub](./): the [Embeddings battery](../../assembly/batteries-embeddings) (or any `VectorEncoderFn`) feeds vectors *in*; this glue feeds search results *out*, into the `Retrievable` callbacks that [Bring your own retrieval](../../assembly/byo-retrieval) describes. The store stays a general-purpose vector database; the glue is the thin, optional layer that makes it a turn's memory.

## Where to go next

* [Bring your own retrieval](../../assembly/byo-retrieval) — the `Retrievable` primitive, trust tiers, and the retrieval pipeline these callbacks plug into.
* [Encoders: vectors or text](./encoders) — what the store needs for store/mutate to encode documents.
* [The Ask ADK showcase](../../showcase/ask-adk) — a full retrieval pipeline (rewrite, HyDE, rerank, abstain) built above a store like this.
