Recipe: vector filter helpers
Use matchesFilter to evaluate vector-store filter predicates outside an adapter, and pair with postgresWithRoles for row-level security.
Two utility recipes that pair with vector memory and row-bound SQL.
#matchesFilter
Every VectorMemory adapter accepts an optional filter β
matchesFilter(record, filter) is the same predicate evaluated
outside the adapter, useful when you want to:
- Pre-filter a hand-curated cache before sending it to the model.
- Sanity-check a filter at runtime before passing it to a backend whose own validator is unhelpful.
- Build a custom retriever (e.g. a hybrid retriever that mixes vector + tag-only matches).
import { matchesFilter } from '@agentskit/memory'
const records = [
{ id: '1', metadata: { tier: 'free', region: 'us-east' } },
{ id: '2', metadata: { tier: 'pro', region: 'eu-west' } },
{ id: '3', metadata: { tier: 'pro', region: 'us-east' } },
]
const filter = {
$and: [
{ tier: 'pro' },
{ region: { $in: ['us-east', 'us-west'] } },
],
}
const matches = records.filter(r => matchesFilter(r, filter))
// β [{ id: '3', ... }]Filters use the same operator vocabulary as the vector backends β
$eq, $ne, $in, $nin, $gt, $gte, $lt, $lte,
$contains, $and, $or, $not. See the
vector adapters recipe for the full table.
#postgresWithRoles β row-level security through agents
Standard postgresQuery runs every query as the same DB role. For
multi-tenant agents you almost always want the query to run as the
user's role so Postgres RLS policies kick in:
import { postgresWithRoles } from '@agentskit/tools/integrations'
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const tool = postgresWithRoles({
pool,
/**
* Map the agent's run-context to a Postgres role.
* The tool issues `SET LOCAL ROLE <role>` before each query.
*/
resolveRole: ctx => `tenant_${ctx.tenantId}`,
/**
* Optional: also set search_path / app.user_id for RLS policies
* that read `current_setting('app.user_id')`.
*/
sessionVars: ctx => ({ 'app.user_id': String(ctx.userId) }),
allowWrites: false,
maxRows: 200,
})Pair with createRuntime:
import { createRuntime } from '@agentskit/runtime'
import { createSharedContext } from '@agentskit/runtime'
const ctx = createSharedContext({ tenantId: '42', userId: 999 })
const runtime = createRuntime({
adapter,
tools: [tool],
context: ctx,
})
await runtime.run('Show all my recent orders.')The agent issues SELECT * FROM orders and Postgres applies the RLS
policy automatically β no need for the agent to know about the
tenant filter.
#Combining both
A common pattern: pre-filter the vector hits in JS with matchesFilter,
then run a tenant-scoped SQL JOIN through postgresWithRoles to
hydrate the results:
const vectorHits = await rag.search(query, { topK: 50 })
const eligible = vectorHits.filter(h =>
matchesFilter(h, { 'metadata.tier': { $in: ['pro', 'enterprise'] } }),
)
const ids = eligible.map(h => h.id)
const rows = await tool.execute(
{ sql: `SELECT * FROM documents WHERE id = ANY($1::text[])`, params: [ids] },
ctx,
)The vector store stays tenant-agnostic (cheap), the SQL layer enforces tenant isolation (correct).
#Related
- Vector adapters
- Recipe: persistent memory
- Mandatory sandbox β pair with
postgresWithRolesfor layered defence.
Explore nearby
- PeerRecipes
Copy-paste solutions grouped by theme. Every recipe end-to-end, runs as written.
- PeerCustom adapter
Wrap any LLM API as an AgentsKit adapter. Plug-and-play with the rest of the kit in 30 lines.
- PeerAdapter contract tests
Verify any adapter against the ADR 0001 invariants A1βA10 with the shared test harness.