Skip to content
Warlock.js v4

Embed text

EmbedderContract is the sibling of ModelContract on the SDK adapter. Text-in, vector-out. No streaming, no tools, no relationship to chat completions. It’s a separate primitive on purpose — different cost profiles, different per-request limits, different failure modes.

interface EmbedderContract {
readonly name: string;
readonly provider: string;
readonly dimensions: number; // 0 until first call when no override given
embed(input: string): Promise<EmbeddingResult>;
embedMany(inputs: string[]): Promise<EmbeddingBatchResult>;
}

embedder() is optional on SDKAdapterContract — not every provider supports embeddings. Check before reaching for it:

if (typeof sdk.embedder === "function") {
const embedder = sdk.embedder({ name: "text-embedding-3-small" });
}
import { OpenAISDK } from "@warlock.js/ai-openai";
const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
const embedder = openai.embedder({ name: "text-embedding-3-small" });
const one = await embedder.embed("Hello, world.");
// { vector: number[], usage: Usage, dimensions: number }
const many = await embedder.embedMany(["foo", "bar", "baz"]);
// { vectors: number[][], usage: Usage, dimensions: number }

Embeddings are deliberately not automatic. Consumers obtain an embedder from the adapter and call it directly. It composes into:

  • Retrieval tools the agent can call (RAG pattern).
  • run steps in a workflow (vector ingest, catalog item embedding).
  • Query vectors for ai.middleware.semanticCache.
  • Vector columns in Cascade for native pgvector search.
  • Cache similarity via cache.set({ vector }) + cache.similar(...).
ai.step({
name: "embed",
run: async (ctx) => {
const text = `${ctx.steps.extract.output.name} ${ctx.steps.extract.output.description}`;
const { vector } = await embedder.embed(text);
ctx.state.embedding = vector;
},
output: { extract: (ctx) => ({ dims: (ctx.state.embedding as number[]).length }) },
});

Wrap the embedder + your vector store in a tool the agent can call:

import { v } from "@warlock.js/seal";
const searchKb = ai.tool({
name: "searchKb",
description: "Search the knowledge base for relevant passages.",
input: v.object({ query: v.string(), k: v.number().optional() }),
execute: async ({ query, k }) => {
const { vector } = await embedder.embed(query);
const hits = await vectorStore.query(vector, { topK: k ?? 5 });
return hits.map((h) => ({ text: h.text, score: h.score, source: h.source }));
},
});
const agent = ai.agent({ model, tools: [searchKb] });

The agent decides when to call it. The model never sees the vector — only the retrieved text.

embedder.dimensions is 0 on a fresh embedder when no override is given — it’s populated from the first embed call’s response. Pre-seed via the adapter’s dimensions config when you need the value BEFORE the first call (sizing a vector column in a Cascade migration, for example).

@warlock.js/ai doesn’t ship a vector store. Bring your own — pgvector, Qdrant, Pinecone, Chroma — and wrap it in an ai.tool({...}). Or use @warlock.js/cache with pgvector to keep both KV and vector retrieval in one driver.