Documentation
Getting Started
Everything you need to install, configure, and integrate UMA memory into your AI agents.
Introduction
UMA is a production-first memory runtime for developers building AI agents. It combines raw evidence, semantic facts, episodic memory, procedural skills, graph links, profiles, and compiled wiki memory into a single SDK with a clean retrieval interface.
Security is a first-class design principle, not a feature layer. The architecture enforces explicit ownership boundaries, tracks provenance through every write, hashes content for integrity, and scans every memory write for prompt-injection patterns. UMA is the first Apache-2.0 memory runtime with OWASP ASI06 (Memory Poisoning) defense built into the canonical write and read paths — source-trust scoring, injection-pattern detection, quarantine, trust-aware retrieval, and an isolation-by-construction vector contract that addresses LLM08 (Vector and Embedding Weaknesses). See the OWASP coverage table below.
UMA manages memory only. It retrieves context, compiles evidence-backed memory, and maintains rebuildable wiki projections. Your agent owns all reasoning, tools, and final responses.
Don't read tons of docs — ask your coding assistant. UMA ships interactive documentation for AI coding assistants. Ask "how do I integrate UMA into my chatbot?" or "how do I write a custom vector backend?" — the relevant reference loads automatically.
What is UMA
UMA is a production-first memory architecture inspired by how human memory evolves: capture, association, consolidation, forgetting, and recall. It transforms raw interaction history into compiled, evidence-backed knowledge, then lets agents recursively navigate that knowledge under explicit budgets.
Key capabilities
- Bounded, deterministic retrieval — RLM loops use strict step, action, and time budgets.
- Memory as environment — agents peek into memory via safe, snippet-first APIs instead of loading full context into prompts.
- Explicit ownership contracts — write-facing and promotion-facing paths use explicit primitive ownership fields.
- Memory poisoning defense — every user input and assistant reply is scanned against an OWASP-aligned pattern catalog; high-severity hits are quarantined out of retrieval.
- Hardened file ingestion — MIME consistency checks reject executables masquerading as documents; HTML and Markdown are sanitized of active payloads before chunking.
- Provenance as invariant — memory answers and compiled artifacts remain traceable back to raw chunks.
- Compiled wiki memory — canonical records synthesized over evidence, with drift detection and regeneration.
- Configurable backends — use embedded SQLite + LanceDB or FAISS. Use your favorite LLM provider — OpenAI, Claude, or Ollama.
- SDK-first — UMA manages memory only; your agent controls reasoning, tools, and final responses.
- Multi-language support for injection scanning — Bundled English, French, Spanish, German, and Simplified Chinese YAML catalogs are aligned with [OWASP Agent Memory Guard](https://owasp.org/www-project-agent-memory-guard/).
Installation
By the end of this guide you will have UMA running on the default embedded Lite profile — SQLite for authoritative storage, LanceDB for vector retrieval — with your LLM and embedding provider configured. No external database is required.
Prerequisites
Before you begin, ensure the following are available on your machine:
- Python 3.9 or later — check with
python --version - Git — to clone the repository
-
An LLM and embedding provider — UMA supports
ollama,openai, andanthropic(LLM only) for the LLM;ollamaandopenaifor embeddings. Have your API key or local endpoint ready before editinguma.yaml.
Step 1 — Clone the repository
git clone https://github.com/fad-schme/UMA.git cd UMA
Step 2 — Create a virtual environment and install
UMA's base install includes SQLite, LanceDB, and support for the Ollama and OpenAI providers. Everything runs in-process — no separate database service to start.
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -e .
Step 3 — Configure your provider
Open config/uma.yaml and set the embedding and
llms.uma sections to match your environment. UMA uses its internal LLM
for memory extraction, fact summarization, and snippet refinement — not for generating
agent replies.
Ollama (local, no API key needed):
embedding:
provider: "ollama"
model: "nomic-embed-text"
dimension: 768
config:
host: "http://localhost:11434"
llms:
uma:
provider: "ollama"
model: "qwen2.5:3b" # or any model you have pulled
config:
host: "http://localhost:11434"
OpenAI:
embedding:
provider: "openai"
model: "text-embedding-3-small"
dimension: 1536
config:
api_key: "${OPENAI_API_KEY}"
llms:
uma:
provider: "openai"
model: "gpt-4o-mini"
config:
api_key: "${OPENAI_API_KEY}"
Anthropic (LLM only — install the extra first):
pip install -e '.[llm]'
embedding:
provider: "openai" # Anthropic is LLM-only; use OpenAI or Ollama for embeddings
model: "text-embedding-3-small"
dimension: 1536
config:
api_key: "${OPENAI_API_KEY}"
llms:
uma:
provider: "anthropic"
model: "claude-haiku-4-5-20251001"
config:
api_key: "${ANTHROPIC_API_KEY}"
Keep secrets out of uma.yaml. Use ${ENV_VAR} placeholders
as shown above — UMA expands them from the environment at startup. Set the
corresponding variables in your shell or a .env file before running.
Step 4 — Verify the install
Run a quick health check to confirm UMA initialises and can reach your provider:
python - <<'PY'
from uma import UMAMemory
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="test")
print(memory.health_check())
PY
A healthy response reports "status": "ok" with per-component checks for
each SQL store (db:episodic, db:semantic,
db:procedural), each vector index, the LLM, and the embedder.
If any component shows "status": "error", the detail field names the
exact failure — usually an unreachable provider endpoint or a missing API key.
Optional extras
| Extra | Install | Adds |
|---|---|---|
[llm] |
pip install -e '.[llm]' |
Anthropic / Claude LLM provider |
[vector] |
pip install -e '.[vector]' |
FAISS vector backend (alternative to LanceDB) |
[ollama] |
pip install -e '.[ollama]' |
Ollama Python client library |
[parsers] |
pip install -e '.[parsers]' |
PDF, HTML, and Markdown document parsing (PyPDF2, BeautifulSoup4) |
[graph] |
pip install -e '.[graph]' |
Neo4j graph backend (optional, disabled by default) |
[dev] |
pip install -r requirements.txt |
Full development install with test suite |
Where UMA stores data
On first use, UMA creates a .uma/ directory in the working directory
where you run it:
| Path | Contents |
|---|---|
.uma/db/chunks.db |
Authoritative chunk text and metadata (SQLite) |
.uma/db/episodic.db |
Episodic memory — interaction history (SQLite) |
.uma/db/semantic.db |
Extracted facts (SQLite) |
.uma/db/procedural.db |
Skills and procedural knowledge (SQLite) |
.uma/vectors/ |
LanceDB vector index files — rebuildable from SQL at any time |
The SQL files are the authoritative source of truth. The vector files are a rebuildable retrieval accelerator. If the vector index is ever corrupted or needs to be moved, rebuild it with:
await memory.rebuild_vector_indexes(tenant_id="default")
Running the test suite
pip install -r requirements.txt PYTHONPATH=. python -m pytest -q
Quick Start
Get a minimal agent with UMA memory running on the default embedded Lite profile.
Start with the default public config
Modify config/uma.yaml to add your desired configuration options for LLM provider,
Embedding, and Vector DB.
Initialize UMAMemory
After initialization, the memory instance is your handle to retrieval, ingest, wiki management, and
the rest of UMA's memory operations. Bind the agent identity once with set_context.
from uma import UMAMemory, InjectionDetectedError memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
Scan user input
Pre-LLM gate: Call scan_user_input at the top of your agent loop, before
retrieve_context and before any LLM call. It returns a result dict and never raises — you
decide what
to do.
## Scan all user input for prompt injection ## before it reaches storage or an LLM. scan = memory.scan_user_input(user_msg) if scan["severity"] == "high": # do not forward to LLM, do not call process_turn return "I can't process that request."
Retrieve context and run your agent
Use retrieve_context to gather relevant evidence, then pass that context into your
prompt construction and LLM calls.
context = await memory.retrieve_context( query_text=user_message, user_id="user-123", tenant_id="default", session_id="session-1", ) messages = [ {"role": "system", "content": system_prompt}, {"role": "system", "content": str(context)}, ] agent_reply = await your_agent_llm_generate(messages)
Persist the turn
Use process_turn to store the interaction as episodic memory after the agent
responds.
Defense-in-depth: process_turn rescans user_msg before writing
anything. On high severity it raises InjectionDetectedError — nothing is stored.
try: await memory.process_turn( user_id="user-123", user_msg=user_message, assistant_reply=agent_reply, session_id="session-1", tenant_id="default", ) except InjectionDetectedError as e: print(e.severity) # "high" print(e.matched_rules) # ["prompt_reset", ...] print(e.score) # numeric scan score # surface error, alert, block user — your decision
Retrieve compiled memory
Use retrieve_memory to gather memory records as data: inspectable, filterable,
auditable, or usable by application logic.
result = await memory.retrieve_memory( query_text=user_message, user_id="user-123", tenant_id="default", )
Ingest an unstructured document into UMA memory
Use ingest_document to run the full capture, derive, and curate pipeline for files
you want the agent to remember and retrieve later.
owner_type can be user, agent, or workspace,
depending on the scope you want.
await memory.ingest_document( file_path="/data/document.txt", owner_id="user-123", owner_type="user", tenant_id="default", )
After ingest, raw chunks are queryable in the raw lane and derived facts are
queryable in the semantic lane. Ingest is hardened: MIME consistency checks reject
executables, file size caps (max_file_bytes, default 50 MB) prevent resource abuse,
and HTML/Markdown content is sanitized before chunking.
Got questions? Don't read tons of docs — ask your coding assistant.
UMA ships interactive documentation for AI coding
assistants. Your assistant loads the relevant reference on demand when you ask things like
"how do I handle InjectionDetectedError?" or
"how do I filter retrieval by lane?" — no setup required.
Anthropic SDK
UMA wraps around your existing Anthropic loop. Four calls — scan, retrieve, generate, persist — cover the full memory lifecycle. Nothing in your LLM call changes.
import asyncio
import anthropic
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
client = anthropic.Anthropic()
# user_id and session_id come from your application at runtime.
# session_id must be stable across all turns in the same conversation thread.
async def handle_turn(user_id: str, session_id: str, user_msg: str) -> str:
# 1. Pre-LLM gate — never raises, you decide what to do
scan = memory.scan_user_input(user_msg)
if scan["severity"] == "high":
return "I can't process that request."
# 2. Pull relevant memory into the prompt
context = await memory.retrieve_context(
query_text=user_msg,
user_id=user_id,
session_id=session_id,
# tenant_id defaults to "default"
)
system_prompt = build_system_prompt(context) # your function
# 3. Your LLM call — UMA does not touch this
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": user_msg}],
)
reply = response.content[0].text
# 4. Persist the turn — defense-in-depth scan runs here too
try:
await memory.process_turn(
user_id=user_id,
user_msg=user_msg,
assistant_reply=reply,
session_id=session_id,
)
except InjectionDetectedError as e:
# Payload slipped Layer 1 but caught at Layer 2.
# Turn not stored — reply already sent.
log_security_event(e.matched_rules, e.score)
return reply
Tool loops: if your agent calls tools, run retrieve_context
before the first LLM call and process_turn after the final reply — not
inside the tool loop. UMA captures the full conversation exchange, not intermediate
tool steps.
OpenAI
Same four-step pattern as the Anthropic SDK — only the client and
model change. UMA is provider-agnostic.
import asyncio
from openai import OpenAI
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
client = OpenAI()
async def handle_turn(user_msg: str) -> str:
# 1. Pre-LLM gate
scan = memory.scan_user_input(user_msg)
if scan["severity"] == "high":
return "I can't process that request."
# 2. Retrieve context
context = await memory.retrieve_context(
query_text=user_msg,
user_id=user_id,
session_id=session_id,
# tenant_id defaults to "default"
)
# 3. LLM call
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": build_system_prompt(context)},
{"role": "user", "content": user_msg},
],
)
reply = response.choices[0].message.content
# 4. Persist
try:
await memory.process_turn(
user_id=user_id,
user_msg=user_msg,
assistant_reply=reply,
session_id=session_id,
)
except InjectionDetectedError as e:
log_security_event(e.matched_rules, e.score)
return reply
LangGraph
UMA slots into a LangGraph agent as a state-aware retrieval step at the start of the node that calls the LLM. Run scan and retrieve before the model node, run persist after the final response. Your graph edges and conditional logic stay unchanged.
import asyncio
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
llm = ChatAnthropic(model="claude-opus-4-6")
# user_id and session_id flow in through graph state — set at graph.invoke() time.
class AgentState(TypedDict):
user_id: str # required: set by caller at graph.invoke() time
session_id: str # required: stable per conversation thread
user_msg: str
uma_ctx: dict # populated by retrieve_node, consumed by llm_node
reply: str
async def scan_node(state: AgentState) -> AgentState:
"""Layer-1 injection gate — runs before any LLM call."""
scan = memory.scan_user_input(state["user_msg"])
if scan["severity"] == "high":
return {**state, "reply": "I can't process that request."}
return state
async def retrieve_node(state: AgentState) -> AgentState:
"""Pull UMA context into state so the LLM node can use it."""
ctx = await memory.retrieve_context(
query_text=state["user_msg"],
user_id=state["user_id"],
session_id=state["session_id"],
# tenant_id defaults to "default"
)
return {**state, "uma_ctx": ctx}
async def llm_node(state: AgentState) -> AgentState:
"""Standard LangGraph LLM node — reads UMA context from state."""
system = build_system_prompt(state["uma_ctx"]) # your function
response = await llm.ainvoke([
("system", system),
("human", state["user_msg"]),
])
return {**state, "reply": response.content}
async def persist_node(state: AgentState) -> AgentState:
"""Store the completed turn in UMA memory."""
try:
await memory.process_turn(
user_id=state["user_id"],
user_msg=state["user_msg"],
assistant_reply=state["reply"],
session_id=state["session_id"], # required, non-empty
)
except InjectionDetectedError as e:
log_security_event(e.matched_rules, e.score)
return state
def already_replied(state: AgentState) -> str:
return "end" if state.get("reply") else "retrieve"
# Build graph
builder = StateGraph(AgentState)
builder.add_node("scan", scan_node)
builder.add_node("retrieve", retrieve_node)
builder.add_node("llm", llm_node)
builder.add_node("persist", persist_node)
builder.set_entry_point("scan")
builder.add_conditional_edges("scan", already_replied, {"end": END, "retrieve": "retrieve"})
builder.add_edge("retrieve", "llm")
builder.add_edge("llm", "persist")
builder.add_edge("persist", END)
graph = builder.compile()
LangChain
Add UMA as a retrieval step before your chain runs and a persistence step after it
returns. Use a RunnableLambda to inject UMA context into the chain
input, keeping the chain itself unchanged.
import asyncio
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
llm = ChatAnthropic(model="claude-opus-4-6")
prompt = ChatPromptTemplate.from_messages([
("system", "{system_context}"),
("human", "{user_msg}"),
])
chain = prompt | llm
async def run_with_memory(user_id: str, session_id: str, user_msg: str) -> str:
# 1. Scan
scan = memory.scan_user_input(user_msg)
if scan["severity"] == "high":
return "I can't process that request."
# 2. Retrieve and inject into chain input
ctx = await memory.retrieve_context(
query_text=user_msg,
user_id=user_id,
session_id=session_id,
# tenant_id defaults to "default"
)
# 3. Run chain
response = await chain.ainvoke({
"system_context": build_system_prompt(ctx),
"user_msg": user_msg,
})
reply = response.content
# 4. Persist
try:
await memory.process_turn(
user_id=user_id,
user_msg=user_msg,
assistant_reply=reply,
session_id=session_id,
)
except InjectionDetectedError as e:
log_security_event(e.matched_rules, e.score)
return reply
CrewAI
In CrewAI, UMA memory wraps the task execution boundary. Retrieve
before the crew runs, persist after it returns. Each agent in a multi-agent crew uses
the same UMAMemory instance but a distinct user_id or
session_id to keep episodic lanes separate.
import asyncio
from crewai import Agent, Task, Crew
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="crew-agent")
researcher = Agent(
role="Research Analyst",
goal="Find and summarise relevant information",
backstory="You are a thorough researcher.",
llm="claude-opus-4-6",
)
async def run_crew_with_memory(user_id: str, session_id: str, topic: str) -> str:
# 1. Scan
scan = memory.scan_user_input(topic)
if scan["severity"] == "high":
return "I can't process that request."
# 2. Retrieve — inject prior knowledge into the task description
ctx = await memory.retrieve_context(
query_text=topic,
user_id=user_id,
session_id=session_id,
# tenant_id defaults to "default"
)
prior = format_context_for_task(ctx) # your function
# 3. Build and run crew
task = Task(
description=f"Research: {topic}\n\nPrior context:\n{prior}",
expected_output="A concise research summary.",
agent=researcher,
)
crew = Crew(agents=[researcher], tasks=[task])
result = crew.kickoff()
reply = str(result)
# 4. Persist
try:
await memory.process_turn(
user_id=user_id,
user_msg=topic,
assistant_reply=reply,
session_id=session_id,
)
except InjectionDetectedError as e:
log_security_event(e.matched_rules, e.score)
return reply
For multi-agent crews, give each agent its own session_id derived from
the agent role. This keeps episodic memory lanes separate while sharing the same
knowledge base — raw chunks and wiki pages are scoped by owner_type, not
session.
PydanticAI
Use PydanticAI's system_prompt dependency to inject UMA context, and
a post-run hook to persist the turn. The agent definition stays clean — UMA is wired
at the run boundary, not inside the agent logic.
import asyncio
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
@dataclass
class UMADeps:
uma_context: dict # populated before agent.run()
agent = Agent(
"claude-opus-4-6",
deps_type=UMADeps,
system_prompt="You are a helpful assistant.",
)
@agent.system_prompt
async def inject_memory(ctx: RunContext[UMADeps]) -> str:
"""Injects UMA context into the system prompt at run time."""
return build_system_prompt(ctx.deps.uma_context) # your function
async def handle_turn(user_msg: str) -> str:
# 1. Scan
scan = memory.scan_user_input(user_msg)
if scan["severity"] == "high":
return "I can't process that request."
# 2. Retrieve
ctx = await memory.retrieve_context(
query_text=user_msg,
user_id=user_id,
session_id=session_id,
# tenant_id defaults to "default"
)
# 3. Run agent with UMA context as dependency
result = await agent.run(user_msg, deps=UMADeps(uma_context=ctx))
reply = result.data
# 4. Persist
try:
await memory.process_turn(
user_id=user_id,
user_msg=user_msg,
assistant_reply=reply,
session_id=session_id,
)
except InjectionDetectedError as e:
log_security_event(e.matched_rules, e.score)
return reply
Custom loop
Not using a framework? UMA is a Python library. Call the four methods directly in whatever execution loop you are running — HTTP handler, CLI, async queue worker, or a raw asyncio loop.
import asyncio
from uma import UMAMemory, InjectionDetectedError
memory = UMAMemory.from_yaml("config/uma.yaml").set_context(agent_id="my-agent")
async def agent_loop(user_id: str, session_id: str, user_msg: str) -> str:
# ── Step 1: pre-LLM gate ────────────────────────────────────────────
# Advisory, synchronous, never raises. You decide what to do with the result.
scan = memory.scan_user_input(user_msg)
if scan["severity"] == "high":
return "I can't process that request."
# ── Step 2: retrieve context ─────────────────────────────────────────
# Pulls ranked evidence from all active memory lanes and merges it.
# Use lane_filter to narrow: ["raw","semantic"] for RAG-only,
# ["working_memory","episodic"] for continuity-only.
context = await memory.retrieve_context(
query_text=user_msg,
user_id=user_id,
session_id=session_id,
# tenant_id defaults to "default"
)
# ── Step 3: your LLM call ────────────────────────────────────────────
# UMA does not call the LLM for you. Build your prompt however you like.
system_prompt = build_system_prompt(context) # your function
reply = await your_llm(system_prompt, user_msg)
# ── Step 4: persist the turn ─────────────────────────────────────────
# Stores episode, extracts semantic facts, updates working memory.
# Defense-in-depth: rescans user_msg before writing anything.
# Raises InjectionDetectedError on high severity — nothing is stored.
try:
await memory.process_turn(
user_id=user_id,
user_msg=user_msg,
assistant_reply=reply,
session_id=session_id,
)
except InjectionDetectedError as e:
# Layer 2 caught what Layer 1 missed — or skip_scan was used.
# The turn was NOT stored. Surface the event; the reply already went out.
log_security_event({
"rules": e.matched_rules, # e.g. ["prompt_reset", "role_impersonation"]
"score": e.score,
"severity": e.severity, # always "high" when this raises
})
return reply
# ── Ingest a document into the knowledge base ─────────────────────────────
# Do this once per document. After ingest, retrieve_context surfaces its chunks.
async def ingest_doc(file_path: str):
report = await memory.ingest_document(
file_path=file_path,
owner_type="user", # or "agent" / "workspace"
owner_id=user_id,
)
print(report) # IngestReport with chunk count, warnings, manifest status
Framework cheat sheet
| Framework | Where scan + retrieve go | Where persist goes |
|---|---|---|
| Anthropic SDK | Before client.messages.create() |
After reading response.content |
| OpenAI | Before client.chat.completions.create() |
After reading response.choices[0].message.content |
| LangGraph | Dedicated scan_node → retrieve_node before the LLM node |
Dedicated persist_node after the LLM node |
| LangChain | Before chain.ainvoke(); inject context into chain input dict |
After reading response.content |
| CrewAI | Before crew.kickoff(); inject context into task description |
After crew.kickoff() returns |
| PydanticAI | Before agent.run(); pass context as a dependency |
After result.data is read |
MCP Server
UMA ships an MCP server at mcp/server.py that exposes its five core operations
as MCP tools over stdio. Connect it to Claude Desktop, Claude Code, Cursor, or any
MCP-compatible client and your AI assistant can retrieve context, recall compiled memory,
store conversation turns, and ingest documents — all backed by UMA's ownership-scoped,
trust-gated memory architecture.
UMA's MCP server runs locally over stdio, not over HTTP.
There is no hosted endpoint and no API key required. The server process starts on
demand when the client launches it, reads uma.yaml from a path you
configure, and shuts down with the client.
Prerequisites
Before connecting any client, ensure you have:
- Python 3.9+ with UMA installed:
pip install -e .from the repo root - The MCP library:
pip install "mcp[cli]>=1.0.0" - A valid
config/uma.yamlwith your LLM and embedding provider configured
UMA is provider-agnostic. Supported LLM providers are ollama,
openai, and anthropic. Supported embedding providers are
ollama and openai. Set provider, model,
and any connection details in uma.yaml before running the server.
See the Config Baseline section for the full YAML reference.
pip install -e . pip install "mcp[cli]>=1.0.0"
Environment variables
The server is configured entirely through environment variables — no arguments, no flags. These are set in the client config and passed to the server process at launch.
| Variable | Required | Default | Description |
|---|---|---|---|
UMA_CONFIG_PATH |
Yes | — | Absolute path to uma.yaml. The server raises at startup if this is missing. |
UMA_AGENT_ID |
No | agent-default |
Agent identity bound to this server instance. All memory written through this server is scoped to this agent ID. Use different values to isolate memory between separate assistant configurations. |
PYTHONPATH |
Recommended | — |
Set to the repo root so the server can import the uma package when
it runs as a standalone process outside a virtual environment.
|
Client setup
Pick your client below. In every case, use the absolute path to your repo clone — relative paths are not reliable when the client launches the server process. After saving the config, fully quit and relaunch the client for changes to take effect.
Claude Desktop
Edit the Claude Desktop config file for your platform:
| Platform | Config file path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
{
"mcpServers": {
"uma": {
"command": "python",
"args": ["/absolute/path/to/uma/mcp/server.py"],
"env": {
"UMA_CONFIG_PATH": "/absolute/path/to/uma/config/uma.yaml",
"UMA_AGENT_ID": "agent-default",
"PYTHONPATH": "/absolute/path/to/uma"
}
}
}
}
Using uv to manage your virtual environment? Replace
"command": "python" with "command": "uv" and set
"args": ["run", "--project", "/absolute/path/to/uma", "python", "mcp/server.py"].
Drop the PYTHONPATH entry — uv run handles the environment.
Claude Code
Register the server from your terminal with claude mcp add. Claude Code
uses stdio transport, so pass the server script directly:
claude mcp add uma \ --command python \ --args "/absolute/path/to/uma/mcp/server.py" \ --env UMA_CONFIG_PATH=/absolute/path/to/uma/config/uma.yaml \ --env UMA_AGENT_ID=agent-default \ --env PYTHONPATH=/absolute/path/to/uma
Verify the server is registered and its tools are visible:
claude mcp list claude mcp get uma
Cursor
Add to your global MCP config at ~/.cursor/mcp.json, or per-project at
.cursor/mcp.json in the project root:
{
"mcpServers": {
"uma": {
"command": "python",
"args": ["/absolute/path/to/uma/mcp/server.py"],
"env": {
"UMA_CONFIG_PATH": "/absolute/path/to/uma/config/uma.yaml",
"UMA_AGENT_ID": "agent-default",
"PYTHONPATH": "/absolute/path/to/uma"
}
}
}
}
Alternatively, go to Cursor Settings → MCP and add a new server entry with the command, args, and environment variables above.
Verify the connection
Before connecting a client, run the server directly from your terminal to confirm it starts without errors:
UMA_CONFIG_PATH=/absolute/path/to/uma/config/uma.yaml \ UMA_AGENT_ID=agent-default \ python mcp/server.py
The server speaks JSON-RPC over stdio. On a healthy start you will see no output —
it waits for a client to connect. If your configured LLM or embedding provider is
unreachable, the error appears in stderr with the connection detail; confirm your
provider is running and that uma.yaml points to the correct host or API
endpoint.
Once connected through a client, ask your assistant:
Call the uma health_check tool and show me the result.
A healthy response looks like {"status": "ok", "stores": {...}, "embedder": "ok"}.
If any component reports an error, check that your configured provider is reachable and that
UMA_CONFIG_PATH points to a valid uma.yaml.
Available Tools
The MCP server exposes five tools. Each maps directly to a UMAMemory method
— no translation layer, no hidden logic. Every tool returns a JSON string.
| Tool | What it does |
|---|---|
retrieve_context |
RAG-style context retrieval. Returns facts, chunks, and supporting evidence scoped to the user and session. Use this to ground an LLM response in stored knowledge before generating. |
retrieve_memory |
Compiled, evidence-backed memory recall. Returns compiled_memory, facts, and evidence. Use memory_intent="continuity" for long-term recall or "topical" for domain-specific queries. |
process_turn |
Ingest a conversation turn. Stores the exchange as episodic memory and extracts semantic facts from both sides. Call this after every assistant reply to keep memory current. |
ingest_document |
Ingest a file into the knowledge base. Chunks, embeds, and indexes the document. Subsequent retrieve_context calls will surface its content. Accepts the absolute path of any file type UMA supports (PDF, Markdown, HTML, plain text). |
health_check |
Returns per-component health status — stores, embedder, LLM, and vector index. Use this to confirm the server can reach your configured provider and that its databases are reachable. |
Tool parameters
retrieve_context
| Parameter | Type | Required | Description |
|---|---|---|---|
query_text | string | Yes | The query to retrieve context for — typically the current user message. |
user_id | string | Yes | The user whose memory to query. Must be consistent across turns. |
session_id | string | No | Scopes working memory and episodic retrieval to this session. Omit to query across sessions. |
tenant_id | string | No | Defaults to "default". Change only in multi-tenant deployments. |
retrieve_memory
| Parameter | Type | Required | Description |
|---|---|---|---|
query_text | string | Yes | The query driving memory recall. |
user_id | string | Yes | The user whose memory to recall. |
session_id | string | No | Narrows recall to a specific session. |
tenant_id | string | No | Defaults to "default". |
memory_intent | string | No | "continuity" (default) for long-term recall; "topical" for domain-specific queries. |
process_turn
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The user who sent the message. |
session_id | string | Yes | The current conversation session. Use a stable identifier — UUIDs work well. |
user_msg | string | Yes | The user's message text. |
assistant_reply | string | Yes | The assistant's reply text. Facts are extracted from both sides. |
tenant_id | string | No | Defaults to "default". |
ingest_document
| Parameter | Type | Required | Description |
|---|---|---|---|
file_path | string | Yes | Absolute path to the file on the same machine running the MCP server. |
owner_type | string | No | Defaults to "agent". Use "user" for user-owned documents or "workspace" for shared knowledge. |
owner_id | string | No | Defaults to the UMA_AGENT_ID env var. Set explicitly to assign documents to a specific user or workspace. |
Recommended flow
For a standard conversation with persistent memory, call the tools in this order on every turn:
Retrieve context before responding
Call retrieve_context with the current user message and session ID.
Inject the result into your system prompt or context window before generating a reply.
Generate the reply
Use the retrieved context to ground the response. The assistant sees relevant facts, prior episodes, and procedural knowledge from UMA's memory lanes.
Store the turn
Call process_turn with both sides of the exchange. UMA extracts
semantic facts, indexes the episode, and updates working memory — so next turn
starts with current context.
Security note: process_turn runs UMA's two-layer
injection scan on every call. High-severity inputs raise InjectionDetectedError
server-side and the tool returns an error — nothing is stored. This applies to both
user_msg and assistant_reply.
MCP Troubleshooting
| Problem | Fix |
|---|---|
| UMA tools don't appear in the client | Fully quit and relaunch the client — config changes are read only at startup. Confirm the server entry name in the config matches what the client expects. |
UMA_CONFIG_PATH environment variable is required |
The UMA_CONFIG_PATH env var is missing or not passed to the server
process. Check the "env" block in your client config and ensure the
path is absolute.
|
ModuleNotFoundError: No module named 'uma' |
Set PYTHONPATH to the repo root in the "env" block,
or use uv run --project /path/to/uma as the command so the package
is resolved from the correct virtual environment.
|
| Connection error on startup, embedder not responding |
Your configured LLM or embedding provider is not reachable. Check that the
provider service is running and that the host or API endpoint in
uma.yaml is correct. For Ollama, confirm the model is pulled:
ollama pull <model-name>.
|
health_check returns a failed component |
Run python mcp/server.py directly with UMA_CONFIG_PATH
set. Errors from individual stores or the embedder appear in stderr and show the
root cause clearly.
|
ingest_document fails with file not found |
The path must be absolute and accessible on the machine where the MCP server process is running — not the machine the client is on. For Claude Desktop on macOS, this is your local machine. |
process_turn returns an injection error |
UMA's injection scanner blocked the input. The input matched one of the 15 high-severity rules in the pattern catalog. This is expected security behavior — the turn was not stored. Review the input for prompt-override patterns. |
Security by Design
UMA is the first Apache-2.0 memory runtime where OWASP ASI06 (memory poisoning) defense lives in the canonical write and read paths — not bolted on as middleware. Every memory write carries provenance, a source-trust score, and a content integrity hash. Suspicious content is scanned at ingest, quarantined out of retrieval, and reviewable through a small management API. Retrieval itself is trust-aware.
The defenses are part of the architecture, not optional features layered on top. They run on every turn, every document ingest, every bootstrap import, with no caller code required to activate them.
The seven primitives
| Primitive | What it does |
|---|---|
| Provenance | Every artifact carries its lineage: where it came from, who wrote it, what it derives from, when it was created. A runtime invariant, not a debugging convenience. |
| Source trust | A classifier scores every write by source kind. User-confirmed turns score 0.9, assistant replies 0.7, ingested documents 0.7, tool outputs 0.5, scraped imports 0.3. The score travels with the memory. |
| Content integrity | Every artifact carries a SHA-256 hash of its canonical content, computed at write time. If a record is altered, the hash no longer matches and on verification the record is quarantined. |
| Injection pattern detection | Every memory write is scanned against a YAML catalog of ~15 OWASP-aligned attack families (jailbreak prompts, role impersonation, config leakage, encoded payloads, debug spoofing, and more). High-severity hits quarantine the write; lower severities reduce its trust score. |
| Quarantine, not silent rejection | Suspicious writes are preserved with a quarantined_at timestamp and excluded
from retrieval. The management API (list_quarantined, reinstate_quarantined,
purge_quarantined) lets operators review and decide.
|
| Trust-aware retrieval | The final ranking combines fusion score with trust score:
final = (1 - w) · fusion + w · trust.
A configurable min_trust_score filters low-trust content before truncation. Quarantined
records are excluded at the store layer and never reach the ranking stage.
|
| File ingestion hardening | Input validation rejects empty paths and non-files. MIME consistency checks reject executables
masquerading as documents. HTML and Markdown are sanitized of <script>,
<iframe>, inline event handlers, javascript: URLs, and conditional
comments before chunking.
|
Two-layer input scanning
For conversation input, UMA exposes a pre-LLM gate and a defense-in-depth write-time scan. Call
scan_user_input at the top of your agent loop before any LLM call. It returns a result
dict and never raises — the caller decides what to do. process_turn rescans the same
input at storage time and raises InjectionDetectedError on high severity, so nothing
poisoned reaches working memory, episodic store, or fact extraction.
from uma import UMAMemory, InjectionDetectedError memory = UMAMemory.from_yaml("config/uma.yaml") # Layer 1 — pre-LLM gate scan = memory.scan_user_input(user_msg) if scan["severity"] == "high": return "I can't process that request." # ... memory.retrieve_context(...) ... # ... your LLM call ... # Layer 2 — defense in depth at write time try: await memory.process_turn( user_id="user-123", user_msg=user_msg, assistant_reply=reply, session_id="session-1", ) except InjectionDetectedError as e: print(e.severity, e.matched_rules, e.score)
Severity to behavior
| Severity | process_turn |
Artifact trust |
|---|---|---|
none |
Proceeds normally | Unchanged |
low |
Logged, proceeds | Reduced by 20% |
medium |
Logged, proceeds | Reduced by 50% |
high |
Raises InjectionDetectedError; nothing stored |
Not stored |
Managing quarantined records
High-severity scans at the document or chunk level set quarantined_at and preserve the
record outside retrieval. Operators review through the management API.
from uma.api.management import ( list_quarantined, reinstate_quarantined, purge_quarantined, verify_integrity, ) # Review what was quarantined records = await list_quarantined(memory, owner_type="agent", owner_id="agent-1") # Restore a false positive await reinstate_quarantined( memory, record_id="...", lane="semantic", owner_type="agent", owner_id="agent-1", reason="reviewed: legitimate quote from research paper", ) # Permanently remove a confirmed attack await purge_quarantined(memory, record_id="...", lane="semantic", owner_type="agent", owner_id="agent-1", reason="confirmed injection payload") # On-demand integrity check; mismatch quarantines the record result = await verify_integrity(memory, record_id="...", lane="semantic", owner_type="agent", owner_id="agent-1")
Independent academic research (SuperLocalMemory, Bhardwaj 2026) arrives at the same architectural conclusions UMA implements. The OWASP Agent Memory Guard project recommends every primitive UMA ships — UMA just builds them into the canonical paths instead of bolting them on as middleware.
OWASP Coverage
UMA is a memory SDK — not every OWASP category applies. The mapping below is the honest accounting of what UMA covers and what belongs to the calling application. UMA addresses six of the ten OWASP Top 10 for LLM Applications 2025 categories and three of the ten Agentic AI Security (ASI) categories.
| Category | Scope | UMA's contribution |
|---|---|---|
| 🟢 ASI06: Memory Poisoning (Agentic AI) | In scope | Write-time scan + quarantine at every storage boundary. Quarantined artifacts never enter retrieval and never seed fact extraction. |
| 🟢 ASI03: Identity & Privilege Abuse (Agentic AI) | Partial — memory-layer | Explicit tenant_id / owner_type / owner_id on every artifact,
enforced at the storage layer. Agent identity itself is the caller's concern. |
| 🟢 ASI05: Unexpected Code Execution (Agentic AI) | Partial — ingest-only | PickleParser removed; MIME consistency check rejects executables; HTML/Markdown
sanitized before storage. UMA itself executes no code from memory. |
| 🟢 LLM01: Prompt Injection | In scope | Two-layer scanning: advisory pre-LLM gate (scan_user_input) + write-time per-artifact
scan. High severity → quarantine; medium/low → trust reduction. |
| 🟢 LLM02: Sensitive Information Disclosure | Partial | Audit log stores SHA-256-hashed query previews only. HTML sanitization strips scripts and active URLs at ingest. |
| ⚪ LLM03: Supply Chain | Out of scope | No training, no fine-tuning. |
| 🟢 LLM04: Data and Model Poisoning | In scope (RAG path) | Quarantined chunks are excluded from fact extraction, injected content cannot seed the semantic lane. SHA-256 content_hash + verify_integrity detect post-hoc tampering. The quarantined filter is in every retrieval query across all four stores. Real coverage.
|
| ⚪ LLM05: Improper Output Handling | Out of scope | UMA returns context, not output. Caller owns rendering and escaping. |
| ⚪ LLM06: Excessive Agency | Out of scope | UMA has no tool use, no function calling, no autonomous action capability. |
| ⚪ LLM07: System Prompt Leakage | Out of scope | System prompts live in the calling application, UMA never sees them. |
| 🟢 LLM08: Vector and Embedding Weaknesses | In scope — strong | Cross-tenant, cross-agent and cross-user leakage is impossible by construction. Write-time injection scanning also directly addresses the poisoning sub-problem that LLM08 covers: both inversion attacks and poisoning attacks — injecting malicious content into the retrieval pipeline. |
| 🟢 LLM09: Misinformation | Partial | Every fact carries provenance back to source chunks. LatestWinsFactResolver excludes
quarantined facts from canonical selection. UMA cannot prevent the LLM from hallucinating. It provides the provenance and evidence infrastructure to ensure that retrieved memory pieces are trustworthy. |
| 🟢 LLM10: Unbounded Consumption | In scope | Optional set_rate_limit_hook on every public method. max_file_bytes and
pdf_max_pages cap ingest resource use.
|
For the full per-feature evidence and mechanism details, ask your coding assistant — UMA's interactive documentation covers the security model in depth.
Retrieval Products
UMA exposes two distinct retrieval products on UMAMemory.
| Method | Use case |
|---|---|
retrieve_context |
Curated evidence for RAG. Raw chunks first, provenance attached, wiki not required. |
retrieve_memory |
Compiled evidence-backed memory for continuity. Full retrieval trace, provenance, expansion support. |
Both paths run through a small lane-aware planner that decides which canonical lanes participate, surfaces excluded lanes and reasons in trace data, and keeps backend mechanics below that boundary.
# RAG-style context for the LLM context = await memory.retrieve_context( query_text=user_message, user_id="user-123" ) # Compiled memory with provenance for continuity result = await memory.retrieve_memory( query_text=user_message, user_id="user-123" )
Ingest Stages
Document ingest is split into three explicit internal stages. Each stage is independent and rerunnable without repeating prior stages — and each stage has its own security boundary, so untrusted input is validated, scanned, and trust-scored before it ever reaches a retrieval query.
| Stage | What it does | Security checks at this boundary |
|---|---|---|
| Capture | Parse and normalize raw input into source records and raw chunks with provenance metadata. |
Caller inputs validated. Byte-level MIME consistency checked against extension (executable types
and mismatches rejected with MimeRejection). HTML and Markdown sanitized — scripts,
iframes, inline event handlers, javascript: and data: URLs,
conditional comments, and inline SVG stripped. Each chunk scanned for prompt-injection patterns;
trust_score and content_hash attached at write.
|
| Derive | Extract semantic facts, graph edges, salience markers, and episodic structure from chunks. Rerunnable without re-parsing. |
Derived facts inherit ownership and provenance from their source chunks. Each derived artifact
is scanned at its own write boundary and gets its own trust_score and
content_hash. High-severity hits set trust_score to 0.0 and
quarantine the record.
|
| Curate | Build or refresh compiled wiki and memory artifacts from evidence and derived artifacts. Markdown output is a rebuildable projection only. | Compiled artifacts pull only from non-quarantined evidence. Trust scores from upstream artifacts flow into the compiled output and into retrieval ranking, so low-trust evidence is downweighted rather than silently mixed in. |
Security primitives at the ingest boundary
Five defenses run before any ingested content can be retrieved. Each maps to an OWASP Agentic Security Initiative control.
| Defense | What it does | OWASP |
|---|---|---|
| MIME validation | Byte-level content type check against the file extension. Executables and mismatches are rejected before parsing — the parser never sees disguised payloads. | ASI09 (supply chain) |
| HTML / Markdown sanitization | Removes scripts, iframes, event handlers, javascript: and data:
URLs, conditional comments, and inline SVG. Per-category removal counts are recorded on
the document manifest for audit. |
ASI09 (supply chain) |
| Injection scan | Every chunk, fact, and episode is checked against the YAML pattern catalog at its write
boundary. High-severity hits set trust_score to 0.0 and the artifact is
quarantined. |
ASI01 (prompt injection), ASI06 (memory poisoning) |
| Trust score + content hash | Every stored artifact carries a classifier-derived trust_score and a SHA-256
content_hash. Retrieval ranking blends trust into the final score, and ranking
drops anything below min_trust_score.
|
ASI06 (memory poisoning) |
| Quarantine | Suspicious artifacts are not deleted — they're stored with a quarantined_at
timestamp and excluded from all retrieval queries. The management API exposes
list_quarantined, reinstate_quarantined, and
purge_quarantined for review.
|
ASI06 (memory poisoning) |
On-demand integrity verification is also available after ingest via
verify_integrity(), which recomputes the canonical hash of any stored Fact, Episode,
Skill, or Chunk and quarantines it on mismatch. Use it ad hoc, or let
lint_memory_drift route typed-lane artifacts through it in a batch.
Memory Lanes
UMA organizes all memory into canonical lanes. Each lane has its own storage, retrieval semantics, and ownership rules.
| Lane | Description |
|---|---|
raw |
Parsed source documents and chunk content. Terminal evidence — provenance chains end here. |
semantic |
Salient facts with confidence and conflict signals. The truth layer over raw evidence. |
episodic |
Conversation turns stored as episodes, clustered into chapters, summarized for fast recall. |
procedural |
Vector-searchable skills and rules. Agents retrieve how to act, not just what they know. |
wiki |
Canonical synthesized records. Managed lifecycle, drift detection, versioned regeneration. |
working memory |
Working memory holds the conversation history needed to keep context across turns. |
The graph is the piece that connects them all. It's the lane that turns a set of memories into a connected memory. Graph DB is not bundled, but you can plug one in the config and the graph lane activates. Skip it, and UMA still works end-to-end across the six lanes.
Ownership Boundaries
UMA enforces a first-class logical separation between an agent's global knowledge and user-specific memory. This separation is enforced through explicit ownership metadata across SQL storage, vector embeddings, graph nodes and edges, retrieval filters, write paths, and promotion paths.
| Scope | Owner | Description |
|---|---|---|
| Agent KB | Agent instance | Durable cross-user knowledge: domain facts, policies, procedures, learned generalizations. |
| User Memory | End user | Private, user-scoped: conversations, preferences, uploaded project data. |
| Project Memory | Project within a user | Isolated sub-context ensuring no cross-project leakage unless explicitly promoted. |
Retrieval searches the appropriate scope first, merges results deterministically across scopes, and applies promotion or demotion policies when knowledge should move between layers.
Compiled Wiki Memory
UMA treats wiki pages as managed memory records, not markdown files. Canonical wiki state lives in
UMA records with kind="wiki_page" and kb_lane="wiki".
What uma.memory.wiki manages
- Page identity and slugging
- Lifecycle status and evidence links
- Deterministic updates and drift checks
- Markdown projection and page regeneration
Markdown under wiki/*.md is projection-only output. Wiki pages are synthesized views
over evidence — they are not terminal truth and can be deleted and rebuilt from the underlying
evidence at any time.
Management API
from uma.api.management import ( update_wiki_page, export_wiki_projection, lint_memory_drift, ) # Lint stale, unsupported, or conflicted wiki state report = await lint_memory_drift(memory) # Export a rebuildable markdown projection await export_wiki_projection( memory, compiled_answer, output_path="wiki/example.md", )
Graph Memory
The graph is the lane that connects stored memories to each other. Facts, episodes, and entities are nodes; the relationships between them are edges. When a retrieval pulls a single fact, the graph lets UMA follow its connections — "what else is linked to this person, this project, this decision?" — and bring back related memories the user didn't ask for directly.
This is how UMA recalls context that lives between documents and turns. A user mentioning a project name can surface the people associated with it, the decisions made about it, and the prior conversations that touched it — even when none of that text matches the query lexically or semantically.
What lives in the graph
Edges are predicate-scoped, meaning every connection carries a typed relationship label
(works_on, mentioned_in, decided_by, etc.) rather than a generic
"is related to". Predicates make traversal precise: you can expand along the relationships that matter
for the current query and ignore the rest.
- Facts and episodes are connected over time. When a new fact references an entity that already exists in the graph, an edge is added — so the graph grows denser as the agent learns more, without rewriting what was already there.
- Edges carry temporal and provenance metadata. When the relationship was first established, when it was last confirmed, which fact and which source chunk it came from, and who owns it.
-
Ownership flows through every edge. A fact → graph edge inherits
owner_typeandowner_idfrom its source fact. Graph traversal cannot cross tenant boundaries.
Graph neighbor queries
When retrieval activates the graph, it walks outward from a seed node — typically a fact already surfaced by vector or lexical search — and pulls in neighboring nodes along chosen predicates. Depth and result limits cap every traversal so a single query can't fan out across the whole graph.
The graph is a supporting lane, not the truth layer. The graph is a routing index that helps retrieval find related memories faster — answers still come from the facts and the chunks that back them. That's why the graph is optional: UMA works end-to-end without it.
Configuration
config/uma.yaml is the committed default. It runs the embedded Lite profile —
SQLite for storage, LanceDB for vectors, no external services. Edit it to point at your
LLM and embedding providers, then load it at runtime:
memory = UMAMemory.from_yaml("config/uma.yaml")
UMA ships with support for OpenAI, Anthropic, and Ollama as LLM providers, and OpenAI or Ollama for embeddings. Set the provider and model in the config:
llms: uma: provider: "ollama" model: "llama3" embedding: provider: "ollama" model: "nomic-embed-text" dimension: 1536
Logging
UMA logs to stdout and to a file by default. Override with environment variables:
| Variable | Description |
|---|---|
UMA_LOG_PATH |
stdout, stderr, or a file path |
UMA_LOG_TO_FILE |
Set to 0 to disable file logging |
The graph lane is not bundled — plug in your own backend in the config if you want it. Production profiles and deployment tooling live outside this public repo.
Maintenance
The management API at uma.api.management covers the day-to-day operational
surface — quarantine review, integrity checks, retrieval auditing, and result explanation.
Everything here is opt-in: call it when you need it, ignore it otherwise. None of it sits
in the hot path of retrieve_context, retrieve_memory, or
process_turn.
Quarantine review
High-severity injection scans don't delete — they quarantine. Artifacts are stored with a
quarantined_at timestamp and excluded from retrieval, but the rows stay in
the database so you can review and decide.
from uma.api.management import list_quarantined, reinstate_quarantined, purge_quarantined # Review what's been quarantined rows = await list_quarantined(memory, lane="semantic") # Restore a false positive await reinstate_quarantined(memory, record_id="fact-abc", lane="semantic") # Permanently delete confirmed bad content await purge_quarantined(memory, record_id="chunk-xyz", lane="raw")
Integrity verification
verify_integrity recomputes the canonical content hash for any stored Fact,
Episode, Skill, or Chunk and compares it to the hash recorded at write time. A match
returns status="verified" without mutating anything; a mismatch quarantines
the record and returns status="failed" with the diff.
from uma.api.management import verify_integrity, lint_memory_drift # Verify a single record on demand result = await verify_integrity(memory, record_id="fact-abc", lane="semantic") # Batch-check typed-lane artifacts drift = await lint_memory_drift(memory, artifact, user_id="user-123")
lint_memory_drift routes typed-lane artifacts through verify_integrity
automatically, so you can run batch checks without calling the function directly. Background
scanning across the full dataset is an Enterprise capability and is not part of this SDK —
in UMA Lite you trigger checks on demand.
Result explanation
Retrieval is supposed to be inspectable. explain_result returns the lane plan,
candidate pool size, fusion ordering, and trust-adjusted scores for a given retrieval
result — useful for debugging unexpected rankings or showing reviewers exactly why an
answer surfaced what it did.
from uma.api.management import explain_result result = await memory.retrieve_memory(query_text=q, user_id="user-123", ...) explanation = await explain_result(memory, result, user_id="user-123")
Retrieval audit log
Every retrieval call is also recorded in an audit log — hashed query preview, scope,
severity, result counts — queryable via list_retrieval_audit. Disable it
by setting security.retrieval_audit_enabled: false in your YAML.
Health and index rebuild
Two more operational primitives live directly on UMAMemory:
health_check() returns the runtime status of each lane and backend, and
rebuild_vector_indexes() regenerates the LanceDB indexes from SQL (the
authoritative source) — useful after a schema migration or if a vector store becomes
corrupt.
# Status snapshot status = memory.health_check() # Rebuild vector indexes from SQL await memory.rebuild_vector_indexes(tenant_id="default")
SQL is always the authoritative store. Vector indexes are a rebuildable accelerator —
if anything goes wrong with LanceDB, rebuild_vector_indexes regenerates
it from the SQL records without re-ingesting any source documents.
Interactive Documentation — Ask, Don't Read
You don't need to read tons of documentation to use UMA. Ask your coding assistant instead.
UMA ships interactive documentation as a set of eight reference files under .claude/skills/.
Each file follows the
Agent Skills
specification, which means Claude Code (and any Agent Skills-compatible assistant) automatically loads
the relevant file when its description matches your question. No setup, no @ mentions, no
manual context-pasting. Just ask.
Ask, don't read
# "How do I integrate UMA into my chatbot?" → uma-agent-loop.md loads — end-to-end pattern with code # "What happens when a user sends a prompt injection?" → uma-security.md + uma-quarantine.md load — full flow from scan to storage # "How do I write a custom vector backend?" → uma-vector-contract.md loads — the contract, atomicity, score normalization # "How do I filter by lane?" → uma-lanes.md loads — the six lanes, when to use each # "Can you help me configure Anthropic as the LLM?" → uma-configure.md loads — full YAML reference
The eight reference files
| File | Covers |
|---|---|
uma-overview.md |
What UMA is, design philosophy, DAT invariants, security primitives at a glance |
uma-api.md |
Full public API — every method, every management function, scope fields |
uma-lanes.md |
Six memory lanes, storage contracts, quarantine semantics, retrieval pipeline |
uma-configure.md |
YAML reference, LLM/embedding providers, security configuration, install surfaces |
uma-security.md |
Two-layer scanning, pattern catalog, severity behavior, integrity verification |
uma-agent-loop.md |
End-to-end integration: scan → retrieve → LLM → process_turn |
uma-vector-contract.md |
Vector isolation contract, push-down filters, custom backend authoring |
uma-quarantine.md |
Quarantine lifecycle, management API, composition with trust scoring |
Each file is under 500 lines, follows the Agent Skills specification, and is verified against the
codebase — no phantom APIs. Assistants that don't follow .claude/skills/ can read the same
files directly, or via a symlink at .agents/skills/ if your tooling uses that path.
Think of this docs site as your orientation layer and the interactive documentation as your queryable depth. The site shows you what exists; your assistant answers how to use it in context, on demand.
UMA contribution
UMA is a library, not a hosted service, and cannot itself hold compliance certifications. UMA's security primitives are designed to contribute to compliance programs in customers building on top of it.
The architecture supports work toward OWASP ASI baseline, NIST AI RMF, ISO/IEC 42001 and 27001, SOC 2 Trust Services Criteria, GDPR (notably Articles 17, 30, 32), CCPA, and the EU AI Act, by providing the technical controls and evidence artifacts (ownership scoping, audit events, integrity verification, deletion attestation) that compliance programs require.
Customers remain responsible for their overall control environment, audit posture, and certification scope.