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 emit a governed LLMCall — tokens and cost captured on the bus, correlated by trace — for 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. 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.

Plugs into the stack

Embeddings ride cendor-core’s bus, so budget caps an index build, track attributes it, AuditLog records it, 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.