Retrieval (RAG)
Give the agent your documents — with the retrieval itself governed. The SDK is not a vector database: embeddings are governed calls, retrieval is a seam, and your store (pgvector, Pinecone, Chroma, …) plugs in as a one-function callable.
Quickstart
from cendor.sdk import Agent, run, VectorIndex
kb = VectorIndex(model="text-embedding-3-small", provider="openai") # or embedder=<your fn>
kb.add(["Refunds within 30 days.", "Support hours are 9-5 UTC."])
agent = Agent(name="rag", model="gpt-4o", retriever=kb.as_retriever(k=3),
instructions="Answer only from the provided context.")
run(agent, "What's the refund window?") # passages retrieved + injected, embed call governed
import OpenAI from 'openai';
import { Agent, run, VectorIndex } from '@cendor/sdk';
const kb = new VectorIndex({ model: 'text-embedding-3-small', client: new OpenAI() });
await kb.add(['Refunds within 30 days.', 'Support hours are 9-5 UTC.']);
const agent = new Agent({ name: 'rag', model: 'gpt-4o', retriever: kb.asRetriever(3),
instructions: 'Answer only from the provided context.' });
await run(agent, "What's the refund window?"); // passages retrieved + injected, embed governed
Core concepts
Governed embeddings — embed
embed(model, inputs) / aembed(...) return one vector per input and ride the instrumented
client, so core captures a governed LLMCall (metadata["embedding"] = True) — tokens and cost
on the bus, correlated by trace, and (since 1.7.0 / 0.10.0, on core ≥ 1.6) the pre-flight
pass applies: a budget(usd=…, on_exceed="block") refuses an over-budget embed before it
fires, and a guard(...) can redact the text before the provider sees it. OpenAI-family
providers:
from cendor.sdk import embed, trace
with trace("index-build"):
vectors = embed("text-embedding-3-small", ["hello", "world"], provider="openai")
import OpenAI from 'openai';
import { embed, trace } from '@cendor/sdk';
const vectors = await trace('index-build', () =>
embed('text-embedding-3-small', ['hello', 'world'], { client: new OpenAI() }));
So an index build shows up in report() and the
audit chain like any other spend — no invisible embedding
bills.
Always-on RAG — Agent(retriever=...)
A retriever is any query -> list[str] callable. Before each model call, retrieved passages
are injected as a system message — and when Agent(context_budget=…) is set, they’re packed into
the window by contextkit alongside the conversation (squeeze
compression engages only when you assemble with contextkit directly — the SDK’s packing path trims
rather than compresses). So retrieval feeds the
same assembly layer the mapping tables point RAG at. VectorIndex is a dependency-free in-memory
cosine index built on the governed embed() — right for small corpora, demos, and tests. For
scale, wrap your own store:
def retriever(query: str) -> list[str]:
return [row.text for row in my_pgvector_search(query, k=3)]
agent = Agent(name="rag", model="gpt-4o", retriever=retriever, instructions="...")
const retriever = async (query: string) =>
(await myPgvectorSearch(query, 3)).map((row) => row.text);
const agent = new Agent({ name: 'rag', model: 'gpt-4o', retriever, instructions: '...' });
Agentic RAG — retrieval as a tool
Let the model decide when to retrieve — expose the store as a @tool, and each retrieval
becomes a governed, audited ToolCall in result.tool_steps:
from cendor.sdk import tool
@tool
def search_kb(query: str, top_k: int = 5) -> list[str]:
"""Retrieve relevant passages."""
return [h.text for h in kb.search(query, k=top_k)]
agent = Agent(name="rag", model="gpt-4o", tools=[search_kb])
import { tool } from '@cendor/sdk';
import { z } from 'zod';
const searchKb = tool(async ({ query, topK }) =>
(await kb.search(query, topK)).map((h) => h.text), {
name: 'search_kb',
description: 'Retrieve relevant passages',
parameters: z.object({ query: z.string(), topK: z.number().default(5) }),
});
const agent = new Agent({ name: 'rag', model: 'gpt-4o', tools: [searchKb] });
Which to pick? Always-on retrieval (retriever=) when every question needs the corpus —
one search per turn, no extra model round-trip. Agentic retrieval (a tool) when retrieval is
occasional or composable with other tools — the model spends a turn deciding, and the decision
itself is in the audit trail.
Semantic memory is the same mechanism
Long-term memory across sessions is retrieval wearing a different hat: store facts in the index,
attach it as the retriever, and past knowledge comes back by relevance. See
Memory & sessions.
How it works
Retrieval is a seam, not a database — one governed embed call, then assembly into the window before the model ever runs:
%%{init: {"flowchart": {"htmlLabels": false}} }%%
graph TD
Q["run(agent, query)"]
RET["retrieve passages<br/>(VectorIndex or your store)"]
EMB["embed(query)<br/>governed LLMCall on the bus (core)"]
ASM["assemble the window<br/>(contextkit packs to the token budget)"]
BUD["pre-flight budget<br/>(tokenguard)"]
CALL["the model call<br/>core.instrument() → the bus"]
OUT["Result + audit chain"]
Q --> RET --> EMB --> ASM --> BUD --> CALL --> OUT
classDef seam fill:#2563EB,color:#ffffff,stroke:#1E40AF;
class CALL seam;
Plugs into the stack
Retrieved passages become part of the prompt, so retrieval sits inside the governed loop, not beside it:
- ↔ contextkit — with
context_budgetset, passages are assembled into the token window alongside the conversation. (squeeze compression engages via direct contextkit use —evict="compress"blocks — not through the SDK’s packing path.) This is the assembly layer the feature map routes RAG to. - ↔ cendor-core — every
embed()is a governedLLMCallon the bus, correlated bytrace, so an index build isn’t an invisible bill. - ↔ tokenguard —
budgetcaps an index build andtrackattributes it, the same as any other spend. - ↔ acttrace / cassette —
AuditLogrecords the retrieval, and cassette replays a whole RAG trajectory — retrieval included — offline in CI.
Honest limits
VectorIndexis in-memory and exact-scan — perfect for tests and small corpora, wrong for millions of chunks. Bring a real store via theretrieverseam; the SDK won’t grow one.- Embedding governance is OpenAI-family today — elsewhere, embed outside and hand vectors in
(
embedder=), or wrap your embedding call withinstrument()yourself. - Injected context counts against the window. Combine
retriever=withcontext_budgetso retrieval can’t crowd out the conversation. - Retrieval quality is yours. Chunking, ranking, and freshness live in your store; the SDK governs the calls, it doesn’t tune them.
- Always-on retrieval injects passages as a system message. Retrieved text enters with system-role trust, so if your corpus holds untrusted or user-submitted content, treat it as a prompt-injection surface — sanitize it, or expose retrieval as a tool (agentic RAG) so passages arrive with tool-role trust instead.