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_budget set, 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 governed LLMCall on the bus, correlated by trace, so an index build isn’t an invisible bill.
  • tokenguardbudget caps an index build and track attributes it, the same as any other spend.
  • acttrace / cassetteAuditLog records the retrieval, and cassette replays a whole RAG trajectory — retrieval included — offline in CI.

Honest limits

  • VectorIndex is in-memory and exact-scan — perfect for tests and small corpora, wrong for millions of chunks. Bring a real store via the retriever seam; 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 with instrument() yourself.
  • Injected context counts against the window. Combine retriever= with context_budget so 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.