Technical Documentation: vectorize-io/hindsight
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (README.md)
What is Hindsight?
Hindsight™ is an agent memory system built to create smarter agents that learn over time. Most agent memory systems focus on recalling conversation history. Hindsight is focused on making agents that learn, not just remember.
It eliminates the shortcomings of alternative techniques such as RAG and knowledge graph and delivers state-of-the-art performance on long term memory tasks.
Contents
- Memory Performance & Accuracy
- Quick Start — server · clients · platforms · embedded
- Adding Hindsight to Your Agent — LLM Wrapper · integrations · coding agents · MCP
- Core Concepts — memory types · retain / recall / reflect · observations · mental models & knowledge pages · banks
- Use Cases
- Running in Production
- Resources
Memory Performance & Accuracy
Hindsight is the most accurate agent memory system ever tested according to benchmark performance. It has achieved state-of-the-art performance on the LongMemEval benchmark, widely used to assess memory system performance across a variety of conversational AI scenarios. The current reported performance of Hindsight and other agent memory solutions as of January 2026 is shown here:
Live, continuously updated results — including per-model accuracy, latency and cost — are published at benchmarks.hindsight.vectorize.io.
The benchmark performance data for Hindsight has been independently reproduced by research collaborators at the Virginia Tech Sanghani Center for Artificial Intelligence and Data Analytics and The Washington Post. Other scores are self-reported by software vendors.
Hindsight is being used in production at Fortune 500 enterprises and by a growing number of AI startups.
🤖 Using a coding agent? Install the Hindsight documentation skill for instant access to docs while you code:
bashnpx skills add https://github.com/vectorize-io/hindsight --skill hindsight-docsWorks with Claude Code, Cursor, and other AI coding assistants.
Quick Start
1. Start a server
Docker (recommended)
export OPENAI_API_KEY=sk-xxx
docker run -it --pull always --name hindsight --restart unless-stopped -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-v hindsight-data:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latestHindsight works with 25+ LLM providers via HINDSIGHT_API_LLM_PROVIDER — hosted (openai, anthropic, gemini, groq, bedrock, vertexai, minimax, deepseek, atlas, meta, …), fully local (ollama, lmstudio, llamacpp), any OpenAI-compatible endpoint, and gateways (litellm, litellmrouter) that reach the rest. Existing subscriptions work too: openai-codex (ChatGPT Plus/Pro), claude-code (Claude Pro/Max), cursor (Cursor) and github-copilot (GitHub Copilot) need no API key. See supported models.
Docker (external PostgreSQL)
export OPENAI_API_KEY=sk-xxx
export HINDSIGHT_DB_PASSWORD=choose-a-password
cd docker/docker-compose
docker compose upOracle AI Database is also supported for enterprise deployments with full feature parity. See the storage documentation for details.
Bare metal (pip)
pip install hindsight-api
export HINDSIGHT_API_LLM_API_KEY=sk-xxx
hindsight-apiKubernetes (Helm)
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set api.llm.provider=openai \
--set api.llm.apiKey=sk-xxx \
--set postgresql.enabled=trueManaged (no server)
Hindsight Cloud is the hosted option: managed infrastructure that scales automatically, plus a dashboard, backups, team collaboration and a 99.9% uptime SLA. Billing is usage-based with free credits to start — no fixed monthly or per-seat fee. Point any client at https://api.hindsight.vectorize.io with your API key and skip the deployment entirely.
Compare self-hosted, Cloud and Enterprise → · Sign up →
All options, including Windows and air-gapped setups, are covered in the installation guide.
2. Connect a client
pip install hindsight-client -U # Python
npm install @vectorize-io/hindsight-client # Node.js / TypeScript
go get github.com/vectorize-io/hindsight/hindsight-clients/go # Go
curl -fsSL https://hindsight.vectorize.io/get-cli | bash # CLIPython
from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888")
# Retain: Store information
client.retain(bank_id="my-bank", content="Alice works at Google as a software engineer")
# Recall: Search memories
client.recall(bank_id="my-bank", query="What does Alice do?")
# Reflect: Generate disposition-aware response
client.reflect(bank_id="my-bank", query="Tell me about Alice")Node.js / TypeScript
const { HindsightClient } = require('@vectorize-io/hindsight-client');
const main = async () => {
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
await client.retain('my-bank', 'Alice loves hiking in Yosemite');
const results = await client.recall('my-bank', 'What does Alice like?');
console.log(results);
}
main();Full reference: Python · Node.js · Go · CLI · REST API
Supported Platforms
| Platform | Docker | Bare Metal (pip) | Embedded DB (pg0) |
|---|---|---|---|
| Linux (x86_64, ARM64) | ✅ | ✅ | ✅ |
| macOS (Apple Silicon / arm64) | ✅ | ✅ | ✅ |
| macOS (Intel / x86_64) | ✅ | ⚠️ | ✅ |
| Windows (x86_64) | ✅ | ✅ | ✅ |
⚠️ Intel Macs: use hindsight-all-slim — see the installation guide for details.
Python Embedded (no server required)
pip install hindsight-all -UOn Intel (x86_64) Macs, install hindsight-all-slim instead — see Supported Platforms.
import os
from hindsight import HindsightServer, HindsightClient
with HindsightServer(
llm_provider="openai",
llm_model="gpt-5-mini",
llm_api_key=os.environ["OPENAI_API_KEY"]
) as server:
client = HindsightClient(base_url=server.url)
client.retain(bank_id="my-bank", content="Alice works at Google")
results = client.recall(bank_id="my-bank", query="Where does Alice work?")A Node.js equivalent and a daemon CLI are also available.
Adding Hindsight to Your Agent
LLM Wrapper (2 lines of code)
The easiest way to add memory to an existing agent is the LLM Wrapper. Swap your LLM client for a wrapped one — memories are then stored and retrieved automatically on every call, with no other changes to your code.
pip install hindsight-litellmfrom openai import OpenAI
from hindsight_litellm import wrap_openai
# Wrap your existing LLM client and you're done.
# Defaults to Hindsight Cloud; pass hindsight_api_url for a self-hosted server.
client = wrap_openai(
OpenAI(),
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)
# Hindsight recalls relevant memories before the call
# and retains the conversation after it.
response = client.chat.completions.create(
model="gpt-5-mini",
messages=[{"role": "user", "content": "What do you know about me?"}],
)wrap_anthropic() does the same for the Anthropic SDK, and every setting — bank, recall budget, fact types, reflect instead of recall — can be overridden per call with hindsight_* kwargs. LiteLLM sits underneath, so the same integration covers 100+ models. See the LiteLLM integration.
If you need explicit control over when memories are stored and recalled, use the SDKs or REST API directly instead.
Integrations
60+ integrations — most need no code changes.
| Coding agents | Claude Code · Codex · Cursor · GitHub Copilot · opencode · Cline · Aider · Zed · Continue · Roo Code · OpenHands |
| Agent frameworks | LangGraph / LangChain · LlamaIndex · CrewAI · Pydantic AI · OpenAI Agents SDK · Google ADK · Agno · Strands · AutoGen · Microsoft Agent Framework · Vercel AI SDK · Haystack |
| No-code / low-code | n8n · Zapier · Dify · Flowise |
| Apps & tools | ChatGPT · Perplexity · Obsidian · Pipecat · Vapi |
Coding Agents
One package gives CLI coding agents long-term project memory: a per-repo bank built automatically from git history and past sessions, injected into the agent as it starts working, plus curated knowledge pages covering architecture, conventions and in-flight work.
npx @vectorize-io/hindsight-coding-agents install all # every detected agent, wired natively
npx @vectorize-io/hindsight-coding-agents install claude-code # or just oneSupports Claude Code, Codex CLI, Cursor CLI, GitHub Copilot CLI, opencode, Kilo CLI, Cline CLI, Antigravity CLI, Devin CLI, pi, Prime Agent, Grok Build and DeepSeek Harness. Ingestion is automatic — there is no setup command. See the coding agents integration.
MCP Server
Every server ships a built-in Model Context Protocol endpoint, one per bank, enabled by default:
http://localhost:8888/mcp/{bank_id}/Point any MCP client at it to expose retain, recall and reflect as tools. See the MCP server docs.
Core Concepts
Memory Types
Most agent memory implementations rely on basic vector search or sometimes use a knowledge graph. Hindsight uses biomimetic data structures to organize agent memories in a way that is more like how human memory works:
- World facts: facts about the world ("The stove gets hot")
- Experiences: the agent's own experiences ("I touched the stove and it really hurt")
- Observations: consolidated, evidence-backed beliefs formed from many memories
- Mental models: learned understanding of the agent's world, synthesized from observations and facts
Memories live in banks. When memories are added, they are pushed into either the world facts or the experiences pathway, then represented as a combination of entities, relationships, and time series with sparse/dense vector representations to aid in later recall.
The Three Operations
Retain
The retain operation is used to push new memories into Hindsight. It tells Hindsight to retain the information you pass in as an input.
client.retain(
bank_id="my-bank",
content="Alice got promoted to senior engineer",
context="career update",
timestamp="2025-06-15T10:00:00Z",
)Behind the scenes, retain uses an LLM to extract key facts, temporal data, entities, and relationships. It passes these through a normalization process to transform extracted data into canonical entities, time series, and search indexes along with metadata. These representations create the pathways for accurate memory retrieval in the recall and reflect operations.
Recall
The recall operation is used to retrieve memories. These memories can come from any of the memory types (world, experiences, etc.)
client.recall(bank_id="my-bank", query="What does Alice do?")
client.recall(bank_id="my-bank", query="What happened in June?") # temporalRecall performs 4 retrieval strategies in parallel:
- Semantic: Vector similarity
- Keyword: BM25 exact matching
- Graph: Entity/temporal/causal links
- Temporal: Time range filtering
The individual results are merged, ordered by relevance using reciprocal rank fusion and a cross-encoder reranking model, then trimmed as needed to fit within the token limit.
Reflect
The reflect operation performs a more thorough analysis of existing memories. This allows the agent to form new connections between memories and build a more thorough understanding of its world — or to answer a question that needs deep thinking rather than lookup.
client.reflect(bank_id="my-bank", query="What should I know about Alice?")For example, reflect supports use cases such as:
- An AI Project Manager reflecting on what risks need to be mitigated on a project.
- A Sales Agent reflecting on why certain outreach messages have gotten responses while others haven't.
- A Support Agent reflecting on opportunities where customers have questions not answered by current product documentation.
Observations
Retained facts don't stay a flat pile. In the background, Hindsight consolidates related facts into observations — deduplicated beliefs the bank has built up over time. Each observation keeps its supporting evidence with exact quotes and a proof count, and is refined rather than overwritten when new evidence arrives, so new information strengthens, weakens or extends an existing belief instead of silently replacing it.
Mental Models & Knowledge Pages
A mental model is a standing answer to a question about a bank ("What are this user's preferences?"). You define the question once; Hindsight writes the answer, stores it, and rewrites it in the background as the bank learns more. Reading one is a database read — no retrieval, no LLM call — so an agent can boot with a page of settled knowledge instead of rediscovering it every session.
Knowledge pages are mental models with the mechanics hidden: living documents a bank writes about itself, organized in folders like a wiki, searchable, and projectable onto disk as ordinary markdown. Supply a name and a question; every other decision is a default you can override.
Mental models → · Knowledge pages →
Memory Banks
A bank is an isolated memory store — one "brain" for one user, agent, or project. Isolation is strict: no cross-bank leakage. Banks carry background context and disposition traits (skepticism, literalism, empathy) that shape how reflect reasons over their memories, and can be created from declarative bank templates.
Two more things worth knowing:
- Multilingual by default. Input language is detected and preserved end to end — facts stay in their original language and entities keep their native script (张伟 stays 张伟, not "Zhang Wei"). Docs →
- Memory Defense. An opt-in, per-bank policy that scans every retain for secrets and PII against 45 patterns and either redacts the match (
[REDACTED:github_token]) or blocks the item before it reaches storage. Docs →
Use Cases
Hindsight is built to support conversational AI agents as well as agents that are intended to perform tasks autonomously. The ideal use case for Hindsight are agents that require a blend of these features such as AI employees that need to handle open-ended tasks, change behavior based on user feedback, and learn to perform complex tasks to automate work at a level that approximates a human work. Hindsight can be used with simple AI workflows like those built with n8n and other similar tools, but may be overkill for such applications.
Per-User Memories and Chat History
One of the simpler use cases you can use Hindsight for is to personalize AI chatbots and other conversational agents by storing and recalling memories associated with individual users.
The requirements for this use case usually look something like this:
Satisfying these requirements in Hindsight is straightforward. When new user inputs and tool calls are ingested into Hindsight using the retain operation, custom metadata can be used to enrich the new memories. Metadata provides a convenient way to isolate memories that need to be restricted to a given user. Once these are fed into the retain operation, any raw memories and mental models that get created can be filtered when retrieving relevant memories.
More patterns in the Cookbook and Best Practices.
Running in Production
| Storage | PostgreSQL + pgvector, or Oracle AI Database 23ai with full feature parity — storage |
| Configuration | Hierarchical: global env vars → per-tenant → per-bank — configuration |
| Monitoring | Prometheus metrics and dashboards for LLM calls, tokens and latency — monitoring |
| Operations | Admin CLI for migrations, bank repair and stuck operations — admin CLI |
| Events | Webhooks for retain, consolidation and refresh lifecycle events — webhooks |
| Extensibility | Tenant, auth and storage extension points — extensions |
| Managed | Skip all of it with Hindsight Cloud — managed, usage-based, 99.9% uptime SLA |
Resources
Documentation:
- Docs · FAQ · Best Practices · Cookbook · Blog
- Paper · Benchmarks · RAG vs Memory
Clients:
Community:
Star History
Contributing
See CONTRIBUTING.md.
License
MIT — see LICENSE
Built by Vectorize.io
Hindsight Docs Docs Cloud Enterprise Overview (`hindsight-docs/docs/cloud-enterprise/overview.md`)
title: Ways to run Hindsight sidebar_label: Overview
Ways to run Hindsight
Hindsight runs three ways. All of them use the same memory engine: the same retain, recall and reflect, the same mental models and knowledge pages, and the same API and SDKs. You can start on one and move to another later.
| Self-hosted (open source) | Hindsight Cloud | Hindsight Enterprise | |
|---|---|---|---|
| What it is | The open-source engine, run by you | Managed, self-serve, multi-tenant service | A single-tenant deployment, installed and run with Vectorize |
| Where it runs | Anywhere: your laptop, Docker, Kubernetes | Hindsight Cloud | Our cloud, your AWS, Google Cloud or Azure account, or your data center |
| Who runs it | You | Vectorize | Vectorize, or you with Vectorize's help on-premises |
| Memory store | PostgreSQL (or Oracle) | Managed by Vectorize | PostgreSQL, or Hindsight Memlake on object storage |
| Models | Bring your own LLM and embeddings | Included | Included, or your own (Bedrock, Vertex AI, Azure OpenAI) |
| Sign-in and access | API keys you configure | Console, teams and API keys | Plus SSO, enforced MFA and private networking |
| Pricing | Free (MIT license) | Pay as you go, from prepaid credits | Contract |
| Get started | Installation | Sign up | Book a call |
How to choose
- Self-hosted if you want full control, want to try Hindsight locally, or already run Postgres and your own models. These docs cover it: start with Installation.
- Hindsight Cloud if you want memory for your agents in minutes, with nothing to deploy and the models included.
- Hindsight Enterprise if you need your own deployment: single-tenant isolation, a region or cloud account of your choice, private networking, SSO and compliance features, contracted support, or memory at a scale where Hindsight Memlake pays off.
Hindsight Cloud and Enterprise have their own documentation at docs.hindsight.vectorize.io.
Hindsight Docs Docs Developer Index (`hindsight-docs/docs/developer/index.mdx`)
sidebar_position: 1 slug: / title: Overview hide_title: true hide_table_of_contents: true
import HomeHero from '@site/src/components/HomeHero';
import HomeFlow from '@site/src/components/HomeFlow';
import HomeBenchmarks from '@site/src/components/HomeBenchmarks';
import SkillBanner from '@site/src/components/SkillBanner';
import Figure from '@site/src/components/Figure';
import whatHindsightDoes from '@site/figures/what-hindsight-does.json';
{/* The page's own # Overview H1 is suppressed via hide_title: this is the site
root, and the hero's headline is the H1 a visitor and a crawler should see.
The sidebar and page metadata still read "Overview" from the frontmatter. */}
Benchmarks
{/* Retrieval accuracy only. The coding-agent chart opens inside the coding-agents
card above, where it is evidence for a path the reader has just been shown,
and is explained in full on /sdks/integrations/coding-agents. */}
Why Hindsight?
AI agents forget everything between sessions. Every conversation starts from zero—no context about who you are, what you've discussed, or what the assistant has learned. This isn't just an implementation detail; it fundamentally limits what AI Agents can do.
The problem is harder than it looks:
- Simple vector search isn't enough — "What did Alice do last spring?" requires temporal reasoning, not just semantic similarity
- Facts get disconnected — Knowing "Alice works at Google" and "Google is in Mountain View" should let you answer "Where does Alice work?" even if you never stored that directly
- AI Agents need to consolidate knowledge — A coding assistant that remembers "the user prefers functional programming" should consolidate this into an observation and weigh it when making recommendations
- Context matters — The same information means different things to different memory banks with different personalities
Hindsight solves these problems with a memory system designed specifically for AI agents.
{/* Rendered here rather than by the DocBreadcrumbs wrapper (which serves every
other page): at the top it sat between the navbar and the hero band as a
bordered strip and broke the full-bleed, and it lands better as the answer
to the problem the section above just described. */}
How It Works
{/* The router: three kinds of visitor land on this page — one who has never
heard of agent memory, one looking for the coding-agent plugin, one looking
for the Hermes/OpenClaw plugin — and this is where each of them can see
their own path. It opens "How It Works" because the cards answer "what does
this look like for me" before the sections below answer "how". */}
Memory Types
Hindsight does not store conversations. It extracts what was said into typed
facts and then builds on them:
- World fact — an objective claim it was told. "Alice works at Google."
- Experience fact — something the bank itself did. "I recommended Python to Bob."
- Observation — a belief consolidated from many facts, with its evidence and
its history. "User was a React enthusiast, has now switched to Vue." - Mental model — a curated summary you write for a question you ask often.
- Knowledge page — a living document the bank writes about itself.
Facts are not a list. Each is linked to the entities it mentions and to the other
facts that share them, which is what makes "where does Alice work?" answerable
from two facts that were never stored together — the graph at the top of this
page is a sample bank's; hover a memory to see what it links to.
Multi-Strategy Retrieval
recall() runs four searches in parallel, because no single one handles
every question:
- Semantic — meaning rather than wording, so "Alice's job" finds "Alice works as a software engineer"
- Keyword (BM25) — the names and technical terms an embedding blurs together; five pluggable Postgres backends, including one that works on a Citus cluster
- Graph — entity links, so a fact reachable in two hops comes back even when it shares no words with the query
- Temporal — time expressions parsed into a window, then filled by relevance and spread across the range, so "what happened in 2023?" is not all from December
Then the part that matters more than any single arm:
- Fused by rank, not score — a memory several strategies agree on wins, and no arm's scoring scale can dominate the others
- Re-ranked by a cross-encoder that reads the query and the memory together
- Cut to a token budget, not a top-k — you say how much context you can afford and Hindsight fills it, because agents budget in tokens and not in result counts
See Recall for the strategies in full.
Observation Consolidation
A background worker keeps turning raw facts into durable beliefs:
- Deduplicated — overlapping facts merge into one observation instead of piling up as repeats
- Evidence-grounded — each observation points at the memories that support it, with exact quotes and a proof count
- Refined, not overwritten — new evidence updates an observation and its history is kept, so you can see a belief change
- Freshness-aware — when memories have landed but not yet been consolidated,
reflecttreats the affected observations as stale and checks them against raw facts before trusting them
Knowledge Pages
Observations answer one question at a time. A knowledge page is a living
document the bank writes about itself — "What are the components here?", "What's
our error-handling convention?" — rewritten incrementally as consolidation
produces new knowledge in its scope:
- A wiki, not a blob — pages live in a tree of folders, browsable and searchable, each answering one question
- Built from observations — synthesized from consolidated beliefs rather than raw conversation, so a page is not a transcript summary
- Never self-citing — a page never reads another page, so they cannot cite each other into a feedback loop
- Real files when you want them —
hindsight fs mountprojects the tree onto disk as ordinary markdown, sogrep, an editor or an agent's file tools all work with no SDK
See Knowledge Pages for the full model.
Reflect
recall() returns memories. reflect() returns an answer, by running an
agentic loop over the bank rather than a single query:
- It gathers its own evidence — the agent decides what it needs and calls its own tools, up to ten rounds, and cannot answer before it has retrieved something
- It checks sources in priority order — mental models and knowledge pages first, then observations, and only then raw facts, so it reads the distilled answer before the transcript
- It cites what it used — and only IDs it actually retrieved can be cited
What makes two banks answer the same question differently is their configuration:
- Mission — the bank's identity in plain language, which tells it what to prioritise. "I am a research assistant specializing in ML. I prefer simplicity over cutting-edge."
- Directives — hard rules it must never break. "Never recommend specific stocks."
- Disposition — skepticism, literalism and empathy on a 1–5 scale, shaping how it interprets what it finds
These shape reflect only. recall returns the same memories whoever is asking.
See Reflect for the loop in detail.
Architecture Deep Dive
Everything above, end to end and in motion: what a document turns into on the
way in, what each of the three operations touches, and what the worker changes
behind them. Play it, or step through it at your own pace.
Integrations
Coding agents — one install wires 20+
harnesses (Claude Code, Codex CLI, Cursor CLI, opencode, Copilot CLI and more) to
a per-repo memory bank.
Personal agents — Hermes and
OpenClaw install Hindsight as their memory
provider: every turn recalls what matters before answering and retains what was
said afterwards. The Hermes desktop app configures it
in Settings, no terminal.
Everything else — frameworks, MCP servers, tools — is in the
Integrations Hub.
Next Steps
Getting Started
- Quick Start — Install and get up and running in 60 seconds
- RAG vs Hindsight — See how Hindsight differs from traditional RAG with real examples
Core Concepts
- Retain — How memories are stored with multi-dimensional facts
- Recall — How the 4-way parallel search retrieves memories
- Reflect — How mission, directives, and disposition shape reasoning
API Methods
- Retain — Store information in memory banks
- Recall — Search and retrieve memories
- Reflect — Agentic reasoning with memory
- Mental Models — User-curated summaries for common queries
- Memory Banks — Configure mission, directives, and disposition
- Documents — Manage document sources
- Operations — Monitor async tasks
Deployment
- Server Setup — Deploy with Docker Compose, Helm, or pip
Hindsight Docs Docs Developer Api Quickstart (`hindsight-docs/docs/developer/api/quickstart.mdx`)
sidebar_position: 0
Quick Start
Get up and running with Hindsight in 60 seconds.
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeSnippet from '@site/src/components/CodeSnippet';
import {ClientsGrid} from '@site/src/components/SupportedGrids';
{/* Import raw source files */}
import quickstartPy from '!!raw-loader!@site/examples/api/quickstart.py';
import quickstartMjs from '!!raw-loader!@site/examples/api/quickstart.mjs';
import quickstartSh from '!!raw-loader!@site/examples/api/quickstart.sh';
import quickstartGo from '!!raw-loader!@site/examples/api/quickstart.go';
Clients
Start the API Server
pip install hindsight-api
export OPENAI_API_KEY=sk-xxx
export HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY
hindsight-apiAPI available at http://localhost:8888
export OPENAI_API_KEY=sk-xxx
docker run -it --pull always --name hindsight --restart unless-stopped --shm-size=1g -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest- API: http://localhost:8888
- Control Plane (Web UI): http://localhost:9999
:::tip Set a stable HINDSIGHT_API_WORKER_ID in production
The worker uses the container hostname as its identity, which Docker sets to the container ID by default. That value changes on every restart, so any task that was being processed when the container went down stays parked under the old ID with no way for the new container to recognize it as its own.
Set HINDSIGHT_API_WORKER_ID to a stable value (e.g., -e HINDSIGHT_API_WORKER_ID=hindsight-prod) so the worker keeps the same identity across restarts. This is recommended even for single-container deployments. For diagnosis and recovery commands, see Admin CLI - Recovering stuck operations.
:::
:::tip LLM Provider
Hindsight requires an LLM with structured output support. Recommended: Groq with gpt-oss-20b for fast, cost-effective inference.
See LLM Providers for more details.
:::
Use the Client
pip install hindsight-clientnpm install @vectorize-io/hindsight-clientcurl -fsSL https://hindsight.vectorize.io/get-cli | bashgo get github.com/vectorize-io/hindsight/hindsight-clients/goWhat's Happening
| Operation | What it does |
|---|---|
| Retain | Content is processed, facts are extracted, entities are identified and linked in a knowledge graph |
| Recall | Four search strategies (semantic, keyword, graph, temporal) run in parallel to find relevant memories |
| Reflect | Retrieved memories are used to generate a disposition-aware response |
Integrations
Browse all supported integrations in the Integrations Hub.
Next Steps
- Retain — Advanced options for storing memories
- Recall — Search and retrieval strategies
- Reflect — Disposition-aware reasoning
- Memory Banks — Configure disposition and mission
- Server Deployment — Docker Compose, Helm, and production setup
Hindsight Docs Docs Developer Memory Defense Index (`hindsight-docs/docs/developer/memory-defense/index.md`)
sidebar_position: 95
Memory Defense
Hindsight scrubs secrets and PII from retain content using a 45-pattern regex set. Each match is replaced with a [REDACTED:type] marker before content reaches memory units or the document body. The feature is configured per bank and disabled by default.
How it works
Memory Defense is opt-in per bank. The extension is always present, but it sits dormant until you give a bank a policy that turns it on. When a policy is set, every memory the agent writes to that bank is scanned before it lands in storage. When the scanner recognizes a credential, an API key, a database connection string, or a known PII format, the matched substring is replaced with a redaction marker like [REDACTED:github_token].
The scrubbed version is what actually gets stored. Memory units and document bodies persist the redacted text, so future recall responses, exports, and reflect operations never see the original secret.
A policy only affects future retain calls on the bank where it is set. Existing memories are not retroactively scanned when you add or change a policy.
Configuring Memory Defense
Memory Defense is configured per bank via the bank's memory_defense config field. You can set the policy at bank creation time or update it later via PATCH /v1/{tenant}/banks/{bank_id}/config.
The open-source version implements the sensitive_data rule with two possible actions:
redact— replace each matched secret with a[REDACTED:type]marker and store the scrubbed memory.block— drop any item that contains a match. If every item in a retain request is blocked, the call returns422.
A minimal policy:
{
"memory_defense": {
"enabled": true,
"rules": [
{ "on": "sensitive_data", "action": "redact" }
]
}
}Once that policy is on a bank, every retain to that bank is screened with the 45 redaction patterns documented below.
:::note Existing memories are not retroactively scanned
Enabling Memory Defense on a bank only affects future retain calls. Memories already in the bank are not re-scanned or modified when you add or change a policy. If you need to scrub a bank that already contains unredacted content, you have to re-ingest the affected memories or remove them manually.
:::
Disabled by default
Memory Defense is off on every bank until you set a policy. A bank with no memory_defense field, with enabled: false, or with no sensitive_data rule is treated identically: the extension returns ALLOW and content passes through unchanged. To stop redacting on a bank that has it on, set enabled: false or remove the policy.
Notifications
When an item is redacted or blocked, Hindsight fires a memory_defense.triggered webhook if a webhook on the bank is subscribed to that event type. The payload reports the action taken, the document ID, and which redaction patterns matched — useful for routing security alerts to a SIEM or Slack. Clean items fire no event.
The same redact/block decisions are also recorded as memory_defense entries in the audit log (when audit logging is enabled), with the action and matched pattern labels in the entry metadata.
Patterns covered
The 45 bundled patterns cover the categories below.
AI and LLM providers
| Label | Catches |
|---|---|
anthropic_key |
sk-ant-... |
openai_key, openai_project_key, openai_admin_key |
sk-..., sk-proj-..., sk-admin-... |
google_api_key |
AIza... (39 chars) |
google_oauth_token |
ya29.<token> |
xai_key |
xai-... |
groq_key |
gsk_... |
huggingface_token |
hf_... |
replicate_token |
r8_... |
perplexity_key |
pplx-... |
databricks_token |
dapi<hex32> |
Cloud providers
| Label | Catches |
|---|---|
aws_access_key |
AKIA<16> |
aws_session_token |
ASIA<16> |
digitalocean_token |
dop_v1_<hex64> |
Source control and CI
| Label | Catches |
|---|---|
github_fg_pat |
github_pat_... |
github_token |
ghp_<36> |
github_app_token |
ghs_<36> |
github_user_token |
ghu_<36> |
github_refresh |
ghr_<36> |
github_oauth |
gho_<36> |
gitlab_pat |
glpat-... |
npm_token |
npm_... |
pypi_token |
pypi-AgEIcHlwaS5vcmc... |
Payment processors
| Label | Catches |
|---|---|
stripe_secret |
sk_live_..., sk_test_... |
stripe_restricted |
rk_live_..., rk_test_... |
square_token |
sq0... |
braintree_token |
access_token$production$... |
Communications and email
| Label | Catches |
|---|---|
slack_token |
xoxb-, xoxp-, xoxa-, xoxr- |
slack_webhook |
https://hooks.slack.com/services/... |
twilio_api_key |
SK<hex32> |
twilio_account_sid |
AC<hex32> |
sendgrid_key |
SG.<22>.<43> |
mailgun_key |
key-<32> |
discord_bot |
<MNO><23>.<6>.<27> |
telegram_bot |
<8-10 digits>:<35> |
Commerce
| Label | Catches |
|---|---|
shopify_token |
shpat_<hex32> |
Database connection strings
| Label | Catches |
|---|---|
db_url_postgres |
postgres://user:pass@host or postgresql://... |
db_url_mysql |
mysql://user:pass@host |
db_url_mongodb |
mongodb://user:pass@host or mongodb+srv://... |
Private keys, JWTs, and generic credentials
| Label | Catches |
|---|---|
private_key_pem |
-----BEGIN ... PRIVATE KEY----- PEM blocks |
jwt |
eyJ<header>.eyJ.<signature> |
PII (US defaults)
| Label | Catches |
|---|---|
credit_card |
13 to 19 digits with regular separators |
ssn_us |
123-45-6789 shape |
Hindsight Docs Docs Cloud Enterprise Enterprise (`hindsight-docs/docs/cloud-enterprise/enterprise.md`)
title: Hindsight Enterprise
Hindsight Enterprise
Hindsight Enterprise is Hindsight for organizations that need their own deployment. Instead of sharing the multi-tenant Hindsight Cloud, you get a single-tenant installation that Vectorize sets up and runs with you. It includes everything in Hindsight Cloud, plus the security, compliance, networking and scale features below.
Book a call · Hindsight Enterprise docs
Deployment options
| Option | Where it runs | Who runs it |
|---|---|---|
| Hindsight Cloud with Enterprise features | Hindsight Cloud (multi-tenant) | Vectorize |
| Private cloud | A dedicated deployment in Vectorize's cloud account, in the region you choose | Vectorize |
| Bring your own cloud (BYOC) | Your AWS, Google Cloud or Azure account | Vectorize, through access you grant |
| On-premises | Your Kubernetes infrastructure, including air-gapped | You, with Vectorize's help |
Private cloud and BYOC can be reached over private networking (AWS PrivateLink, Google Cloud Private Service Connect, Azure Private Link), so traffic never crosses the public internet. See Choosing a deployment option for a full comparison, including who is responsible for what.
What Enterprise adds
- Single-tenant deployment: its own database, storage, compute and identity service.
- Hindsight Memlake: memory on object storage, for large and growing banks.
- Single sign-on and MFA: Okta, Microsoft Entra ID, Google Workspace or any OIDC or SAML provider.
- Memory Defense: detect and redact secrets and sensitive data, and block prompt injection before it is stored.
- Data privacy (GDPR): erase, retain and export a person's data across banks.
- Audit logs, webhooks and SIEM: a record of who did what, streamed to your own systems.
- Enterprise support: a ticket portal, a shared Slack channel and contracted response times.
- Invoiced billing: annual or contract billing instead of card payments.
Hindsight Memlake
Hindsight Memlake is the Enterprise storage engine for agent memory. Open-source Hindsight stores memories in PostgreSQL, which works well for most banks. But agent memory only grows, and as banks reach hundreds of thousands of memories, a relational database gets slower and more expensive to keep fast.
Memlake keeps memories in object storage (S3, Google Cloud Storage, Azure Blob Storage, or an S3-compatible store on-premises), with one bucket per organization:
- Built for scale. In our tests, 100 million memories were served at under 100 ms p99 recall, on four servers.
- Fast and steady. Recall time barely moves as a bank grows, because each bank has its own index, kept warm in a cache on the servers.
- Lower cost as you grow. Memories live in object storage, the cheapest durable storage there is, and servers hold only a cache, so you add servers for traffic, not for data.
- Durable. A write is in object storage before it is acknowledged, and continuously backed up to a separate bucket.
- Nothing changes for your agents. Same API, SDKs and features; banks, settings and background jobs stay in PostgreSQL.
If your banks stay under roughly 50,000 memories each, PostgreSQL is a good fit and Memlake is not needed. Memlake is available on private cloud, BYOC and on-premises deployments.
Read more: Hindsight Memlake overview, architecture, performance and cost and tradeoffs.
Learn more
- Hindsight Enterprise documentation
- Ways to run Hindsight: self-hosted, Cloud and Enterprise compared
- Book a call with the Hindsight team
Hindsight Docs Docs Cloud Enterprise Hindsight Cloud (`hindsight-docs/docs/cloud-enterprise/hindsight-cloud.md`)
title: Hindsight Cloud
Hindsight Cloud
Hindsight Cloud is Hindsight run for you. Sign up, create a memory bank and connect your agents, with nothing to deploy or operate. It runs the same engine as open-source Hindsight, so everything in these docs about how memory works applies.
Start on Hindsight Cloud · Hindsight Cloud docs
What you get
- Nothing to run. No servers, databases or upgrades. The LLM, embedding and reranking models are included.
- The same API. Retain, recall, reflect, mental models and knowledge pages, through the same Python, TypeScript and Go clients, the CLI and the MCP server. Point them at your Cloud API URL and key.
- Integrations. Pre-built integrations with agent frameworks, coding agents and AI tools.
- A console. Browse banks, memories, mental models and knowledge pages, and see how a bank is learning.
- Teams. Invite your team with role-based access, and manage API keys.
- Usage analytics. Dashboards for what each bank uses.
Pricing
You pay for what you use, from a prepaid credit balance: per token for retain and recall, per call for reflect and for creating or refreshing mental models and knowledge pages, and for storage of memories kept longer than 30 days. See Billing for current rates.
Enterprise features on Cloud
Some features are part of Hindsight Enterprise and can be turned on for a Cloud organization: SSO, enforced MFA, Memory Defense, data privacy (GDPR), audit logs, webhooks and SIEM, and Enterprise support. See Hindsight Cloud with Enterprise features.
Hindsight Cloud is shared and multi-tenant. If you need a dedicated deployment, see Hindsight Enterprise.
Learn more
- Getting started: your first memory bank, step by step
- Hindsight Cloud documentation
- Ways to run Hindsight: self-hosted, Cloud and Enterprise compared
Hindsight Docs Docs Developer Admin Cli (`hindsight-docs/docs/developer/admin-cli.md`)
Admin CLI
The hindsight-admin CLI provides administrative commands for managing your Hindsight deployment, including database migrations, backup, and restore operations.
Installation
The admin CLI is included with the hindsight-api package — installing it puts the hindsight-admin executable on your PATH:
pip install hindsight-api
# or
uv add hindsight-apiRunning the CLI
hindsight-admin connects directly to PostgreSQL — it does not call the HTTP API. It reads the same configuration as the API service (environment variables, and a .env file in the current working directory), so it operates on whatever database HINDSIGHT_API_DATABASE_URL points to:
- Default:
pg0, the embedded development database (must be run on the host that owns the pg0 data). - Production: set
HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/hindsight.
Because it talks to the database directly (binary COPY, TRUNCATE, etc.), the admin CLI is PostgreSQL-only (not supported on Oracle). Run it on the same host/container as your API deployment so it inherits the right configuration and has network access to the database:
# Bare metal / virtualenv (with the API's env or a .env in the working dir)
hindsight-admin worker-status
# Docker — exec into the API container
docker exec -it hindsight-api hindsight-admin backup /data/backup.zip
# Kubernetes — exec into an API pod
kubectl exec deploy/hindsight-api -- hindsight-admin run-db-migrationUse --schema to target a specific tenant schema (commands default to the configured base schema). See Environment Variables below.
Commands
run-db-migration
Run database migrations to the latest version. By default this migrates the base schema plus all tenant schemas discovered by the tenant extension. Use --schema for targeted migration of one schema. This is useful when you want to run migrations separately from API startup (e.g., in CI/CD pipelines or before deploying a new version).
hindsight-admin run-db-migration [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--schema, -s |
Database schema to run migrations on. If omitted, migrate the base schema plus all discovered tenant schemas. | All schemas |
--embedding-dimension |
Expected embedding dimension to enforce after migrations. Omit to skip the post-migration dimension sync. | Skipped |
--skip-extension-reconcile |
Skip the post-migration vector / text-search index reconcile (it only does work when HINDSIGHT_API_VECTOR_EXTENSION / HINDSIGHT_API_TEXT_SEARCH_EXTENSION differs from a schema's existing indexes). Makes a no-change re-migration across many tenant schemas much faster; only use when the backend is unchanged. |
Reconcile runs |
Examples:
# Run migrations on the base schema plus all discovered tenant schemas
hindsight-admin run-db-migration
# Run migrations on a specific tenant schema
hindsight-admin run-db-migration --schema tenant_acme:::tip Disabling Auto-Migrations
To disable automatic migrations on API startup, set HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=false. This is useful when you want to run migrations as a separate step in your deployment pipeline.
:::
repair-bank
Verify and repair a bank's per-(bank, fact_type) vector index coverage.
These partial indexes are normally created when a bank is first created (instant on an empty bank), and PostgreSQL maintains them incrementally as the bank grows. A bank that becomes populated outside that create-time path — via a logical restore, a cross-version upgrade, or a vector-extension switch — never gets them, so its bank-scoped recall silently falls back to a global index + post-filter. That fallback is both slower and can under-return results (the approximate nearest-neighbour search draws its candidates from every bank, then filters to yours afterward).
Run this after any of those events to restore full coverage. It detects missing or invalid coverage (an INVALID leftover from an interrupted build, or an index whose type drifted after a backend switch, both count as missing) and rebuilds it with CREATE INDEX CONCURRENTLY, so it never blocks live retain/recall/consolidation. It is idempotent and safe to re-run.
hindsight-admin repair-bank (--bank BANK_ID | --all) [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--bank, -b |
Bank id to repair. Mutually exclusive with --all. |
— |
--all |
Repair every bank in the base schema and all discovered tenant schemas. | — |
--schema, -s |
Limit to a single schema. | All schemas |
--dry-run |
Report what would be repaired without creating or dropping any index. | Off |
Exactly one of --bank or --all is required. No-op for backends that use a single global vector index (AlloyDB ScaNN, Oracle). It is idempotent — safe to re-run and safe to run while the API is serving traffic.
It is also a no-op for a bank whose memories a custom store owns, this time decided
per bank rather than per deployment: such a bank has no rows in memory_units, so it
is owed no per-bank index and nothing is built for it. Nothing is dropped either — a
bank that carries indexes from before it moved to the store keeps them, because
shedding them is an operator decision with its own timing, not something a repair
does on your behalf. A store-owned bank therefore reports 0 present, 0 created
whether or not it still carries indexes; to see what one actually carries, query
the catalog for that bank's own partial indexes:
SELECT indexname FROM pg_indexes
WHERE schemaname = '<tenant schema>'
AND tablename = 'memory_units'
AND indexname LIKE 'idx_mu_emb_%'
AND strpos(indexdef, $$bank_id = '<bank id>'$$) > 0;Drop the last condition to list every per-bank index in the schema instead.
Those indexes are empty, and Postgres plans against every index on a relation, so on
a deployment with many store-owned banks they are charged to every other query that
touches memory_units. Drop them with DROP INDEX CONCURRENTLY when you are ready.
Exit code. 0 when every schema was reconciled. 1 when an index failed to build
(the names are printed, and a re-run retries them), or when a schema was not fully
reconciled — for example because the memories store could not say which banks it
owns. The command does not guess there, because guessing would rebuild the very
indexes described above.
A schema is named as skipped whether it failed before its first bank or partway
through, so it may already have had some banks reconciled; when it got that far the
per-schema line says how many, and those builds are not undone. Both the failed-index names and the
skipped schemas are reported before the command exits, so one never hides the other.
If the database connection itself is lost mid-sweep, the run stops there rather than
repeating the same error for every remaining schema, and still reports what it did.
Examples:
# See which banks are missing coverage without changing anything
hindsight-admin repair-bank --all --dry-run
# Repair every bank across all schemas (run once after a restore/upgrade)
hindsight-admin repair-bank --all
# Repair a single bank
hindsight-admin repair-bank --bank acme-prodrename-bank
:::tip Consider an alias instead
A rename requires stopping the bank's clients, because the old id stops working the moment it commits. If what you want is to move callers onto a new id, bank aliases do that with no downtime and no row rewrite: the bank answers to both ids until you are done.
Reach for rename-bank when the old id must genuinely stop existing — an alias never removes one.
:::
Change a bank's id in place. Every memory, document, entity, mental model and knowledge page is kept exactly as it is. Nothing is re-extracted or re-embedded, and no LLM calls are made.
hindsight-admin rename-bank --from OLD_ID --to NEW_ID [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--from |
Current bank id. | — |
--to |
New bank id. Must not already exist. | — |
--schema, -s |
Schema the bank lives in. | Configured base schema |
--dry-run |
Run the whole rename, print the rows that would move per table, then roll back. | Off |
Before you run it:
- Stop the bank's clients. Anything still calling the old id gets a 404, or, if it creates banks on demand, silently starts a new, empty bank under the old id. Point every client, and any API key scoped to the bank, at the new id before restarting them.
- Let the bank's operations finish. The command refuses while the bank has pending or processing operations.
A file's storage key spells the bank id, so after the rows move the bank's stored files — document attachments and uploaded originals — are copied to keys under the new id, their rows repointed, and the old id's prefix cleared. That last step also removes any export or import archive left under the old id: their download links stop working at the rename, and the operation can produce them again. Deleting the bank afterwards removes its files with it.
The rename runs in a single transaction and only locks that bank's rows. Other banks keep serving. Its duration grows with the bank's size. Afterwards, the bank's vector indexes are rebuilt with CREATE INDEX CONCURRENTLY, and until that finishes, recall on the renamed bank runs without them. Running API servers may keep answering for the old id until their bank-info cache expires. PostgreSQL only.
Examples:
# See what would move, change nothing
hindsight-admin rename-bank --from acme --to acme-prod --dry-run
hindsight-admin rename-bank --from acme --to acme-prodbackup
Create a backup of all Hindsight data to a zip file.
hindsight-admin backup OUTPUT [OPTIONS]Arguments:
| Argument | Description |
|---|---|
OUTPUT |
Output file path (will add .zip extension if not present) |
Options:
| Option | Description | Default |
|---|---|---|
--schema, -s |
Database schema to backup | public |
Examples:
# Backup to a file
hindsight-admin backup /backups/hindsight-2024-01-15.zip
# Backup a specific tenant schema
hindsight-admin backup /backups/tenant-acme.zip --schema tenant_acmeThe backup includes:
- Memory banks and their configuration
- Documents and chunks
- Entities and their relationships
- Memory units (facts, experiences, observations)
- Entity cooccurrences and memory links
- Mental models and directives
- Webhooks and file storage
- Internal operational tables (async operations, audit log, graph-maintenance queue, and similar bookkeeping) so a restore reproduces a faithful full-database snapshot
:::note Consistency
Backups are created within a database transaction with REPEATABLE READ isolation, ensuring a consistent snapshot across all tables.
:::
restore
Restore data from a backup file. Warning: This deletes all existing data in the target schema.
hindsight-admin restore INPUT [OPTIONS]Arguments:
| Argument | Description |
|---|---|
INPUT |
Input backup file (.zip) |
Options:
| Option | Description | Default |
|---|---|---|
--schema, -s |
Database schema to restore to | public |
--yes, -y |
Skip confirmation prompt | false |
Examples:
# Restore with confirmation prompt
hindsight-admin restore /backups/hindsight-2024-01-15.zip
# Restore without confirmation (for scripts)
hindsight-admin restore /backups/hindsight-2024-01-15.zip --yes
# Restore to a specific tenant schema
hindsight-admin restore /backups/tenant-acme.zip --schema tenant_acme --yes:::warning Data Loss
Restore will delete all existing data in the target schema before importing the backup. Always verify you have a recent backup before performing a restore.
:::
decommission-worker
Release all tasks owned by a worker, resetting them from "processing" back to "pending" status so they can be picked up by other workers.
hindsight-admin decommission-worker WORKER_ID [OPTIONS]Arguments:
| Argument | Description |
|---|---|
WORKER_ID |
ID of the worker to decommission |
Options:
| Option | Description | Default |
|---|---|---|
--schema, -s |
Database schema | public |
--yes, -y |
Skip confirmation prompt | false |
Examples:
# Before scaling down - release tasks from workers being removed
hindsight-admin decommission-worker hindsight-worker-4
hindsight-admin decommission-worker hindsight-worker-3
# Release tasks from a crashed worker
hindsight-admin decommission-worker worker-2
# For a specific tenant schema
hindsight-admin decommission-worker worker-1 --schema tenant_acmeWhen to Use:
- Scaling down: Before removing worker replicas in Kubernetes
- Graceful removal: When taking a worker offline for maintenance
- Crash recovery: If a worker crashed while processing tasks
- Stuck worker: When a worker is unresponsive
:::tip Finding Worker IDs
Worker IDs default to the hostname. In Kubernetes StatefulSets, this is the pod name (e.g., hindsight-worker-0). You can also set a custom ID with HINDSIGHT_API_WORKER_ID or --worker-id.
:::
decommission-workers
Release all currently-processing tasks from every worker, resetting them from "processing" back to "pending" status. Use this when one or more workers have crashed or been removed without graceful shutdown and you don't know which worker IDs to target.
hindsight-admin decommission-workers [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--schema, -s |
Database schema | public |
--yes, -y |
Skip confirmation prompt | false |
Examples:
# Release all processing tasks across all workers (with confirmation)
hindsight-admin decommission-workers
# Skip the confirmation prompt (useful in scripts)
hindsight-admin decommission-workers --yes
# Release tasks in a specific tenant schema
hindsight-admin decommission-workers --schema tenant_acmeWhen to Use:
- Unknown dead workers: Multiple workers crashed and you do not know their IDs
- Fleet-wide recovery: After an infrastructure event where many workers went down
- "Just fix everything": A quick full-queue drain when per-worker cleanup is overkill
:::warning Disruptive
This releases every processing task regardless of worker, including tasks owned by healthy workers. Prefer decommission-worker <WORKER_ID> when you know which workers need cleanup.
:::
worker-status
Show all currently-processing tasks grouped by worker, including operation type, bank, how long each task has been running, and when it was last updated. Useful for identifying orphaned tasks before decommissioning.
hindsight-admin worker-status [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--schema, -s |
Database schema | public |
Examples:
# Show all processing tasks across all workers
hindsight-admin worker-status
# Show processing tasks for a specific tenant schema
hindsight-admin worker-status --schema tenant_acmeWhen to Use:
- Before decommissioning: Inspect which workers have stale tasks and how long they have been stuck
- Debugging throughput: Diagnose why the queue is not draining (are tasks stuck in processing?)
- Worker health check: Spot workers whose
last_update_agokeeps growing, indicating a dead or unresponsive worker
export-bank
Export an entire bank to a portable ZIP archive — documents, facts, observations, bank configuration, mental models, directives, and webhooks. Embeddings are never included; they are regenerated on import. This is the source half of a cross-instance migration (e.g. moving to a different embedding model, vector extension, or text-search backend). PostgreSQL only.
hindsight-admin export-bank --bank <BANK_ID> --output <FILE.zip> [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--bank, -b |
Bank id to export. | (required) |
--output, -o |
Path to write the .zip archive. |
(required) |
--schema, -s |
Schema the bank lives in. | base schema |
--include-history |
Also export operational history (audit_log, llm_requests). |
false |
Examples:
hindsight-admin export-bank --bank my-bank --output my-bank.zip
# include operational history
hindsight-admin export-bank --bank my-bank --output my-bank.zip --include-historyRead-only — safe to run against a live instance.
import-bank
Restore a whole-bank archive (produced by export-bank) into this instance. Facts are re-embedded with this instance's configured embedding model and links/indexes are rebuilt; bank configuration, mental models, directives, and webhooks are restored exactly. No LLM fact-extraction runs, and because a migration restores state, it does not fire webhooks or re-run consolidation. PostgreSQL only.
hindsight-admin import-bank --archive <FILE.zip> [OPTIONS]Options:
| Option | Description | Default |
|---|---|---|
--archive, -a |
Path to the .zip produced by export-bank. |
(required) |
--schema, -s |
Target schema. | base schema |
--target-bank |
Override the bank id (defaults to the archive's source bank). | source bank |
--include-history |
Also restore history if present in the archive. | false |
Examples:
hindsight-admin import-bank --archive my-bank.zipRun this against an instance configured with the target embedding model / vector extension / text-search backend — that's what re-embedding uses.
:::warning Target bank must not exist
Import restores a whole bank (config, facts, mental models, …) — it is not a merge. If a bank with the target id already exists, the command fails. Delete that bank first, or use --target-bank to restore under a fresh id.
:::
Migrating a bank to a new instance
Changing a bank's embedding model (e.g. a 384-dim encoder → a 1024-dim one), vector extension (pgvector / vchord / pgvectorscale), or text-search backend can't be done in place on a populated bank — the stored vectors and indexes are tied to those settings. Because every embedding and index is a deterministic function of text already on disk, the supported path is to move the bank to a fresh instance configured with the new settings and re-derive everything there — with no LLM re-extraction.
export-bank / import-bank carry documents, facts, observations, bank config, mental models, directives, and webhooks — but never embeddings, which the target instance regenerates with its own model.
Blue-green runbook:
- Stand up a new instance on a fresh database, configured with the new embedding model / vector extension / text-search backend.
- Quiesce writes to the source bank (maintenance window) and run
hindsight-admin backupfor safety. - Export from the source, then import into the target:bash
# on the source instance: hindsight-admin export-bank --bank my-bank --output my-bank.zip # on the target instance (configured with the new settings): hindsight-admin import-bank --archive my-bank.zip - Verify on the target: run representative recall queries and compare results.
- Cut traffic over to the new instance. The old instance stays as an instant rollback until you're confident.
:::note Why a new instance, not in-place
The embedding model is server-level, and a bank's memory_units.embedding column has a single dimension shared across the schema, so a different-dimension or different-backend bank needs its own instance/database. The old vectors are never mutated, which makes rollback trivial.
:::
Recovering stuck or zombie operations
A "zombie" operation is one stuck in processing indefinitely because the worker that claimed it is gone. The most common cause is an unstable HINDSIGHT_API_WORKER_ID: when it defaults to the container hostname, a Docker restart produces a new container ID, the new worker doesn't recognize the old worker's claims as its own, and those tasks are stranded.
How to spot them:
# List processing tasks grouped by worker — workers with a growing last_update_ago are dead
hindsight-admin worker-status
# Bank-level counters; pending_consolidation that never decreases is the usual symptom
curl -s http://localhost:8888/v1/default/banks/<bank_id>/statsHow to recover:
# You know which worker is dead (e.g. from worker-status):
hindsight-admin decommission-worker <old-worker-id>
# You don't know — release every processing task across the fleet:
hindsight-admin decommission-workersBoth commands reset processing rows back to pending so a live worker can claim them on the next poll.
How to prevent it:
Set HINDSIGHT_API_WORKER_ID to a stable value so worker identity survives restarts:
- Docker: pass
-e HINDSIGHT_API_WORKER_ID=hindsight-prod(or per-replica names if running multiple containers) - Kubernetes (Helm): the chart's StatefulSet uses the pod name automatically — no extra config needed
- Bare metal / pip: pass
--worker-id <name>or set the env var per process
See Installation - Docker and Configuration - Distributed Workers.
Environment Variables
The admin CLI uses the same environment variables as the API service. The most important one is:
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_DATABASE_URL |
PostgreSQL connection string | pg0 (embedded) |
Example:
# Use a specific database
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
hindsight-admin backup /backups/mybackup.zipHindsight Docs Docs Developer Configuration (`hindsight-docs/docs/developer/configuration.mdx`)
import CodeSnippet from '@site/src/components/CodeSnippet';
import configurationPy from '!!raw-loader!@site/examples/api/configuration.py';
import configurationSh from '!!raw-loader!@site/examples/api/configuration.sh';
Configuration
Complete reference for configuring Hindsight services through environment variables.
Hindsight has two services, each with its own configuration prefix:
| Service | Prefix | Description |
|---|---|---|
| API Service | HINDSIGHT_API_* |
Core memory engine |
| Control Plane | HINDSIGHT_CP_* |
Web UI |
API Service
The API service handles all memory operations (retain, recall, reflect).
Database
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_DATABASE_URL |
PostgreSQL connection string | pg0 (embedded) |
HINDSIGHT_API_READ_DATABASE_URL |
Optional read-replica PostgreSQL URL. When set, recall queries (semantic, BM25, graph, temporal) are routed through a separate connection pool against this URL, offloading the primary. Typically points to a read-only endpoint (e.g., CNPG's <cluster>-ro service or Aurora reader endpoint). |
Unset (uses primary) |
HINDSIGHT_API_MIGRATION_DATABASE_URL |
Direct PostgreSQL URL for running migrations, bypassing connection poolers (e.g. PgBouncer). When set, advisory locks and Alembic migrations use this URL instead of DATABASE_URL. |
Falls back to DATABASE_URL |
HINDSIGHT_API_DATABASE_SCHEMA |
PostgreSQL schema name for tables | public |
HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP |
Run database migrations on API startup | true |
HINDSIGHT_API_MIGRATION_ISOLATION |
Run migrations in a subprocess instead of in the calling process: true (keeps Alembic's import graph and its psycopg2 sync engine out of a long-lived server process) or false |
false |
HINDSIGHT_API_MIGRATION_CONCURRENCY |
Number of tenant schemas to migrate concurrently (PostgreSQL only). Each schema runs in its own process; within a schema migrations are always sequential. Each worker has a fixed startup cost (~1–2s to boot a fresh interpreter), so this only pays off with many schemas (roughly tens or more) or slow/high-latency migrations — for a handful of schemas it is slower than sequential. Each worker uses ~3 database connections, so keep concurrency × 3 within your database's spare max_connections (and any PgBouncer pool limit). 1 = fully sequential. Measured at 20k schemas: the per-restart no-op resweep dropped from ~60min to ~11min (≈5×) at concurrency=12. |
1 |
HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES |
Comma-separated list of maintenance discovery routines this deployment installs itself (see Owning a maintenance routine). Migrations skip anything named here. | Empty (every routine installed) |
HINDSIGHT_API_DATABASE_BACKEND |
Database engine backend: postgresql or oracle (Oracle 23ai) |
postgresql |
If not provided, the server uses embedded pg0 — convenient for development but not recommended for production.
To tune the embedded database, add PostgreSQL settings as a query string: pg0://hindsight?max_connections=300&shared_buffers=256MB. They apply when the embedded database starts, so restart it after changing them.
To run against Oracle Database 23ai instead, set HINDSIGHT_API_DATABASE_BACKEND=oracle and use an oracle+oracledb://… URL. See the Oracle Database guide for full setup instructions.
The DATABASE_SCHEMA setting allows you to use a custom PostgreSQL schema instead of the default public schema. This is useful for:
- Multi-database setups where you want Hindsight tables in a dedicated schema
- Hosting platforms (e.g., Supabase) where
publicschema is reserved or shared - Organizational preferences for schema naming conventions
# Example: Using a custom schema
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname
export HINDSIGHT_API_DATABASE_SCHEMA=hindsightMigrations will automatically create the schema if it doesn't exist and create all tables in the configured schema.
Owning a maintenance routine
The background maintenance loop finds work by calling four PostgreSQL routines —mental_models_with_cron, banks_needing_consolidation, schemas_with_expired_rows andschemas_with_expired_operations. The stock implementations scan every schema in the
database on each tick. On a large multi-tenant install that is a serial loop over
thousands of schemas, repeated once per tick in every process running the loop.
Because they are called by name, you can replace one with an implementation suited to
your installation — typically a registry table maintained by a row trigger, so discovery
costs one indexed read instead of a scan. List the ones you have replaced and migrations
will leave them alone:
export HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES=mental_models_with_cron,banks_needing_consolidationRead from the environment of whatever process runs the migration, so it applies equally tohindsight-admin run-db-migration, a migration Job, and migrate-on-startup.
Without this, your replacement survives only until the next migration that reinstalls the
routine: they are all CREATE OR REPLACE against the same name, so the migration wins and
nothing records that a custom implementation was discarded.
A few things to know:
- Your replacement must match the stock signature. The loop calls these with fixed
arguments and reads fixed columns back. - Nothing verifies that a replacement exists. Naming a routine you have not installed
leaves whatever was there before — nothing, on a fresh database — and the maintenance
loop then fails on the missing function rather than quietly running the scan you were
trying to avoid. - Ownership is not stamped into the database. Remove a name from the list and the next
migration reinstalls the stock routine, with no data fix-up. - Empty by default. An installation that has not replaced anything is unaffected.
Database Connection Pool
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_DB_POOL_MIN_SIZE |
Minimum connections in the primary pool | 5 |
HINDSIGHT_API_DB_POOL_MAX_SIZE |
Maximum connections in the primary pool | 100 |
HINDSIGHT_API_READ_DB_POOL_MIN_SIZE |
Minimum connections in the read-replica pool (only used when READ_DATABASE_URL is set) |
Falls back to DB_POOL_MIN_SIZE |
HINDSIGHT_API_READ_DB_POOL_MAX_SIZE |
Maximum connections in the read-replica pool (only used when READ_DATABASE_URL is set) |
Falls back to DB_POOL_MAX_SIZE |
HINDSIGHT_API_DB_COMMAND_TIMEOUT |
PostgreSQL command timeout in seconds (asyncpg client-side) | 60 |
HINDSIGHT_API_DB_ACQUIRE_TIMEOUT |
Connection acquisition timeout in seconds. Bounds how long a caller waits for a free pool connection before failing (retried by the caller); 0 waits indefinitely. |
30 |
HINDSIGHT_API_DB_POOL_SLOW_ACQUIRE_THRESHOLD_SECONDS |
Log a [DB POOL] Slow acquire warning when getting a pool connection takes longer than this many seconds. Raise it when new connections are slow by nature (e.g. TLS to Aurora Serverless); 0 disables the warning. |
0.05 |
HINDSIGHT_API_DB_STATEMENT_TIMEOUT |
Postgres statement_timeout applied to every pool connection, in seconds. Server-side safety net for runaway queries. Does not apply to Alembic migrations (which run on a separate psycopg2 engine). Set to 0 to disable. |
600 |
HINDSIGHT_API_DB_MAX_PARALLEL_WORKERS_PER_GATHER |
Optional Postgres max_parallel_workers_per_gather applied to every pool connection of this process. Unset leaves the server default. Set to 0 on background-worker processes so bulk maintenance queries (consolidation, graph upkeep) run serially instead of fanning out across CPU cores shared with latency-sensitive traffic. |
unset |
HINDSIGHT_API_DB_SESSION_SETUP_ON_ACQUIRE |
Whether the per-connection session settings above (statement_timeout, max_parallel_workers_per_gather, the trigram threshold, the vector-search tuning, and — on the vchord text-search backend — the search path) are re-applied every time a connection is taken from the pool, not only when it is first opened. Keep this on unless the same settings are already pinned on the database role or the database itself (ALTER ROLE … SET), because behind a transaction-mode connection pooler an acquire can be linked to a server connection that never received them (on a direct connection they now survive, so the re-apply is redundant there) — with the re-apply off and nothing pinned server-side, reused connections quietly run without them, and on vchord recall fails outright rather than merely degrading. When they are pinned server-side the re-apply changes nothing and only costs a round trip per acquire, which is worth reclaiming on busy deployments behind a transaction-mode connection pooler. application_name is always re-applied and is unaffected by this setting. |
true |
HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD |
Postgres pg_trgm.similarity_threshold applied to every pool connection, governing how close a name must be for entity resolution's % trigram match to treat it as a candidate. Must be between 0 (exclusive) and 1. Never applied below HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY, since a candidate under the merge floor can never be merged. Raise it above that floor to look at fewer, closer names. |
0.3 |
HINDSIGHT_API_ENTITY_INTRABATCH_MERGE_SIMILARITY |
Trigram similarity (pg_trgm-equivalent, computed in-memory) at/above which two brand-new names created by the same retain are merged into a single entity (in-batch dedup of surface-form variants — e.g. the same name with different emoji/case/suffix). Must be between 0 (exclusive) and 1. This is a merge cutoff, deliberately stricter than the recall-only threshold above; raise it toward 1.0 to merge only near-identical forms. |
0.5 |
HINDSIGHT_API_ENTITY_MERGE_MIN_SIMILARITY |
Minimum trigram similarity a name must have with an existing entity before that entity can be reused for it, whatever the other resolution signals say. Sits between the recall threshold above (0.15, which only decides what is considered) and the same-batch fold-in cutoff below (0.5). Must be between 0 (exclusive) and 1. Lower it for corpora of very short names, where trigram similarity is unavoidably low (Jon/John is 0.29); raise it to merge only clear surface variants. |
0.3 |
For high-concurrency workloads, increase DB_POOL_MAX_SIZE. Each concurrent recall/think operation can use 2-4 connections.
To run migrations manually (e.g., before starting the API), use the admin CLI:
# Migrate the base schema plus all discovered tenant schemas
hindsight-admin run-db-migration
# Or migrate a specific schema only:
hindsight-admin run-db-migration --schema tenant_acmeVector Extension
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_VECTOR_EXTENSION |
Vector index algorithm: pgvector, vchord, pgvectorscale, or scann |
pgvector |
HINDSIGHT_API_ANN_ITERATIVE_SCAN |
Let a vector index scan resume until the query's LIMIT is satisfied, instead of stopping when its first candidate list drains. With it off, a recall can never retrieve more rows than that list holds — on pgvector, hnsw.ef_search (200) — so a larger recall budget widens the SQL and retrieves nothing extra. Requires pgvector 0.8.0+; older servers reject the setting and it is dropped automatically after the first attempt. This is the operational kill switch: setting it to false and restarting restores the previous retrieval depth exactly, with no code change. |
true |
HINDSIGHT_API_ANN_MAX_SCAN_TUPLES |
Ceiling on how many tuples a single resumed scan may visit (hnsw.max_scan_tuples). This is the knob that governs what iterative scans cost: the filters that thin a result — the similarity floor, tags, date ranges — are applied after the index scan, so a selective query resumes repeatedly, and this bounds both the CPU it can spend and the memory it can hold (pgvector otherwise caps the latter at work_mem × hnsw.scan_mem_multiplier, which at this default is never approached). Lower it to trade retrieval depth back for latency; the initial scan is not counted, so even 1 leaves the pre-existing depth intact. pgvector's own default is 20000. Ignored when iterative scans are off. |
4000 |
Hindsight supports four PostgreSQL vector extensions:
**pgvector** (HNSW - default)
- In-memory index using Hierarchical Navigable Small World algorithm
- Works well for most embeddings and dataset sizes
- Fast for small-medium datasets (<10M vectors)
- Higher memory usage for large datasets
- Most widely deployed and supported
**pgvectorscale** (DiskANN - recommended for scale) ⭐
- Disk-based index using StreamingDiskANN algorithm
- 28x lower p95 latency and 16x higher throughput vs dedicated vector DBs
- 60-75% cost reduction at scale (SSDs cheaper than RAM)
- Superior filtering performance with streaming retrieval model
- Optimized for large datasets (10M+ vectors)
- Supports both pgvectorscale (open source) and pg_diskann (Azure)
- Installation:
- Open source/self-hosted:
CREATE EXTENSION vector; CREATE EXTENSION vectorscale CASCADE; - Azure PostgreSQL:
CREATE EXTENSION vector; CREATE EXTENSION pg_diskann CASCADE;
- Open source/self-hosted:
**vchord** (vchordrq)
- Alternative high-performance vector index
- Optimized for high-dimensional embeddings (3000+ dimensions)
- Includes integrated BM25 search capabilities
- Requires
vchordextension
**scann** (AlloyDB ScaNN)
- Google's ScaNN index, available on AlloyDB and AlloyDB Omni
- Uses a single global vector index in
AUTOmode (per-bank partial indexes are not used) - Installation:
CREATE EXTENSION vector; CREATE EXTENSION alloydb_scann CASCADE; - Index build is deferred until a table reaches 10,000 populated embedding rows — AlloyDB cannot build a ScaNN AUTO index on a near-empty table. Until that threshold is crossed, recall falls back to a sequential scan; the global index is built on the next API startup once enough rows exist.
- A ready-to-use Docker Compose stack is provided at
docker/docker-compose/alloydb/docker-compose.yamlfor running Hindsight against AlloyDB Omni locally.
When to use pgvectorscale (DiskANN):
- Large datasets (10M+ vectors) ⭐
- Complex filtering requirements
- Cost-sensitive deployments
- Production workloads requiring high throughput
- When disk I/O is not a bottleneck
When to use pgvector (HNSW):
- Small-medium datasets (<10M vectors)
- Maximum query speed when all data fits in memory
- Simple nearest-neighbor queries without filters
- Standard PostgreSQL deployment preference
When to use vchord:
- High-dimensional embeddings (3000+ dimensions)
- Want integrated BM25 search
- Already using vchord for text search
When to use scann:
- Running on Google AlloyDB or AlloyDB Omni
- Want managed ScaNN with
AUTOmode tuning
Limiting vector indexes on large deployments
On pgvector, pgvectorscale and vchord, a bank's memories are indexed per(bank, fact_type). By default every bank gets its own indexes when it is
created, which is the right thing for most deployments and needs no
configuration.
It stops being the right thing when you have thousands of banks. These indexes
all live on one shared table, and PostgreSQL inspects and locks every index
on a table whenever it plans a query against it — so an index created for one
bank is a cost paid by searches in every other bank. Past a few thousand banks,
planning slows sharply and the server eventually runs out of lock-table space,
failing reads and bank deletion alike.
Setting a minimum size fixes that: a bank only gets its own indexes once it is
large enough to benefit from them. Below the threshold PostgreSQL answers the
same search from the (bank_id, fact_type) B-tree plus a top-N sort, which for
a small bank is both faster and exact rather than approximate — so small banks
lose nothing. The number of indexes becomes proportional to the number of
large banks rather than to the number of banks, and bank count stops being a
limit.
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_VECTOR_INDEX_MIN_ROWS |
Memories a bank needs, in one fact type, before that fact type gets its own vector index. 0 (the default) turns the threshold off: every bank is indexed from the moment it is created. 10000 is a good starting point for deployments with thousands of banks. A bank whose memories a custom store owns is outside this at any value — it has no rows here, so it is never given a per-bank index, and never has an existing one taken away (see repair-bank in the admin CLI). |
0 |
HINDSIGHT_API_VECTOR_INDEX_MAINTENANCE_MIN_INTERVAL_SECONDS |
Shortest gap between two index-maintenance runs for one bank. Stops a bank whose size hovers at the threshold from building and dropping the same index repeatedly. Unused while the threshold is off. | 900 |
With the threshold off (the default), indexes are created inside the
transaction that creates the bank — instantly, because the bank is empty — and
dropped when the bank is deleted. Nothing inspects bank sizes, and no background
operation runs.
With a threshold set, bank creation, ingestion and import build no indexes at
all, so no request ever waits on index DDL. Instead, a write that could move a
bank across the threshold queues a background vector_index_maintenance
operation, which builds an index when a bank grows past the threshold and removes
it if the bank shrinks well below it (the gap between those two points prevents
churn at the boundary). You will see these operations in the bank's operations
list.
To reconcile without waiting for a write — after a restore, an upgrade, or an
extension switch — run:
hindsight-admin repair-bank --allThis works in both modes: with the threshold off it rebuilds any index a bank is
missing, and with one set it also drops what a bank no longer earns.
Switching extensions:
If you need to switch from one extension to another:
- Set
HINDSIGHT_API_VECTOR_EXTENSIONto your desired extension (pgvector,vchord,pgvectorscale, orscann) - If your database has existing data, you'll get an error with migration instructions (note: switching to
scannis allowed even with data — the existing index is dropped and rebuilt as ScaNN once the table has at least 10,000 embedding rows) - For empty databases, indexes will be automatically recreated on startup
Learn more:
Text Search Extension
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_TEXT_SEARCH_EXTENSION |
Text search backend: native, vchord, pg_textsearch, pgroonga, or pg_search |
native |
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE |
PostgreSQL text search dictionary used by the native backend (e.g. english, french, simple, zhparser) |
english |
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER |
ParadeDB pg_search tokenizer used when creating BM25 indexes. Empty uses ParadeDB's default tokenizer (unicode_words). |
unset |
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_FUNCTION_SCHEMA |
Schema containing ParadeDB pg_search functions (score, boolean, match). Default is paradedb; set to pgsearch (or another valid identifier) on managed PostgreSQL distributions that install functions under a custom schema. |
paradedb |
HINDSIGHT_API_BM25_MAX_QUERY_TERMS |
Cap on the number of terms in the native PostgreSQL BM25 tsquery. Long queries OR-join every normalized token, and because native ranking has no IDF and re-ranks every match, an uncapped query over common terms can scan and rank a large fraction of the bank and time out. When the query exceeds the cap, the most selective terms are kept — those with the lowest tenant-wide document frequency, read for free from pg_stats (autovacuum-maintained, no reindex) — so a blunt truncation doesn't discard the high-signal terms. 0 restores the historical uncapped behavior; the cap bounds only the native backend (other BM25 backends receive the raw query and rank via their own index-native operators). |
16 |
HINDSIGHT_API_BM25_SELECTIVE_TERMS |
When the cap above trims a query, whether to keep the most selective terms (lowest document frequency, from pg_stats) rather than the first N. true is strictly better for recall at no extra cost when stats exist; set false to opt out of the catalog read and cap by position. Ignored when the cap is 0. |
true |
HINDSIGHT_API_LLM_OUTPUT_LANGUAGE |
When set, forces every LLM-generated artifact (retain facts, consolidation observations, reflect responses) into this language. Free-form (e.g. Spanish, Japanese). |
unset |
Hindsight supports five backends for BM25 keyword retrieval:
- native — PostgreSQL's built-in full-text search (
tsvector+ GIN). Language configurable. - vchord — VectorChord BM25 (uses the
llmlingua2multilingual tokenizer). - pg_textsearch — Timescale's pg_textsearch extension. English-only.
- pgroonga — pgroonga full-text search. Multilingual / CJK out of the box.
- pg_search — ParadeDB pg_search. True BM25; the only backend that is Citus-compatible.
A bank can also use none of them: enable_text_search
switches the keyword arm off per bank, leaving pure vector search. Every setting in
this table is then unused by that bank.
To switch backends: set HINDSIGHT_API_TEXT_SEARCH_EXTENSION. With existing data, you'll get an error and migration instructions; with an empty database the columns/indexes are recreated automatically on startup.
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER only applies when HINDSIGHT_API_TEXT_SEARCH_EXTENSION=pg_search, and only when BM25 indexes are created. Changing it for an existing database requires rebuilding the pg_search indexes or recreating the database. Supported values are empty/unset, unicode_words, simple, whitespace, literal, literal_normalized, chinese_compatible, icu, jieba, source_code, chinese_lindera/lindera(chinese), japanese_lindera/lindera(japanese), korean_lindera/lindera(korean), ngram(min,max), and edge_ngram(min,max).
For non-English banks (especially CJK) and the language/extraction-language tradeoffs, see the Multilingual Support page.
LLM Provider
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_LLM_PROVIDER |
Provider: openai, openai-responses, openai-codex, chatgpt, claude-code, cursor, github-copilot, anthropic, gemini, groq, minimax, deepseek, zai, opencode-go, meta, nous, xai-oauth, fireworks, ollama, ollama-cloud, lmstudio, llamacpp, vertexai, bedrock, litellm, litellmrouter, volcano, openrouter, requesty, none |
openai |
HINDSIGHT_API_LLM_API_KEY |
API key for providers that require one; unused by github-copilot. Optional for anthropic when Workload Identity Federation is configured (see the Anthropic example below) |
- |
HINDSIGHT_API_LLM_MODEL |
Model name | gpt-5-mini |
HINDSIGHT_API_LLM_BASE_URL |
Custom LLM endpoint | Provider default |
HINDSIGHT_API_LLM_MAX_CONCURRENT |
Max concurrent LLM requests. When the cap is full, live API requests get the next free slot ahead of background jobs (async retain, consolidation) run by the in-process worker. | 32 |
HINDSIGHT_API_LLM_MAX_RETRIES |
Max retry attempts for LLM API calls | 3 |
HINDSIGHT_API_LLM_INITIAL_BACKOFF |
Initial retry backoff in seconds (exponential backoff) | 1.0 |
HINDSIGHT_API_LLM_MAX_BACKOFF |
Max retry backoff cap in seconds | 60.0 |
HINDSIGHT_API_LLM_TIMEOUT |
LLM request timeout in seconds. Honoured by every provider as a total deadline for one request, including the time spent reading a streamed response — a backend that keeps sending bytes is cut off at the deadline rather than read indefinitely. Raise it if your provider is legitimately slow; the per-operation variables below override it. | 120 |
HINDSIGHT_API_LLM_CONNECT_TIMEOUT |
Ceiling on the connect phase (TCP + TLS handshake) of an LLM request, in seconds. The read, write and pool phases keep the full HINDSIGHT_API_LLM_TIMEOUT budget; capping connect separately means an unreachable or wedged endpoint fails in seconds instead of consuming the whole request timeout. Effective value is the smaller of this and the request timeout. Set to 0 to disable the cap and use one value across all four phases. Applies to every provider whose HTTP client Hindsight constructs (all OpenAI-compatible backends, openai-responses, chatgpt, anthropic, openai-codex, xai-oauth, fireworks, nous, llamacpp); litellm/litellmrouter and gemini/vertexai own their transports and take a single total deadline instead, and claude-code/cursor/github-copilot drive a CLI rather than a socket. |
10 |
HINDSIGHT_API_LLM_HTTP_LOG_LEVEL |
Log level for the httpx and httpcore loggers. Set to DEBUG to trace each LLM request through its transport phases (connect_tcp, send_request_headers, receive_response_headers) — the way to tell a request that stalled before it was ever sent from one that was sent and never answered. Verbose; for diagnosis only. |
WARNING |
HINDSIGHT_API_LLM_REASONING_EFFORT |
Reasoning effort for providers/models that support it (for example none, low, medium, high, xhigh). Set it and the value is sent as given, whatever your model is called — which is how you control thinking-token volume on a self-hosted reasoning model (vLLM, Ollama, llama.cpp, TGI), where none is often the only value that removes the thinking block. Leave it unset and no reasoning parameter is sent at all, so each model runs at its own default effort. Honoured by openai and every OpenAI-compatible provider, openai-responses, chatgpt, openai-codex, xai, llamacpp, and litellm/litellmrouter (which translate it per target provider). The native gemini/vertexai, anthropic, claude-code and cursor providers have no reasoning-effort control and log a warning at startup if you set one — reach those models through litellm to apply it. |
Unset (model's own default) |
HINDSIGHT_API_LLM_TEMPERATURE |
Global override for the sampling temperature of internal LLM calls. Set a number in [0.0, 2.0], or none (also default/off/empty) to omit the temperature parameter entirely — required for models that reject explicit temperatures, e.g. Azure gpt-5.5, which only accepts its default value. Per-operation variables below override this. |
Per-operation defaults |
HINDSIGHT_API_LLM_TEMPERATURE_VERIFICATION |
Temperature for the startup connection check. Number in [0.0, 2.0] or none to omit. Overrides HINDSIGHT_API_LLM_TEMPERATURE. |
0.0 |
HINDSIGHT_API_LLM_TEMPERATURE_RETAIN |
Temperature for fact extraction during retain. Number in [0.0, 2.0] or none to omit. Overrides HINDSIGHT_API_LLM_TEMPERATURE. |
0.1 |
HINDSIGHT_API_LLM_TEMPERATURE_REFLECT |
Temperature for the reflect "thinking" step. Number in [0.0, 2.0] or none to omit. Overrides HINDSIGHT_API_LLM_TEMPERATURE. |
0.9 |
HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION |
Temperature for consolidation (dedup), and for the mental-model refresh's delta and retraction calls unless HINDSIGHT_API_LLM_TEMPERATURE_MENTAL_MODEL_REFRESH is set. Number in [0.0, 2.0] or none to omit. Overrides HINDSIGHT_API_LLM_TEMPERATURE. |
0.0 |
HINDSIGHT_API_LLM_TEMPERATURE_MENTAL_MODEL_REFRESH |
Temperature for the mental-model refresh's structured edit calls (delta and retraction). Number in [0.0, 2.0] or none to omit. Set it when the refresh runs its own model (HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_*) whose recommended temperature differs from consolidation's. |
Falls back to HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION |
HINDSIGHT_API_LLM_VISION |
Whether the configured LLM can read images, overriding what the provider reports about itself. Leave unset and each provider answers for itself: Anthropic and Gemini report yes, OpenAI reports yes, none reports no, and every gateway-style backend (LiteLLM, Ollama, LM Studio, OpenRouter, an OpenAI-compatible proxy) reports unknown, because its catalogue mixes vision-capable and text-only models. A retain carrying inline attachments is refused with 422 on both "no" and "unknown" — dropping an attachment silently would leave a document that looks retained with the information the caller cared about gone. Set true when you are running a vision model behind such a gateway; set false to refuse images against an endpoint that rejects them despite its model name. |
Provider decides |
HINDSIGHT_API_VLM_PROVIDER |
Provider for the vision slot — the model used only for retain chunks that carry an inline attachment. Every text-only chunk keeps using the retain LLM, so a bank whose documents mostly are text does not pay a vision model's price for all of it. Unset, attachments go to the retain LLM. | Retain LLM |
HINDSIGHT_API_VLM_API_KEY |
API key for the vision slot. | Retain LLM's |
HINDSIGHT_API_VLM_MODEL |
Model for the vision slot. This is the model HINDSIGHT_API_LLM_VISION and the 422 vision check apply to, since it is the one that will actually be handed the attachment. |
Retain LLM's |
HINDSIGHT_API_VLM_BASE_URL |
Base URL for the vision slot. Follows HINDSIGHT_API_VLM_PROVIDER's own default when that is set and this is not — it never inherits the retain provider's host, which would send the request to the wrong endpoint with the wrong key. |
Provider default |
HINDSIGHT_API_LLM_SEND_BANK_AS_USER |
Tag outbound LLM and embedding calls with user= so gateways (OpenRouter usage accounting, LiteLLM, Helicone) can attribute spend per bank. When enabled, the bank id is transmitted to the upstream provider as the end-user identifier. |
false |
HINDSIGHT_API_LLM_GROQ_SERVICE_TIER |
Groq service tier: on_demand, flex, auto |
auto |
HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER |
OpenAI service tier: flex for 50% cost savings (OpenAI Flex Processing) |
None (default) |
HINDSIGHT_API_LLM_BEDROCK_SERVICE_TIER |
Bedrock service tier: flex for 50% cost savings (best-effort inference), priority (guaranteed throughput), or reserved (provisioned capacity) |
Unset (default tier) |
HINDSIGHT_API_LLM_GEMINI_SERVICE_TIER |
Gemini service tier: flex for 50% cost savings (best-effort inference) |
Unset (default tier) |
HINDSIGHT_API_LLM_EXTRA_BODY |
JSON dict of extra request-body params (e.g. temperature, top_p, max_tokens) merged into every LLM call. Applied across the OpenAI-compatible, Fireworks, Anthropic, Gemini/VertexAI and LiteLLM (incl. Bedrock/Router) providers. Each provider merges them in its own native parameter space, so use that provider's field names (e.g. max_tokens for OpenAI/Anthropic vs max_output_tokens for Gemini). The native Ollama structured-output path is a special case — see the note below. Also useful for custom model servers (e.g. vLLM chat_template_kwargs). |
null |
HINDSIGHT_API_LLM_DEFAULT_HEADERS |
JSON dict passed as default_headers to provider SDK clients. Used by operators routing through proxies / request-tracing middleware (e.g. Cloudflare AI Gateway, Helicone, corporate proxies). Wired into the Anthropic provider, the OpenAI-compatible providers, Fireworks, Nous, the OpenAI Responses API, and the LiteLLM-backed providers (litellm, litellmrouter, bedrock); other providers can opt in. |
null |
HINDSIGHT_API_LLM_CACHE_AFFINITY |
Pin a conversation to one backend prompt cache on the OpenAI-compatible providers, Fireworks and Nous. Server-side prompt caches are per backend server, so the same conversation has to reach the same one for a cache hit. xai_conv_id sends xAI's documented x-grok-conv-id header; openai_prompt_cache_key sends OpenAI's prompt_cache_key field; auto picks one from the configured base URL host (x.ai / grok.com → xai_conv_id; native OpenAI or an openai.com host → openai_prompt_cache_key; anything else, including Azure OpenAI, → none); none sends nothing. The id is the operation's trace id, so every LLM call of one retain/reflect/consolidation run shares it. auto is an allowlist rather than a best-effort probe, so an unknown OpenAI-compatible backend (vLLM, ollama, groq, openrouter, a custom proxy) receives byte-identical requests to before; set none to disable entirely. Anthropic and Gemini/Vertex do their own caching and ignore this. |
auto |
HINDSIGHT_API_LLM_STRICT_SCHEMA |
Grammar-enforce structured output via json_schema strict: true instead of the soft "schema-in-prompt + json_object" path. Typed Pydantic response models are serialized directly into the OpenAI strict subset: every object rejects additional properties, every declared property is required, and nullable fields remain nullable. Use it with weaker self-hosted models that return prose preambles, markdown ` ``` |
|
| /* Detailed source-code truncated for AI context efficiency. */ |
# Groq (recommended for fast inference)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
# For free tier users: override to on_demand if you get service_tier errors
# export HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=on_demand
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Optional: Use Flex Processing for 50% cost savings (with variable latency)
# export HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER=flex
# OpenAI Responses API (/v1/responses)
export HINDSIGHT_API_LLM_PROVIDER=openai-responses
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-5.6
# Uses the OpenAI Responses API instead of chat/completions. Unlike
# chat/completions, it supports reasoning together with function tools, so
# reflect's tool-calling search loop can run with a real reasoning effort.
# Recommended for reasoning models (gpt-5.x, o-series) that reject
# `reasoning_effort` alongside tools on chat/completions (e.g. gpt-5.6-terra).
# export HINDSIGHT_API_LLM_REASONING_EFFORT=high
# Gemini
export HINDSIGHT_API_LLM_PROVIDER=gemini
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
# Optional: Use Gemini Flex for 50% cost savings (best-effort inference)
# export HINDSIGHT_API_LLM_GEMINI_SERVICE_TIER=flex
# Anthropic
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
# Or, with no static key: Workload Identity Federation. Leave HINDSIGHT_API_LLM_API_KEY
# unset and the Anthropic SDK trades the platform identity token for short-lived
# access tokens, refreshing them on its own.
# export ANTHROPIC_FEDERATION_RULE_ID=fdrl_xxxxxxxxxxxx
# export ANTHROPIC_ORGANIZATION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_xxxxxxxxxxxx
# export ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic/token # e.g. a projected k8s service-account token
# Vertex AI (Google Cloud - uses native genai SDK)
export HINDSIGHT_API_LLM_PROVIDER=vertexai
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-gcp-project-id
export HINDSIGHT_API_LLM_VERTEXAI_REGION=us-central1
# Optional: use ADC (gcloud auth application-default login) or provide service account key:
# export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/service-account-key.json
# Ollama (local, no API key)
export HINDSIGHT_API_LLM_PROVIDER=ollama
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
export HINDSIGHT_API_LLM_MODEL=llama3
# Ollama Cloud (hosted Ollama endpoint, requires API key)
export HINDSIGHT_API_LLM_PROVIDER=ollama-cloud
export HINDSIGHT_API_LLM_API_KEY=your-ollama-cloud-api-key
export HINDSIGHT_API_LLM_MODEL=gemma3:12b
# LM Studio (local, no API key)
export HINDSIGHT_API_LLM_PROVIDER=lmstudio
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1
export HINDSIGHT_API_LLM_MODEL=your-local-model
# llama.cpp (built-in local inference, no external server needed)
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
# No API key, base URL, or external server required.
# Auto-downloads Gemma 4 E2B (~3.5 GB GGUF) on first run.
# See "Built-in llama.cpp" section below for all configuration options.
# OpenAI-compatible endpoint (Chat Completions API, /v1/chat/completions)
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_BASE_URL=https://your-endpoint.com/v1
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_MODEL=your-model-name
# OpenAI-compatible endpoint (Responses API, /v1/responses)
# Same as above but targets /v1/responses instead of /v1/chat/completions.
export HINDSIGHT_API_LLM_PROVIDER=openai-responses
export HINDSIGHT_API_LLM_BASE_URL=https://your-endpoint.com/v1
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_MODEL=your-model-name
# OpenAI Codex (ChatGPT Plus/Pro subscription - uses OAuth, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
export HINDSIGHT_API_LLM_MODEL=gpt-5.6-luna
# No API key needed - uses OAuth tokens from ~/.codex/auth.json
# For long-running services, set CODEX_HOME to a dedicated auth directory so
# Hindsight doesn't share (and lose) its refresh token with another Codex process.
# See Models docs → "Isolating Codex auth for long-running services".
# export CODEX_HOME=/var/lib/hindsight/codex
# Per-provider override of CODEX_HOME. Set it (with the indexed
# HINDSIGHT_API_LLM_<n>_CODEX_HOME below) to run two independently authorized
# ChatGPT profiles in one process.
# export HINDSIGHT_API_LLM_CODEX_HOME=/var/lib/hindsight/codex-a
# Claude Code (Claude Pro/Max subscription - uses OAuth)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-5-20250929
# No API key needed - uses `claude auth login` credentials.
# Set HINDSIGHT_API_LLM_API_KEY to a token from `claude setup-token` to authenticate with a key instead.
# Cursor (Cursor subscription - drives the cursor-agent CLI in headless mode)
export HINDSIGHT_API_LLM_PROVIDER=cursor
export HINDSIGHT_API_LLM_MODEL=auto # or any id from `cursor-agent --list-models`
# No API key needed - uses `cursor-agent login` credentials.
# Set HINDSIGHT_API_LLM_API_KEY (or CURSOR_API_KEY) to authenticate with a key instead.
# An agent CLI turn takes 15-30s, well past the 30s reflect default, so raise the
# timeouts or reflect burns its first iteration on a timeout retry.
export HINDSIGHT_API_LLM_TIMEOUT=300
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=180
# The CLI exposes no response-format or tool-definition flags, so Hindsight emulates
# structured output and tool calling through the prompt and parses the JSON back. That
# is less reliable than a native schema: prefer a stronger named model over `auto` for
# reflect, which is the operation that leans hardest on tool calls.
# Volcano Engine (ByteDance - OpenAI-compatible)
export HINDSIGHT_API_LLM_PROVIDER=volcano
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
export HINDSIGHT_API_LLM_MODEL=doubao-pro-32k
# OpenRouter (OpenAI-compatible, access 100+ models)
export HINDSIGHT_API_LLM_PROVIDER=openrouter
export HINDSIGHT_API_LLM_API_KEY=your-openrouter-api-key
export HINDSIGHT_API_LLM_MODEL=qwen/qwen3.5-9b
# Structured-output calls (fact extraction, consolidation) send
# provider.require_parameters=true, so OpenRouter only routes them to upstreams
# that support response_format. Any "provider" keys you set in
# HINDSIGHT_API_LLM_EXTRA_BODY (e.g. "only") are kept and take precedence.
# That also means every parameter sent must be supported, temperature included.
# Models that reject temperature (e.g. anthropic/claude-haiku-5.5) then fail with
# "No endpoints found that can handle the requested parameters". Fix: omit it.
# export HINDSIGHT_API_LLM_TEMPERATURE=none
# Reflect forces tool calls: tool_choice "required" after its first searches and a
# named tool on the forced steps. Some upstreams accept only "auto" and fail those
# turns with "No endpoints found that support the provided 'tool_choice' value"
# (reported for several gpt-oss-120b hosts, #5832). Pin ones that support it with
# HINDSIGHT_API_LLM_EXTRA_BODY='{"provider": {"only": ["deepinfra", "cerebras"]}}'.
# Requesty (OpenAI-compatible gateway)
export HINDSIGHT_API_LLM_PROVIDER=requesty
export HINDSIGHT_API_LLM_API_KEY=your-requesty-api-key
export HINDSIGHT_API_LLM_MODEL=openai/gpt-4o-mini
# DeepSeek (OpenAI-compatible, https://api.deepseek.com)
export HINDSIGHT_API_LLM_PROVIDER=deepseek
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
# Notes:
# - `deepseek-v4-flash` defaults to thinking mode at the API level (treated as
# `deepseek-reasoner`). Hindsight handles this transparently; the reflect
# agent will not crash with "deepseek-reasoner does not support this
# tool_choice".
# - Use `deepseek-v4-pro` for the higher-quality reasoning route.
# - Use `deepseek-chat` for the non-thinking alias (faster, cheaper).
# z.ai (Zhipu GLM series, OpenAI-compatible, https://z.ai)
export HINDSIGHT_API_LLM_PROVIDER=zai
export HINDSIGHT_API_LLM_API_KEY=your-zai-api-key
export HINDSIGHT_API_LLM_MODEL=glm-4.5-flash # or glm-4.5-air for the paid tier
# Default base_url: https://api.z.ai/api/coding/paas/v4 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# opencode-go (OpenAI-compatible)
export HINDSIGHT_API_LLM_PROVIDER=opencode-go
export HINDSIGHT_API_LLM_API_KEY=your-opencode-go-api-key
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
# Default base_url: https://opencode.ai/zen/go/v1 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# Meta Model API (Muse Spark, OpenAI-compatible, https://ai.developer.meta.com)
export HINDSIGHT_API_LLM_PROVIDER=meta
export HINDSIGHT_API_LLM_API_KEY=your-meta-model-api-key
export HINDSIGHT_API_LLM_MODEL=muse-spark-1.3
# Default base_url: https://api.meta.ai/v1 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# Muse Spark always reasons: leave HINDSIGHT_API_LLM_REASONING_EFFORT unset or set it to
# minimal/low/medium/high/xhigh. "none" is rejected with HTTP 400. Reasoning tokens are
# billed against the output budget, so keep the per-operation max-token limits generous.
# Muse Spark reasons before every reply, so calls are slow: reflect's 30s default
# deadline is not enough for its final synthesis and the call fails after retries.
# Raise it (and the global deadline) when using this provider:
export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=300
export HINDSIGHT_API_LLM_TIMEOUT=300
# Nous Portal (OpenAI-compatible; no API key — uses your `hermes portal` login)
export HINDSIGHT_API_LLM_PROVIDER=nous
export HINDSIGHT_API_LLM_MODEL=deepseek/deepseek-v4-flash
# No API key needed — reads a rotating JWT from ~/.hermes/auth.json (run `hermes portal` first).
# Default base_url: https://inference-api.nousresearch.com/v1 (override with HINDSIGHT_API_LLM_BASE_URL if needed)
# See the "Nous Portal Setup" section in the Models guide for the login flow.
# SuperGrok subscription (device-code OAuth; no API key). This is the subscription
# lane, NOT xAI API support — for API-key access to api.x.ai use
# HINDSIGHT_API_LLM_PROVIDER=openai with HINDSIGHT_API_LLM_BASE_URL=https://api.x.ai/v1.
export HINDSIGHT_API_LLM_PROVIDER=xai-oauth
export HINDSIGHT_API_LLM_MODEL=grok-4.5
# No API key needed — reads an OAuth grant from ~/.hindsight/xai_oauth.json.
# Log in once: python -m hindsight_api.engine.providers.xai_oauth_auth login
# Default base_url: https://api.x.ai/v1
# See the "SuperGrok Subscription Setup" section in the Models guide for the login flow.
# AWS Bedrock (native support - no API key needed, uses AWS credentials)
export HINDSIGHT_API_LLM_PROVIDER=bedrock
export HINDSIGHT_API_LLM_MODEL=us.amazon.nova-2-lite-v1:0
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
export AWS_REGION_NAME=us-east-1
# Optional: Use Flex tier for 50% cost savings (with variable latency)
# export HINDSIGHT_API_LLM_BEDROCK_SERVICE_TIER=flex
# LiteLLM (100+ providers via LiteLLM SDK)
# Azure OpenAI via LiteLLM
export HINDSIGHT_API_LLM_PROVIDER=litellm
export HINDSIGHT_API_LLM_API_KEY=your-azure-api-key
export HINDSIGHT_API_LLM_MODEL=azure/gpt-4o
# Together AI via LiteLLM
export HINDSIGHT_API_LLM_PROVIDER=litellm
export HINDSIGHT_API_LLM_API_KEY=your-together-api-key
export HINDSIGHT_API_LLM_MODEL=together_ai/meta-llama/Llama-3-70b-chat-hf
# No LLM (chunk storage + semantic search only, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=none
# Retain automatically uses chunks mode (no fact extraction)
# Recall works normally (semantic search, BM25, graph retrieval)
# Reflect returns HTTP 400 (requires an LLM)
# Consolidation/observations are disabled/* Detailed source-code truncated for AI context efficiency. */
export HINDSIGHT_API_LLM_PROVIDER=litellmrouter
export HINDSIGHT_API_LLM_LITELLMROUTER_CONFIG='{
"model_list": [
{"model_name": "default", "litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-..."}},
{"model_name": "fallback", "litellm_params": {"model": "anthropic/claude-sonnet-4-5", "api_key": "sk-ant-..."}}
],
"fallbacks": [{"default": ["fallback"]}]
}'/* Detailed source-code truncated for AI context efficiency. */
# Primary OpenAI, failover to Groq then Anthropic
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-...
export HINDSIGHT_API_LLM_1_PROVIDER=groq
export HINDSIGHT_API_LLM_1_API_KEY=gsk-...
export HINDSIGHT_API_LLM_2_PROVIDER=anthropic
export HINDSIGHT_API_LLM_2_API_KEY=sk-ant-...
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "failover"}'
# Weighted round-robin: serve the primary 3x as often as member 1
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "round-robin", "weights": [3, 1]}'
# Failover, re-probing a failed member after 5 minutes (then 10, 20, ... up to 1h)
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "failover", "cooldown_seconds": 300}'Metadata routing
{"mode": "metadata"} sends each retained item to the chain member its own metadata selects, so one deployment can extract different documents with different models:
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-...
export HINDSIGHT_API_LLM_1_PROVIDER=ollama
export HINDSIGHT_API_LLM_1_MODEL=qwen3:8b
export HINDSIGHT_API_LLM_STRATEGY='{
"mode": "metadata",
"routes": [{"key": "classification", "value": "sensitive", "member": 1}]
}'/* Detailed source-code truncated for AI context efficiency. */
# Minimal setup (auto-downloads model, uses GPU)
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
# Custom model with tuning
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2
export HINDSIGHT_API_LLAMACPP_MODEL_PATH=~/.hindsight/models/my-model.gguf
export HINDSIGHT_API_LLAMACPP_CONTEXT_SIZE=16384
export HINDSIGHT_API_LLAMACPP_NO_GRAMMAR=true # faster, less reliable JSON
export HINDSIGHT_API_LLAMACPP_EXTRA_ARGS="--n_threads 8"/* Detailed source-code truncated for AI context efficiency. */
# Default LLM (used as fallback)
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Use GPT-4o for retain (strong structured output)
export HINDSIGHT_API_RETAIN_LLM_MODEL=gpt-4o
# Use faster/cheaper model for reflect
export HINDSIGHT_API_REFLECT_LLM_PROVIDER=groq
export HINDSIGHT_API_REFLECT_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_REFLECT_LLM_MODEL=llama-3.3-70b-versatileExample: Tuning Retry Behavior for Rate-Limited APIs
# For Anthropic with tight rate limits (10k output tokens/minute)
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
# Reduce concurrent requests for retain to avoid rate limits
export HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT=3
# Fail faster with fewer retries
export HINDSIGHT_API_RETAIN_LLM_MAX_RETRIES=3
# Or increase backoff times to wait out rate limit windows
export HINDSIGHT_API_RETAIN_LLM_INITIAL_BACKOFF=2.0 # Start at 2s instead of 1s
export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1min/* Detailed source-code truncated for AI context efficiency. */
pip install 'hindsight-api-slim[local-onnx]'
# or, in this repository:
uv sync --project hindsight-api-slim --extra local-onnxCUDA execution is opt-in. Set HINDSIGHT_API_EMBEDDINGS_ONNX_DEVICE=cuda only in an environment that has a compatible onnxruntime-gpu wheel and CUDA/cuDNN runtime. The official images intentionally keep the CPU runtime and do not grow when this feature is unused. For a Docker deployment, use the ready-to-build recipe at docker/docker-compose/cuda-onnx/, which installs the GPU wheel into a private image based on the slim image:
docker compose -f docker/docker-compose/cuda-onnx/docker-compose.yaml up --buildThe recipe accepts BASE_IMAGE, ONNXRUNTIME_GPU_VERSION, and HINDSIGHT_API_EMBEDDINGS_ONNX_CUDA_DEVICE_ID for release, runtime, and device selection. Hindsight fails startup when CUDA is requested but the provider is unavailable or the initialized session does not activate it; it does not silently fall back to CPU in that mode. The recipe installs CUDA/cuDNN libraries in the private image; the host needs a compatible NVIDIA driver and GPU passthrough. Individual operators may still run on CPU through normal graph partitioning. See the recipe README for dependency isolation, unreleased-checkout builds, and real GPU tests.
You can either let Hindsight download the model from Hugging Face at startup by setting HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID, or pre-download the ONNX graph and tokenizer files under the Hindsight repository root.
cd /path/to/hindsight
mkdir -p models
MODEL_ID=intfloat/multilingual-e5-small
MODEL_DIR=models/intfloat__multilingual-e5-small
uv run --project hindsight-api-slim --extra local-onnx python - <<'PY'
import os
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=os.environ["MODEL_ID"],
local_dir=os.environ["MODEL_DIR"],
allow_patterns=[
"onnx/model.onnx",
"onnx/model.onnx_data",
"*.json",
"*.txt",
"*.model",
],
)
PYThen start Hindsight with paths relative to the repository root:
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_PATH=./models/intfloat__multilingual-e5-small/onnx/model.onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_TOKENIZER_NAME_OR_PATH=./models/intfloat__multilingual-e5-small
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX="query: "
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX="passage: "For Docker deployments, mount the same model directory and use container paths:
services:
hindsight:
volumes:
- ./models:/app/models:ro
environment:
HINDSIGHT_API_EMBEDDINGS_PROVIDER: onnx
HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_PATH: /app/models/intfloat__multilingual-e5-small/onnx/model.onnx
HINDSIGHT_API_EMBEDDINGS_ONNX_TOKENIZER_NAME_OR_PATH: /app/models/intfloat__multilingual-e5-small
HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS: "384"
HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX: "query: "
HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX: "passage: "Model-specific examples:
# sentence-transformers/all-MiniLM-L6-v2: 384 dimensions, no E5 prefixes
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX=""
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX=""
# intfloat/multilingual-e5-small: 384 dimensions, keep E5 prefixes
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=intfloat/multilingual-e5-small
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX="query: "
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX="passage: "
# sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2: 384 dimensions, no E5 prefixes
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=384
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX=""
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX=""
# BAAI/bge-m3: 1024 dimensions, no E5 prefixes; keep onnx/model.onnx_data next to model.onnx
export HINDSIGHT_API_EMBEDDINGS_ONNX_MODEL_ID=BAAI/bge-m3
export HINDSIGHT_API_EMBEDDINGS_ONNX_DIMENSIONS=1024
export HINDSIGHT_API_EMBEDDINGS_ONNX_QUERY_PREFIX=""
export HINDSIGHT_API_EMBEDDINGS_ONNX_PASSAGE_PREFIX=""/* Detailed source-code truncated for AI context efficiency. */
# Local (default) - uses SentenceTransformers
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
# Local with custom model requiring trust_remote_code
# WARNING: Only enable trust_remote_code for models you trust (security risk)
# export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=your-custom-model
# export HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE=true
# OpenAI - cloud-based embeddings
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=*** # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dimensions by default
# export HINDSIGHT_API_EMBEDDINGS_OPENAI_DIMENSIONS=384 # optional reduced output size
# OpenAI Codex OAuth - uses existing ChatGPT/Codex login, no API key needed
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai-codex
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dimensions by default
# export HINDSIGHT_API_EMBEDDINGS_OPENAI_DIMENSIONS=384 # optional reduced output size
# Azure OpenAI - embeddings via Azure endpoint
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=your-azure-api-key
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
export HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment
# TEI - HuggingFace Text Embeddings Inference (recommended for production)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
# OpenRouter - access 100+ embedding models
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openrouter
export HINDSIGHT_API_EMBEDDINGS_OPENROUTER_API_KEY=your-openrouter-api-key # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_OPENROUTER_MODEL=perplexity/pplx-embed-v1-0.6b
# ZeroEntropy - zembed-1
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=zeroentropy
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_MODEL=zembed-1
export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_DIMENSIONS=1280
# export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_ENCODING_FORMAT=base64 # optional
# export HINDSIGHT_API_EMBEDDINGS_ZEROENTROPY_LATENCY=fast # optional
# NVIDIA NIM - asymmetric retrieval embeddings (sends input_type=query/passage)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=nvidia
export HINDSIGHT_API_EMBEDDINGS_NVIDIA_API_KEY=nvapi-your-key
# export HINDSIGHT_API_EMBEDDINGS_NVIDIA_MODEL=nvidia/llama-nemotron-embed-vl-1b-v2 # default
# export HINDSIGHT_API_EMBEDDINGS_NVIDIA_DIMENSIONS=1024 # default
# Cohere - cloud-based embeddings
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions
# Optional: override output dimensions (for Matryoshka-capable models)
# export HINDSIGHT_API_EMBEDDINGS_COHERE_OUTPUT_DIMENSIONS=512
# Azure-hosted Cohere - embeddings via custom endpoint
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-azure-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
export HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
# LiteLLM proxy - unified gateway for multiple providers
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
export HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY=your-litellm-key # optional
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small # or cohere/embed-english-v3.0
# export HINDSIGHT_API_EMBEDDINGS_LITELLM_DIMENSIONS=1536 # optional: declare the width, skip the startup probe
# Google - Gemini API (API key auth)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=google
export HINDSIGHT_API_EMBEDDINGS_GEMINI_API_KEY=xxxxxxxxxxxx # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL=gemini-embedding-001 # 768 dimensions (default)
# export HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY=768 # configurable: 256, 512, 768, 1024, etc.
# Google - Vertex AI auth (auto-detected when project ID is set)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=google
export HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL=gemini-embedding-001
export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_PROJECT_ID=your-gcp-project-id # falls back to HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID
# export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_REGION=us-central1 # falls back to HINDSIGHT_API_LLM_VERTEXAI_REGION
# export HINDSIGHT_API_EMBEDDINGS_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/key.json # falls back to LLM config, or uses ADC
# LiteLLM SDK - direct API access without proxy server (recommended)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_KEY=your-provider-api-key
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=cohere/embed-english-v3.0
# Optional: request a specific output dimension when the provider supports it
# export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS=768
# Optional (any provider): the input-token limit inputs are truncated to.
# Defaults to 8192; set it to your model's real limit, or 0 to send text uncapped
# export HINDSIGHT_API_EMBEDDINGS_MAX_INPUT_TOKENS=8192
# Supported LiteLLM SDK embedding providers:
# - cohere/embed-english-v3.0 (1024 dimensions)
# - openai/text-embedding-3-small (1536 dimensions)
# - together_ai/togethercomputer/m2-bert-80M-8k-retrieval
# - huggingface/sentence-transformers/all-MiniLM-L6-v2
# - voyage/voyage-2/* Detailed source-code truncated for AI context efficiency. */
# Primary: a self-hosted reranker that is not always up.
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://workstation:8081
# Member 1: a hosted reranker to fall back on.
export HINDSIGHT_API_RERANKER_1_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_1_COHERE_API_KEY=...
# Member 2: last resort — no reranking, keep the fusion order rather than fail.
export HINDSIGHT_API_RERANKER_2_PROVIDER=rrfEnding the chain with rrf makes recall fail open: results come back in the order
the retrieval stages produced, exactly as if no reranker were configured, instead of
the request failing. Without an rrf member (or with every member down) the error still
surfaces, which is the default behaviour.
Notes:
- Members are tried in order on every request; there is no circuit breaker, so a member
that is down costs its own timeout on each request before the next one is tried. Keep
the timeouts of an unreliable primary short (HINDSIGHT_API_RERANKER_TEI_HTTP_TIMEOUT
and the per-provider*_TIMEOUTsettings). - A member that fails to initialize at startup is logged, not fatal — it is retried on
the next request that reaches it. - Scores are not comparable across providers (some return calibrated
[0, 1]relevance,
others logits), so a request served by a fallback member can score differently from one
served by the primary. Failovers are logged atWARNING. - The indexed members are credential fields — never returned by the bank-config API, and
server-level only (not per-bank configurable).
:::tip Sizing a TEI reranker
TEI reserves one slot per text in a rerank request and rejects the request outright
once its pool is full, so start it with at leastHINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT × HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE
slots — 1024 for the defaults above, versus TEI's own default of 512:
text-embeddings-router --max-concurrent-requests 2048 ...Below that threshold, heavy recall traffic makes the reranker reject work it has the
capacity to do. Rejections are retried with backoff, so an undersized pool shows up as
slower recall rather than errors.
:::
# Local (default) - uses SentenceTransformers CrossEncoder
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
# Local with custom model requiring trust_remote_code (e.g., jina-reranker-v2)
# WARNING: Only enable trust_remote_code for models you trust (security risk)
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=jinaai/jina-reranker-v2-base-multilingual
export HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE=true
# TEI - for high-performance inference
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
# OpenRouter - access reranking models via OpenRouter
export HINDSIGHT_API_RERANKER_PROVIDER=openrouter
export HINDSIGHT_API_RERANKER_OPENROUTER_API_KEY=your-openrouter-api-key # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_RERANKER_OPENROUTER_MODEL=cohere/rerank-v3.5
# Cohere - cloud-based reranking
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
# Any Cohere-compatible /rerank endpoint (Azure AI Foundry, Jina, Voyage, self-hosted BGE, etc.)
#
# Setting HINDSIGHT_API_RERANKER_COHERE_BASE_URL switches the `cohere` provider
# off the Cohere SDK and onto a plain HTTP client that speaks the standard
# Cohere rerank wire format:
# Request: POST {base_url} (or {base_url}/rerank, depending on host)
# Authorization: Bearer <api_key>
# {"model": "...", "query": "...", "documents": [...], "return_documents": false}
# Response: {"results": [{"index": 0, "relevance_score": 0.9}, ...]}
#
# Any service implementing this contract works here. For Azure AI Foundry the
# base_url is the full invoke URL; for SiliconFlow you can also use the
# dedicated `siliconflow` provider below.
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0 # whatever model the endpoint serves
export HINDSIGHT_API_RERANKER_COHERE_BASE_URL=https://your-cohere-compatible-endpoint.com
# ZeroEntropy - cloud-based reranking (state-of-the-art accuracy)
export HINDSIGHT_API_RERANKER_PROVIDER=zeroentropy
export HINDSIGHT_API_RERANKER_ZEROENTROPY_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_ZEROENTROPY_MODEL=zerank-2 # or zerank-2-small
# export HINDSIGHT_API_RERANKER_ZEROENTROPY_BASE_URL=https://your-custom-endpoint.com # optional
# SiliconFlow - cloud reranking via SiliconFlow's Cohere-compatible /rerank endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=siliconflow
export HINDSIGHT_API_RERANKER_SILICONFLOW_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_SILICONFLOW_MODEL=BAAI/bge-reranker-v2-m3
# export HINDSIGHT_API_RERANKER_SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1 # default
# NVIDIA NIM - native reranking API (POST {base}/retrieval/{model}/reranking)
export HINDSIGHT_API_RERANKER_PROVIDER=nvidia
export HINDSIGHT_API_RERANKER_NVIDIA_API_KEY=nvapi-your-key
# export HINDSIGHT_API_RERANKER_NVIDIA_MODEL=nvidia/llama-nemotron-rerank-vl-1b-v2 # default
# TypeSafe - relevance judged by a typed-decision model (Jev), not a /rerank endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=typesafe
export HINDSIGHT_API_RERANKER_TYPESAFE_API_KEY=your-api-key
# export HINDSIGHT_API_RERANKER_TYPESAFE_MODEL=jev-latest # default
# export HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES=true # also prune irrelevant candidates
# Alibaba Cloud DashScope - qwen3-rerank via Cohere-compatible /reranks endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=alibaba
export HINDSIGHT_API_RERANKER_ALIBABA_API_KEY=your-dashscope-api-key # or set DASHSCOPE_API_KEY
export HINDSIGHT_API_RERANKER_ALIBABA_MODEL=qwen3-rerank # default, can omit
# LiteLLM proxy - unified gateway for multiple reranking providers (requires running LiteLLM proxy server)
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
export HINDSIGHT_API_RERANKER_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_RERANKER_LITELLM_API_KEY=your-litellm-key # optional
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0 # or voyage/rerank-2, together_ai/...
# LiteLLM SDK - direct API access without proxy (recommended for simplicity)
export HINDSIGHT_API_RERANKER_PROVIDER=litellm-sdk
export HINDSIGHT_API_RERANKER_LITELLM_SDK_API_KEY=your-deepinfra-api-key
export HINDSIGHT_API_RERANKER_LITELLM_SDK_MODEL=deepinfra/Qwen3-reranker-8B # or cohere/rerank-english-v3.0, etc.
# Google Discovery Engine - cloud-based semantic reranking
export HINDSIGHT_API_RERANKER_PROVIDER=google
export HINDSIGHT_API_RERANKER_GOOGLE_PROJECT_ID=your-gcp-project-id
export HINDSIGHT_API_RERANKER_GOOGLE_SERVICE_ACCOUNT_KEY=/path/to/service-account.json # optional, uses ADC if unset
export HINDSIGHT_API_RERANKER_GOOGLE_MODEL=semantic-ranker-default-004 # or semantic-ranker-fast-004
# Jina MLX - Apple Silicon native reranking (no GPU/cloud required)
# Model (~1.2 GB) is downloaded automatically from HuggingFace Hub on first use.
export HINDSIGHT_API_RERANKER_PROVIDER=jina-mlx/* Detailed source-code truncated for AI context efficiency. */
uvx --from "optimum-onnx[onnxruntime]" optimum-cli export onnx \
--model nlpai-lab/KURE-Reranker-nano --task text-classification ./models/kure-reranker-nano
export HINDSIGHT_API_RERANKER_PROVIDER=onnx
export HINDSIGHT_API_RERANKER_ONNX_MODEL_PATH=./models/kure-reranker-nano/model.onnx
export HINDSIGHT_API_RERANKER_ONNX_TOKENIZER_NAME_OR_PATH=./models/kure-reranker-nano/* Detailed source-code truncated for AI context efficiency. */
export HINDSIGHT_API_DECISION_MODEL_PROVIDER=typesafe
export HINDSIGHT_API_DECISION_MODEL_API_KEY=your-typesafe-key
export HINDSIGHT_API_REFLECT_DECISION_MODEL_MODEL=jev-preview # reflect only
export HINDSIGHT_API_CONSOLIDATION_DECISION_MODEL_PROVIDER=openai # consolidation on OpenAI
export HINDSIGHT_API_CONSOLIDATION_DECISION_MODEL_API_KEY=your-openai-key/* Detailed source-code truncated for AI context efficiency. */
# Enable the built-in API key authentication
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
export HINDSIGHT_API_TENANT_API_KEY=your-secret-api-keyWhen enabled, all requests must include the API key in the Authorization header:
curl -H "Authorization: Bearer your-secret-api-key" \
http://localhost:8888/v1/default/banksRequests without a valid API key receive a 401 Unauthorized response.
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_TENANT_EXTENSION |
Dotted path to the loaded tenant extension. Set to hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension to require an API key on every request. |
(none; auth disabled) |
HINDSIGHT_API_TENANT_API_KEY |
Shared API key checked by the built-in API-key extension. Sent by clients as Authorization: Bearer <key>. |
(none) |
HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS |
Comma-separated request headers copied into RequestContext.extra_headers for extensions to read, matched case-insensitively. Empty means extensions see only the Authorization header. |
(none) |
If you are enabling Memory Defense, see docs/developer/memory-defense/ for the policy schema, detector catalog, and audit trail.
Forwarding extra headers to extensions
A custom TenantExtension normally only sees the Authorization header, as RequestContext.api_key. That is not enough when the bearer token identifies the proxy rather than the caller — for example behind an authenticating gateway that presents one shared service identity to Hindsight, and carries the per-caller identity in a second header.
List those headers to have them forwarded:
export HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertion,x-request-origin/* Detailed source-code truncated for AI context efficiency. */
{
"content": [
{"type": "text", "text": "To reset the VPN, click the button shown:"},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}},
{"type": "text", "text": "...then reconnect."},
{"type": "file", "source": {"type": "base64", "media_type": "application/pdf", "data": "..."},
"filename": "escalation-policy.pdf"}
]
}/* Detailed source-code truncated for AI context efficiency. */
# Fireworks batch retain (50% cost savings, async only)
export HINDSIGHT_API_RETAIN_LLM_PROVIDER=fireworks
export HINDSIGHT_API_RETAIN_LLM_API_KEY=fw_xxxxxxxxxxxx
export HINDSIGHT_API_RETAIN_LLM_MODEL=accounts/fireworks/models/llama-v3p1-8b-instruct
export HINDSIGHT_API_FIREWORKS_ACCOUNT_ID=your-account-id
export HINDSIGHT_API_RETAIN_BATCH_ENABLED=true/* Detailed source-code truncated for AI context efficiency. */
export HINDSIGHT_API_STORE_DOCUMENT_TEXT=false/* Detailed source-code truncated for AI context efficiency. */
export HINDSIGHT_API_RETAIN_MISSION="Focus on technical decisions, architecture choices, and team member expertise. Deprioritize social or personal information."HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbose — more detail per fact
Use when you need richer facts with full context, relationships, and verbosity. Slower and uses more tokens than concise.
HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbatim — store chunks as-is
Each chunk is stored as a single memory unit with its original text preserved exactly — no summarization or rewriting. The LLM still runs to extract entities, temporal information, and location so the chunk is fully indexed and retrievable. Useful for RAG-style indexing, document ingestion pipelines, or benchmarks where you want the original text in memory rather than LLM-generated summaries.
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbatimretain_strategies / retain_default_strategy — per-call extraction strategy
Named strategies let you ingest different content types into the same bank using different extraction settings. A strategy is a set of hierarchical field overrides applied on top of the resolved bank config.
Any field in the hierarchical config can be overridden per strategy, including retain_extraction_mode, retain_chunk_size, retain_structured_chunk_size, entity_labels, entities_allow_free_form, retain_mission, etc.
Configure strategies via the bank config API:
{
"retain_default_strategy": "conversations",
"retain_strategies": {
"conversations": {
"retain_extraction_mode": "concise",
"retain_chunk_size": 3000,
"retain_structured_chunk_size": 12000
},
"documents": {
"retain_extraction_mode": "chunks",
"retain_chunk_size": 800,
"entity_labels": null,
"entities_allow_free_form": false
}
}
}Then specify the strategy at retain time:
If no strategy is specified in a retain call, retain_default_strategy is used. If neither is set, the bank/global config applies directly.
HINDSIGHT_API_RETAIN_EXTRACTION_MODE=chunks — zero LLM cost
Each chunk is stored as-is with no LLM call whatsoever. No entity extraction, no temporal indexing — only embeddings are generated for semantic search. User-provided entities passed via RetainContent.entities are the sole source of entity data. Use when ingestion speed and cost matter more than structured metadata.
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=chunksHINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom + HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS — full control
Replaces the built-in selectivity rules entirely. The structural parts of the prompt (output format, temporal handling, coreference resolution) remain intact — only the extraction guidelines are replaced.
Use this when retain_mission isn't sufficient and you need strict inclusion/exclusion logic.
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
export HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS="ONLY extract facts that are:
✅ Technical decisions and their rationale
✅ Architecture patterns and design choices
✅ Performance metrics and benchmarks
DO NOT extract:
❌ Greetings or social conversation
❌ Process chatter (\"let me check\", \"one moment\")
❌ Anything that would not be useful in 6 months"/* Detailed source-code truncated for AI context efficiency. */
# Server default: try iris first, fall back to markitdown if iris fails
export HINDSIGHT_API_FILE_PARSER=iris,markitdown
# Restrict what clients may request (optional — defaults to all registered parsers)
export HINDSIGHT_API_FILE_PARSER_ALLOWLIST=markitdown,iris// Per-request override (in the JSON body of the file retain endpoint)
{
"parser": "iris",
"files_metadata": [
{ "document_id": "report" },
{ "document_id": "fallback_doc", "parser": ["iris", "markitdown"] }
]
}/* Detailed source-code truncated for AI context efficiency. */
# Configure a dedicated OpenAI-compatible OCR/vision endpoint for MarkItDown OCR
export HINDSIGHT_API_FILE_PARSER=markitdown
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_ENABLED=true
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_API_KEY=your-vision-api-key
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_BASE_URL=https://vision.example/v1
export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_MODEL=ocr-or-vision-model
# Optional custom headers for proxies / request tracing
# export HINDSIGHT_API_FILE_PARSER_MARKITDOWN_OCR_DEFAULT_HEADERS='{"X-Component-Id":"hindsight-ocr"}'Parser: iris
Cloud-based extraction via Vectorize Iris. Higher quality extraction for complex documents, powered by a remote AI service.
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN |
Vectorize API token | — |
HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID |
Vectorize organization ID | — |
Supported formats: PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, JPEG, PNG, GIF, BMP, TIFF, WEBP), HTML, TXT, MD, CSV.
# Use iris as the only parser
export HINDSIGHT_API_FILE_PARSER=iris
export HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN=your-vectorize-token
export HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID=your-org-id
# Or: try iris first, fall back to markitdown if iris fails or rejects the file type
export HINDSIGHT_API_FILE_PARSER=iris,markitdownParser: llama_parse
Cloud-based extraction via LlamaParse (LlamaIndex). Strong extraction for complex layouts — tables, charts, multi-column PDFs.
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY |
LlamaCloud API key (typically starts with llx-) |
— |
Supported formats: PDF, DOCX, PPTX, XLSX, HTML, EPUB, RTF, TXT, and many more — see the LlamaParse docs for the full list.
# Use llama_parse as the only parser
export HINDSIGHT_API_FILE_PARSER=llama_parse
export HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY=llx-your-api-key
# Or: try llama_parse first, fall back to markitdown
export HINDSIGHT_API_FILE_PARSER=llama_parse,markitdown# Increase batch limits for large file imports
export HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE=20
export HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB=500
# Keep files after processing (useful for debugging or re-processing)
export HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN=falseFile Storage
Files uploaded via the file retain API are stored in an object storage backend before conversion. Choose the backend that fits your infrastructure.
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_FILE_STORAGE_TYPE |
Storage backend: native, s3, gcs, or azure |
native |
HINDSIGHT_API_FILE_STORAGE_EXTENSION |
module.path:ClassName naming your own FileStorage implementation, used instead of the built-in backends. Every other HINDSIGHT_API_FILE_STORAGE_* variable is passed to it as a lowercased config dict. Implement delete_prefix too, or deleting a bank leaves that bank's stored files in your backend. |
unset |
Native (PostgreSQL)
Files are stored as BYTEA in the file_storage table. No additional infrastructure required. Suitable for development and small deployments.
# Native storage is the default — no additional configuration needed
export HINDSIGHT_API_FILE_STORAGE_TYPE=nativeS3 / S3-Compatible
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_FILE_STORAGE_S3_BUCKET |
S3 bucket name | - |
HINDSIGHT_API_FILE_STORAGE_S3_REGION |
AWS region | - |
HINDSIGHT_API_FILE_STORAGE_S3_ENDPOINT |
Custom endpoint URL (for S3-compatible stores like MinIO, Cloudflare R2, Tigris) | AWS default |
HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID |
AWS access key ID | - |
HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY |
AWS secret access key | - |
For S3-compatible providers that don't expose AWS-style regions (MinIO, Cloudflare R2, Tigris), set HINDSIGHT_API_FILE_STORAGE_S3_REGION=auto. The value is required for SigV4 request signing but is ignored by the service.
# AWS S3
export HINDSIGHT_API_FILE_STORAGE_TYPE=s3
export HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-hindsight-files
export HINDSIGHT_API_FILE_STORAGE_S3_REGION=us-east-1
export HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# S3-compatible (MinIO, Cloudflare R2, etc.)
export HINDSIGHT_API_FILE_STORAGE_TYPE=s3
export HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-bucket
export HINDSIGHT_API_FILE_STORAGE_S3_REGION=auto
export HINDSIGHT_API_FILE_STORAGE_S3_ENDPOINT=https://your-minio.example.com
export HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=minioadmin
export HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=minioadmin
# Tigris (S3-compatible, single global endpoint)
export HINDSIGHT_API_FILE_STORAGE_TYPE=s3
export HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-hindsight-bucket
export HINDSIGHT_API_FILE_STORAGE_S3_REGION=auto
export HINDSIGHT_API_FILE_STORAGE_S3_ENDPOINT=https://t3.storage.dev
export HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=tid_your_access_key
export HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=tsec_your_secret_keyGoogle Cloud Storage
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_FILE_STORAGE_GCS_BUCKET |
GCS bucket name | - |
HINDSIGHT_API_FILE_STORAGE_GCS_SERVICE_ACCOUNT_KEY |
Path to service account JSON key file | ADC if not set |
export HINDSIGHT_API_FILE_STORAGE_TYPE=gcs
export HINDSIGHT_API_FILE_STORAGE_GCS_BUCKET=my-hindsight-files
# Optional: use service account key file (otherwise falls back to ADC)
export HINDSIGHT_API_FILE_STORAGE_GCS_SERVICE_ACCOUNT_KEY=/path/to/key.jsonAzure Blob Storage
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_FILE_STORAGE_AZURE_CONTAINER |
Azure container name | - |
HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_NAME |
Azure storage account name | - |
HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_KEY |
Azure storage account key | - |
export HINDSIGHT_API_FILE_STORAGE_TYPE=azure
export HINDSIGHT_API_FILE_STORAGE_AZURE_CONTAINER=hindsight-files
export HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_NAME=mystorageaccount
export HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_KEY=base64encodedkey==/* Detailed source-code truncated for AI context efficiency. */
HINDSIGHT_API_WORKER_ENABLED=false/* Detailed source-code truncated for AI context efficiency. */
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are broad summaries of project events grouped by week. Each observation should capture what happened, what was decided, and what was blocked — not individual facts. Merge related events into cohesive weekly narratives."Example: Person-centric knowledge
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are durable facts about specific named people: their preferences, skills, relationships, and behavioral patterns. Only create observations for facts that are stable over time and tied to a named individual."Example: Support ticket patterns
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are recurring patterns in customer support interactions: common failure modes, frequently requested features, and pain points that appear across multiple tickets."/* Detailed source-code truncated for AI context efficiency. */
# Expose only recall (read-only deployment)
export HINDSIGHT_API_MCP_ENABLED_TOOLS=recall
# Expose recall and reflect only
export HINDSIGHT_API_MCP_ENABLED_TOOLS=recall,reflectAvailable tool names: retain, recall, reflect, list_banks, create_bank, list_mental_models, get_mental_model, create_mental_model, update_mental_model, delete_mental_model, refresh_mental_model, list_directives, create_directive, delete_directive, list_memories, get_memory, list_documents, get_document, delete_document, list_operations, get_operation, cancel_operation, list_tags, get_bank, get_bank_stats, update_bank, delete_bank, clear_memories, get_knowledge_base_tree, search_knowledge_base, get_knowledge_page, create_knowledge_folder, create_knowledge_page, update_knowledge_node, delete_knowledge_node.
This can also be overridden per bank via the config API:
When a bank-level mcp_enabled_tools is set, tools not in the list return a clear error when invoked (they still appear in the tools list for MCP protocol compatibility).
MCP Authentication:
By default, the MCP endpoint is open. For production deployments, set HINDSIGHT_API_MCP_AUTH_TOKEN to require Bearer token authentication:
export HINDSIGHT_API_MCP_AUTH_TOKEN=your-secret-tokenClients must then include the token in the Authorization header. See MCP Server documentation for details.
Local MCP instructions:
# Example: instruct MCP to also store assistant actions
export HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, including tool calls and decisions made."/* Detailed source-code truncated for AI context efficiency. */
HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY="shadow-*:10,staging-*:5,*:1"/* Detailed source-code truncated for AI context efficiency. */
from hindsight_api import MemoryEngine
memory = MemoryEngine.from_env()
await memory.initialize()/* Detailed source-code truncated for AI context efficiency. */
# Enable tracing
export HINDSIGHT_API_OTEL_TRACES_ENABLED=true
# Configure endpoint (example: OpenLIT Cloud)
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.openlit.io
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer olit-xxx"
# Optional: Custom service name and environment
export HINDSIGHT_API_OTEL_SERVICE_NAME=hindsight-production
export HINDSIGHT_API_OTEL_DEPLOYMENT_ENVIRONMENT=productionLocal Development:
For local development, we recommend the Grafana LGTM stack which provides traces, metrics, and logs in a single container:
./scripts/dev/start-grafana.sh/* Detailed source-code truncated for AI context efficiency. */
HINDSIGHT_API_PROFILE='{"every": 60, "top": 20}'[profile] thread hindsight-loop- 1.41 cores
[profile] thread asyncio_ 0.37 cores
[profile] 1598 functions active in the last 60s
[profile] tottime=0.923 cumtime=1.833 ncalls=15842 <method 'encode' of 'builtins.CoreBPE' objects>
[profile] tottime=0.691 cumtime=7.039 ncalls=1511590/* Detailed source-code truncated for AI context efficiency. */
# Point Control Plane to a remote API service
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888
# Authenticate to an auth-protected API service
export HINDSIGHT_CP_DATAPLANE_API_KEY=my-dataplane-bearer-token
# Protect the Control Plane with an access key
export HINDSIGHT_CP_ACCESS_KEY=my-secret-keyHierarchical Configuration
Hindsight supports per-bank configuration overrides through a hierarchical system: Global (env vars) → Tenant → Bank.
Type-Safe Config Access
To prevent accidentally using global defaults when bank-specific overrides exist, Hindsight enforces type-safe config access:
In Application Code:
from hindsight_api.config import get_config
# ✅ Access static (infrastructure) fields
config = get_config()
host = config.host # OK - static field
port = config.port # OK - static field
# ❌ Attempting to access bank-configurable fields raises an error
chunk_size = config.retain_chunk_size # ConfigFieldAccessError!Error Message:
ConfigFieldAccessError: Field 'retain_chunk_size' is bank-configurable and cannot
be accessed from global config. Use ConfigResolver.resolve_full_config(bank_id, context)
to get bank-specific config.For Bank-Specific Config:
# Internal code that needs bank-specific settings
from hindsight_api.config_resolver import ConfigResolver
# Resolve full config for a specific bank
config = await config_resolver.resolve_full_config(bank_id, request_context)
chunk_size = config.retain_chunk_size # ✅ Uses bank-specific value/* Detailed source-code truncated for AI context efficiency. */
export HINDSIGHT_API_DEFAULT_BANK_TEMPLATE='{"version":"1","bank":{"reflect_mission":"Help support agents remember customer interactions.","retain_extraction_mode":"verbose","disposition_empathy":5},"directives":[{"name":"Be concise","content":"Always respond concisely.","priority":10}]}'API Endpoints
GET /v1/default/banks/{bank_id}/config- View resolved config (filtered by permissions)PATCH /v1/default/banks/{bank_id}/config- Update bank overrides (only allowed fields)DELETE /v1/default/banks/{bank_id}/config- Reset to defaults
Permission System
Tenant extensions can control which fields banks are allowed to modify via get_allowed_config_fields():
class CustomTenantExtension(TenantExtension):
async def get_allowed_config_fields(self, context, bank_id):
# Option 1: Allow all configurable fields
return None
# Option 2: Allow specific fields only
return {"retain_chunk_size", "retain_custom_instructions"}
# Option 3: Read-only (no modifications)
return set()Examples
Security Notes:
- Credentials (API keys, base URLs) are never returned in responses
- Only configurable fields can be modified
- Responses are filtered by tenant permissions
- Attempting to set credentials returns 400 error
Reverse Proxy / Subpath Deployment
To deploy Hindsight under a subpath (e.g., example.com/hindsight/):
Set both environment variables to the same path:
bashHINDSIGHT_API_BASE_PATH=/hindsight NEXT_PUBLIC_BASE_PATH=/hindsightConfigure your reverse proxy to:
- Forward
/hindsight/*requests to Hindsight - Preserve the full path in forwarded requests
- Set appropriate proxy headers (X-Forwarded-Proto, X-Forwarded-For)
- Forward
Example: Nginx Configuration
location /hindsight/ {
proxy_pass http://localhost:8888/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Example: Traefik Configuration
http:
routers:
hindsight:
rule: "PathPrefix(`/hindsight`)"
service: hindsight
middlewares:
- hindsight-stripprefix
middlewares:
hindsight-stripprefix:
stripPrefix:
prefixes:
- "/hindsight"
services:
hindsight:
loadBalancer:
servers:
- url: "http://localhost:8888"Important Notes:
- The base path must start with
/and should NOT end with/ - Both API and Control Plane should use the same base path
- After setting environment variables, restart both services
- OpenAPI docs will be available at
<base-path>/docs(e.g.,/hindsight/docs)
Complete Examples:
See docker/compose-examples/ directory for:
- Nginx configuration files (
simple.conf,api-and-control-plane.conf) - Docker Compose setups (
docker-compose.yml,reverse-proxy-only.yml) - Traefik and other reverse proxy examples
- Full deployment documentation
Example .env File
# API Service
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
# HINDSIGHT_API_DATABASE_SCHEMA=public # optional, defaults to 'public'
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
# Authentication (optional, recommended for production)
# HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
# HINDSIGHT_API_TENANT_API_KEY=your-secret-api-key
# File storage (optional, defaults to PostgreSQL native storage)
# HINDSIGHT_API_FILE_STORAGE_TYPE=s3
# HINDSIGHT_API_FILE_STORAGE_S3_BUCKET=my-hindsight-files
# HINDSIGHT_API_FILE_STORAGE_S3_REGION=us-east-1
# HINDSIGHT_API_FILE_STORAGE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
# HINDSIGHT_API_FILE_STORAGE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Control Plane
HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888For configuration issues not covered here, please open an issue on GitHub.
Hindsight Docs Docs Developer Development (`hindsight-docs/docs/developer/development.md`)
sidebar_position: 7
Development Guide
Guide to setting up a local development environment for contributing to Hindsight.
Prerequisites
- Python 3.11+
- uv - Fast Python package manager
- Docker and Docker Compose
- An LLM provider credential, or a local/subscription-backed provider such as Ollama or GitHub Copilot
Local Development Setup
1. Clone the Repository
git clone https://github.com/vectorize-io/hindsight.git
cd hindsight2. Install Dependencies
uv sync3. Start PostgreSQL
Start only the database via Docker:
cd docker && docker-compose up -d postgres4. Configure Environment
cp .env.example .envEdit .env with your LLM API key:
# Database (connects to Docker postgres)
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
# LLM Provider (choose one)
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
HINDSIGHT_API_LLM_MODEL=llama-3.1-70b-versatile5. Start the API Server
./scripts/start-server.sh --env localThe server will be available at http://localhost:8888.
Running Tests
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/test_retrieval.py
# Run with verbose output
uv run pytest -vCode Generation
Regenerate API Clients
When you modify the OpenAPI spec, regenerate the clients:
./scripts/generate-clients.shThis generates:
- Python client in
hindsight-clients/python/ - TypeScript client in
hindsight-clients/typescript/
Export OpenAPI Schema
./scripts/export-openapi.shProject Structure
hindsight/
├── hindsight-api/ # Main API server
│ ├── hindsight_api/
│ │ ├── api/ # HTTP endpoints
│ │ ├── engine/ # Memory engine, retrieval, reasoning
│ │ └── web/ # Server entry point
│ └── tests/
├── hindsight-clients/ # Generated SDK clients
│ ├── python/
│ └── typescript/
├── hindsight-control-plane/ # Admin UI (Next.js)
├── docker/ # Docker Compose setup
└── scripts/ # Development scriptsContributing
Hindsight does not accept pull requests from outside the team. To report a bug or
ask for a feature, open an issue.
See CONTRIBUTING.md
for details.
Troubleshooting
Database Connection Issues
Ensure PostgreSQL is running:
docker-compose psCheck database connectivity:
psql postgresql://hindsight:hindsight_dev@localhost:5432/hindsightML Model Download
On first run, Hindsight downloads embedding and reranking models. This may take a few minutes. Models are cached in ~/.cache/huggingface/.
Port Conflicts
If port 8888 is in use:
HINDSIGHT_API_PORT=8889 ./scripts/start-server.sh --env localHindsight Docs Docs Developer Extensions (`hindsight-docs/docs/developer/extensions.md`)
Extensions
Extensions allow you to customize and extend Hindsight behavior without modifying core code. They enable multi-tenancy, custom authentication, additional HTTP endpoints, and operation hooks.
Available Extensions
TenantExtension
Handles multi-tenancy and API key authentication. Validates incoming requests and determines which PostgreSQL schema to use for database operations, enabling tenant isolation at the database level.
Built-in: ApiKeyTenantExtension
A simple implementation that validates API keys against an environment variable and uses the public schema for all authenticated requests.
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
HINDSIGHT_API_TENANT_API_KEY=your-secret-keyNo longer built in: SupabaseTenantExtension
Validates Supabase JWTs and gives each authenticated user their own PostgreSQL schema. It now lives in the extensions registry, which documents its configuration and ships a Dockerfile that builds an image with it.
:::warning Breaking change in 0.9.3
Up to 0.9.2 this extension was built in, at hindsight_api.extensions.builtin.supabase_tenant. That path no longer exists, so an install still pointing at it fails at startup with ModuleNotFoundError. Add the extension to your image and set HINDSIGHT_API_TENANT_EXTENSION=hindsight_ext_supabase_tenant:SupabaseTenantExtension. All HINDSIGHT_API_TENANT_* settings and the schema naming are unchanged.
:::
External: StaticKeysTenantExtension
A fully self-hosted multi-user mode: users and their API keys are declared in environment variables (no external identity provider, no users table). Each user maps to their own PostgreSQL schema ({prefix}_{user_id}), provisioned lazily on first access, giving database-level memory isolation between users. Multiple API keys may map to the same user and schema.
User IDs are case-insensitive: they are lowercased (and dashes normalized to underscores) before building the schema name, so Rafael, rafael and RAFAEL all resolve to the same tenant schema.
It lives in the extensions registry, which documents its configuration and ships a Dockerfile that builds an image with it.
For other multi-tenant setups with separate schemas per tenant (e.g., custom JWT-based auth), implement a custom TenantExtension.
HttpExtension
Adds custom HTTP endpoints under the /ext/ path prefix. Useful for adding domain-specific APIs that integrate with Hindsight's memory engine.
Provides two router methods:
get_router(memory)— returns a FastAPI router mounted at/ext/get_root_router(memory)— returns a FastAPI router mounted at the application root (for well-known endpoints or other paths that must be at specific locations). ReturnsNoneby default.
No built-in implementation - implement your own to add custom endpoints.
HINDSIGHT_API_HTTP_EXTENSION=mypackage.ext:MyHttpExtensionOperationValidatorExtension
Hooks into retain/recall/reflect operations for validation and monitoring. Use cases include:
- Rate limiting and quota enforcement
- Permission checks and content filtering
- Audit logging and usage tracking
- Custom metrics collection
No built-in implementation - implement your own based on your requirements.
self.context is the process-wide extension context — use it for process-global handles such as get_memory_engine(). It holds no per-request state: take the tenant and bank from each hook's own argument (ctx.bank_id and ctx.request_context, the latter holding the identity resolved by the tenant extension).
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidatorMCPExtension
Registers additional MCP (Model Context Protocol) tools on the Hindsight MCP server. Enables external packages to add custom tools without modifying core code.
No built-in implementation - implement your own to add custom MCP tools.
HINDSIGHT_API_MCP_EXTENSION=mypackage.mcp:MyMCPExtensionLLMRequestExtension
Decides, per LLM call, which LLM the call goes to and with which credentials. Use it to:
- Bill each customer on their own key. Front your LLM spend with a gateway (LiteLLM proxy, Bifrost, OpenRouter, Portkey, ...) that gives each customer a virtual key, and send each bank's calls with its customer's key.
- Pick the model per bank or per operation. A cheap model for one bank's retain and a strong one for its reflect, a different provider or cloud account per customer.
It is asked once per LLM call with the tenant schema, the bank and the operation the call runs for — retain, reflect, consolidation, mental-model refresh — in the API process and in the worker alike. It answers with an LLMRequestOverrides, or None to use the server's own LLM. Raising refuses the call.
HINDSIGHT_API_LLM_REQUEST_EXTENSION=mypackage.llms:MyLLMsAn answer can carry:
| Field | Effect |
|---|---|
llm |
Send the call to this LLM instead of the server's: provider, model, base URL, key and the other per-LLM settings (LLMMemberConfig, the shape of a multi-LLM chain member). Fields left unset inherit the server's global LLM defaults, except its credentials (default headers, Vertex service account, Codex home, LiteLLM router config); timeouts and retries inherit the operation's. |
api_key |
The key for the LLM the call goes to (the server's, or llm's — it fills an llm that has no key of its own). |
headers |
Extra request headers, e.g. a gateway's customer id. |
Things to know:
- The choice is server-side. Nothing comes from the API caller. If your extension builds an answer from something a caller can write (bank config, metadata), validate it — otherwise that is where an endpoint or a key would come from user input.
- Credentials without
llmcost no new client. The key and headers go on a per-call copy of the existing SDK client that shares its connection pool. Supported on OpenAI-compatible endpoints (openai,groq,deepseek,openrouter,fireworks,lmstudio, any gateway set asbase_url, ...),anthropicandlitellmrouted to a keyed backend (OpenAI, Anthropic, Azure, Gemini AI Studio, OpenRouter, a LiteLLM proxy, ...). A call to any other provider (gemini,vertexai,ollama,bedrock,litellmrouter, the OAuth/CLI providers) that the answer gives a key or headers to is refused, never sent on the configured credentials. To change the key of one of those, answer with a wholellminstead. A key is also refused when the server's default headers carry a credential (Authorization,x-api-key,x-portkey-api-key, ...) the answer does not override, since the SDK would send that one instead. - Asked once per LLM in a chain. With a multi-LLM chain or a provider per operation, each LLM is asked separately; check
target.providerbefore handing out a key meant for one of them. - Reflect prompt caching is off while the extension is installed: Gemini's step-by-step reflect cache is created on the server's LLM, outside the calls the extension decides.
- An
llmanswer is built once and reused, up to 64 distinct LLMs per process (least recently used are dropped). An extension that answers with one LLM per user on a large user base should use a gateway andapi_keyinstead. - No batch retain.
HINDSIGHT_API_RETAIN_BATCH_ENABLED=truewith this extension fails at startup: batch jobs are submitted on the server's LLM. - LLM calls only. Embeddings and reranking keep their own configuration.
- Lifecycle:
on_startup()runs when the memory engine initializes, before any LLM call, andon_shutdown()when it closes. - Startup check: the connection check at boot uses the server's own LLM. If it has no usable key (for example, the gateway has none for the server), set
HINDSIGHT_API_SKIP_LLM_VERIFICATION=true.
Load the same extension in the API and in every worker: a worker without it runs consolidation and refreshes on the server's LLM.
The bank-llm-routing community extension routes on bank-id and operation patterns from configuration, and is a working example of the interface.
Writing Custom Extensions
Extension Basics
Extensions are Python classes loaded via environment variables:
HINDSIGHT_API_<TYPE>_EXTENSION=mypackage.module:MyExtensionClassConfiguration is passed via prefixed environment variables:
HINDSIGHT_API_<TYPE>_SOME_CONFIG=value
# Extension receives: {"some_config": "value"}All extensions support lifecycle hooks:
on_startup()- Called when the application startson_shutdown()- Called when the application shuts down
Extensions have access to an ExtensionContext that provides:
run_migration(schema)- Run database migrations for a schemaget_memory_engine()- Get the MemoryEngine interface
Shipping your own database migrations
An extension that keeps state of its own returns the directory holding its Alembic
revision files, and they are applied in the same migration run as Hindsight's own —
same ordering guarantees, same alembic_version table:
class MyExtension(TenantExtension):
def alembic_version_locations(self) -> list[str]:
return [str(Path(__file__).parent / "alembic" / "versions")]The directory holds revision files only — there is no env.py; Hindsight's own
configures the schema and the connection. The tree is independent of Hindsight's: give
its first revision down_revision = None and a branch_labels naming your extension,
so an operator can address it (alembic upgrade <label>@head).
Independent branches have no ordering between them. A revision that needs a Hindsight
table to exist first must say so explicitly, which orders it without making Hindsight's
revision its parent:
depends_on = ("a1b2c3d4e5f6",) # a Hindsight revision idA directory that does not exist is skipped with a warning rather than failing the
migration — a misconfigured extension must never leave a database unmigratable.
Example: Custom TenantExtension with JWT
import jwt
from hindsight_api.extensions import TenantExtension, TenantContext, AuthenticationError
class JwtTenantExtension(TenantExtension):
def __init__(self, config: dict[str, str]):
super().__init__(config)
self.jwt_secret = config.get("jwt_secret")
if not self.jwt_secret:
raise ValueError("HINDSIGHT_API_TENANT_JWT_SECRET is required")
async def authenticate(self, context: RequestContext) -> TenantContext:
token = context.api_key
if not token:
# Optional headers dict is forwarded in HTTP/MCP error responses
raise AuthenticationError("Bearer token required")
try:
payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
tenant_id = payload.get("tenant_id")
if not tenant_id:
raise AuthenticationError("Missing tenant_id in token")
return TenantContext(schema_name=f"tenant_{tenant_id}")
except jwt.InvalidTokenError as e:
raise AuthenticationError(str(e))AuthenticationError accepts an optional headers dict that is forwarded in both HTTP and MCP error responses. This is useful for returning custom headers like WWW-Authenticate:
raise AuthenticationError(
"Authorization required",
headers={"WWW-Authenticate": 'Bearer realm="example"'},
)Reading additional request headers
RequestContext carries the Authorization header as api_key. To authenticate on a different header — for instance when a gateway terminates auth with one shared identity and forwards the per-caller identity separately — name the headers you want forwarded:
HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertionThey are available as context.extra_headers, keyed by lower-cased name:
async def authenticate(self, context: RequestContext) -> TenantContext:
assertion = context.extra_headers.get("x-user-assertion")
if not assertion:
raise AuthenticationError("x-user-assertion header required")
user_id = verify_assertion(assertion) # your verification
return TenantContext(schema_name=f"tenant_{user_id}")This works on both the HTTP and MCP transports, and the same RequestContext is passed to OperationValidatorExtension hooks, so a validator can enforce rules against the identity resolved here.
Only headers you list are forwarded, and only when present on the request. The variable is unset by default, so extensions see no header data unless you opt in.
A header sent more than once is not forwarded at all, and a warning is logged. There is no safe way to choose between the copies — a proxy may append its trusted value either before or after a client-supplied one — so an extension reading it sees nothing and fails the request, rather than silently accepting a value that may be spoofed. Make sure your proxy replaces the identity header it injects instead of appending to it.
:::caution Deferred operationsextra_headers describes the request being served. Operations that run later — a queued retain, a scheduled consolidation, a mental-model refresh — are executed by a background worker with no request behind them, so their RequestContext carries no headers. Authorize on the header at request time; do not rely on it inside work that continues after the response.
:::
Example: Custom HttpExtension
from fastapi import APIRouter
from hindsight_api.extensions import HttpExtension
class MyHttpExtension(HttpExtension):
def get_router(self, memory: MemoryEngine) -> APIRouter:
router = APIRouter()
@router.get("/hello")
async def hello():
return {"message": "Hello from extension!"}
@router.post("/custom/{bank_id}/action")
async def custom_action(bank_id: str):
# Access memory engine for database operations
pool = await memory._get_pool()
# ... custom logic
return {"status": "ok"}
return router
def get_root_router(self, memory: MemoryEngine) -> APIRouter | None:
"""Optional: mount routes at the application root (not under /ext/)."""
router = APIRouter()
@router.get("/.well-known/my-metadata")
async def metadata():
return {"version": "1.0"}
return routerRoutes from get_router are available at /ext/hello, /ext/custom/{bank_id}/action, etc.
Routes from get_root_router are mounted at the app root (e.g., /.well-known/my-metadata).
Example: Custom OperationValidatorExtension
from hindsight_api.extensions import (
OperationValidatorExtension,
ValidationResult,
PrecheckContext,
RetainContext,
RecallContext,
ReflectContext,
RetainResult,
)
class MyValidator(OperationValidatorExtension):
# Pre-body validation (optional)
async def precheck(self, ctx: PrecheckContext) -> ValidationResult:
if ctx.content_length is not None and ctx.content_length > 10_000_000:
return ValidationResult.reject("Payload is too large")
return ValidationResult.accept()
# Pre-operation validation (required)
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
# Implement your validation logic
return ValidationResult.accept()
# Or reject: return ValidationResult.reject("Reason")
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
return ValidationResult.accept()
# Post-operation hooks (optional)
async def on_retain_complete(self, result: RetainResult) -> None:
# Log usage, update metrics, send notifications, etc.
passprecheck runs before the request body is read or deserialized. ItsPrecheckContext.content_length is the parsed Content-Length header as an
integer, or None when the header is missing or cannot be parsed (for example,
chunked transfer encoding). Use it for cheap size-aware quota or cost guards;
the full validate_* hooks still run after parsing and should enforce precise
per-operation limits.
Memory curation hooks
Curating a memory (PATCH /v1/default/banks/{bank_id}/memories/{memory_id}:
edit, invalidate, or revert) has its own pair of hooks, in addition to thevalidate_bank_write access check that runs first:
validate_memory_update(ctx: MemoryUpdateContext)runs before any work. The
context carries the requestedtext(when editing it), the requestedstate,
andedits_fields. Reject here to refuse the curation; the returnedstatus_codeis passed through to the HTTP response.on_memory_update_complete(result: MemoryUpdateResult)runs once the change
has committed.result.actionisedit,invalidate,revert, orreason,
andresult.reembedded_tokensis the size of the text the engine embedded
again (0 for a plain invalidation or a reason-only update). An edit or revert
re-embeds the memory and re-consolidates the bank, so this is the figure to
meter if curation should cost the same as ingesting that text.
Transfer-archive import hooks
Importing a transfer archive (a document import, or a whole-bank restore) brings
pre-extracted facts into a bank without running fact extraction:
validate_import(ctx: ImportContext)runs at submission, before the archive
is stored or queued.ctx.documentslists the archive's documents
(ImportArchiveDocument:document_idand the originalcontent), so a validator can size the import
for a quota or cost check. A rejection reaches the caller directly. The
default accepts.- Each imported document is then reported to
on_retain_completewithresult.source == RetainSource.IMPORT, zero LLM token counts, andprocessed_content_tokens=0. Usesourceto tell an import apart from a
re-retain whose chunks were all deduplicated, which also reportsprocessed_content_tokens=0; ordinary retains reportRetainSource.RETAIN.
Cloning a bank within the same instance is not an import: it is neither gated
by validate_import nor reported to on_retain_complete.
Confining a caller to a tag scope
When several people share one bank and tags separate what each may see,resolve_tag_scope pins every operation of a caller to a tag filter. Return
the tag groups the caller's reachable data must satisfy (they are AND-ed), orNone for no restriction:
from hindsight_api.engine.search.tags import TagGroupLeaf
from hindsight_api.extensions import OperationValidatorExtension, TagScopeContext
class TeamScopes(OperationValidatorExtension):
async def resolve_tag_scope(self, ctx: TagScopeContext):
user = ctx.request_context.api_key_id # or whatever your tenant extension resolved
# Dan reads his own memories plus the shared team rules.
return [TagGroupLeaf(tags=[f"user:{user}", "kind:rule"], match="any_strict")]The engine combines the scope with whatever filter the caller sends, so a
caller can narrow its view but never widen it:
- Filtered reads — recall, reflect (and every tool it runs), the memory,
document and mental-model lists, observation scopes, tag lists, the memory
graph, the timeseries, entities and the entity graph, and the knowledge-base
tree, search and export — only see rows inside the scope. Entity counts are
recounted over the visible memories. - Reads and writes by id — a memory, a document and its chunks, a mental
model or knowledge page — answer404when the item is outside the scope.
A retain that names an existing document outside the scope is refused (403),
since replacing or appending to it would touch someone else's text. - Source text follows the document. A memory can be visible through its own
tags (say, akind:rulefact extracted from someone's private note) while the
note is not. Its chunk and document text are only returned when the
document's tags pass the filter. This applies to every tag-filtered recall
and reflect, with or without an extension. - Mental models and knowledge pages created or updated by a scoped caller
must carry tags inside the scope (403otherwise — the caller could not see
what it made), and record the scope in their trigger (scope_tag_groups),
which every refresh AND-s with the model's own filter, so they can never be
built from memories their creator could not read. Background refreshes run
unscoped and rely on that recorded scope. A recorded scope only ever narrows
the model and is not editable through the API: to drop it, recreate the model.
Use a _strict match mode: the non-strict ones also admit untagged rows, and
an untagged mental model is built from the whole bank.
Writing is a separate permission. A caller can read a shared scope without
being allowed to change it. resolve_write_tag_scope returns the tags a caller
may write, as shell-style patterns (user:dan, project:*), or None for no
restriction (the default):
class TeamScopes(OperationValidatorExtension):
async def resolve_write_tag_scope(self, ctx: TagScopeContext):
user = ctx.request_context.api_key_id
# Everyone writes their own memories; only Kate writes the team rules.
return [f"user:{user}", "kind:rule"] if user == "kate" else [f"user:{user}"]Every tag on anything the caller writes or changes must match one of the
patterns, otherwise the write is refused with 403 (an item the caller cannot
even read still answers 404). An untagged item belongs to everyone, so a
restricted writer cannot produce or change one. That covers:
- retain (text and files): each item's tags and the batch's
document_tags,
plus every tag the retain strategy's entity labels
withtag: truecould add (key:valuefor each allowed value,key:*for an
open vocabulary), and explicitobservation_scopes("shared"writes untagged
observations). They are checked before extraction, so a refused retain costs
no LLM call; - memories and documents: editing, invalidating or clearing the observations
of a memory; updating (including the new tags), reprocessing or deleting a
document; - mental models and knowledge pages: creating one (its tags), updating,
refreshing, clearing or deleting one, and renaming, moving or deleting a
knowledge node (every page under it, and under the folder it moves into); - directives: creating, updating or deleting one. An untagged directive
steers everyone's reflect, so a restricted writer cannot create one.
Directives are read like reflect reads them: untagged directives apply to
everyone, tagged ones only inside the caller's read scope.
Whole-bank operations reach every memory regardless of tags, so a caller with a
read or write scope cannot run them at all (403): export, clone and import,
clearing the bank's memories or observations, deleting the bank, changing or
resetting its config, mission or disposition, and running (or retrying failed)
consolidation on request. The consolidation the engine queues after a scoped
caller's own writes still runs. Operation status never returns the raw task payload to a
scoped caller. Reads that are not tag-scoped (stats, webhooks) still go throughvalidate_bank_read / validate_bank_write, which can only allow or deny them.
Deferring an operation
In addition to accept and reject, a validate_* hook can ask the
worker to requeue the operation for a future time by raisingDeferOperation. Use this for backpressure (rate-limited upstream,
quota window not yet open, dependency warming up) — unlike a retry, it
does not increment retry_count or write error_message. The worker
sets next_retry_at to your exec_date and the task is invisible to
claim queries until that time.
from datetime import datetime, timedelta, timezone
from hindsight_api.extensions import (
DeferOperation,
OperationValidatorExtension,
RetainContext,
ValidationResult,
)
class QuotaAwareValidator(OperationValidatorExtension):
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
if not await self._quota_available(ctx.bank_id):
raise DeferOperation(
exec_date=datetime.now(timezone.utc) + timedelta(minutes=5),
reason="bank quota window exhausted",
)
return ValidationResult.accept()DeferOperation is worker-only: do not raise it fromvalidate_recall or validate_reflect in synchronous HTTP request
paths — there is no queue to defer to and it will surface as a 500.
Inside the worker, ctx.request_context.operation_id is the id of the
operation being run (None on synchronous requests; for retains folded
into one execution, the primary operation's id). A hook that decides the
operation should not run at all, rather than fail, can cancel it withself.context.get_memory_engine().cancel_operation(...) and
then raise DeferOperation: the row stays cancelled and no failure is
recorded.
Example: Custom MCPExtension
from mcp.server.fastmcp import FastMCP
from hindsight_api.extensions import MCPExtension
from hindsight_api.engine import MemoryEngine
class MyMCPExtension(MCPExtension):
async def register_tools(self, mcp: FastMCP, memory: MemoryEngine) -> None:
@mcp.tool()
async def custom_search(query: str) -> str:
"""Custom MCP tool for specialized search."""
# Access memory engine for operations
pool = await memory._get_pool()
# ... custom logic
return f"Results for: {query}"Example: Custom LLMRequestExtension
from hindsight_api.config import LLMMemberConfig
from hindsight_api.extensions import (
LLMRequestExtension,
LLMRequestOverrides,
LLMRequestRefused,
LLMRequestTarget,
)
class PerCustomerLLMs(LLMRequestExtension):
def __init__(self, config: dict[str, str]) -> None:
super().__init__(config)
self.keys = load_keys(config["keys_file"]) # your own store: {(schema, bank_id): key}
async def resolve(self, target: LLMRequestTarget) -> LLMRequestOverrides | None:
key = self.keys.get((target.schema, target.bank_id))
if key is None:
# Refuse rather than return None: None sends the call on the server's own LLM.
raise LLMRequestRefused(f"no LLM key for bank {target.bank_id!r}")
if target.operation == "reflect":
# A stronger model for reflect, on the customer's own account.
return LLMRequestOverrides(llm=LLMMemberConfig(provider="anthropic", model="claude-sonnet-5-5", api_key=key))
return LLMRequestOverrides(api_key=key, headers={"x-customer": target.bank_id})resolve runs on every LLM call, so cache anything slow. Bank ids are unique per tenant schema only — a multi-tenant deployment must key on target.schema too, or one tenant can name a bank after another tenant's and be billed to its key. LLMRequestOverrides keeps the LLM and key out of its repr; keep them out of your own logs as well.
Deploying Custom Extensions
With Docker
Extensions are not bundled in the image. Build one on top of Hindsight that installs your extension's dependencies and copies it in. Install into the image's virtualenv explicitly — it was created by uv sync and ships no pip of its own, so a bare pip install lands where the server can't see it:
FROM ghcr.io/vectorize-io/hindsight:latest
RUN uv pip install --python /app/api/.venv/bin/python --no-cache \
'PyJWT[crypto]>=2.12.0' 'httpx>=0.27.0'
COPY my_extension /app/extensions/my_extension
ENV PYTHONPATH=/app/extensions
# Fail the build, not the first request, if it isn't importable.
RUN /app/api/.venv/bin/python -c "import my_extension"Then point the service at that image and pass the extension's variables as environment. Give the API and worker containers the same extension configuration — the worker uses the tenant extension to enumerate schemas for background consolidation.
See the extensions registry README for the full recipe.
Bare Metal
Install your extension package in the same Python environment as Hindsight:
# Install Hindsight
pip install hindsight-api
# Install your extension package
pip install ./my-extensions
# or
pip install my-extensions-package
# Configure
export HINDSIGHT_API_TENANT_EXTENSION=my_extensions.auth:JwtTenantExtension
export HINDSIGHT_API_TENANT_JWT_SECRET=your-secret
# Run
hindsight-apiContributing Extensions
If you've built an extension that solves a common use case, for example:
- Authentication providers (OAuth, SAML, API gateways)
- Rate limiting or quota management
- Audit logging integrations
- Metrics exporters (Datadog, New Relic, etc.)
- Custom HTTP endpoints for specific platforms
open a feature request with a link to your repository, and we can list it in the extensions registry. Hindsight does not accept pull requests from outside the team, so the team adds registry entries. That README covers the extension layout, the development workflow, and Docker packaging.
Extensions live outside the server so that installing Hindsight does not pull in a vendor's client library, and so changing an extension does not require a Hindsight release. Only extensions that add no dependencies and are useful to any deployment (ApiKeyTenantExtension, MemoryDefenseRegexExtension) stay in hindsight_api.extensions.builtin.
Hindsight Docs Docs Developer Installation (`hindsight-docs/docs/developer/installation.md`)
Installation
Hindsight can be deployed in several ways depending on your infrastructure and requirements.
:::tip Don't want to manage infrastructure?
Hindsight Cloud is a fully managed service that handles all infrastructure, scaling, and maintenance — sign up here.
:::
Supported Platforms
Hindsight runs on Linux, macOS, and Windows:
| Platform | Docker | Bare Metal (pip) | Embedded DB (pg0) | Notes |
|---|---|---|---|---|
| Linux (x86_64, ARM64) | ✅ | ✅ | ✅ | Fully supported, recommended for production |
| macOS (Apple Silicon / arm64) | ✅ | ✅ | ✅ | Fully supported |
| macOS (Intel / x86_64) | ✅ | ⚠️ slim only | ✅ | Use hindsight-all-slim / hindsight-api-slim. The full bundle's local ML models (PyTorch, MLX) publish no Intel-Mac wheels, so pip install hindsight-all silently backtracks to a months-old release. Pair the slim bundle with a hosted embeddings/reranker provider or the in-process ONNX backend (hindsight-api-slim[local-onnx]). |
| Windows (x86_64) | ✅ | ✅ | ✅ | Fully supported — see Windows setup for external PostgreSQL option |
All platforms support the embedded database (pg0) for development. On Windows, you can also use an external PostgreSQL installation — see the Windows section for a step-by-step guide.
Prerequisites
PostgreSQL
Hindsight requires PostgreSQL 14+ with a vector extension for similarity search. The supported extensions are:
- pgvector (default)
- pgvectorscale
- vchord
- scann (AlloyDB)
Configure which one to use with HINDSIGHT_API_VECTOR_EXTENSION. See Configuration for details.
By default, Hindsight uses pg0 — an embedded PostgreSQL that runs locally on your machine. This is convenient for development but not recommended for production.
For production, use an external PostgreSQL with one of the supported vector extensions:
- Supabase — Managed PostgreSQL with pgvector built-in
- Neon — Serverless PostgreSQL with pgvector
- Azure Database for PostgreSQL — With pgvector and pgvectorscale support
- Google AlloyDB / AlloyDB Omni — With pgvector and ScaNN support
- AWS RDS / Cloud SQL — With pgvector extension enabled
- Self-hosted — PostgreSQL 14+ with your preferred vector extension
LLM Provider
You need an LLM API key for fact extraction, entity resolution, and answer generation. See Models for supported providers, model recommendations, and configuration.
Hardware
Hindsight is designed to run on commodity hardware. The footprint depends mainly on whether the full image (which bundles local embedding and reranker models) or the slim image (which delegates those to external providers) is used.
| Component | Minimum RAM | Recommended RAM | Notes |
|---|---|---|---|
| API — Full image | 1.5 GB | 2 GB | Loads local BGE embedder (~130 MB) and MiniLM cross-encoder (~90 MB) into memory, plus PyTorch/ONNX runtime arenas. Idle RSS settles around 0.8–1.0 GB; expect 1.2–1.5 GB under load. |
| API — Slim image | 512 MB | 1 GB | No local models. Steady-state RSS is dominated by Python runtime and DB connections. Requires external embedding and reranker providers (e.g. TEI, OpenAI, Cohere). |
| Control Plane (UI) | 128 MB | 256 MB | Next.js process, lightweight. |
| Worker (if separated) | Same as API image variant | Same as API image variant | Workers load the same models as the API server. |
| PostgreSQL | 512 MB | 1 GB+ | Scales with the number of memories and indexes. |
:::tip Reducing the footprint
The bulk of the full image's memory comes from the bundled embedding and reranker models and their PyTorch/ONNX runtimes. To shrink the deployment to a few hundred MB of RAM, switch to the slim image and configure external embedding and reranker providers.
:::
CPU vs GPU: 2 vCPUs on CPU-only is fine for development and basic workloads. For production traffic, the local reranker (cross-encoder) is the main bottleneck and typically benefits from a GPU to keep recall latency reasonable; alternatively, offload reranking to an external reranker provider (e.g. TEI, Cohere) on dedicated GPU hardware.
Docker
Best for: Quick start, development, small deployments
Run everything in one container with embedded PostgreSQL:
export OPENAI_API_KEY=sk-xxx
docker run -it --pull always --name hindsight --restart unless-stopped --shm-size=1g -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-v hindsight-data:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest- API Server: http://localhost:8888
- Control Plane (Web UI): http://localhost:9999
:::note Persisting data: named volume vs. host bind mount
The container runs as a non-root user (UID 1000). The hindsight-data named volume above is recommended — Docker creates it owned by the container user, so it works with no extra setup.
If you instead bind-mount a host directory (-v $HOME/.hindsight-docker:/home/hindsight/.pg0), that directory must be owned by UID 1000, or the embedded database fails to start with Permission denied:
sudo chown -R 1000:1000 $HOME/.hindsight-dockerDo not run the image under a different UID with --user to work around this. The image only defines the hindsight user (UID 1000), so any other UID has no /etc/passwd entry, and startup crashes in a library that looks the running user up by ID:
KeyError: 'getpwuid(): uid not found: 1042'Running as UID 1000 (the default) with the directory chowned to match is the supported way to use a host bind mount. If you cannot chown the directory — for example on a NAS share mounted with a fixed uid= — use a named volume instead.
:::
All published images are signed with Cosign — verification is optional.
:::tip Set a stable HINDSIGHT_API_WORKER_ID in production
The worker uses the container hostname as its identity, which Docker sets to the container ID by default. That value changes on every restart, so any task that was being processed when the container went down stays parked under the old ID with no way for the new container to recognize it as its own.
Set HINDSIGHT_API_WORKER_ID to a stable value (e.g., -e HINDSIGHT_API_WORKER_ID=hindsight-prod) so the worker keeps the same identity across restarts. This is recommended even for single-container deployments. For diagnosis and recovery commands, see Admin CLI - Recovering stuck operations.
:::
Docker Image Variants
| Variant | Size (AMD64) | Size (ARM64) | When to use |
|---|---|---|---|
Full (latest) |
~9 GB | ~3.7 GB | Default. Embeddings and reranking run in the image; the LLM is always external, including for local inference. |
Slim (slim) |
~500 MB | ~500 MB | Use when you already rely on external services for embeddings and reranking (OpenAI, Cohere, TEI). Significantly smaller image, faster deploys. Requires external providers. |
The slim image corresponds to the hindsight-api-slim pip package. See Configuration for external provider options.
Neither image bundles llama.cpp, so the built-in llamacpp provider is not available in Docker. To run inference locally, start llama.cpp (or Ollama, LM Studio, vLLM) alongside Hindsight and point HINDSIGHT_API_LLM_BASE_URL at it — see docker/docker-compose/local-llm/ for a working compose file.
NVIDIA GPU Acceleration (CUDA)
To run in-process local embedding and reranker models on an NVIDIA GPU, build a custom CUDA-enabled image using the recipe in docker/docker-compose/cuda/:
docker compose -f docker/docker-compose/cuda/docker-compose.yaml up --buildThe recipe upgrades in-process PyTorch to CUDA 12.6 and configures GPU passthrough via the NVIDIA Container Toolkit. Build it on the machine that will run it — an emulated cross-architecture image cannot reach the GPU.
The CUDA runtime is additive: the base image's CPU PyTorch wheel stays in the lower layers, so the result is roughly 11 GB on disk against the ~9 GB Full image. This is why no CUDA image is published — the cost is only worth paying when you actually have a GPU to use.
See docker/docker-compose/cuda/README.md for prerequisites, manual build steps, and verification.
Bundling Custom Models in a Custom Image
:::tip Production deployments with non-default local models
If you use a non-default local embedder or reranker, bake the models into a custom image at build time rather than enabling the Helm modelCache PVC. See docker/docker-compose/custom-models/ for a runnable example.
:::
Available Tags
# Standalone (API + Control Plane)
ghcr.io/vectorize-io/hindsight:latest # Full, latest release
ghcr.io/vectorize-io/hindsight:latest-slim # Slim, latest release
ghcr.io/vectorize-io/hindsight:0.4.9 # Full, specific version
ghcr.io/vectorize-io/hindsight:0.4.9-slim # Slim, specific version
# API only
ghcr.io/vectorize-io/hindsight-api:latest
ghcr.io/vectorize-io/hindsight-api:latest-slim
# Control Plane only
ghcr.io/vectorize-io/hindsight-control-plane:latestVerifying image signatures
Images are signed with Cosign keyless OIDC. To verify any tag:
cosign verify ghcr.io/vectorize-io/hindsight:<tag> \
--certificate-identity-regexp '^https://github\.com/vectorize-io/hindsight/\.github/workflows/(sign-images|release)\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comHelm / Kubernetes
Best for: Production deployments, auto-scaling, cloud environments
# Install with built-in PostgreSQL
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set api.llm.provider=groq \
--set api.llm.apiKey=gsk_xxxxxxxxxxxx \
--set postgresql.enabled=true
# Or use external PostgreSQL
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set api.llm.provider=groq \
--set api.llm.apiKey=gsk_xxxxxxxxxxxx \
--set postgresql.enabled=false \
--set api.database.url=postgresql://user:[email protected]:5432/hindsight
# Install a specific version
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight --version 0.1.3
# Upgrade to latest
helm upgrade hindsight oci://ghcr.io/vectorize-io/charts/hindsightRequirements:
- Kubernetes cluster (GKE, EKS, AKS, or self-hosted)
- Helm 3.8+
Distributed Workers
For high-throughput deployments, enable dedicated worker pods to scale task processing independently:
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
--set worker.enabled=true \
--set worker.replicaCount=3The chart deploys workers as a StatefulSet, so each pod gets a stable name (e.g. hindsight-worker-0) that the worker uses as its HINDSIGHT_API_WORKER_ID. Tasks claimed by a pod are recognized as its own across restarts. If you swap the chart for a plain Deployment, set HINDSIGHT_API_WORKER_ID explicitly per replica — otherwise hostnames are randomized and previously-claimed tasks become orphaned. See Admin CLI - Recovering stuck operations for diagnosis.
See Services - Worker Service for configuration details and architecture.
See the Helm chart values.yaml for all chart options.
Bare Metal (pip)
Best for: Running Hindsight as a standalone service on a host machine.
Install
pip install hindsight-api # Full — works out of the box
pip install hindsight-api-slim # Slim — requires external services for embeddings, reranking, and the databaseWhen using hindsight-api-slim, you must configure external providers for all model operations. See Configuration for details.
Run with Embedded Database
For development and testing, Hindsight can run with an embedded PostgreSQL (pg0):
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
hindsight-apiThis creates a database in ~/.hindsight/data/ and starts the API on http://localhost:8888.
Run with External PostgreSQL
For production, connect to your own PostgreSQL instance:
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
hindsight-apiNote: The database must exist and have pgvector enabled (CREATE EXTENSION vector;).
CLI Options
hindsight-api --port 9000 # Custom port (default: 8888)
hindsight-api --host 127.0.0.1 # Bind to localhost only
hindsight-api --workers 4 # Multiple worker processes
hindsight-api --log-level debug # Verbose loggingControl Plane
The Control Plane (Web UI) can be run standalone using npx:
npx @vectorize-io/hindsight-control-plane --api-url http://localhost:8888This connects to your running API server and provides a visual interface for managing memory banks, exploring entities, and testing queries.
Options
| Option | Environment Variable | Default | Description |
|---|---|---|---|
-p, --port |
PORT |
9999 | Port to listen on |
-H, --hostname |
HOSTNAME |
0.0.0.0 | Hostname to bind to |
-a, --api-url |
HINDSIGHT_CP_DATAPLANE_API_URL |
http://localhost:8888 | Hindsight API URL |
HINDSIGHT_CP_ACCESS_KEY |
(none) | Access key to protect the Control Plane UI. When set, users must enter this key to log in. |
Examples
# Run on custom port
npx @vectorize-io/hindsight-control-plane --port 9999 --api-url http://localhost:8888
# Using environment variables
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com
npx @vectorize-io/hindsight-control-plane
# Production deployment
PORT=80 HINDSIGHT_CP_DATAPLANE_API_URL=https://api.hindsight.io npx @vectorize-io/hindsight-control-planeWindows
Best for: Running Hindsight natively on Windows without Docker
Hindsight works on Windows with the embedded database (pg0) out of the box — just install and run:
pip install hindsight-api
set HINDSIGHT_API_LLM_PROVIDER=openai
set HINDSIGHT_API_LLM_API_KEY=sk-xxx
set HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
hindsight-apiUsing External PostgreSQL (optional)
If you prefer to use your own PostgreSQL instance instead of the embedded database:
# Install PostgreSQL
winget install PostgreSQL.PostgreSQL.17
# Build pgvector (requires Visual Studio Build Tools)
git clone https://github.com/pgvector/pgvector.git
cd pgvector
# Open "x64 Native Tools Command Prompt for VS" and run:
set PGROOT=C:\Program Files\PostgreSQL\17
nmake /F Makefile.win
nmake /F Makefile.win install
# Create the database and enable the vector extension
psql -U postgres -c "CREATE DATABASE hindsight;"
psql -U postgres -d hindsight -c "CREATE EXTENSION vector;"Then run Hindsight pointing to your database:
pip install hindsight-api
set HINDSIGHT_API_DATABASE_URL=postgresql://postgres@localhost:5432/hindsight
set HINDSIGHT_API_LLM_PROVIDER=openai
set HINDSIGHT_API_LLM_API_KEY=sk-xxx
set HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
hindsight-api- API Server: http://localhost:8888
:::tip
You can also use the slim package (pip install hindsight-api-slim) if you configure external providers for embeddings and reranking. See Configuration for details.
:::
Windows + China Network Notes
If you are running on Windows behind China network restrictions:
- DeepSeek works well for
HINDSIGHT_API_LLM_PROVIDER, but DeepSeek does not provide an embeddings endpoint. - Use local embeddings (recommended for privacy and reliability in restricted networks).
- Set
HF_ENDPOINT=https://hf-mirror.combefore starting Hindsight so Hugging Face model downloads use a China-accessible mirror.
set HF_ENDPOINT=https://hf-mirror.com
set HINDSIGHT_API_LLM_PROVIDER=deepseek
set HINDSIGHT_API_LLM_API_KEY=sk-your-deepseek-key
set HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
set HINDSIGHT_API_LLM_BASE_URL=https://api.deepseek.com
set HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
set HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
set HINDSIGHT_API_RERANKER_PROVIDER=flashrank
hindsight-apiThe HF_ENDPOINT variable is used by Hugging Face tooling (huggingface_hub), not by Hindsight itself.
Embedded in a Python Application
Best for: Using Hindsight programmatically from Python without running a separate server process.
pip install hindsight-all # Full — works out of the box (Linux, Windows, Apple Silicon Macs)
pip install hindsight-all-slim # Slim — requires external services for embeddings, reranking, and the databaseOn Intel (x86_64) Macs, install hindsight-all-slim — see Supported Platforms.
hindsight-all supports two modes of embedding:
In-process (HindsightServer): the server runs in a background thread inside your application. Best when you want the tightest integration and are already managing your own process lifecycle.
from hindsight import HindsightServer, HindsightClient
with HindsightServer(llm_provider="openai", llm_api_key="sk-xxx") as server:
client = HindsightClient(base_url=server.url)
client.retain(bank_id="alice", content="Alice prefers concise answers.")
results = client.recall(bank_id="alice", query="How should I respond to Alice?")Managed subprocess (HindsightEmbedded): the server runs as a background daemon process, shared across multiple Python processes or sessions. The daemon starts on first use and runs until it is stopped.
from hindsight import HindsightEmbedded
client = HindsightEmbedded(llm_provider="openai", llm_api_key="sk-xxx")
client.retain(bank_id="alice", content="Alice prefers concise answers.")
results = client.recall(bank_id="alice", query="How should I respond to Alice?")See the Python SDK for the full API reference.
Next Steps
- Configuration — Environment variables and settings
- Models — ML models and providers
- Monitoring — Metrics and observability
Hindsight Docs Docs Developer Knowledge Pages (`hindsight-docs/docs/developer/knowledge-pages.mdx`)
sidebar_position: 7
import Figure from '@site/src/components/Figure';
import knowledgePagesFigure from '@site/figures/knowledge-pages.json';
Knowledge Pages
Knowledge pages are living documents a bank writes about itself. Each page answers one question — "What are the components here?", "What's our error-handling convention?" — and rewrites itself as the bank learns more. They're organized in folders, browsable, searchable, and can be projected onto disk as ordinary markdown files.
The shape is a wiki. The engine underneath is memory.
Mental Models, Simplified
A knowledge page is a mental model. Same synthesis, same background refresh, same provenance.
What's different is how much you have to know to use one. A mental model exposes its mechanics — what it reads, when it rebuilds, how it edits itself. Nobody should have to think about synthesis scope and refresh triggers to keep a wiki. So a page comes with those decisions already made:
- It's built from the bank's observations — consolidated, deduplicated beliefs — rather than raw conversational detail.
- It refreshes incrementally whenever consolidation produces new knowledge in its scope, editing the document rather than regenerating it.
- It never reads other pages, so pages can't cite each other into a feedback loop.
- It gets a larger content budget, because it's a document rather than an answer.
You supply a name and a question. Everything else is a default you can override if you need to — every mental model setting still applies. See the Knowledge Pages API for the exact defaults and how to change them.
Organized Like a Wiki
Pages live in a tree of folders. Nesting is arbitrary, page names are unique within their folder, and deleting a folder deletes its subtree. That's the whole structural model — a hierarchy, the way anyone would organize documents by hand.
The tree is what makes a knowledge base navigable rather than a flat list of synthesized blobs: Architecture/, Runbooks/, Decisions/, each holding pages that stay current on their own.
Projected as Real Files
Agents already know how to work with a filesystem. So the CLI can mirror a bank's knowledge base onto disk:
hindsight fs mount --bank my-bankThe folder tree becomes real directories, each page a real markdown file with YAML frontmatter, kept current by a background refresh loop. From there, everything ordinary works — ls, cat, grep, rg, fzf, your editor, an agent's file tools. No SDK, no API client, no new vocabulary.
The same content is available as a portable markdown bundle over the API, for exporting or committing elsewhere.
Searchable
Pages are searchable at the document level: a query returns whole pages, ranked, with snippets. It combines full-text and semantic matching, fused server-side, with no reranking step — fast enough to be the first thing an agent reaches for.
That last part matters. Search is a tool an agent chooses to call, visible in the transcript, rather than content pushed into its context on every turn. Retrieval the agent asked for informs what it's doing; retrieval it didn't ask for tends to derail it.
This is a different path from recall, which searches individual memories. Use page search to pick a document; use recall for a specific fact.
Why Not Just Raw Files?
If the answer is documents in a tree, haven't we reinvented files — the thing a memory system exists to replace?
The difference is what sits underneath. A file is where information goes to age. Whoever wrote it last wins, contradictions accumulate quietly, and nothing ever reconciles them. Left alone, a hand-maintained wiki becomes a beautifully formatted lie — not because anyone lied, but because keeping it true is a chore, and chores lose.
A knowledge page is a projected view over processed memory, the way a database view is not a table. Before a page is written, Hindsight has already done the work files can't do for themselves: extracted facts from the raw sessions, commits, and documents; deduplicated them; and reconciled their contradictions through consolidation. So when a team decided X and later amended it to Y, the page says Y — and can say why — instead of preserving both a paragraph apart.
Your raw documents remain the source of truth about what was said. The pages are the reconciled truth about what holds.
This is also why pages heal themselves rather than rot: they aren't the storage, they're the rendering. Delete a page and nothing is lost — it re-projects from memory. Try deleting a wiki and getting it back.
Working With Pages
- Control plane — the Knowledge Base view renders the tree, page contents, and which pages have fallen behind.
- HTTP API — see Knowledge Pages API for the full endpoint surface.
- CLI —
hindsight fsmirrors a bank to a local folder of markdown files. - Agent tools — the Claude Code plugin exposes
agent_knowledge_*MCP tools so an agent can list, read, create, and update its own pages during a session.
Hindsight Docs Docs Developer Mcp Server (`hindsight-docs/docs/developer/mcp-server.md`)
sidebar_position: 5
MCP Server
Hindsight includes a built-in Model Context Protocol (MCP) server that allows AI assistants to store and retrieve memories directly.
Access
The MCP server is enabled by default and mounted at /mcp on the API server. Each memory bank has its own MCP endpoint:
http://localhost:8888/mcp/{bank_id}/For example, to connect to the memory bank alice:
http://localhost:8888/mcp/alice/To disable the MCP server, set the environment variable:
export HINDSIGHT_API_MCP_ENABLED=falseAuthentication
By default, the MCP endpoint is open (no authentication required).
To enable authentication, configure the API key tenant extension:
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
export HINDSIGHT_API_TENANT_API_KEY=your-secret-keyWhen authentication is enabled, include your API key in the Authorization header:
Claude Code
claude mcp add --transport http hindsight http://localhost:8888/mcp \
--header "Authorization: Bearer your-secret-key" \
--header "X-Bank-Id: my-bank"Claude Desktop
Add to ~/.claude_desktop_config.json:
{
"mcpServers": {
"hindsight": {
"url": "http://localhost:8888/mcp",
"headers": {
"Authorization": "Bearer your-secret-key",
"X-Bank-Id": "my-bank"
}
}
}
}Direct HTTP Request
curl -X POST http://localhost:8888/mcp \
-H "Authorization: Bearer your-secret-key" \
-H "X-Bank-Id: my-bank" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'If the key is missing or invalid, requests will receive a 401 Unauthorized response.
Bank Selection
The memory bank is resolved in this priority order:
- URL path (highest priority):
http://localhost:8888/mcp/my-bank/ - X-Bank-Id header:
--header "X-Bank-Id: my-bank" - Default: Uses
HINDSIGHT_MCP_BANK_IDenv var (default: "default")
Per-Bank Endpoints
Unlike traditional MCP servers where tools require explicit identifiers, Hindsight uses per-bank endpoints. The bank_id is part of the URL path, so tools don't need to specify which bank to use—it's implicit from the connection.
This design:
- Simplifies tool usage — no need to pass
bank_idwith every call - Enforces isolation — each MCP connection is scoped to a single bank
- Enables multi-tenant setups — connect different users to different endpoints
Two Modes
The MCP server operates in two modes depending on the URL:
| Mode | URL | Tools | bank_id |
|---|---|---|---|
| Single-bank | /mcp/{bank_id}/ |
27 tools (memory, mental models, directives, documents, operations, tags, bank management) | Implicit from URL |
| Multi-bank | /mcp/ |
All 30 tools including list_banks, create_bank, get_bank_stats |
Explicit bank_id parameter on each tool |
Single-bank mode (recommended) scopes all operations to the bank in the URL. Tools don't expose a bank_id parameter.
Multi-bank mode exposes all tools with an optional bank_id parameter, plus bank management tools (list_banks, create_bank, get_bank_stats).
Tool Metadata and Instructions
Hindsight can append deployment-specific guidance to the retain and recall MCP tool descriptions. Set HINDSIGHT_API_MCP_INSTRUCTIONS on the API server when clients should see local rules, such as which tags to use or which memories should be retained.
export HINDSIGHT_API_MCP_INSTRUCTIONS="Use project:<name> tags for project-specific memories."MCP clients that read tool annotations also receive safety hints from the built-in tools:
- Read-only operations such as
recall,reflect,list_*, andget_*are marked withreadOnlyHint: true. - Delete, clear, and invalidate operations are marked with
destructiveHint: true. openWorldHintisfalsefor the built-in tools because Hindsight operates on its configured memory store rather than the open internet.- Write operations such as
retain,create_*,update_*,refresh_mental_model, andcancel_operationare not marked destructive.
Available Tools
retain
Store information to long-term memory.
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | Yes | The fact or memory to store |
context |
string | No | Category for the memory (default: general) |
timestamp |
string | No | ISO 8601 timestamp for when the event occurred |
tags |
list[string] | No | Tags for organizing and filtering this memory |
metadata |
object | No | Key-value metadata to attach (e.g., {"source": "slack"}) |
document_id |
string | No | Associate this memory with an existing document |
Example:
{
"name": "retain",
"arguments": {
"content": "User prefers Python over JavaScript for backend development",
"context": "programming_preferences",
"tags": ["user:alice", "preferences"]
}
}When to use:
- User shares personal facts, preferences, or interests
- Important events or milestones are mentioned
- Decisions, opinions, or goals are stated
- Work context or project details are discussed
sync_retain
Store information to long-term memory and wait for completion. Unlike retain (which is asynchronous), sync_retain blocks until the memory is fully stored and immediately available for recall — useful for read-after-write flows where you query right after storing.
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | Yes | The fact or memory to store |
context |
string | No | Category for the memory (default: general) |
timestamp |
string | No | ISO 8601 timestamp for when the event occurred |
tags |
list[string] | No | Tags for organizing and filtering this memory |
metadata |
object | No | Key-value metadata to attach (e.g., {"source": "slack"}) |
document_id |
string | No | Associate this memory with an existing document |
Example:
{
"name": "sync_retain",
"arguments": {
"content": "User prefers Python over JavaScript for backend development",
"context": "programming_preferences",
"tags": ["user:alice", "preferences"]
}
}When to use:
- You need the memory queryable immediately after storing (read-after-write)
- A workflow step depends on the stored memory being available before continuing
- Otherwise prefer
retain(asynchronous) to avoid blocking on storage
recall
Search memories to provide personalized responses.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | Natural language search query |
max_tokens |
integer | No | Token budget for the facts' text (default: 4096). Per-result metadata (id, tags, scores, dates) is not counted, so the response is several times larger |
budget |
string | No | Search thoroughness: low, mid, or high (default: high) |
types |
list[string] | No | Filter by fact type: world, experience, observation. Defaults to all |
tags |
list[string] | No | Filter memories by tags. Omit for no filter |
tags_match |
string | No | any (default), all, any_strict, all_strict, or exact. With exact, pass tags: [] to select the untagged/global scope |
tag_groups |
list[object] | No | Compound boolean tag filter. Mutually exclusive with tags; each leaf has its own match value |
query_timestamp |
string | No | ISO 8601 timestamp — recall as if asking at this point in time; anchors relative temporal expressions and recency scoring |
min_scores |
object | No | Optional per-stage score floors, e.g. {"reranker": 0.5}. Keys: semantic/keyword (retrieval-level cutoffs), reranker/final (post-ranking). All inclusive and AND-ed; omit for no filtering. Reranker scores aren't calibrated across queries — calibrate before use |
temporal_window |
object | No | An explicit {"start": ISO, "end": ISO} period to search over, used instead of reading dates out of the query text. Ranks memories dated inside the window higher; it does not drop the ones outside it, so it can't restrict results to a period |
include_chunks |
boolean | No | Also return the source text chunks the facts came from. Use it when you need exact strings (paths, IDs, commands) that extraction may have reworded. Default: false |
max_chunk_tokens |
integer | No | Max tokens of source chunks when include_chunks is set (default: 8192) |
include_entities |
boolean | No | Also return observations about the entities in the results. Default: false |
max_entity_tokens |
integer | No | Max tokens of entity observations when include_entities is set (default: 500) |
include_source_facts |
boolean | No | For observation results, also return the facts they were built from. Default: false |
max_source_facts_tokens |
integer | No | Max tokens of source facts when include_source_facts is set (default: 4096) |
Example:
{
"name": "recall",
"arguments": {
"query": "What are the user's programming language preferences?",
"tags": ["preferences"],
"budget": "high"
}
}When to use:
- Start of conversation to recall relevant context
- Before making recommendations
- When user asks about something they may have mentioned before
- To provide continuity across conversations
reflect
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | The question or topic to reflect on |
context |
string | No | Optional context about why this reflection is needed |
budget |
string | No | Search budget: low, mid, or high (default: low) |
max_tokens |
integer | No | Maximum tokens in the response (default: 4096) |
response_schema |
object | No | JSON Schema for structured output. When provided, the response includes a structured_output field |
tags |
list[string] | No | Scope memories, observations, mental models, and tagged directives. Omitted tags leave memory retrieval unfiltered but load only untagged directives |
tags_match |
string | No | any (default), all, any_strict, all_strict, or exact. Untagged directives remain global in every mode |
include_trace |
boolean | No | Include tool_trace and llm_trace debugging output. Defaults to false to keep responses small |
The MCP tool forwards tags_match only when tags is present. Passtags: [] with tags_match: "exact" to select the empty/global scope for raw
facts, observations, and mental models; directive loading also selects only
untagged directives.
Example:
{
"name": "reflect",
"arguments": {
"query": "Based on my past decisions, what architectural style do I prefer?",
"budget": "mid",
"tags": ["architecture"]
}
}When to use:
- When reasoned analysis is needed, not just fact retrieval
- Questions like "What should I do?" rather than "What did I say?"
- Synthesizing patterns across multiple memories
create_mental_model
Create a mental model — a living document that stays current with your memories. Mental models are pre-computed reflections that get automatically refreshed as new memories are stored.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable name for the mental model |
source_query |
string | Yes | The query used to generate and refresh the model |
mental_model_id |
string | No | Custom ID (alphanumeric lowercase with hyphens). Auto-generated if not provided |
tags |
list[string] | No | Tags for organizing and filtering models |
tags_match |
string | No | How the model's tags are matched against memories on refresh: any, all, any_strict, all_strict, or exact. See the note below on the default |
trigger |
object | No | Refresh policy — see Trigger settings |
max_tokens |
integer | No | Maximum tokens for model content (default: 2048) |
trigger_refresh_after_consolidation |
boolean | No | Legacy shorthand for trigger.refresh_after_consolidation |
trigger is the preferred form for refresh settings; tags_match andtrigger_refresh_after_consolidation remain as shorthands for existing
integrations. Setting the same field both ways is an error rather than one
silently winning.
:::warning Tagged models default to all_strict
When a mental model has tags but no explicit tags_match, its refresh matches memories with all_strict — a memory must carry every one of the model's tags to be included. If your memories use narrow, single-topic tags (e.g. ["project:status"]) while the model is tagged broadly (e.g. ["projects", "mental-model"]), the refresh filters out everything and the content comes back empty.
Pass tags_match: "any" (the same default that recall and reflect use) to match memories that carry any of the model's tags:
{
"name": "create_mental_model",
"arguments": {
"name": "Current projects",
"source_query": "Which projects is the user currently working on?",
"tags": ["projects", "mental-model"],
"tags_match": "any"
}
}:::
Example:
{
"name": "create_mental_model",
"arguments": {
"name": "Team Directory",
"source_query": "Who works here and what do they do?",
"tags": ["team", "people"],
"tags_match": "any"
}
}Content generation runs asynchronously. The response includes an operation_id to track progress.
list_mental_models
List all mental models in a bank, optionally filtered by tags.
Returns metadata only — id, name, tags, is_stale and timestamps. To read a
model's synthesized content, call get_mental_model with an id from this list.
| Parameter | Type | Required | Description |
|---|---|---|---|
tags |
list[string] | No | Filter models by tags |
get_mental_model
Retrieve a specific mental model by ID, including its full content.
| Parameter | Type | Required | Description |
|---|---|---|---|
mental_model_id |
string | Yes | The ID of the mental model to retrieve |
update_mental_model
Update a mental model's metadata or settings.
| Parameter | Type | Required | Description |
|---|---|---|---|
mental_model_id |
string | Yes | The ID of the mental model to update |
name |
string | No | New name |
source_query |
string | No | New source query |
tags |
list[string] | No | New tags |
max_tokens |
integer | No | New max tokens |
trigger |
object | No | Refresh-policy fields to change — see Trigger settings. This is a patch: omitted fields keep their current values |
tags_match |
string | No | Legacy shorthand for trigger.tags_match |
trigger_refresh_after_consolidation |
boolean | No | Auto-refresh after consolidation. Only set when you want to change this setting |
Trigger settings
trigger carries a mental model's (or knowledge page's) refresh policy: when
it rebuilds itself and what it rebuilds from. It accepts every field the HTTP
API accepts, so anything you can configure through PATCH /mental_models/{id}
you can also configure from an agent over MCP.
| Field | Type | Description |
|---|---|---|
mode |
string | full regenerates the content from scratch on each refresh; delta makes surgical edits, preserving unchanged sections byte-for-byte. Delta falls back to full when there is no existing content or the source query changed |
refresh_after_consolidation |
boolean | Rebuild after each memory consolidation |
refresh_cron |
string | UTC five-field cron, e.g. 0 3 * * * for daily at 03:00 UTC. Only runs when the model is stale, so an unchanged scope costs no LLM call |
full_refresh_cron |
string | UTC five-field cron for a scheduled full refresh of a delta model, e.g. 0 3 * * 0. Independent of the two triggers above; skipped when no refresh ran since the last full one |
min_refresh_interval_seconds |
integer | Floor between two automatic refreshes. Triggers arriving sooner fold into one queued refresh, so a burst of retains costs one rebuild. Explicit refreshes ignore it |
fact_types |
list[string] | Which of world, experience, observation the refresh retrieves. Omit for all three |
exclude_mental_models |
boolean | Exclude all mental models from the refresh, so a model never reflects on its siblings |
exclude_mental_model_ids |
list[string] | Exclude specific mental models by ID |
tags_match |
string | How the model's tags select memories: any, all, any_strict, all_strict, exact |
tag_groups |
list[object] | Compound boolean tag expressions used instead of the model's flat tags |
include_chunks |
boolean | Whether the refresh's internal recall returns raw chunk text |
recall_max_tokens |
integer | Token budget for facts from the refresh's internal recall |
recall_chunks_max_tokens |
integer | Token budget for raw chunks from the refresh's internal recall |
budget |
string | How many agent steps a refresh may spend, as a multiple of the server's reflect iteration limit: low halves it, mid keeps it, high doubles it. Omit for mid |
response_schema |
object | JSON Schema for structured output, stored alongside the markdown under reflect_response.structured_output |
keep_trace |
boolean | Record how each refresh reached its result under reflect_response.trace. The only way to diagnose a cron- or consolidation-driven refresh after the fact |
refresh_after_consolidation and refresh_cron are mutually exclusive — a
model refreshes either after consolidation or on a schedule, never both. Setting
one clears the other.
On create, omitted fields take the engine defaults (mode: full, no
schedule, all fact types); for a knowledge page they take the page defaults
(mode: delta, observation-only, auto-refresh, siblings excluded).
On update, trigger is a patch: only the fields you send change, so putting
a model on a cron schedule keeps its fact_types and mode. Send an explicitnull to clear a nullable setting. Read the current policy back withget_mental_model (detail content or full) if you want to inspect before
changing.
{
"name": "update_mental_model",
"arguments": {
"mental_model_id": "team-conventions",
"trigger": { "refresh_cron": "0 3 * * *", "fact_types": ["observation"] }
}
}delete_mental_model
Permanently delete a mental model.
| Parameter | Type | Required | Description |
|---|---|---|---|
mental_model_id |
string | Yes | The ID of the mental model to delete |
refresh_mental_model
Re-generate a mental model's content from the latest memories. Runs asynchronously.
| Parameter | Type | Required | Description |
|---|---|---|---|
mental_model_id |
string | Yes | The ID of the mental model to refresh |
clear_mental_model
Clear a mental model's content while keeping its definition. After clearing, call refresh_mental_model to rebuild it from the latest memories.
| Parameter | Type | Required | Description |
|---|---|---|---|
mental_model_id |
string | Yes | The ID of the mental model to clear |
list_banks (multi-bank mode only)
List available memory banks, most recently written first. The response carries the
total number of matching banks alongside the page, so large deployments can be
walked with offset.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | No | Case-insensitive substring matched against bank ID and name |
limit |
integer | No | Maximum number of banks to return (default: 100) |
offset |
integer | No | Number of banks to skip (default: 0) |
create_bank (multi-bank mode only)
Create a new memory bank or retrieve an existing one.
| Parameter | Type | Required | Description |
|---|---|---|---|
bank_id |
string | Yes | The ID for the new bank |
name |
string | No | Human-friendly name for the bank |
mission |
string | No | Mission describing who the agent is and what they're trying to accomplish |
list_directives
List directives in a bank. This management tool does not use reflect's
directive-isolation behavior: omitting tags lists every directive, including
tagged directives.
| Parameter | Type | Required | Description |
|---|---|---|---|
tags |
list[string] | No | Filter using any matching. When present, returns untagged/global directives plus directives sharing at least one tag. When omitted or empty, returns all directives |
active_only |
boolean | No | Only return active directives (default: true) |
create_directive
Create a new directive in a bank.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable name for the directive |
content |
string | Yes | The directive content/instruction |
priority |
integer | No | Priority level (higher = more important) |
is_active |
boolean | No | Whether the directive is active (default: true) |
tags |
list[string] | No | Execution scope for the directive. Empty/omitted (default) means global; non-empty means reflect must use a matching tag scope |
delete_directive
Delete a directive by ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
directive_id |
string | Yes | The ID of the directive to delete |
list_memories
Browse stored memories with optional filtering and pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | No | Filter by fact type: world, experience, or observation |
q |
string | No | Search query to filter memories |
limit |
integer | No | Maximum number of results (default: 100) |
offset |
integer | No | Number of results to skip for pagination (default: 0) |
get_memory
Retrieve a specific memory by ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
memory_id |
string | Yes | The ID of the memory to retrieve |
list_documents
List documents that have been ingested into the memory bank.
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | Search query to filter documents |
limit |
integer | No | Maximum number of results (default: 100) |
offset |
integer | No | Number of results to skip for pagination (default: 0) |
get_document
Retrieve a specific document by ID, including its metadata.
| Parameter | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | The ID of the document to retrieve |
delete_document
Delete a document and all memories linked to it.
| Parameter | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | The ID of the document to delete |
list_operations
List async operations (retain processing, mental model refresh, etc.) with optional status and type filtering.
| Parameter | Type | Required | Description |
|---|---|---|---|
status |
string | No | Filter by status: pending, running, completed, failed, cancelled |
type |
string | No | Filter by type: retain, consolidation, refresh_mental_model, file_convert_retain, webhook_delivery |
limit |
integer | No | Maximum number of results, 1–100 (default: 20) |
offset |
integer | No | Number of results to skip for pagination (default: 0) |
exclude_parents |
boolean | No | Exclude parent batch operations (default: false) |
get_operation
Get the status and details of an async operation.
| Parameter | Type | Required | Description |
|---|---|---|---|
operation_id |
string | Yes | The ID of the operation to check |
cancel_operation
Cancel a pending or running async operation.
| Parameter | Type | Required | Description |
|---|---|---|---|
operation_id |
string | Yes | The ID of the operation to cancel |
list_tags
List all unique tags used in a bank, optionally filtered by pattern.
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | Glob pattern to filter tags (e.g., project:*) |
limit |
integer | No | Maximum number of results (default: 100) |
offset |
integer | No | Number of results to skip for pagination (default: 0) |
get_bank
Get information about a memory bank, including its name, mission, and disposition.
get_bank_stats (multi-bank mode only)
Get statistics for a memory bank (node/link counts).
update_bank
Update a memory bank's configuration. Updates the bank's name and/or any bank-level configuration fields — only provided fields are updated; omitted fields remain unchanged.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | No | Human-friendly display name for the bank |
mission |
string | No | Deprecated — alias for config_updates.reflect_mission |
config_updates |
object | No | Dictionary of configuration fields to update. Supports all bank-configurable fields (see below). Non-configurable or credential fields are rejected |
The config_updates object accepts any bank-configurable field by its Python field name, including:
reflect_mission— mission/context for Reflect operationsretain_mission— steers what gets extracted duringretain()retain_extraction_mode—concise(default),verbose,custom,verbatim, orchunksretain_custom_instructions— custom extraction prompt (active when mode iscustom)retain_chunk_size— target maximum characters for each content chunkretain_structured_chunk_size— maximum characters for a single JSONL line or conversation turn to keep wholeretain_chunk_batch_size— number of chunks to process in parallelenable_observations— toggle observation consolidation afterretain()observations_mission— controls observation synthesis rulesdisposition_skepticism— critical evaluation level (1–5)disposition_literalism— literal vs. abstract interpretation (1–5)disposition_empathy— emotional context consideration (1–5)entity_labels— controlled vocabulary for entity classificationentities_allow_free_form— allow labels outsideentity_labelsrecall_include_chunks— include raw chunks in recall resultsrecall_prefer_observations— defaultprefer_observationsfor recall calls that omit itrecall_max_tokens— max tokens for recall resultsmcp_enabled_tools— tool allowlist for this bank
delete_bank
Permanently delete a memory bank and all its data (memories, documents, entities, mental models).
clear_memories
Clear all memories from a bank without deleting the bank itself. Optionally filter by fact type to only clear specific kinds of memories.
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | No | Fact type to clear: world, experience, or observation. If not specified, clears all |
get_knowledge_base_tree
Browse the knowledge base as a nested tree of folders and pages. Each page reports is_stale: false means it is provably up to date, true means the bank has been written to since the page last refreshed.
search_knowledge_base
Find knowledge pages by relevance (hybrid BM25 + vector search over page names and content). Returns ranked pages with a snippet each.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | What to search for |
limit |
integer | No | Maximum pages to return, 1–50 (default: 10) |
get_knowledge_page
Read a knowledge page as a markdown document (YAML frontmatter + synthesized body).
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id |
string | Yes | The ID of the page to read (a kp-... node id) |
create_knowledge_folder
Create a folder in the knowledge base. Folders group pages and hold no content of their own.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Folder name |
parent_id |
string | No | Parent folder id (a kf-... node id). Omit to create at the top level |
create_knowledge_page
Create a page — a living document whose content is synthesized from the bank's memories by running source_query. Content is generated asynchronously; use the returned operation_id to track completion.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Page name (unique within its folder) |
source_query |
string | Yes | The question this page answers and rebuilds itself from |
parent_id |
string | No | Parent folder id (a kf-... node id). Omit to create at the top level |
tags |
array | No | Tags scoping which memories the page is built from |
max_tokens |
integer | No | Maximum tokens for the generated content (default: 4096) |
trigger |
object | No | Refresh policy — see Trigger settings. Omitted fields keep the knowledge-page defaults: delta rebuilds from consolidated observations after each consolidation, ignoring sibling pages |
refresh_after_consolidation |
boolean | No | Legacy shorthand for trigger.refresh_after_consolidation |
Set trigger.refresh_cron to move a page off consolidation-driven rebuilds and
onto a fixed UTC schedule — the two are mutually exclusive, so the cron clears
the auto-refresh while leaving the page's mode and fact_types alone.
update_knowledge_node
Rename or move a folder/page, and/or update a page's options. Only the arguments you pass are changed. Changing source_query schedules an async refresh so the page rebuilds against the new question.
| Parameter | Type | Required | Description |
|---|---|---|---|
node_id |
string | Yes | The folder (kf-...) or page (kp-...) to update |
name |
string | No | New name for the node |
parent_id |
string | No | Folder id to move the node into, or "root" to move it to the top level |
source_query |
string | No | Pages only — the new question the page answers |
tags |
array | No | Pages only — replacement tag list (pass [] to clear) |
max_tokens |
integer | No | Pages only — new maximum tokens for the generated content |
trigger |
object | No | Pages only — refresh-policy fields to change; see Trigger settings. This is a patch: omitted fields keep their current values |
refresh_after_consolidation |
boolean | No | Pages only — legacy shorthand for trigger.refresh_after_consolidation |
delete_knowledge_node
Delete a folder or page and its whole subtree. Each deleted page takes its backing mental model with it.
| Parameter | Type | Required | Description |
|---|---|---|---|
node_id |
string | Yes | The folder (kf-...) or page (kp-...) to delete |
:::note
Exporting the knowledge base is deliberately not an MCP tool — it returns the whole
bank as a single markdown bundle. Use the HTTP endpointGET /v1/default/banks/{bank_id}/knowledge-base/export instead.
:::
Integration with AI Assistants
The MCP server can be used with any MCP-compatible AI assistant. See the Authentication section above for Claude Code and Claude Desktop configuration examples.
Each user can have their own configuration pointing to their personal memory bank using either:
- A bank-specific URL path like
/mcp/alice/(recommended) - The
X-Bank-Idheader
Hindsight Docs Docs Developer Mental Models (`hindsight-docs/docs/developer/mental-models.mdx`)
sidebar_position: 6
import Figure from '@site/src/components/Figure';
import mentalModelsFigure from '@site/figures/mental-models.json';
Mental Models
A mental model is a standing answer to a question about a bank. You define the question once; Hindsight writes the answer, keeps it stored, and rewrites it in the background as the bank learns more.
Where observations are produced automatically and are atomic — one belief at a time — a mental model is deliberately curated: you decide which questions deserve a permanent, always-current answer.
The Answer Is Already Written
The reason to use a mental model is speed. Reasoning over a bank's memory is expensive — retrieval, synthesis, an LLM writing an answer. A mental model moves all of that off the request path: the work happens in the background, ahead of time, and your application simply reads the current version.
Fetching a mental model is a database read. No retrieval, no synthesis, no LLM call, no waiting. An agent that boots by loading its mental models starts with a page of settled knowledge instead of spending its first few seconds rediscovering it.
This also makes answers consistent. Two users asking the same question get the same document, because there is only one document — not two independently generated answers that happen to disagree on the details.
Mental models are also the first thing reflect reaches for. Its retrieval ladder goes:
| Layer | Produced by | Granularity |
|---|---|---|
| Mental models | You, explicitly | A whole document per question |
| Observations | Consolidation, automatically | One belief per fact cluster |
| Raw facts | Retain, automatically | One fact per statement |
Each layer is a cheaper, more settled version of the one below it. If the first step turns up a mental model that is fresh and covers the question, reflect can answer from it instead of descending through observations and raw facts — the same saving, applied inside the agentic loop.
Always Current, Without Asking
A mental model is not a cached answer that goes stale silently. Hindsight tracks whether new memories have arrived that the model is supposed to cover, and rebuilds it when they have — either as soon as new knowledge is consolidated, or on a schedule you set.
The check comes first, and it is scoped: a rebuild only happens when something within this model's own scope actually changed. A busy bank does not cause unrelated models to churn, and a scheduled rebuild over an unchanged bank costs nothing.
When a model is handed to the reflect agent, it comes with a freshness signal — whether memories in its scope have landed since it was last written. A model that has fallen behind is still shown, but it no longer short-circuits retrieval, and the agent is expected to check it against the layers below rather than trust it blindly.
Stable Across Rewrites
A document that is rewritten hundreds of times has a problem an LLM cannot solve by being asked nicely: told to "preserve the unchanged parts", it will still drift. Bullets become numbers, casing shifts, sentences get quietly paraphrased. Generating text is what the model does; copying it verbatim is not.
Hindsight can instead refresh a model incrementally — applying only the changes the new knowledge implies, and leaving everything else physically untouched rather than regenerating and hoping. A long-lived playbook stays the document you wrote, with the new parts added, instead of slowly becoming a different document that says roughly the same thing.
Scope and Isolation
A mental model's tags decide two things: which memories it is allowed to read, and which callers are allowed to see it. A model scoped to one customer, team, or user is built only from that scope's memories and surfaces only for requests in that scope — the same isolation rules that govern the rest of the bank, applied to synthesized knowledge.
Provenance
A mental model is not free-floating prose. It records the facts and observations it was built from, and it keeps the previous version of its content every time it changes. You can see what the model said last month and what evidence it was standing on — which matters when a model states something surprising and someone needs to know where it came from.
See also: Mental Models API — creating, refreshing, and configuring them, including refresh triggers, scoping options, and history.
Hindsight Docs Docs Developer Models (`hindsight-docs/docs/developer/models.mdx`)
import {LLMProvidersGrid} from '@site/src/components/SupportedGrids';
import {LLMProvidersTable} from '@site/src/components/LLMProvidersTable';
import {LLMProviderCapabilities} from '@site/src/components/LLMProviderCapabilities';
Models
Hindsight uses several machine learning models for different tasks.
Overview
- LLM — Fact extraction, reasoning, and generation. Provider-specific, fully configurable.
- Embedding — Vector representations for semantic search. Default:
BAAI/bge-small-en-v1.5. - Cross-Encoder — Reranking search results. Default:
cross-encoder/ms-marco-MiniLM-L-6-v2.
Embedding and cross-encoder models are downloaded automatically from HuggingFace on first run.
LLM
Used for fact extraction, entity resolution, mental model consolidation, and answer synthesis.
Supported providers:
Also supports any OpenAI-compatible API (e.g., Azure OpenAI, Together AI, Fireworks) and 100+ providers via LiteLLM (e.g., AWS Bedrock, Azure OpenAI, Together AI).
:::tip OpenAI-Compatible Providers
Hindsight works with any provider that exposes an OpenAI-compatible API. Set HINDSIGHT_API_LLM_PROVIDER=openai and point HINDSIGHT_API_LLM_BASE_URL at the endpoint that serves /chat/completions — for most providers that is the URL ending in /v1, not the account or resource root.
Azure OpenAI does not serve the API at the resource root, so https://<resource>.openai.azure.com on its own returns 404 Resource not found. See Azure OpenAI Setup for the two URL shapes that work.
The openai provider talks to the Chat Completions API (/v1/chat/completions). For the newer Responses API (/v1/responses), use HINDSIGHT_API_LLM_PROVIDER=openai-responses — see the tip below. Both accept a custom HINDSIGHT_API_LLM_BASE_URL, so an OpenAI-compatible endpoint that exposes /v1/responses works the same way as a Chat Completions one.
See Configuration for setup examples.
:::
:::tip OpenAI Responses API (reasoning + tools together)
Set HINDSIGHT_API_LLM_PROVIDER=openai-responses to call OpenAI's Responses API (/v1/responses) instead of Chat Completions.
Why it exists: some reasoning models — e.g. gpt-5.6-terra — reject reasoning_effort when function tools are present on Chat Completions (HTTP 400 unless reasoning_effort="none"). Reflect is a tool-calling loop, so on the openai (Completions) provider that forces the whole operation — including the final synthesis — to run with reasoning disabled. (Hindsight retries that 400 once with reasoning_effort="none" so the call still succeeds — but it succeeds without reasoning.) The Responses API keeps the model's chain-of-thought as a first-class reasoning item, so reasoning and tools coexist: reflect's search loop runs with a real HINDSIGHT_API_LLM_REASONING_EFFORT (e.g. high).
Recommended for reasoning models (gpt-5.x, o-series) that use tools. It also honors a custom HINDSIGHT_API_LLM_BASE_URL, so any OpenAI-compatible endpoint exposing /v1/responses (gateways, Azure-style deployments) can be used just like the Chat Completions path.
See Configuration for setup examples.
:::
:::info Reasoning/thinking models and max_tokens
On a thinking model (Gemini 2.5+/3.x, GPT-5/o-series, Grok reasoning, Claude extended thinking) the provider's output budget covers reasoning tokens plus visible output — the reasoning is billed against the same max_output_tokens/max_completion_tokens cap. A small cap can therefore be fully consumed by reasoning, leaving the visible answer truncated mid-word.
Hindsight keeps the reflect/mental-model max_tokens meaning visible page length: it is applied as a prompt-level target plus a post-hoc rewrite, not as a hard cap on the provider call. Reflect's synthesis call is uncapped by default so reasoning never starves the answer. If you want a hard cost ceiling on that call, set HINDSIGHT_API_REFLECT_MAX_COMPLETION_TOKENS — but leave enough headroom above your page length for reasoning, or thinking models will truncate again.
When a Gemini call does hit its cap, Hindsight logs a truncated at max_output_tokens warning instead of returning the half-written text as a silent success.
:::
:::tip AWS Bedrock
Set HINDSIGHT_API_LLM_PROVIDER=bedrock to use AWS Bedrock models directly. Model names use Bedrock model IDs (e.g., us.amazon.nova-2-lite-v1:0). No API key is required — authentication uses AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION_NAME) or IAM roles. For 50% cost savings on throughput, set HINDSIGHT_API_LLM_BEDROCK_SERVICE_TIER=flex (see Configuration).
See Configuration for setup examples.
:::
:::tip Built-in llama.cpp (fully local, no API key)
Set HINDSIGHT_API_LLM_PROVIDER=llamacpp to run a built-in llama.cpp server with no external dependencies. A Gemma 4 E2B GGUF model (~3.5 GB) is auto-downloaded on first run. Requires the local-llm extra: pip install 'hindsight-api-slim[local-llm]'.
The published Docker image does not bundle llama-cpp-python (to keep the image small). For a runnable Docker setup that adds it on top, see docker/docker-compose/local-llm/.
See Configuration for all options.
:::
:::tip LiteLLM Provider (Azure, Together AI, and more)
Set HINDSIGHT_API_LLM_PROVIDER=litellm to use any model supported by LiteLLM, including Azure OpenAI, Together AI, Fireworks AI, and many more. Model names use LiteLLM's provider prefix format (e.g., azure/gpt-4o).
See Configuration for setup examples.
:::
:::tip LiteLLM Router (fallback chains, load-balancing, per-deployment limits)
Set HINDSIGHT_API_LLM_PROVIDER=litellmrouter to run the default LLM through LiteLLM's Router — ordered fallback across deployments, load-balanced same-tier routing, weighted picks, per-deployment rpm/tpm limits, and cooldowns are all available via the Router config. Hindsight passes the JSON config through verbatim.
See Configuration for setup.
:::
Provider Capabilities
Beyond basic generation, some providers support optional features that lower cost or latency. Hindsight uses each feature automatically when the configured provider supports it.
- Batch API — submits bulk retain extraction through the provider's asynchronous batch endpoint, typically at ~50% lower cost. Used automatically when available; otherwise calls run synchronously.
- Explicit prompt caching — reuses the large, fixed system prefix that retain (fact extraction), consolidation, and the reflect tool-loop send on every call, billing it at the provider's cached-input rate. On Gemini/Vertex this uses the
CachedContentAPI. On by default; disable withHINDSIGHT_API_LLM_PROMPT_CACHE_ENABLED=false. Hindsight structures these prompts so the cached prefix is bank-agnostic — one cache is shared across all banks rather than one per bank/mission, and creation soft-fails to an uncached call, so it never breaks a request.
:::note
A blank "Explicit prompt caching" cell does not mean a provider has no caching. OpenAI, for example, caches a stable leading prompt prefix automatically server-side, so it benefits with no configuration; Anthropic supports caching via cache_control breakpoints which can be wired up through the same provider hook. The column tracks only Hindsight's explicit get_or_create_cached_prefix hook, which Gemini/Vertex implement today.
:::
Benchmarks
Not sure which model to use? The Model Leaderboard benchmarks models across accuracy, speed, cost, and reliability for retain, reflect, and observation consolidation so you can pick the right trade-off for your use case.
Tested Models
The following models have been tested and verified to work correctly with Hindsight:
| Provider | Model |
|---|---|
| OpenAI | gpt-5.2 |
| OpenAI | gpt-5 |
| OpenAI | gpt-5-mini |
| OpenAI | gpt-5-nano |
| OpenAI | gpt-4.1-mini |
| OpenAI | gpt-4.1-nano |
| OpenAI | gpt-4o-mini |
| Anthropic | claude-sonnet-4-20250514 |
| Anthropic | claude-3-5-sonnet-20241022 |
| Gemini | gemini-3.5-flash |
| Gemini | gemini-3.1-pro-preview |
| Gemini | gemini-3.1-flash-lite |
| Groq | openai/gpt-oss-120b |
| Groq | openai/gpt-oss-20b |
| Meta | muse-spark-1.3 |
Provider Default Models
Each provider has a recommended default model that's used when HINDSIGHT_API_LLM_MODEL is not explicitly set. This makes configuration simpler - just specify the provider and get a sensible default:
Example: Setting just the provider uses its default model:
# Uses claude-haiku-4-5 automatically
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxxYou can override the default by explicitly setting HINDSIGHT_API_LLM_MODEL:
# Override to use Sonnet instead
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-5-20250929This also applies to per-operation overrides:
# Global: OpenAI gpt-4o-mini (default)
export HINDSIGHT_API_LLM_PROVIDER=openai
# Retain: Anthropic claude-haiku-4-5 (default)
export HINDSIGHT_API_RETAIN_LLM_PROVIDER=anthropicUsing Other Models
Other LLM models not listed above may work with Hindsight, but they must support at least 65,000 output tokens to ensure reliable fact extraction. If you need support for a specific model that doesn't meet this requirement, please open an issue to request an exception.
:::tip Models with Limited Output Tokens
If your model only supports 32k or fewer output tokens (e.g., some older models), you can reduce the retain completion token limit:
# For models that support 32k output tokens
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=32000
# For models that support 16k output tokens
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=16000Important: HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS must be greater than HINDSIGHT_API_RETAIN_CHUNK_SIZE (default: 3000). The system will validate this on startup and provide an error message if the configuration is invalid.
:::
:::warning Groq free tier is not suitable for Hindsight
Groq's free tier only allows 8,000 tokens per minute — far below what Hindsight needs for a single retain call (~64k). Free-tier Groq models therefore can't be used with Hindsight; use a paid Groq tier or a different provider.
:::
Configuration
/* Detailed source-code truncated for AI context efficiency. */Note: The LLM is the primary bottleneck for retain operations. See Performance for optimization strategies.
GitHub Copilot Setup
Use a GitHub Copilot subscription for Hindsight's extraction, consolidation, and reflection calls through the official GitHub Copilot SDK.
Prerequisites:
- An active GitHub Copilot entitlement
- Copilot CLI signed in under the same operating-system user that runs Hindsight
Configure Hindsight:
export HINDSIGHT_API_LLM_PROVIDER=github-copilot
export HINDSIGHT_API_LLM_MODEL=gpt-5.6-terra
# No HINDSIGHT_API_LLM_API_KEY is needed.The provider starts one shared Copilot runtime for all Hindsight LLM lanes. That runtime uses a hook-free temporary COPILOT_HOME containing only the signed-in account selection; the credential itself is resolved by the runtime from the system keychain or an existing gh CLI login. Each call then uses an isolated transient session with repository instructions, skills, Copilot memory, built-in tools, and the cross-session store disabled. This prevents the memory integration from recursively retaining its own extraction calls.
For automation, the Copilot SDK also accepts COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN. With one of those set, Copilot CLI never has to have been run on the host, so containers and CI images work with no ~/.copilot at all. GitHub Actions and server-to-server deployments must have the appropriate Copilot organization policy and token permissions.
Important notes:
- Usage counts against the authenticated account or organization's Copilot allowance and AI Credits.
HINDSIGHT_API_LLM_BASE_URLoptionally points to an existing headless Copilot runtime, such ashttp://127.0.0.1:4321; it is not an LLM-provider endpoint for this provider.- GitHub-hosted Copilot sessions do not expose temperature or maximum-output-token controls through the SDK, so those Hindsight settings are not applied.
- Embeddings and reranking continue to use Hindsight's separately configured providers; their local defaults require no API key.
ChatGPT Setup (Sign in with ChatGPT)
The chatgpt provider runs Hindsight's LLM calls on your ChatGPT plan through
OpenAI's documented Sign in with ChatGPT flow for open-source apps.
Hindsight registers itself as its own OAuth client on first sign-in, and every call goes to the publichttps://api.openai.com/v1/responses endpoint with your OAuth access token. It does not use an API key.
How it differs from openai-codex: that provider reuses the Codex CLI's sign-in and calls ChatGPT's privatechatgpt.com/backend-api/codex endpoint. chatgpt uses the supported route and keeps its own
credentials, so it never shares or rotates the Codex CLI's tokens.
Prerequisites:
- A ChatGPT plan that allows plan usage in third-party apps
- A browser on the machine where you run the login
Setup on a machine with a browser (for example a laptop):
Sign in:
bashhindsight-admin chatgpt loginThis opens your browser and waits for the callback on
http://127.0.0.1:1455/auth/callback
(--portchanges the port;--no-browserprints the URL instead). Approve the request, including
use of your ChatGPT plan. The command validates the ID token and the granted scopes, then writes~/.hindsight/chatgpt/credentials.json(mode0600) next to this host'shost_id.
If you declined plan usage, runlogin --enable-plan-usageto be asked again.Configure Hindsight:
bashexport HINDSIGHT_API_LLM_PROVIDER=chatgpt # export HINDSIGHT_API_LLM_MODEL=gpt-6-luna # the default # export HINDSIGHT_API_LLM_REASONING_EFFORT=medium # sent as reasoning.effortCheck the stored session (prints account, client id, scopes and expiry, never token values):
bashhindsight-admin chatgpt status
Self-hosted server or VM (no browser): the 127.0.0.1 callback reaches the machine running the
browser, so sign in on your laptop and move the credential file to the server.
- On the laptop: run
loginas above. - Copy the file over SSH and install it on the server with
login --from-file, as the user that runs Hindsight (add--home <dir>beforeloginif the server setsHINDSIGHT_API_LLM_CHATGPT_HOME).--from-filekeeps the server's ownhost_id(creating one on first use) instead of the laptop's:bashscp ~/.hindsight/chatgpt/credentials.json server:/tmp/chatgpt.json ssh server 'hindsight-admin chatgpt login --from-file /tmp/chatgpt.json && rm /tmp/chatgpt.json' - On the laptop, stop using that copy without revoking it:Refresh tokens rotate, so after the copy only the server may use and refresh this session. A plainbash
hindsight-admin chatgpt logout --local-onlylogoutwould revoke it and end the server's session too.--local-onlyclears the laptop's
tokens but keeps its client registration for a laterlogin.--from-filerefuses to replace a
different account's registration on the server unless you pass--force.
The server then owns refreshes. Sign in again on the laptop later if you also want Hindsight there.
Important notes:
- Access tokens last one hour and are refreshed automatically (five minutes before expiry, and once on
an HTTP 401). Refresh tokens rotate; refreshes take a file lock and re-read the file first, so
several Hindsight processes can share one credential directory. - When the refresh token is no longer valid, calls fail with the
logincommand to run. - On ChatGPT Plus, the five-hour usage limit is shared with every other app that uses your plan,
including Codex. When the limit is reached, Hindsight defers retain and consolidation work and
retries it 30 minutes later instead of failing it. Review usage and per-app limits at
ChatGPT Settings → Usage. - The route is a preview: Hindsight sends
store: falseandstream: true, omits fields it rejects
(temperature,max_output_tokens,user, and others), and sends system prompts as developer
messages. logoutrevokes the session and clears local tokens but keeps the client registration and host id
for the nextlogin.logout --local-onlyclears the tokens without revoking.HINDSIGHT_API_LLM_CHATGPT_HOMErelocates the credential directory. Use one directory per
ChatGPT account. The directory must be writable, because each refresh writes the rotated token
back; in a container, mount a writable volume there. If a write fails, the process keeps the new
tokens in memory, but other processes and restarts will not see them.
OpenAI Codex Setup (ChatGPT Plus/Pro)
Use your ChatGPT Plus or Pro subscription for Hindsight without separate OpenAI Platform API costs.
Prerequisites:
- Active ChatGPT Plus or Pro subscription
- Node.js/npm installed (for Codex CLI)
Setup Steps:
Install Codex CLI:
bashnpm install @openai/codexLogin with ChatGPT credentials:
bashcodex auth loginThis opens a browser window to authenticate with your ChatGPT account and saves OAuth tokens to
~/.codex/auth.json.Verify authentication:
bashls ~/.codex/auth.json # Should show the auth file existsConfigure Hindsight:
bashexport HINDSIGHT_API_LLM_PROVIDER=openai-codex # export HINDSIGHT_API_LLM_MODEL=gpt-5.6-luna # the default # No API key needed - reads from ~/.codex/auth.json automaticallyStart Hindsight:
bashhindsight-api
You can use any model supported by OpenAI Codex CLI
Important Notes:
- OAuth tokens are stored in
~/.codex/auth.json - Tokens refresh automatically when needed
- Usage is billed to your ChatGPT subscription (not separate API costs)
- For personal development use only (see ChatGPT Terms of Service)
Isolating Codex auth for long-running services
By default Hindsight reads Codex credentials from ~/.codex/auth.json — the
same file the @openai/codex CLI, editor plugins, and other agent runtimes use.
This is convenient for local development but can cause a subtle failure mode when
Hindsight runs as a long-lived service (systemd unit, container, background
daemon) alongside another Codex process:
- Codex refresh tokens are single-use and rotate on refresh.
- If another process refreshes the shared token, Hindsight's long-running
process is left holding a stale refresh token. - Recall and
/healthkeep working (the database and API are fine), but/reflectfails with an error such as:textCodex refresh_token is permanently invalid (error.code=refresh_token_reused). Run 'codex auth login' to re-authenticate.
To avoid this, give the Hindsight service its own dedicated Codex auth home
via the CODEX_HOME environment variable. Hindsight honors CODEX_HOME exactly
like the @openai/codex CLI: when set, it reads $CODEX_HOME/auth.json instead
of ~/.codex/auth.json.
# Dedicated credentials directory for the Hindsight service
export CODEX_HOME=/var/lib/hindsight/codex
# One-time login into that isolated home (opens a browser / device-code flow)
codex auth login # writes $CODEX_HOME/auth.json
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
hindsight-apiFor a systemd unit, set it in the service definition so it never shares auth
with an interactive Codex session:
[Service]
Environment=CODEX_HOME=/var/lib/hindsight/codexAfter a fresh login into the dedicated home and restarting only the Hindsight
service, /reflect uses its own token that other Codex processes will not
rotate out from under it.
CODEX_HOME is also honored by the openai-codex embeddings provider.
Two Codex profiles in one process
CODEX_HOME is process-wide, so every Codex provider a Hindsight process builds
resolves the same auth.json. To run more than one independently authorized
ChatGPT account — for example, to fail over when the preferred account hits its
usage limit — give each one its own credentials directory withHINDSIGHT_API_LLM_CODEX_HOME (primary) and HINDSIGHT_API_LLM_<n>_CODEX_HOME
(indexed multi-LLM chain
members). Each falls back to CODEX_HOME, then ~/.codex, when unset.
# Two profiles, logged in separately
CODEX_HOME=/var/lib/hindsight/codex-a codex auth login
CODEX_HOME=/var/lib/hindsight/codex-b codex auth login
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
export HINDSIGHT_API_LLM_CODEX_HOME=/var/lib/hindsight/codex-a
export HINDSIGHT_API_LLM_1_PROVIDER=openai-codex
export HINDSIGHT_API_LLM_1_CODEX_HOME=/var/lib/hindsight/codex-b
export HINDSIGHT_API_LLM_STRATEGY='{"mode": "failover"}'Token refresh is coordinated per auth-file path, so the two profiles refresh
independently and never overwrite each other's tokens. Failover is the generic
multi-LLM behaviour: a member is tried after the previous one has exhausted its
own retries and raised — there is no separate quota classifier or cooldown, so a
rate-limited primary is re-tried (and fails) at the head of each request before
the fallback serves it.
Meta Model API Setup
Meta Model API serves the Muse Spark models over an
OpenAI-compatible endpoint. Get a key from the Model API dashboard, then:
export HINDSIGHT_API_LLM_PROVIDER=meta
export HINDSIGHT_API_LLM_API_KEY=your-meta-model-api-key
export HINDSIGHT_API_LLM_MODEL=muse-spark-1.3The base URL defaults to https://api.meta.ai/v1. Available models aremuse-spark-1.3 (recommended), muse-spark-1.2, muse-spark-1.1, and the
discounted -contributor variants of 1.3 and 1.2, which permit training on your
prompts and completions. All share a 1,048,576-token context window.
Required knobs
Muse Spark always reasons before it replies. That single property drives every
setting below, so treat these as required rather than optional tuning:
| Variable | Set it to | Why |
|---|---|---|
HINDSIGHT_API_REFLECT_LLM_TIMEOUT |
300 |
Reflect's default is 30s. Muse Spark's final synthesis exceeds that, and the call fails after its retries rather than degrading — reflect returns nothing. |
HINDSIGHT_API_LLM_TIMEOUT |
300 |
The global deadline (default 120s) covers retain and consolidation, which are slower here than on a non-reasoning model. |
HINDSIGHT_API_LLM_REASONING_EFFORT |
unset, or minimal/low/medium/high/xhigh |
none is rejected with HTTP 400. Leave it unset to let the model choose its own depth. |
HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS |
leave at the 64000 default |
Reasoning tokens are billed against the output budget. Lower this too far and a reply comes back with no content at all. |
Good to know
- Prompt caching is automatic. There is no key, flag, or breakpoint to set — Meta
reuses a matching prompt prefix on its own, so the capability table below lists no
explicit prompt-caching support even though the benefit applies. - No batch API and no embeddings endpoint. Embeddings continue to come from
whicheverHINDSIGHT_API_EMBEDDINGS_PROVIDERyou configure. - Recursive JSON schemas are rejected with
HTTP 400. No Hindsight code path
sends one, so this only matters if you add a self-referencing response model. - Expect slower calls. A trivial prompt can spend more tokens reasoning than it
returns as output.
Nous Portal Setup (Hermes)
Use your Nous Portal subscription for Hindsight via the Hermes CLI login — no static API key required.
Prerequisites:
- A Nous Portal account
- The Hermes CLI installed
Setup Steps:
Log in to Nous Portal:
bashhermes portalThis opens a browser to authenticate with Nous Portal and saves OAuth credentials to
~/.hermes/auth.json.Verify authentication:
bashhermes portal status # should show "Auth: ✓ logged in"Configure Hindsight:
bashexport HINDSIGHT_API_LLM_PROVIDER=nous # export HINDSIGHT_API_LLM_MODEL=deepseek/deepseek-v4-flash # defaults to deepseek/deepseek-v4-flash # No API key needed — reads from ~/.hermes/auth.json automaticallyStart Hindsight:
bashhindsight-api
You can use any model hosted on the Nous Portal inference API.
Important Notes:
- Credentials are read from
~/.hermes/auth.json(the same store the Hermes agent uses) — no static API key in Hindsight's config. - The short-lived inference JWT is refreshed automatically, before expiry and reactively on a 401.
- Refreshes coordinate with a running Hermes agent through the shared auth store, so the two never disrupt each other's session.
- Default base URL:
https://inference-api.nousresearch.com/v1(override withHINDSIGHT_API_LLM_BASE_URL).
Claude Code Setup (Claude Pro/Max)
Use your Claude Pro or Max subscription for Hindsight without separate Anthropic API costs.
:::warning Terms of Service Notice
This integration uses the Claude Agent SDK with your personal Claude Pro/Max subscription
credentials. You must be logged into Claude Code on your own machine before using this provider.
Please be aware:
- Anthropic's Agent SDK documentation
states that third-party developers should not offer claude.ai login or rate limits for
their products. Hindsight does not perform any login on your behalf — it uses
credentials you've already authenticated viaclaude auth login. - In January 2026, Anthropic enforced restrictions
against third-party tools using Claude subscription OAuth tokens. Those restrictions
targeted tools that spoofed the Claude Code client identity — Hindsight uses the
official Claude Agent SDK instead. - This provider is intended for local, personal development use only. Do not use it
in production deployments or shared environments. - Anthropic's terms may change. If you want guaranteed compliance, use the
anthropic
provider with an API key instead. - Usage counts against your Claude Pro/Max subscription limits.
For production or team use, we recommend using HINDSIGHT_API_LLM_PROVIDER=anthropic with
an API key from the Anthropic Console.
:::
Prerequisites:
- Active Claude Pro or Max subscription
- Claude Code CLI installed
Setup Steps:
Install Claude Code CLI:
bashnpm install @anthropics/claude-code # Or via Homebrew brew install anthropics/claude-code/claude-codeLogin with Claude credentials:
bashclaude auth loginThis opens a browser window to authenticate with your Claude account. Authentication is automatically managed by the Claude Agent SDK.
Verify authentication:
bashclaude --version # Should show version without errorsConfigure Hindsight:
bashexport HINDSIGHT_API_LLM_PROVIDER=claude-code # No API key needed - uses claude auth login credentialsServer-held token (optional, for a personal machine without a browser login): instead of
relying on the host'sclaude auth loginsession, you can generate a long-lived token once and
give it to Hindsight. This is still subscription access, so the Terms of Service notice above
applies in full: it does not make the provider suitable for servers, Docker deployments or shared
hosts. For those, use theanthropicprovider with an API key.bashclaude setup-token # prints sk-ant-oat01-... export HINDSIGHT_API_LLM_API_KEY=sk-ant-oat01-...Hindsight passes it to every spawned CLI as
CLAUDE_CODE_OAUTH_TOKEN, so the auth session lives
in the server's config (and can be set per operation or per bank like any other LLM key).
With no key set, the CLI's own login is used.Start Hindsight:
bashhindsight-api
You can use any model supported by Claude Code CLI.
Important Notes:
- Authentication handled by Claude Agent SDK (uses bundled CLI)
- Credentials managed securely by Claude Code
- Usage billed to your Claude subscription (not separate API costs)
- For personal development use only (see Claude Terms of Service)
Cursor Setup (Cursor subscription)
Serve Hindsight's extraction, consolidation and reflection calls from a Cursor
subscription, by driving the cursor-agent CLI in its headless print mode. No API key
and no second per-token billing relationship.
This is the opposite direction from the Cursor integration,
which makes Cursor a client of Hindsight. Here Cursor is the model backend Hindsight calls.
Prerequisites:
- An active Cursor subscription (a free plan works, but only with
auto— see below) cursor-agentinstalled and signed in under the same OS user that runs Hindsight
Setup Steps:
Install the CLI:
bashcurl https://cursor.com/install -fsS | bashLog in:
bashcursor-agent loginVerify:
bashcursor-agent --version cursor-agent --list-modelsConfigure Hindsight:
bashexport HINDSIGHT_API_LLM_PROVIDER=cursor export HINDSIGHT_API_LLM_MODEL=auto # An agent CLI turn takes 15-30s, well past the 30s reflect default. export HINDSIGHT_API_LLM_TIMEOUT=300 export HINDSIGHT_API_REFLECT_LLM_TIMEOUT=180 # No API key needed. To authenticate with a key instead: # export HINDSIGHT_API_LLM_API_KEY=... # or CURSOR_API_KEY
Important notes:
- Raise the timeouts. Each call spawns a
cursor-agentturn, which takes 15-30s
againstauto— slower than a chat-completions request and past the 30sHINDSIGHT_API_REFLECT_LLM_TIMEOUTdefault. Left at the default, reflect spends its
first iteration on timeout retries before recovering. - Structured output and tool calling are prompt-level emulations. The CLI exposes no
response_format, JSON-schema, temperature or tool-definition flag — only--output-format text|json|stream-json, which describes the envelope. Hindsight puts
the schema (or the tool list) in the prompt and parses the model's JSON back out,
retrying on a parse failure. That is less reliable than a native schema, so prefer a
strong named model overautofor reflect, the operation that leans hardest on tools. - Free plans are limited to
auto. A named--modelreturnsNamed models unavailable Free plans can only use Auto. Paid plans can use any id fromcursor-agent --list-models. - The agent's own tools are turned off.
cursor-agentis an agent, not a completions
endpoint: it ships Shell, Read, Write, Delete, web fetch and more, and--mode askis
not a tool switch — a headless run in ask mode will still read files out of its
working directory. Hindsight therefore runs the CLI in an empty scratch workspace, with
its ownCURSOR_CONFIG_DIR(so it never sees your~/.cursorconfig, hooks or session
history), and writes acli-config.jsonthere that denies every tool by name. That is
the equivalent of thetools=[]theclaude-codeprovider passes its SDK, and it
matters because retain prompts are built from whatever text you store in Hindsight. - Usage counts against your Cursor subscription.
- Reasoning effort is not supported — the CLI has no such flag, and Hindsight warns once
at startup ifHINDSIGHT_API_LLM_REASONING_EFFORTis set. - Embeddings and reranking continue to use Hindsight's separately configured providers.
SuperGrok Subscription Setup (device-code OAuth)
Serve LLM calls from a SuperGrok subscription (device-code OAuth) — no API
key. For API-key access to api.x.ai, use provider: openai withHINDSIGHT_API_LLM_BASE_URL=https://api.x.ai/v1; this provider is the
subscription lane, the same category as openai-codex (ChatGPT subscription)
and claude-code (Claude subscription). Both routes reach the same published
endpoint, so the credential is the whole difference: a flat-rate consumer
subscription authorized once in a browser, instead of a per-token metered key.
Authentication is xAI's own OIDC issuer at https://auth.x.ai using the RFC
8628 device-code flow, with the public OAuth client id published in xAI's
Apache-2.0 Grok CLI sources. Hindsight keeps its own credential file and never
reads or writes the Grok CLI's ~/.grok/auth.json.
:::note Subscription entitlement
xAI may restrict api.x.ai access by SuperGrok subscription tier: an OAuth
grant can be valid and still be refused with HTTP 403. Hindsight reports that
as a distinct entitlement error naming the cause rather than as a credential
failure, and it neither re-tries nor discards the token. If you hit it, verify
the account tier or use an API-key provider instead.
Usage counts against your SuperGrok subscription limits. When the account's
spending limit stops a call, Hindsight raises a distinct quota error and leaves
the credential alone.
:::
Prerequisites:
- An active SuperGrok subscription
- A browser on the machine you run the login from (the device-code flow needs
an interactive approval; the running service never performs one)
Setup steps:
Log in once, on the host that will own the credential:
bashpython -m hindsight_api.engine.providers.xai_oauth_auth loginThe command prints a verification URL and a user code, waits for approval,
then writes~/.hindsight/xai_oauth.jsonwith owner-only permissions.Configure Hindsight:
bashexport HINDSIGHT_API_LLM_PROVIDER=xai-oauth # export HINDSIGHT_API_LLM_MODEL=grok-4.5 # defaults to grok-4.5 # No API key neededStart Hindsight:
bashhindsight-api
Important notes:
- The access token is refreshed automatically: proactively 60 seconds before
expiry (or before the configured request timeout, whichever is longer), and
once reactively on an HTTP 401. Several configured lanes share one credential
file safely — the refresh takes an advisory lock and re-reads the file under
it, so siblings do not each refresh. - A running service never starts the device-code flow. When only a login can
restore service, calls fail with the exact command to run. - Unlike reaching
api.x.aithroughprovider: openai, this provider sends the
member's configuredreasoning_effort, so that setting takes effect here.
The one value not forwarded isnone: xAI rejects it outright (HTTP 400),
so this lane expresses it the way the API expects — by omitting the field. - Only non-streaming calls are implemented. Streaming responses omit
prompt_tokens_details, so cached-token accounting would read low.
Optional environment overrides:
| Variable | Purpose | Default |
|---|---|---|
HINDSIGHT_API_XAI_OAUTH_BASE_URL |
Point at a different upstream. Takes precedence over HINDSIGHT_API_LLM_BASE_URL. |
https://api.x.ai/v1 |
HINDSIGHT_API_XAI_OAUTH_TOKEN_PATH |
Relocate the credential store. Both the read and the write follow it. | ~/.hindsight/xai_oauth.json |
HINDSIGHT_API_XAI_OAUTH_CLIENT_ID |
Override the OAuth client id used for login and refresh. | xAI's published public client id |
HINDSIGHT_API_XAI_OAUTH_SCOPE |
Override the scope string requested at login. | openid profile email offline_access grok-cli:access api:access |
HINDSIGHT_API_XAI_OAUTH_REFRESH_SKEW_SECONDS |
Refresh this many seconds before expiry. Widen it for deployments that touch the provider rarely. | 60 |
HINDSIGHT_API_XAI_OAUTH_REFRESH_TIMEOUT_SECONDS |
Per-request timeout for discovery, login and refresh calls. | 20 |
Deployment note
The credential is a host-local file, and only the interactive login can create
it. A containerized engine therefore needs the store mounted from a host where
the login was run (or the login run inside the container once, interactively)
and HINDSIGHT_API_XAI_OAUTH_TOKEN_PATH pointed at it. The mount must be
writable: unlike the Grok CLI's own file, this store is where Hindsight
persists each rotated token, so a read-only mount degrades to failing once the
current token expires.
xAI issues a new refresh token on every refresh and retires the one it
replaces. Replicas must therefore share the one credential file rather than
each starting from its own copy of a single login's output: a copy stops
working as soon as another replica refreshes, and the login has to be repeated.
A single shared writable volume is the supported shape; if you cannot provide
one, run one replica on this provider and give the others an API-key lane.
Azure OpenAI Setup
Azure OpenAI is reached through the openai provider — there is no azure
provider, and setting one fails at startup withInvalid LLM provider: azure.
The one thing that trips people up is the base URL. Azure does not serve the
OpenAI API at the resource root, so the endpoint shown in the Azure portal is
not usable on its own:
HINDSIGHT_API_LLM_BASE_URL |
Result |
|---|---|
https://<resource>.openai.azure.com |
404 Resource not found |
https://<resource>.openai.azure.com/openai/deployments/<deployment> |
404 Resource not found (no api-version) |
https://<resource>.openai.azure.com/openai/v1 |
works |
https://<resource>.openai.azure.com/openai/deployments/<deployment>?api-version=<version> |
works |
Recommended — the v1 surface:
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=<azure-openai-resource-key>
export HINDSIGHT_API_LLM_MODEL=<deployment-name>
export HINDSIGHT_API_LLM_BASE_URL=https://<resource>.openai.azure.com/openai/v1Or the deployment-scoped form. Keep the api-version query string — Hindsight
parses it out of the base URL and passes it to the SDK:
export HINDSIGHT_API_LLM_BASE_URL=https://<resource>.openai.azure.com/openai/deployments/<deployment>?api-version=2025-01-01-previewImportant notes:
HINDSIGHT_API_LLM_MODELis your deployment name, not the model name. Agpt-4odeployed asmy-gpt4ois configured asmy-gpt4o.- The key is the Azure OpenAI resource key (
az cognitiveservices account keys list -n <resource> -g <group>). An API Management subscription key is a
different credential: with APIM in front, the base URL must be the APIM route
and APIM has to forward theapi-keyheader. Test against the Azure endpoint
directly first to isolate which layer is failing. - Gateways and proxies must preserve the same path shape (
/openai/v1or/openai/deployments/...?api-version=). - Azure OpenAI accepts
prompt_cache_keyon GPT deployments (it improves cache
routing on GPT-5.6 and later), but the same*.openai.azure.comendpoint also
serves non-OpenAI Foundry models (DeepSeek, Llama, Mistral) that reject it withunrecognized_request_argument. The host alone can't tell the two apart, soHINDSIGHT_API_LLM_CACHE_AFFINITYresolvesautotononefor Azure hosts. If your deployment serves an OpenAI model,
set it explicitly toopenai_prompt_cache_key.
Vertex AI Setup (Google Cloud)
Google Cloud's Vertex AI provides access to Gemini models via the native Google GenAI SDK.
Prerequisites:
- GCP project with Vertex AI API enabled
- IAM role
roles/aiplatform.userfor your credentials
Environment Variables:
| Variable | Description | Required |
|---|---|---|
HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID |
Your GCP project ID | Yes |
HINDSIGHT_API_LLM_VERTEXAI_REGION |
GCP region (e.g., us-central1) |
No (default: us-central1) |
HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY |
Path to service account JSON key file | No (uses ADC if not set) |
Authentication Methods:
Application Default Credentials (ADC) - Recommended for development
bash# Setup ADC gcloud auth application-default login # Configure Hindsight export HINDSIGHT_API_LLM_PROVIDER=vertexai export HINDSIGHT_API_LLM_MODEL=gemini-3.1-flash-lite export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-project-idService Account Key - Recommended for production
bash# Create service account and download key gcloud iam service-accounts create hindsight-api gcloud projects add-iam-policy-binding your-project-id \ --member="serviceAccount:[email protected]" \ --role="roles/aiplatform.user" gcloud iam service-accounts keys create key.json \ --iam-account=hindsight-api@your-project-id.iam.gserviceaccount.com # Configure Hindsight export HINDSIGHT_API_LLM_PROVIDER=vertexai export HINDSIGHT_API_LLM_MODEL=gemini-3.1-flash-lite export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-project-id export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/key.json
Notes:
- Model names can optionally include the
google/prefix (e.g.,google/gemini-3.1-flash-lite) — it will be stripped automatically - The native SDK handles token refresh automatically
- Uses service account credentials if provided, otherwise falls back to ADC
Embedding Model
Converts text into dense vector representations for semantic similarity search.
Default: BAAI/bge-small-en-v1.5 (384 dimensions, ~130MB)
Supported Providers
| Provider | Description | Best For |
|---|---|---|
local |
SentenceTransformers (default) | Development, low latency |
onnx |
In-process ONNX Runtime embedder (no Ollama/TEI/API sidecar) | Lightweight local CPU, multilingual |
openai |
OpenAI embeddings API | Production, high quality |
openai-codex |
OpenAI embeddings via Codex OAuth (ChatGPT Plus/Pro, no API key) | Existing ChatGPT/Codex subscribers |
openrouter |
OpenRouter embeddings (OpenAI-compatible gateway) | Multi-provider setups |
cohere |
Cohere embeddings API | Production, multilingual |
google |
Google embeddings (Gemini API or Vertex AI) | Production, multilingual, high quality |
tei |
HuggingFace Text Embeddings Inference | Production, self-hosted |
zeroentropy |
ZeroEntropy zembed-1 | Production, high quality retrieval |
nvidia |
NVIDIA NIM retrieval embeddings (input_type-aware) |
Users on NVIDIA NIM / build.nvidia.com |
litellm |
LiteLLM proxy (unified gateway) | Multi-provider setups |
litellm-sdk |
LiteLLM SDK (direct API, no proxy) | Multi-provider, simpler setup |
Local Models
| Model | Dimensions | Use Case |
|---|---|---|
BAAI/bge-small-en-v1.5 |
384 | Default, fast, good quality |
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 |
384 | Multilingual (50+ languages) |
OpenAI Models
| Model | Dimensions | Use Case |
|---|---|---|
text-embedding-3-small |
1536 | Default OpenAI, cost-effective |
text-embedding-3-large |
3072 | Higher quality, more expensive |
text-embedding-ada-002 |
1536 | Legacy model |
Google Models
| Model | Dimensions | Use Case |
|---|---|---|
gemini-embedding-001 |
768 (configurable) | Default Google, general purpose |
gemini-embedding-2-preview |
768 (configurable) | Gemini Embedding 2 family; multimodal, one vector per input |
Google's gemini-embedding-001 supports configurable output dimensionality via truncation, google recommend using: 768, 1536, 3072, via HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY. Default is 768.
The gemini-embedding-2 family, including gemini-embedding-2-preview, is supported on both the Gemini API and Vertex AI. These models aggregate multi-input requests, so Hindsight automatically embeds one input per call to keep per-fact vectors aligned.
Cohere Models
| Model | Dimensions | Use Case |
|---|---|---|
embed-english-v3.0 |
1024 | English text |
embed-multilingual-v3.0 |
1024 | 100+ languages |
ZeroEntropy Models
| Model | Dimensions | Use Case |
|---|---|---|
zembed-1 |
1280 default (2560/1280/640/320/160/80/40 configurable) | High quality asymmetric retrieval |
Hindsight sends retained memory text to ZeroEntropy as document inputs and recall/search text as query inputs. ZeroEntropy's API default is 2560 dimensions; Hindsight defaults to 1280 so pgvector HNSW works without changing the vector extension.
:::warning Embedding Dimensions
Hindsight automatically detects the embedding dimension at startup and adjusts the database schema. Once memories are stored, you cannot change dimensions without losing data.
:::
Configuration Examples:
/* Detailed source-code truncated for AI context efficiency. */:::tip AWS Bedrock application inference profiles
If your org's Service Control Policy denies bedrock:InvokeModel on the bare model id once an application inference profile exists, keep ..._MODEL as-is and add the profile ARN separately:
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
# Stays a recognizable model id — this is what picks the request format
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=bedrock/amazon.titan-embed-text-v2:0
# The profile actually invoked
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL_ID=arn:aws:bedrock:eu-west-1:123456789012:application-inference-profile/abc123The two are separate because Bedrock's embedding request and response formats differ per model family (Titan, Cohere, TwelveLabs, Nova), and the family is read off the model id. A profile ARN is opaque and account-scoped, so it can't be used for that — put it in ..._MODEL_ID and leave ..._MODEL naming the real model.
Chat models don't need this: set HINDSIGHT_API_LLM_MODEL=bedrock/converse/<arn>, since the Converse API takes one format for every model.
:::
See Configuration for all options including Azure OpenAI and custom endpoints.
For a runnable Docker Compose setup that serves both embeddings and reranking from self-hosted TEI sidecars, see docker/docker-compose/tei/.
Cross-Encoder (Reranker)
Reranks initial search results to improve precision.
Default: cross-encoder/ms-marco-MiniLM-L-6-v2 (~85MB)
Supported Providers
| Provider | Description | Best For |
|---|---|---|
local |
SentenceTransformers CrossEncoder (default) | Development, low latency |
cohere |
Cohere rerank API | Production, high quality |
openrouter |
OpenRouter rerank API (Cohere-compatible gateway) | Multi-provider setups |
zeroentropy |
ZeroEntropy rerank API (zerank-2) | Production, state-of-the-art accuracy |
siliconflow |
SiliconFlow rerank API (Cohere-compatible /rerank endpoint) |
Users in China or anyone on SiliconFlow's platform |
nvidia |
NVIDIA NIM reranking API (native /retrieval/{model}/reranking endpoint) |
Users on NVIDIA NIM / build.nvidia.com |
typesafe |
TypeSafe typed-decision API (Jev) — ranks the whole pool in one question | Production, highest ranking quality; can also return only the relevant candidates |
alibaba |
Alibaba Cloud DashScope rerank API (qwen3-rerank) | Users on Alibaba Cloud / DashScope |
google |
Google Discovery Engine ranking API (REST + Google auth) | Production, GCP integration |
tei |
HuggingFace Text Embeddings Inference | Production, self-hosted |
flashrank |
FlashRank (lightweight, fast) | Resource-constrained environments |
onnx |
Any cross-encoder exported to ONNX, in-process via ONNX Runtime | Custom models without PyTorch or a TEI server |
litellm |
LiteLLM proxy (unified gateway) | Multi-provider setups |
litellm-sdk |
LiteLLM SDK (direct API, no proxy) | Multi-provider, simpler setup |
jina-mlx |
Jina rerank v3 via Apple Silicon MLX (local, no API key) | Apple Silicon (M1+) local inference |
rrf |
RRF-only (no neural reranking) | Testing, minimal resources |
Local Models
| Model | Use Case |
|---|---|
cross-encoder/ms-marco-MiniLM-L-6-v2 |
Default, fast |
cross-encoder/ms-marco-MiniLM-L-12-v2 |
Higher accuracy |
cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 |
Multilingual |
Cohere Models
| Model | Use Case |
|---|---|
rerank-english-v3.0 |
English text |
rerank-multilingual-v3.0 |
100+ languages |
ZeroEntropy Models
| Model | Use Case |
|---|---|
zerank-2 |
Flagship multilingual reranker (default) |
zerank-2-small |
Faster, lighter variant |
SiliconFlow Models
SiliconFlow hosts a range of open-weight rerankers behind a Cohere-compatible /rerank endpoint:
| Model | Use Case |
|---|---|
BAAI/bge-reranker-v2-m3 |
Multilingual, strong default |
Qwen/Qwen3-Reranker-8B |
Larger, higher accuracy |
TypeSafe Models
TypeSafe is not a /rerank endpoint — it answers typed questions against a state.
Hindsight makes the candidates the options of a Choice question, so the returned
probability distribution is the ranking. When the candidate pool fits within the 250-option
limit and context token budget, a single call ranks the entire pool. Larger pools or pools
with long documents are partitioned into groups whose top candidates advance to a finals
ranking round, with token truncation applied to fit context windows.
| Model | Use Case |
|---|---|
jev-latest |
Default; tracks the current Jev release |
Setting HINDSIGHT_API_RERANKER_TYPESAFE_PRUNE_CANDIDATES=true adds a second question
that cuts the ranked list where relevance ends, so recall returns the relevant
candidates and nothing else. It is off by default because it meaningfully shrinks what
recall returns. See Configuration for the full
behaviour and its trade-offs.
TypeSafe's scores are rank positions within one recall, not relevance scores, so recall
publishes no scores.reranker under it and rejects min_scores.reranker with HTTP 400.
Alibaba Cloud Models
Alibaba Cloud DashScope exposes qwen3-rerank via a Cohere-compatible /reranks endpoint:
| Model | Use Case |
|---|---|
qwen3-rerank |
100+ languages, default |
LiteLLM Supported Providers
LiteLLM supports multiple reranking providers via the /rerank endpoint:
| Provider | Model Example |
|---|---|
| Cohere | cohere/rerank-english-v3.0 |
| Together AI | together_ai/... |
| Voyage AI | voyage/rerank-2 |
| Jina AI | jina_ai/... |
| AWS Bedrock | bedrock/... |
Configuration Examples:
/* Detailed source-code truncated for AI context efficiency. */See Configuration for all options including Azure-hosted endpoints and batch settings.
Hindsight Docs Docs Developer Monitoring (`hindsight-docs/docs/developer/monitoring.md`)
Monitoring
Hindsight provides comprehensive observability through Prometheus metrics, OpenTelemetry distributed tracing, and pre-built Grafana dashboards.
Local Development
For local observability, use the Grafana LGTM (Loki, Grafana, Tempo, Mimir) all-in-one stack:
./scripts/dev/start-monitoring.shThis starts a single Docker container providing:
- Grafana UI: http://localhost:3000 (anonymous admin access)
- Traces (Tempo): OTLP endpoint at http://localhost:4318 (HTTP) and http://localhost:4317 (gRPC)
- Metrics (Prometheus/Mimir): Scrapes http://localhost:8888/metrics automatically
- Logs (Loki): Available for log aggregation
- Pre-built Dashboards: Hindsight Operations, LLM Metrics, API Service
Enable tracing in your API:
export HINDSIGHT_API_OTEL_TRACES_ENABLED=true
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318:::note Production Deployment
The local monitoring stack is for development only. In production, deploy Grafana LGTM separately or use commercial platforms (Grafana Cloud, DataDog, New Relic, etc.).
:::
Grafana Dashboards
Pre-built dashboards are available in monitoring/grafana/dashboards/. Import these JSON files into your Grafana instance:
| Dashboard | Description |
|---|---|
| Hindsight Operations | Operation rates, latency percentiles, per-bank metrics |
| Hindsight LLM Metrics | LLM calls, token usage, latency by scope/provider |
| Hindsight API Service | HTTP requests, error rates, DB pool, process metrics |
The dashboards are automatically provisioned when using the monitoring stack script.
Metrics Endpoint
Hindsight exposes Prometheus metrics at /metrics:
curl http://localhost:8888/metricsHealth Endpoints
The API server (port 8888) and every worker (port 8889) expose the same three
endpoints. They answer two different questions, and pointing a probe at the wrong
one is the difference between riding out a database blip and turning it into an
outage:
| Endpoint | Checks the database? | Use for |
|---|---|---|
/health/live |
No | Liveness probes |
/health/ready |
Yes | Readiness probes |
/health |
Yes | Readiness — alias of /health/ready, kept for compatibility |
/health/live returns 200 whenever the process can serve a request, and does
no I/O to get there:
{ "status": "alive", "version": "0.4.0", "uptime_seconds": 812.4 }Answering at all is the check. Hindsight runs request handlers and task work on a
single event loop, so a loop wedged by a blocking call cannot respond inside the
probe timeout — which is exactly the failure a restart fixes. The worker's payload
adds worker_id, is_shutdown, and seconds_since_last_poll (the age of its last
completed claim cycle, null before the first one). That last field is there to
alert on: it never changes the status code, because a poller stalled behind a
saturated database is the case where restarting makes things worse.
/health and /health/ready acquire a pooled connection and run SELECT 1,
returning 200 when the database is reachable and 503 when it is not. The payload
separates the two ways that can go wrong — db_acquire_ms and db_pool_waiting
point at pool exhaustion, a slow query at the database itself:
{ "status": "healthy", "database": "connected", "db_acquire_ms": 0.4, "db_pool_waiting": 0 }:::warning Never point a liveness probe at a dependency check
A liveness failure means "restart this process." If your liveness probe checks the
database, then a slow database restarts every pod at once: in-flight requests are
dropped, claimed async operations are requeued with retry_count incremented
toward the permanent-failure cliff, and each restarted pod reconnects to re-warm
its pool against a database that is already struggling. Readiness failing is the
correct response — it takes the pod out of the Service and puts it back when the
database recovers.
The bundled Helm chart is wired this way already (livenessProbe → /health/live,readinessProbe → /health). If you wrote your own manifests against an older
version, move the liveness path over.
:::
Available Metrics
:::note High-cardinality labels are opt-inbank_id and tenant (the tenant schema) are left off every metric by default, because each
one adds a series set per bank or tenant. Turn them on withHINDSIGHT_API_METRICS_INCLUDE_BANK_ID=true / HINDSIGHT_API_METRICS_INCLUDE_TENANT=true
only on deployments with few banks or tenants. The backlog gauges always carry tenant.
:::
Operation Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.operation.duration |
Histogram | operation, bank_id, source, budget, max_tokens, success, memories_backend | Duration of operations in seconds |
hindsight.operation.total |
Counter | operation, bank_id, source, budget, max_tokens, success, memories_backend | Total number of operations executed |
Labels:
operation: Operation type (retain,recall,reflect, plus async worker task types such asconsolidation)bank_id: Memory bank identifiersource: Where the operation was triggered from (api,reflect,internal,worker)budget: Budget level if specified (low,mid,high)max_tokens: Max tokens if specifiedsuccess: Whether the operation succeeded (true,false)memories_backend: The store serving the bank, when the memories extension names one
(MemoriesExtension.backend_name_for). Absent by default, so existing series are unchanged; a
deployment whose banks live in different stores uses it to compare their latency without
turning ontenant. Recall phase metrics recorded inside the operation carry it too.
The source label allows distinguishing between:
api: Direct API calls from clientsreflect: Internal recall calls made during reflect operationsinternal: Other internal operationsworker: Async worker completions recorded when a claimed task reaches a terminal outcome
For source="worker", the success label is a completion-throughput signal:false means the task raised out to the poller after retries were exhausted or
an unexpected error occurred. Failures handled inside the executor and returned
normally still record success="true" here; usehindsight_async_operations{status="failed"} for authoritative async operation
failure status.
Retain Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.retain.documents.total |
Counter | outcome, bank_id | Documents processed by retain, by extraction outcome |
Labels:
outcome:factswhen the document has memory units after the retain,no_factswhen it has nonebank_id: Memory bank identifier
outcome="no_facts" is the signal that a document was stored but produced no memories. Those documents are invisible to recall and reflect until they are reprocessed — nothing else reports them, because the retain itself succeeded. A rising share usually means a retain mission is excluding more than intended:
sum(rate(hindsight_retain_documents_total{outcome="no_facts"}[15m]))
/ sum(rate(hindsight_retain_documents_total[15m]))LLM Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.llm.duration |
Histogram | provider, model, scope, success | Duration of LLM API calls in seconds |
hindsight.llm.calls.total |
Counter | provider, model, scope, success | Total number of LLM API calls |
hindsight.llm.tokens.input |
Counter | provider, model, scope, success, token_bucket | Input tokens for LLM calls |
hindsight.llm.tokens.output |
Counter | provider, model, scope, success, token_bucket | Output tokens from LLM calls |
Labels:
provider: LLM provider (openai,anthropic,gemini,groq,ollama,lmstudio,bedrock,litellm)model: Model name (e.g.,gpt-4,claude-3-sonnet)scope: What the LLM call is for (memory,reflect,consolidation,answer)success: Whether the call succeeded (true,false)token_bucket: Token count bucket for cardinality control (0-100,100-500,500-1k,1k-5k,5k-10k,10k-50k,50k+)
Consolidation Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.consolidation.batch_failures |
Counter | failure_class, error_type | Consolidation LLM batch calls that failed, including those whose facts were later recovered |
Labels:
failure_class: How the call was treated (fail_fast— the model returned something the response schema rejects, so a re-send of the same payload cannot help;retry— transport-shaped, an unchanged re-send may succeed;propagate— not a batch failure, re-raised to the task handler)error_type: Exception class name (e.g.ValidationError,JSONDecodeError)
This is not the same signal as the failed_consolidation field in bank stats. That
field counts facts still waiting to be consolidated after a failure; when a batch
call fails, consolidation halves the batch and retries, so a call that failed at
batch size 8 and succeeded at size 1 leaves failed_consolidation at 0. Everything
the failed response asked for is dropped, though — including any observations it
wanted to delete — so a bank can look completely healthy while its
supersession cleanup does nothing.
Alert on the schema-rejection rate, which no other metric exposes:
sum(rate(hindsight_consolidation_batch_failures_total{failure_class="fail_fast"}[15m]))A sustained non-zero rate usually means the consolidation model does not reliably
emit valid JSON for the response schema. Switching to a model with stronger
structured output, or enabling HINDSIGHT_API_LLM_STRICT_SCHEMA_CONSOLIDATION if
the provider supports grammar-enforced schemas, is the usual fix.
The same count is reported per run as llm_batch_failures in the consolidation
operation's result, and the consolidation log summary prints a warning line
whenever it is non-zero. Both count attempts — one batch call retried three times
contributes three — so they are not bounded by the number of batches in the run.
HTTP Request Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.http.duration |
Histogram | method, endpoint, status_code, status_class | Duration of HTTP requests in seconds |
hindsight.http.requests.total |
Counter | method, endpoint, status_code, status_class | Total number of HTTP requests |
hindsight.http.requests.in_progress |
UpDownCounter | method, endpoint | Number of HTTP requests currently being processed |
Labels:
method: HTTP method (GET,POST,PUT,DELETE)endpoint: Template of the route the request matched, with path parameters named rather than filled in (e.g./v1/default/banks/{bank_id}/documents/{document_id},/v1/default/banks/{bank_id}/operations/{operation_id}), so the label has one value per route however many banks, documents or ids are requested. The template does not include abase_path/ root-path prefix. Requests that match no route (404s) are labelledunmatched; a request matched by a route that has no path template is labelledunknown_route.status_code: HTTP status code (200,400,500, etc.)status_class: Status code class (2xx,4xx,5xx)
Database Pool Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.db.pool.size |
Gauge | - | Current number of connections in the pool |
hindsight.db.pool.idle |
Gauge | - | Number of idle connections in the pool |
hindsight.db.pool.min |
Gauge | - | Minimum pool size |
hindsight.db.pool.max |
Gauge | - | Maximum pool size |
Process Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
hindsight.process.cpu.seconds |
Gauge | type | Process CPU time in seconds |
hindsight.process.memory.bytes |
Gauge | type | Process memory usage in bytes |
hindsight.process.open_fds |
Gauge | - | Number of open file descriptors |
hindsight.process.threads |
Gauge | - | Number of active threads |
Labels:
type(CPU):userorsystemtype(Memory):rss_max(maximum resident set size)
Histogram Buckets
Custom bucket boundaries are configured for better percentile accuracy:
Operation Duration Buckets (seconds):
0.1, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 60.0, 120.0LLM Duration Buckets (seconds):
0.1, 0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0HTTP Duration Buckets (seconds):
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0Prometheus Configuration
scrape_configs:
- job_name: 'hindsight'
static_configs:
- targets: ['localhost:8888']Example Queries
Average operation latency by type
rate(hindsight_operation_duration_sum[5m]) / rate(hindsight_operation_duration_count[5m])LLM calls per minute by provider
rate(hindsight_llm_calls_total[1m]) * 60P95 LLM latency
histogram_quantile(0.95, rate(hindsight_llm_duration_bucket[5m]))Total tokens consumed by model
sum by (model) (hindsight_llm_tokens_input_total + hindsight_llm_tokens_output_total)Internal vs API recall operations
sum by (source) (rate(hindsight_operation_total{operation="recall"}[5m]))HTTP requests per second by endpoint
sum by (endpoint) (rate(hindsight_http_requests_total[1m]))HTTP error rate (5xx)
sum(rate(hindsight_http_requests_total{status_class="5xx"}[5m])) / sum(rate(hindsight_http_requests_total[5m]))P95 HTTP latency
histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))Database pool utilization
hindsight_db_pool_size / hindsight_db_pool_maxActive database connections
hindsight_db_pool_size - hindsight_db_pool_idleCPU usage rate
rate(hindsight_process_cpu_seconds{type="user"}[1m])Distributed Tracing
Hindsight supports OpenTelemetry distributed tracing for memory operations and LLM calls, following GenAI semantic conventions v1.37+.
Configuration
See Configuration - OpenTelemetry Tracing for environment variables.
Quick Start:
# Enable tracing
export HINDSIGHT_API_OTEL_TRACES_ENABLED=true
export HINDSIGHT_API_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# View traces with Grafana LGTM (local dev)
./scripts/dev/start-monitoring.sh
# Open http://localhost:3000 → Explore → TempoSupports any OTLP-compatible backend (Grafana LGTM, Langfuse, OpenLIT, DataDog, New Relic, Honeycomb, Pydantic Logfire, etc.).
Span Hierarchy
Parent Spans (Operations):
hindsight.retain- Memory ingestionhindsight.recall- Memory retrievalhindsight.recall_embedding- Query embeddinghindsight.recall_retrieval- Parallel search (semantic, BM25, graph, temporal)hindsight.recall_fusion- Reciprocal Rank Fusionhindsight.recall_rerank- Cross-encoder reranking
hindsight.reflect- Agentic reasoninghindsight.reflect_tool_call- Tool execution (recall, lookup, etc.)
hindsight.consolidation- Observation synthesishindsight.mental_model_refresh- Mental model updates
Child Spans (LLM Calls):
- Named by scope (e.g.,
hindsight.memory,hindsight.reflect) - Contain full prompts/completions as events
- Follow GenAI semantic conventions for attributes
Span Attributes
Operation Spans:
hindsight.operation- Operation typehindsight.bank_id- Memory bank IDhindsight.query- Query text (truncated to 100 chars)hindsight.fact_types- Fact types for recallhindsight.thinking_budget- Budget allocationhindsight.max_tokens- Token limit
LLM Spans (GenAI Semantic Conventions):
gen_ai.operation.name- Always"chat"gen_ai.provider.name- Provider (openai,anthropic,google, etc.)gen_ai.request.model- Model namegen_ai.usage.input_tokens- Input tokensgen_ai.usage.output_tokens- Output tokenshindsight.scope- LLM call purpose (memory,reflect,consolidation, etc.)
Events:
gen_ai.client.inference.operation.details- Full prompts and completions
Trace Context Propagation
Hindsight participates in your existing traces rather than starting parallel ones. When a caller sends W3C trace context (the standard traceparent header, which most OpenTelemetry HTTP client instrumentations add automatically), Hindsight continues that trace: the API request appears as a server span under the caller, and every memory operation and LLM call it triggers nests beneath it.
Requests that arrive without trace context still start their own trace, so nothing changes for un-instrumented callers.
Health and metrics endpoints are excluded from tracing so probe traffic doesn't drown out real work. Set OTEL_PYTHON_FASTAPI_EXCLUDED_URLS to a comma-separated list of URL patterns to override this.
Worker Processes
Standalone worker processes honour the same HINDSIGHT_API_OTEL_* variables as the API and export their own spans. This matters for deployments that run dedicated workers, since consolidation, background retain and mental model refresh — most of the long-running work and token spend — happen there.
Give the worker its own HINDSIGHT_API_OTEL_SERVICE_NAME to separate it from the API in your tracing backend. When left unset, workers report themselves as hindsight-worker.
Worker spans are currently their own traces: a background operation is not linked to the request that queued it, because it runs long after that request has returned.
Hindsight Docs Docs Developer Multilingual (`hindsight-docs/docs/developer/multilingual.mdx`)
sidebar_position: 5
import CodeSnippet from '@site/src/components/CodeSnippet';
import multilingualPy from '!!raw-loader!@site/examples/api/multilingual.py';
import Figure from '@site/src/components/Figure';
import multilingualFigure from '@site/figures/multilingual.json';
Multilingual Support
Hindsight automatically detects the language of your input and responds in the same language. This means facts, entities, and reflect responses are preserved in their original language without translation to English.
How It Works
When you retain content or reflect on a query, Hindsight:
- Detects the input language automatically from the content
- Extracts facts in the original language - preserving nuance and meaning
- Stores entities in their native script - 张伟 stays 张伟, not "Zhang Wei"
- Responds in the same language - queries in Chinese get Chinese answers
Retain with Non-English Content
When you retain content in any language, Hindsight extracts and stores facts in that same language.
Example: Chinese Content
Example: Japanese Content
Reflect with Non-English Queries
The reflect operation also respects the input language, generating thoughtful responses in the same language as the query.
Example: Chinese Reflection
Mixed Language Content
Hindsight handles mixed-language content gracefully, preserving both languages where appropriate.
Example: Chinese Text with English Company Names
Supported Languages
Hindsight's multilingual support depends entirely on your LLM's language capabilities. Hindsight instructs the LLM to detect the input language and respond in that same language. If your LLM supports a language, Hindsight will work with it.
Most modern LLMs (GPT-4, Claude, Gemini, Llama 3, etc.) support dozens of languages including:
- East Asian: Chinese (Simplified/Traditional), Japanese, Korean
- European: Spanish, French, German, Italian, Portuguese, Dutch, Polish, Russian
- Middle Eastern: Arabic, Hebrew, Turkish
- South Asian: Hindi, Bengali, Tamil
- Southeast Asian: Thai, Vietnamese, Indonesian
To verify support for your target language, test your LLM directly with content in that language. If the LLM can understand and generate text in the language, Hindsight will preserve it correctly.
Configuring for Multilingual Use
For optimal multilingual performance, configure all four components of the pipeline:
1. LLM (Required)
Your LLM must support the target languages. Most modern LLMs do, but verify with your specific model.
2. Embedding Model (Recommended)
The default embedding model (BAAI/bge-small-en-v1.5) is English-only. For multilingual content, use a multilingual embedding model:
# In your .env file
HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-m3Recommended multilingual embedding models:
| Model | Languages | Notes |
|---|---|---|
BAAI/bge-m3 |
100+ | Best overall multilingual performance |
intfloat/multilingual-e5-large |
100+ | Good alternative |
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 |
50+ | Lighter weight |
3. Reranker Model (Recommended)
The default reranker (cross-encoder/ms-marco-MiniLM-L-6-v2) is English-only. For multilingual content, use a multilingual reranker:
# In your .env file
HINDSIGHT_API_RERANKER_LOCAL_MODEL=BAAI/bge-reranker-v2-m3Recommended multilingual reranker models:
| Model | Languages | Notes |
|---|---|---|
BAAI/bge-reranker-v2-m3 |
100+ | Best multilingual reranking |
cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 |
14 | Lighter alternative |
4. BM25 / Full-Text Search Backend
The semantic (embedding) arm covers cross-lingual matches by meaning. Hindsight runs a BM25 keyword arm in parallel, and BM25 is inherently within-language — it's character/token matching against a tokenizer's lexemes. The default native backend uses PostgreSQL's English dictionary, which produces poor results for non-English content (and no useful tokenization at all for Chinese / Japanese / Korean, which lack whitespace word boundaries).
There are two knobs that interact:
HINDSIGHT_API_TEXT_SEARCH_EXTENSION— selects the backend (native,vchord,pg_textsearch,pgroonga, orpg_search).HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE— selects the PostgreSQL dictionary used by thenativebackend (default:english).
Pick the backend based on the languages your bank stores:
| Backend | Multilingual / CJK | Notes |
|---|---|---|
native |
European languages only (English, French, German, Spanish, Italian, Portuguese, Russian, Dutch, Swedish, Norwegian, Danish, Finnish, Hungarian, Turkish, Arabic, plus simple). CJK requires a third-party dictionary like zhparser. |
Stock PostgreSQL — no extra extensions. Configure the language via HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE. |
vchord |
Multilingual via llmlingua2 tokenizer. |
Best when you're already using vchord for vector search. |
pg_textsearch |
English only (hardcoded). | Industry-standard BM25 ranking + Block-Max WAND. |
pgroonga |
Yes — out of the box. Single index handles English, CJK, and mixed-script content via the TokenBigram polyglot tokenizer + NormalizerNFKC150 Unicode normalization. |
Recommended for non-English / mixed-language banks. Requires the pgroonga extension. See docker/docker-compose/pgroonga/. |
pg_search |
Multilingual via configurable tokenizer (e.g. chinese_compatible, jieba, chinese_lindera, japanese_lindera, korean_lindera, ngram). |
ParadeDB pg_search extension; the only Citus-compatible BM25 backend. Tokenizer set via HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER. See docker/docker-compose/pg_search/. |
Choosing for a single-language bank (e.g. all Spanish content):
HINDSIGHT_API_TEXT_SEARCH_EXTENSION=native
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE=spanishChoosing for a CJK or mixed-language bank:
HINDSIGHT_API_TEXT_SEARCH_EXTENSION=pgroonga:::note External PostgreSQL required
The pgroonga and pg_search backends depend on PostgreSQL extensions that are not bundled with the embedded pg0 database Hindsight uses when HINDSIGHT_API_DATABASE_URL is unset. On that default single-container setup neither backend can be enabled — point HINDSIGHT_API_DATABASE_URL at an external PostgreSQL that has the extension installed, as the docker/docker-compose/pgroonga/ and docker/docker-compose/pg_search/ recipes do.
:::
The native and pgroonga knobs do not apply to each other — pgroonga's tokenizer is set at index creation and ignores HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE.
Forcing the LLM Output Language
Independent from the BM25 backend, HINDSIGHT_API_LLM_OUTPUT_LANGUAGE forces every LLM-generated artifact into a single language regardless of the source content. This applies uniformly to:
- Retain — fact text, context, and entity names extracted from source documents.
- Consolidation — observations / mental models synthesized from those facts.
- Reflect — the final natural-language response returned by the reflect API.
# Every LLM call (retain, consolidation, reflect) emits Spanish regardless of source language.
HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=SpanishCommon patterns:
- Aligned, single-language bank:
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE=spanish+HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=Spanish— store, index, and respond in Spanish even when sources are mixed. - Mixed-language bank with multilingual indexing:
HINDSIGHT_API_TEXT_SEARCH_EXTENSION=pgroonga+ leaveHINDSIGHT_API_LLM_OUTPUT_LANGUAGEunset — preserve source-language facts; pgroonga handles all of them in one index; reflect responds in the query's language. - Cross-lingual unification:
HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=English— every fact, observation, and reflect response in English regardless of source. Useful when the consumer (an English-only LLM, dashboard, or downstream pipeline) needs uniform output.
Leave HINDSIGHT_API_LLM_OUTPUT_LANGUAGE unset to preserve the source/query language across the pipeline (the default).
Default behaviour when no output language is set
With HINDSIGHT_API_LLM_OUTPUT_LANGUAGE unset, retain and consolidation are both instructed to keep their output in the language of the source material. For observations specifically:
- Language is decided per observation, from the facts that observation is built on — not from the batch. A batch mixing Chinese and English facts produces Chinese observations for the Chinese facts and English observations for the English ones. When a single observation merges facts written in several languages, the majority language of those facts wins.
- Updates follow the new facts. When an existing observation is written in a different language from the facts updating it, the whole observation is rewritten in the new facts' language. A bank whose observations previously drifted into the wrong language converges back as new facts arrive.
- Names and technical terms are never translated — proper nouns, product and place names, identifiers, code, and units stay as written in the source facts, whatever the surrounding language.
This is prompt-level guidance, not a hard guarantee: a model that ignores instructions can still emit the wrong language. Set HINDSIGHT_API_LLM_OUTPUT_LANGUAGE explicitly when a bank must be single-language no matter what its sources look like.
Best Practices
1. Use Multilingual Models for Non-English Content
If you primarily work with non-English content, configure multilingual embedding and reranker models. English-only models will still store your content correctly, but semantic search quality will be degraded.
2. Keep Content in One Language Per Retain Call
While mixed content works, keeping each retain call in a single language produces more consistent results.
3. Query in the Same Language as Your Content
For best results, query using the same language as your stored content. Cross-language queries (e.g., English query for Chinese content) may work but results can vary depending on your embedding model.
Technical Details
Multilingual support is implemented through LLM prompt instructions rather than external language detection libraries. This approach:
- Requires no additional dependencies
- Works with any LLM that supports multiple languages
- Handles edge cases like mixed-language content naturally
- Preserves semantic meaning better than rule-based translation
The LLM is instructed to:
- Detect the input language
- Extract all facts, entities, and descriptions in that same language
- Never translate to English unless the input is in English
Hindsight Docs Docs Developer Observations (`hindsight-docs/docs/developer/observations.mdx`)
sidebar_position: 5
import CodeSnippet from '@site/src/components/CodeSnippet';
import recallPy from '!!raw-loader!@site/examples/api/recall.py';
import memoryBanksPy from '!!raw-loader!@site/examples/api/memory-banks.py';
import observationsPy from '!!raw-loader!@site/examples/api/observations.py';
import Figure from '@site/src/components/Figure';
import observationsFigure from '@site/figures/observations.json';
Observations: Knowledge Consolidation
After memories are retained, Hindsight automatically consolidates related facts into observations — deduplicated, evidence-grounded beliefs the bank has built up from multiple memories. Each observation tracks its supporting evidence (with exact quotes) and a proof count, and is refined rather than overwritten when new evidence arrives.
What Are Observations?
Observations are consolidated knowledge built from multiple facts. Unlike raw facts — which are individual pieces of information — observations represent deduplicated beliefs, preferences, and learnings grounded in accumulated evidence. They are not summaries the LLM invents on the fly: each observation is backed by specific source memories, carries a proof count, and evolves as new evidence supports, contradicts, or extends it.
| Raw Facts | Observation |
|---|---|
| "Alice prefers Python" | "Alice is a Python-focused developer who values readability and simplicity" |
| "Alice dislikes verbose code" | |
| "Alice recommends type hints" |
Observations provide:
- Deduplication: One durable belief instead of many overlapping facts
- Grounding: Every observation references the specific memories (with quotes) that support it
- Evolution: Refined as evidence strengthens, weakens, or contradicts it — history is preserved
- Freshness awareness: when newer memories haven't been consolidated yet,
reflecttreats the affected observations as stale and verifies them against raw facts - Efficiency: Condensed knowledge for faster retrieval
How Consolidation Works
Automatic Background Processing
After retain() completes, the consolidation engine runs automatically:
- New facts analyzed — Each new fact is compared against existing observations
- Pattern detection — Related facts are grouped and synthesized
- Observation creation/update — New observations are created or existing ones refined
- Evidence tracking — Each observation maintains references to supporting facts
Near-Duplicate Reconciliation
Consolidation can still produce two observations that say the same thing in slightly different words — for example when a weaker model writes a near-identical observation instead of refining the existing one, or when refining an observation reshapes its wording so it overlaps another one. Left alone, these near-duplicates clutter recall with redundant beliefs.
When enabled, Hindsight reconciles them automatically. Whenever an observation is created or updated, it is compared against the existing observations it most closely resembles. If one is highly similar, a focused check decides whether to merge them into a single belief (folding both sets of supporting evidence together) or keep them separate. Because the check reads the full text of both, observations that differ in a meaningful detail — a number, a negation, a named entity or language — are correctly kept apart rather than collapsed.
This is controlled by the HINDSIGHT_API_CONSOLIDATION_DEDUP_THRESHOLD setting: the cosine similarity at or above which two observations are reconciled. It is enabled by default (0.97); a lower value reconciles more aggressively, and 1.0 disables it. Reconciliation runs on PostgreSQL deployments only — it is skipped on Oracle regardless of the threshold.
Reconciliation only compares observations within the same tag scope. If you tag retains with a unique per-call value (e.g. a session-id), each session lands in its own scope and never dedups against the others — producing one near-identical observation per session. To consolidate across those volatile tags, retain with observation_scopes: "shared", which scopes observations to one global, untagged belief while leaving the session tag on the source facts for recall filtering.
Disabling Auto-Consolidation
Set HINDSIGHT_API_ENABLE_AUTO_CONSOLIDATION=false (or configure per-bank via the bank config API) to prevent consolidation from running automatically after retain, delete, and update operations. When disabled, consolidation only runs when you explicitly call the consolidate endpoint.
This is useful when you want full control over consolidation timing — for example, batching many retains before consolidating, or running consolidation only for specific scopes.
Targeted Consolidation
By default, consolidation processes all unconsolidated memories in a bank. You can scope it to specific tag sets using the observation_scopes parameter on the consolidate endpoint:
Each scope is a list of tags. A memory matches a scope if its tags contain all tags in that scope. For example, scope ["user:alice"] matches memories tagged ["user:alice", "team:eng"].
When observation_scopes is omitted, all unconsolidated memories are processed (backward compatible).
Evidence-Based Evolution
Observations evolve as new evidence arrives:
| Event | What the bank learns | Observation state |
|---|---|---|
| Day 1 | "Redis is open source under BSD license" | "Redis is excellent for caching — fast, reliable, and OSS-friendly" (2 supporting facts) |
| Day 2 | "Redis has great community support" | Observation reinforced (3 supporting facts) |
| Day 30 | "Redis changed license to SSPL" | Observation refined: "Redis is technically strong, but has license concerns for cloud" |
| Day 45 | "Valkey forked Redis under BSD" | New observation: "Consider Valkey for new projects requiring true OSS" |
Handling Contradictory Evidence
What happens when a new fact contradicts an existing observation?
The consolidation engine doesn't blindly overwrite — it reconciles the contradiction by capturing the evolution:
Example: User preference changes
| Time | Fact | Observation |
|---|---|---|
| Week 1 | "User says they love React" | "User prefers React for frontend development" |
| Week 2 | "User praises React's component model" | "User is enthusiastic about React, particularly its component model" |
| Week 3 | "User says they've switched to Vue and won't use React anymore" | "User was previously a React enthusiast who appreciated its component model, but has now switched to Vue and no longer uses React" |
Notice how the final observation captures the full journey — not just "User prefers Vue" but the complete evolution of their preference. This nuanced understanding means:
- Your agent won't recommend React tutorials to someone who explicitly moved away from it
- Your agent understands why this matters (they were enthusiastic before, so this is a deliberate choice)
- Your agent can reference this history when relevant ("I know you used to work with React...")
The system:
- Detects the conflict — New fact contradicts existing observation
- Preserves history — Incorporates the previous understanding into the new observation
- Creates nuanced observation — Synthesizes a richer understanding that captures the change
- Updates freshness — Marks the observation as recently updated
Example: Correcting misinformation
| Time | Fact | Observation |
|---|---|---|
| Day 1 | "Alice works at Google" | "Alice is a Google employee" |
| Day 10 | "Alice actually works at Meta, not Google" | "Alice works at Meta (previously thought to work at Google)" |
When a fact explicitly corrects previous information, the observation is updated to reflect the correction while noting the previous understanding. The raw facts are always preserved, so you can trace back to see what was originally stated and when it was corrected.
Observations in Retrieval
Observations are automatically included in both recall() and reflect() operations:
In Recall
Observations are returned alongside raw facts, filtered by the types parameter:
In Reflect
The reflect agent uses hierarchical retrieval:
- Mental Models — User-curated summaries (highest priority)
- Observations — Consolidated knowledge with freshness awareness
- Raw Facts — Ground truth for verification
The agent automatically queries observations and uses them to inform its reasoning.
Freshness Awareness
Observations track when they were last updated. During reflect, the agent considers freshness:
- Fresh observations: Used directly for reasoning
- Stale observations: Agent verifies against current facts before relying on them
This ensures responses stay accurate even as the underlying data changes.
Observation Scopes
By default, observations are scoped to all of a memory's tags combined. The observation_scopes retain parameter lets you control this — building separate observations per tag, per combination, or with a custom list of scopes. This is key when a single memory carries multiple tags and you want each tag to accumulate its own observations independently.
See observation_scopes in the Retain API for the full explanation and options.
To inspect the scopes that already exist in a bank, call GET /v1/default/banks/{bank_id}/observations/scopes. The response lists each exact tag set with its observation count; the empty tag list is the global scope. The listing is paged (limit, default 100, and offset), with total reporting how many distinct scopes the bank holds. Use a returned scope as tags with tags_match: "exact" when you need to filter to that precise observation scope without also matching observations that carry extra tags. To recall only the global scope — the untagged observations written by observation_scopes: "shared" — pass an empty list with exact matching: tags: [], tags_match: "exact".
Changing the Scopes of Existing Documents {#changing-scopes}
The scoping is stored on each memory when it is retained. To move documents that are already in the bank, for example a bank retained with per-session tags under the default combined scoping that should now consolidate across sessions, send the new value on the document:
It takes the same values as retain ("combined" returns to the default). Nothing is re-extracted, and the memories keep their tags. A memory is consolidated again only if the change moves the scopes it resolves to. In that case its observations are deleted and rebuilt in the new scope by the next consolidation, and so are observations that other documents' memories shared with it. A change that resolves to the same scopes deletes nothing and queues nothing. Examples: combined over a single tag versus [["that-tag"]], or re-sending the current value. The new value is also saved on the document, so a later reprocess keeps it.
To move a whole bank, list its documents (filtering by tag if needed) and send one PATCH per document. Two things to know first:
- Update the client first. A retain that appends to a document sends its own
observation_scopes. If your client still retains without the new value, the next append moves that document back. - Expect a rebuild. Every moved memory loses its observations until consolidation runs again. On a large bank that is one LLM pass over most of its facts, and recall returns fewer observations meanwhile. Mental models citing the deleted observations refresh as usual. Moving documents in batches spreads the cost.
Observations Mission
You can define exactly what this bank should synthesise by setting an observations mission (observations_mission). This replaces the built-in durable-knowledge rules with your own instructions, letting you control what shape observations take.
e.g. Observations are stable facts about people and projects.
Always include preferences, skills, and recurring patterns.
Ignore one-off events and ephemeral state.Leave it blank to use the server default — durable, specific facts that stay true over time (preferences, skills, relationships, recurring patterns), with ephemeral state filtered out.
Examples:
observations_mission |
What gets synthesised |
|---|---|
| (unset — default) | Durable facts: preferences, skills, relationships, recurring patterns |
| "Observations are weekly summaries of sprint outcomes and blockers" | Broad event summaries grouped by time period |
| "Observations are stable facts about named individuals only" | Person-centric knowledge, tied to specific people |
| "Observations are recurring patterns in customer support interactions" | Failure modes, common requests, pain points |
Set observations_mission via the bank config API or the HINDSIGHT_API_OBSERVATIONS_MISSION environment variable.
Consolidation Strategies
The mission, the observation cap and the source-facts token limits apply to every scope in the bank. Consolidation strategies (consolidation_strategies) let specific scopes use their own instead.
This is what makes one bank work across several audiences. Say each memory is tagged with its author, their team and the company, and retained with observation_scopes: [["user:dana"], ["team:exec"], ["company:acme"]]. Each scope builds its own observations. But with a single mission, the company-wide scope gets the same detail as Dana's own — names, deal sizes, anything said in confidence — just visible to more people. A strategy fixes that:
[
{
"scopes": [{"tags": ["company:*"]}],
"observations_mission": "This scope is shared with the whole company. Record only general, industry-level trends. Never name a specific company, person, deal size or funding stage.",
"max_observations_per_scope": 20
},
{
"scopes": [{"tags": ["team:*"]}],
"observations_mission": "Record decisions the team must act on."
}
]With that in place, Dana's scope keeps "Acme Robotics signed a 3-year lease for a 4,000 GPU cluster", while the company scope gets "Companies are leasing GPU clusters to run open-weight models".
How a strategy is matched:
scopesis a list of alternatives. Each is{"tags": [...], "tags_match": "all" | "exact"}, wheretagsare fnmatch patterns (*matches any text). A strategy applies to a consolidation scope when any of its alternatives matches it (OR); the tags inside one alternative must all be present (AND).tags_match, set per alternative, decides whether other tags are allowed:tags_match{"tags": ["company:*", "team:*"]}matches……but not "all"(default){company:acme, team:exec},{user:dana, team:exec, company:acme}{company:acme}(no team)"exact"{company:acme, team:exec}only{user:dana, team:exec, company:acme}(extrauser:tag)Because the mode is per alternative, one strategy can mix them —
[{"tags": ["company:*"], "tags_match": "exact"}, {"tags": ["team:*"]}]claims scopes that are only a company, or that have a team among other tags. To match any one of several tags, give each its own alternative:[{"tags": ["company:*"]}, {"tags": ["team:*"]}]. Excluding a tag ("has a company but no user") is not expressible.:::caution The untagged scope can never be claimed
No pattern matches a scope with no tags, including a bare["*"]. That means a bank usingobservation_scopes: "shared", which consolidates over the single untagged
scope, has one scope that no strategy can reach: it always uses the bank-wide values. If you need a
differently-briefed catch-all, give it a real tag and target that instead of relying onshared.
:::Remember that a strategy matches observation scopes — the tag sets consolidation groups observations under, set by
observation_scopesat retain time — not the tags of individual memories. A memory tagged with a user, a team and a company but retained withobservation_scopes: [["user:dana"], ["team:exec"], ["company:acme"]]produces three single-tag scopes, none of which has both a company and a team.What a strategy can set:
observations_mission,max_observations_per_scope,consolidation_source_facts_max_tokensandconsolidation_source_facts_max_tokens_per_observation. Every one is optional.Anything a strategy leaves unset, and every scope no strategy matches, uses the bank-wide value. The control plane shows these bank-wide values as the Default strategy.
When two strategies match the same scope
The first one in the list wins, whole. Exactly one strategy applies to a scope. List order is priority order — left to right in the control plane's tabs.
The strategies are never mixed. If the winning strategy leaves a setting empty, that setting comes from Default, not from a later strategy that also matches. For example, with:
[
{"scopes": [{"tags": ["company:acme"]}], "max_observations_per_scope": 5},
{"scopes": [{"tags": ["company:*"]}], "observations_mission": "Record only general trends."}
]the scope company:acme gets a cap of 5 and the bank-wide mission, not "Record only general trends". The second strategy still applies to every other company:* scope. To give company:acme both, put both settings on the first strategy.
To see which strategy a scope uses, open the strategy in the control plane: each rule shows how many existing scopes it matches and how many an earlier strategy takes, with examples. The same answer is available from POST /v1/default/banks/{bank_id}/consolidation-strategies/preview, which takes a draft strategies list (nothing is saved) and reports, per strategy and rule, the matching scopes and which strategy actually handles each — computed with consolidation's own matching. It scans up to 10,000 distinct scopes; beyond that complete is false and the counts are lower bounds. Only scopes that already have observations exist to be previewed.
A strategy that sets nothing is ignored: it matches no scope and doesn't block later ones.
A malformed strategy is rejected when you save it through the control plane or the bank config API — an unknown key
("scope" for "scopes"), a tag list that is not a list of strings, an unknown tags_match — with a 400 naming the
offending entry. The HINDSIGHT_API_CONSOLIDATION_STRATEGIES environment variable is not validated this way: a
malformed entry is dropped silently, and an unrecognised tags_match falls back to "all" rather than erroring. If you
configure strategies by environment variable, check the preview afterwards to confirm each one claims what you expect. An incomplete one is accepted, because the control plane saves strategies as you type them: a rule with no tags yet, or a strategy with no setting yet, is stored and simply ignored until you finish it. Strategies survive bank template export and import unchanged.
Each scope is consolidated in its own LLM call, so one scope's mission never reaches another scope's call.
Set consolidation_strategies in the control plane (bank Configuration → Observations), via the bank config API, or with the HINDSIGHT_API_CONSOLIDATION_STRATEGIES environment variable. It replaces the older observation_scope_limits, which still works but is checked only after the strategies, and only
ever carried the observation cap.
:::note Porting a rule from observation_scope_limits
The two fields match differently. observation_scope_limits is exact-cover only: its pattern must account for every tag
in the scope. A strategy defaults to tags_match: "all", which allows extra tags. Copying a pattern across unchanged
therefore widens what it matches. Add "tags_match": "exact" to keep the old behaviour.
Note also that a strategy claiming a scope without setting max_observations_per_scope leaves the cap toobservation_scope_limits, falling back past any later strategy that would have set one.
:::
Observation Lifecycle & Invalidation
When Memories Are Deleted
Observations are derived from source memories. When source memories are removed, Hindsight automatically keeps observations consistent:
| Action | Effect on observations |
|---|---|
| Delete a document | All observations derived from the document's memories are deleted |
| Delete individual memories (by type) | Observations sourced from those memories are deleted |
| Delete an entire bank | All observations are deleted along with everything else |
After deletion, the remaining source memories that fed the affected observations have their consolidation state reset, so they will be re-consolidated on the next consolidation run and produce fresh observations.
Clearing Observations for a Specific Memory
You can clear all observations derived from a single memory without deleting the memory itself. This is useful when you want to force re-synthesis of a memory's contribution to consolidated knowledge.
Use the DELETE /v1/default/banks/{bank_id}/memories/{memory_id}/observations endpoint. This will:
- Delete all observations that list the memory as a source
- Reset
consolidated_aton the memory itself and any other source memories that contributed to those observations - Trigger a consolidation job so fresh observations are produced automatically
Resetting All Observations
To wipe all consolidated knowledge and start over:
This resets the consolidation state for all source memories in the bank, so the next consolidation run will re-derive all observations from scratch.
Trigger Consolidation {#trigger-consolidation}
Use the consolidate endpoint to manually trigger consolidation:
POST /v1/default/banks/{bank_id}/consolidate
Content-Type: application/json
{
"observation_scopes": [["user:alice"], ["team:engineering"]]
}The request body is optional. When omitted (or sent as an empty body), all unconsolidated memories in the bank are processed.
| Parameter | Type | Description |
|---|---|---|
observation_scopes |
list[list[str]] | null |
Optional list of tag scopes. Only memories whose tags contain all tags in at least one scope are processed. Omit for a full-bank sweep. |
Configuration
Observation consolidation runs automatically by default. You can disable auto-consolidation with HINDSIGHT_API_ENABLE_AUTO_CONSOLIDATION and trigger it on-demand via the consolidate endpoint. Monitor consolidation progress via the Operations API.
Next Steps
- Retain — How facts are stored and trigger consolidation
- Recall — How observations are retrieved
- Reflect — How the agentic loop uses observations
- Mental Models — User-curated summaries for common queries
Hindsight Docs Docs Developer Oracle (`hindsight-docs/docs/developer/oracle.md`)
Oracle Database
Hindsight uses PostgreSQL as its default storage backend, but it also runs on
Oracle Database 23ai for organizations that standardize on Oracle
infrastructure. All memory operations — retain, recall, and reflect — work the
same way on Oracle; the backend is selected with a single environment variable.
This guide covers everything needed to run Hindsight against Oracle: the
prerequisites, the driver, a local quick start, provisioning a production
database, running migrations, and the handful of behavioural differences from
PostgreSQL.
:::info When to use Oracle
Oracle is the right choice when your organization already runs Oracle and needs
Hindsight to live inside that footprint. For everything else, the default
PostgreSQL backend is simpler to operate — see Storage for the
rationale. Oracle and PostgreSQL are configured independently; you pick one per
deployment.
:::
Requirements
| Requirement | Details |
|---|---|
| Oracle Database | 23ai (23.4+). Oracle Database Free 23ai works for development. |
VECTOR type |
Used for embeddings. Requires the schema to live in an ASSM tablespace (see below). |
| Oracle Text | Full-text search uses Oracle Text indexes. The schema user needs the CTXAPP role. |
| Driver | python-oracledb ≥ 2.5.0, running in thin mode — pure Python, no Oracle Instant Client required. |
:::warning The schema must use an ASSM tablespace
Oracle's SYSTEM tablespace uses manual segment space management (MSSM),
which does not support VECTOR columns. Create the Hindsight user in a
tablespace with Automatic Segment Space Management (ASSM) — otherwise
migrations fail when they create embedding columns. The provisioning SQL below
does this for you.
:::
Install the driver
The Oracle driver is an optional extra — it is not bundled with the default
packages. Install it alongside Hindsight:
# With the packaged extra
pip install "hindsight-api-slim[oracle]"
# Or add the driver to an existing install (e.g. the full hindsight-api package)
pip install hindsight-api oracledbIf the driver is missing at startup, Hindsight fails with:python-oracledb is required for Oracle backend. Install it with: pip install oracledb.
Quick start (local Oracle)
The fastest way to try Hindsight on Oracle is the bundled helper script, which
starts a local Oracle Database Free 23ai container, provisions the test
user with the correct tablespace and grants, and prints a ready-to-use
connection URL:
# Start Oracle Free in Docker and bootstrap the hindsight_test user
./scripts/dev/start-oracle.sh
# ...prints:
# export HINDSIGHT_API_DATABASE_BACKEND=oracle
# export HINDSIGHT_API_DATABASE_URL='oracle+oracledb://hindsight_test:hindsight_test@localhost:1521/FREEPDB1'
# Stop and remove the container when done
./scripts/dev/stop-oracle.shA cold start takes 60–120s while the database initializes. If the first run
reports a provisioning error, the database was still starting up — just re-run./scripts/dev/start-oracle.sh once the container is healthy (it is idempotent).
Once the script prints the connection URL, export the variables it shows, and
also set the schema to the Oracle user it created:
export HINDSIGHT_API_DATABASE_SCHEMA=HINDSIGHT_TESTThen run migrations and start the API (see the steps below). Setting the schema
is required on Oracle — see step 3. This is the same
setup Hindsight's CI uses to test the Oracle backend.
Production setup
1. Provision the schema user
Connect to your pluggable database as a privileged user (for example SYSTEM)
and create a dedicated tablespace and user for Hindsight. The tablespace must
use ASSM so VECTOR columns are supported:
-- ASSM tablespace (required for VECTOR columns). Size to your data volume.
CREATE BIGFILE TABLESPACE hindsight_ts
DATAFILE 'hindsight_ts.dbf' SIZE 2G AUTOEXTEND ON NEXT 500M MAXSIZE UNLIMITED
EXTENT MANAGEMENT LOCAL
SEGMENT SPACE MANAGEMENT AUTO;
-- Dedicated schema user
CREATE USER hindsight IDENTIFIED BY ""
DEFAULT TABLESPACE hindsight_ts
TEMPORARY TABLESPACE temp
QUOTA UNLIMITED ON hindsight_ts;
-- Object privileges Hindsight's migrations need
GRANT CONNECT, RESOURCE, CREATE TABLE, CREATE SEQUENCE, CREATE VIEW, CREATE PROCEDURE TO hindsight;
-- Oracle Text (full-text search indexes)
GRANT CTXAPP TO hindsight;:::note Least privilegeCONNECT and RESOURCE cover the basics; the explicit CREATE TABLE / SEQUENCE / VIEW / PROCEDURE grants and CTXAPP are what the schema migrations require.
No DBA role is needed. On a managed service where CREATE TABLESPACE is not
available directly, provision the schema through the platform's admin tooling —
the requirements are unchanged: an ASSM default tablespace (needed forVECTOR columns) plus the CTXAPP role.
:::
2. Build the connection URL
Hindsight uses SQLAlchemy-style URLs. The Oracle form is:
oracle+oracledb://USER:PASSWORD@HOST:PORT/SERVICE_NAME| Part | Example | Notes |
|---|---|---|
USER / PASSWORD |
hindsight / s3cret |
The schema user from step 1. URL-encode reserved characters (@, /, :) in the password. |
HOST:PORT |
db.internal:1521 |
The listener host and port (Oracle default is 1521). |
SERVICE_NAME |
FREEPDB1 |
The service name of your pluggable database (not the SID). FREEPDB1 for Oracle Free. |
Example:
oracle+oracledb://hindsight:[email protected]:1521/ORCLPDB1Full connect descriptors and TNS aliases
The form above is Easy Connect, which can only express host:port/service. When you
need more — Autonomous Database over TCPS, retry_count, ssl_server_dn_match, or a
TNS alias from a tnsnames.ora — leave the host empty and pass the descriptor as adsn query parameter:
oracle+oracledb://USER:PASSWORD@/?dsn=DESCRIPTOR_OR_TNS_ALIASThe descriptor has to be percent-encoded, like the password. For example, the
Autonomous Database descriptor
(description=(retry_count=20)(address=(protocol=tcps)(port=1522)(host=adb.example.com))(connect_data=(service_name=abc_low))(security=(ssl_server_dn_match=yes)))becomes
export HINDSIGHT_API_DATABASE_URL="oracle+oracledb://hindsight:s3cret@/?dsn=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.stdin.read().strip()))' <<< '(description=(retry_count=20)(address=(protocol=tcps)(port=1522)(host=adb.example.com))(connect_data=(service_name=abc_low))(security=(ssl_server_dn_match=yes)))')":::warning Wallet-based mTLS is still not supported
Hindsight passes no wallet directory or wallet password to the driver, so
connections that require a wallet (mTLS) do not work. TCPS withssl_server_dn_match does. Otherwise secure the connection at the network layer
(private networking, VPN, or a TLS-terminating proxy).
:::
3. Configure Hindsight
Point Hindsight at Oracle with two environment variables:
export HINDSIGHT_API_DATABASE_BACKEND=oracle
export HINDSIGHT_API_DATABASE_URL='oracle+oracledb://hindsight:[email protected]:1521/ORCLPDB1'
export HINDSIGHT_API_DATABASE_SCHEMA=HINDSIGHT # the Oracle user from step 1HINDSIGHT_API_DATABASE_BACKEND defaults to postgresql; set it to oracle to
select the Oracle backend.
:::warning Set DATABASE_SCHEMA to your Oracle userHINDSIGHT_API_DATABASE_SCHEMA defaults to public — a PostgreSQL concept. On
Oracle a schema is a user, there is no public schema, and leaving the
default makes migrations fail with ORA-01435: user does not exist. Set it to
the schema user you created in step 1, spelled exactly as Oracle stores it —
uppercase (e.g. HINDSIGHT) unless you created the user with a quoted
lower-case name.
:::
See Configuration → Database for the full list of
database variables.
4. Run migrations
Hindsight runs the same schema migrations on Oracle as on PostgreSQL. By default
the API applies them automatically on startup
(HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=true). To run them explicitly — for
example in a controlled deploy step — use:
hindsight-admin run-db-migrationThis routes through the dialect-aware migration runner and creates the Oracle
schema. (Unlike the admin CLI's data-movement commands, run-db-migration
is fully supported on Oracle — see Limitations.)
:::warning Migrate with your runtime embedding dimension
The embedding VECTOR columns are sized to the dimension of the configured
embeddings model. Run migrations with the same embeddings provider/model you
will serve with — otherwise the column dimension won't match the vectors the
API produces and retain fails with ORA-51803: Vector dimension count must match… (for example, a schema built for a 384-dim local model rejects the
1536-dim vectors from OpenAI text-embedding-3-small). If you change the
embeddings model later, re-run migrations with --embedding-dimension <N> to
resize the columns.
:::
5. Start the API
hindsight-apiOn startup Hindsight logs the resolved database (with credentials masked); it
should show your Oracle host and confirm the Oracle backend is active.
Configuration reference
Oracle-relevant settings, all documented in full on the
Configuration page:
| Variable | Purpose |
|---|---|
HINDSIGHT_API_DATABASE_BACKEND |
postgresql (default) or oracle. |
HINDSIGHT_API_DATABASE_URL |
oracle+oracledb://… connection URL. |
HINDSIGHT_API_DATABASE_SCHEMA |
Schema/user for the tables. On Oracle set this to your schema user (uppercase); the public default fails. |
HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP |
Auto-apply migrations when the API boots (default true). |
Limitations vs PostgreSQL
Memory operations behave identically on Oracle, but a few operational and
internal details differ:
- Admin CLI data commands are PostgreSQL-only.
hindsight-adminbackup,
restore, bank export/import, and worker-status use asyncpg binaryCOPYandTRUNCATE, which are PostgreSQL-specific and not available on Oracle.
Schema migrations (run-db-migration) are supported on Oracle. - No embedded database. The
pg0embedded PostgreSQL used for zero-config
local development has no Oracle equivalent — Oracle always requires a running
instance (use the quick-start script locally). - Consolidation reconciliation is skipped. The similarity-based
near-duplicate reconciliation pass in consolidation
(HINDSIGHT_API_CONSOLIDATION_DEDUP_THRESHOLD) is a PostgreSQL-only path;
consolidation still runs on Oracle, without that extra reconciliation step. - Entity resolution uses Oracle fuzzy matching. Fuzzy entity lookup during
retain uses Oracle's text matching rather than PostgreSQL'spg_trgmtrigram
matching. Behaviour is equivalent; the underlying mechanism differs.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
python-oracledb is required for Oracle backend |
The driver isn't installed. Run pip install oracledb (or install the [oracle] extra). |
ORA-01435: user does not exist on migration |
HINDSIGHT_API_DATABASE_SCHEMA is unset (defaults to public) or misspelled. Set it to your Oracle schema user, uppercase (e.g. HINDSIGHT). |
ORA-51803: Vector dimension count must match on retain |
The schema was migrated with a different embedding dimension than the running embeddings model. Migrate with the same embeddings config, or re-run run-db-migration --embedding-dimension <N>. |
Migration errors when creating embedding/VECTOR columns |
The schema user's default tablespace is not ASSM (often the SYSTEM tablespace). Recreate the user in an ASSM tablespace as shown above. |
| Full-text search errors / missing Oracle Text index | The schema user is missing the CTXAPP role. Run GRANT CTXAPP TO <user>;. |
ORA-12514 / service not found |
The URL uses a SID or wrong service name. Use the pluggable database service name (e.g. FREEPDB1), not the SID. |
| Login works manually but fails from Hindsight | A reserved character in the password isn't URL-encoded. Encode @ / : ? in the DATABASE_URL. |
See also
- Storage — why PostgreSQL is the default, and how Oracle fits in
- Configuration — all database environment variables
- Installation — packaging and deployment options
- Admin CLI — administrative commands (PostgreSQL-only data operations)
Hindsight Docs Docs Developer Performance (`hindsight-docs/docs/developer/performance.md`)
Performance
Hindsight is designed for high-performance semantic memory operations at scale. This page covers performance characteristics, optimization strategies, and best practices.
Overview
Hindsight's performance is optimized across three key operations:
- Retain (Ingestion): Batch processing with async operations for large-scale memory storage
- Recall (Search): Sub-second semantic search with configurable thinking budgets
- Reflect (Reasoning): Disposition-aware answer generation with controllable compute
Design Philosophy: Optimized for Fast Reads
Hindsight is architected from the ground up to prioritize read performance over write performance. This design decision reflects the typical usage pattern of memory systems: memories are written once but read many times.
The system makes deliberate trade-offs to ensure sub-second recall operations:
- Pre-computed embeddings: All memory embeddings are generated and indexed during retention
- Optimized vector search: HNSW indexes enable fast approximate nearest neighbor search
- Fact extraction at write time: Complex LLM-based fact extraction happens during retention, not retrieval
- Structured memory graphs: Relationships and temporal information are resolved upfront
This means Recall (search) operations are blazingly fast because all the heavy lifting has already been done.
Performance Comparison
| Operation | Typical Latency | Primary Bottleneck | Optimization Strategy |
|---|---|---|---|
| Recall | 100-600ms | Re-ranker (on CPU) | Use GPU for re-ranking, or reduce budget |
| Reflect | 800-3000ms | LLM generation | Use faster LLM |
| Retain | 500ms-2000ms per batch | LLM fact extraction | Use high-throughput LLM provider |
Hindsight is designed to ensure your application's read path (recall/reflect) is always fast, even if it means spending more time upfront during writes. This is the right trade-off for memory systems where:
- Memories are retained in background processes or during low-traffic periods
- Memories are queried frequently in user-facing, latency-sensitive contexts
- The ratio of reads to writes is high (typically 10:1 or higher)
Retain Performance
Retain (write) operations are inherently slower because they involve LLM-based fact extraction, entity recognition, temporal reasoning, relationship mapping, and embedding generation. The LLM is the primary bottleneck for write latency.
Hindsight Doesn't Need a Smart Model
The fact extraction process is structured and well-defined, so smaller, faster models work extremely well. Our recommended model is gpt-oss-20b (available via Groq and other providers).
To maximize retention throughput:
Use high-throughput LLM providers: Choose providers with high requests-per-minute (RPM) limits and low latency
- Fast: Groq with
gpt-oss-20bor other openai-oss models, self-hosted models on GPU clusters (vLLM, TGI) - Slow: Standard cloud LLM providers with rate limits
- Fast: Groq with
Batch your operations: Group related content into batch requests. Send as much data as you want in a single request — the only limit is the HTTP payload size.
Use async mode for large datasets: Queue operations in the background
Parallel processing: For very large datasets, use multiple concurrent retention requests with different
document_idvalues
Automatic Batch Optimization
When using async retain, Hindsight automatically handles batch sizing for you. You don't need to manually tune batch sizes or worry about optimal chunking.
How it works:
- Send large batches: Submit hundreds or thousands of items in a single async retain request
- Automatic splitting: Hindsight automatically splits large batches (>10,000 tokens) into optimized sub-batches
- Parallel processing: Sub-batches are processed concurrently in the background
- Status tracking: Parent operation aggregates status from all sub-batches
- Token-based: Batching counts real tokens, not characters
Benefits:
- Send entire documents or datasets in one API call
- Let Hindsight optimize the processing strategy
- Track overall progress via the parent operation status
- No need to manually split data into small batches
Throughput
Factors affecting throughput:
- Document size and complexity
- LLM provider rate limits (for fact extraction)
- Database write performance
- Available CPU/memory resources
Tuning for Local & Small Environments
Hindsight's defaults are tuned for cloud LLM providers and multi-core servers. When you run it on a laptop, a single GPU box, or against a local LLM server (llama.cpp, vLLM, LM Studio, Ollama) with a small fixed slot pool, those defaults can saturate the backend, time out, or thrash the CPU. This section collects the knobs that matter for low-resource setups.
LLM concurrency
The default HINDSIGHT_API_LLM_MAX_CONCURRENT=32 assumes a cloud provider that can absorb dozens of parallel requests. A local server with a handful of slots cannot — Hindsight will fill every slot and starve any other client sharing the endpoint (your main agent, another app, or a second Hindsight operation).
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2A value of 2 lets retain and consolidation run concurrently without blocking each other. If the endpoint is shared with other clients (other applications, agents, or workflows hitting the same llama-server / vLLM / LM Studio instance), reserve slots for them by lowering further — leave at least one slot free per shared client.
You can also split the budget per operation so background work never crowds out live reads. The per-operation caps compose on top of the global cap:
# global=4, with retain/consolidation capped low so reflect always has headroom
export HINDSIGHT_API_LLM_MAX_CONCURRENT=4
export HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT=1
export HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT=1Timeouts and retries
Small models on modest hardware generate tokens slowly, and the first request after startup pays a model-load cost. The default HINDSIGHT_API_LLM_TIMEOUT=120 (seconds) can be too tight for a large local model on CPU — raise it to avoid spurious timeouts and wasted retries:
export HINDSIGHT_API_LLM_TIMEOUT=300 # allow slow local generation
export HINDSIGHT_API_LLM_MAX_RETRIES=2 # fail faster locally — retries rarely help a slow boxA local endpoint isn't rate-limited, so aggressive retry/backoff mostly adds latency on real failures. Lower retries and let genuine errors surface quickly.
Smaller, faster models — and reasoning effort
Retain (fact extraction) is structured work that does not need a frontier model; reflect can use a lighter model still. On a constrained box, point each operation at the smallest model that holds up:
# Reflect on a small/fast model; retain on a slightly stronger structured-output model
export HINDSIGHT_API_REFLECT_LLM_MODEL=<small-fast-model>
export HINDSIGHT_API_RETAIN_LLM_MODEL=<structured-output-model>If your model exposes a reasoning/thinking budget, set it low — extra reasoning tokens are pure latency for the extraction and consolidation paths. Nothing is sent unless you set it, so an unconfigured deployment runs at whatever effort the model defaults to (none is the value that switches thinking off entirely, where the backend supports it):
export HINDSIGHT_API_LLM_REASONING_EFFORT=lowConsolidation sends multiple facts to the LLM in a single call (default 8). On a small model with a limited context window, a large batch produces an oversized prompt and a long, error-prone response. Shrink the batch so each consolidation call stays small and reliable:
export HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=2 # default 8; lower = smaller prompts, more callsBuilt-in llama.cpp tuning
The bundled llamacpp provider runs a llama.cpp server as a managed subprocess — no external server needed. Key knobs for small machines:
export HINDSIGHT_API_LLM_PROVIDER=llamacpp
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2 # retain + consolidation without blocking
export HINDSIGHT_API_LLAMACPP_GPU_LAYERS=-1 # -1 = offload all layers to GPU; 0 = CPU only
export HINDSIGHT_API_LLAMACPP_CONTEXT_SIZE=8192 # lower to save RAM/VRAM; raise for big batches
export HINDSIGHT_API_LLAMACPP_EXTRA_ARGS="--n_threads 8" # match physical cores on CPU-only boxes
# export HINDSIGHT_API_LLAMACPP_NO_GRAMMAR=true # faster, but less reliable JSON outputSee Built-in llama.cpp for the full option list.
Reranker on CPU
Recall's bottleneck on a machine without a GPU is the cross-encoder reranker. The local reranker has several CPU/Apple-Silicon knobs that are quality-neutral but materially faster:
# Apple Silicon (MPS): half precision is 27–36% faster, quality-identical
export HINDSIGHT_API_RERANKER_LOCAL_FP16=true
# Sort pairs by length before batching — 36–54% faster, quality-identical by construction
export HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=true
# Cap reranker parallelism so it doesn't thrash a small CPU under load (default 4)
export HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=2
# On macOS, force CPU if MPS/XPC causes instability
# export HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=trueThe biggest single win on CPU is reranking fewer candidates. By default Hindsight reranks up to 300 candidates per recall — shrink that pool to cut cross-encoder work proportionally:
export HINDSIGHT_API_RERANKER_MAX_CANDIDATES=100 # default 300; RRF pre-filters the restFor a pure-CPU box that struggles with the cross-encoder, flashrank is a lighter ONNX-based reranker:
export HINDSIGHT_API_RERANKER_PROVIDER=flashrankYou can also reduce recall work directly: use a lower budget (low/mid) for everyday queries and reserve high for comprehensive reasoning. See Recall Performance below.
Recall Performance
Budget
The budget parameter controls the search depth and quality. Choose based on query complexity — comprehensive questions that need thorough analysis benefit from higher budgets:
| Budget | Use Case |
|---|---|
low |
Quick lookups, real-time chat |
mid |
Standard queries, balanced performance |
high |
Comprehensive questions, thorough analysis |
Optimization
- Appropriate budgets: Use lower budgets for simple queries, higher for comprehensive reasoning
- Limit result tokens: Set
max_tokensto control response size (default: 4096) - Include chunks: Use
include_chunksto retrieve the raw text that generated memories when you need additional context
Database Performance
Hindsight uses PostgreSQL with pgvector for efficient vector search:
- Index type: HNSW for approximate nearest neighbor search
- Typical query time: 10-50ms for vector search on 100K+ facts
- Scalability: Tested with millions of facts per bank
Reflect Performance
Performance Characteristics
| Component | Latency | Description |
|---|---|---|
| Memory search | 100-600ms | Based on budget (low/mid/high) |
| LLM generation | 500-2000ms | Depends on provider and response length |
| Total | 600-2600ms | Typical end-to-end latency |
Optimization Strategies
- Budget selection: Use lower budgets when context is sufficient
- Context provision: Provide relevant
contextto reduce recall requirements and steer towards more focused answers
Best Practices
Operations
- Use appropriate budgets: Don't over-provision for simple queries; use higher budgets for comprehensive reasoning
- Batch retain operations: Group related content together for better efficiency
- Cache frequent queries: Cache at the application level for repeated queries
- Profile with trace: Use the
traceparameter to identify slow operations
Scaling
- Horizontal scaling: Deploy multiple API instances behind a load balancer with shared PostgreSQL
- Concurrency: 100+ simultaneous requests supported; memory search scales with CPU cores
- LLM rate limits: Distribute load across multiple API keys/providers (typically 60-500 RPM per key)
Cost Optimization
- Use efficient models:
gpt-oss-20bvia Groq for retain — Hindsight doesn't need frontier models - Enable provider Batch API: Set
HINDSIGHT_API_RETAIN_BATCH_ENABLED=truewith async retain to cut LLM fact-extraction costs by 50% (supported on OpenAI and Groq; results delivered within 24 hours) - Control token budgets: Limit
max_tokensfor recall, use lower budgets when possible - Optimize chunks: Larger chunks (1000-2000 tokens) are more efficient than many small ones
Monitoring
- Prometheus metrics: Available at
/metrics— track latency percentiles, throughput, and error rates - Key metrics:
hindsight_recall_duration_seconds,hindsight_reflect_duration_seconds,hindsight_retain_items_total
Hindsight Docs Docs Developer Rag Vs Hindsight (`hindsight-docs/docs/developer/rag-vs-hindsight.md`)
sidebar_position: 2
RAG vs Memory
Traditional RAG (Retrieval-Augmented Generation) retrieves documents similar to a query. Hindsight provides structured memory with temporal reasoning, entity understanding, and belief formation.
Capability Comparison
| Capability | RAG | Hindsight |
|---|---|---|
| Search strategy | Semantic similarity only | Semantic + keyword + graph + temporal |
| Multi-hop reasoning | Limited to retrieved chunks | Graph traversal across entity relationships |
| Temporal queries | Keyword matching ("spring") | Date parsing and range filtering |
| Entity understanding | None | Entity resolution, co-occurrence tracking |
| Knowledge consolidation | Stateless | Mental models that synthesize and evolve |
| Disposition | None | 3 traits (skepticism, literalism, empathy) influence interpretation |
Architecture Comparison
RAG
| Step | Operation |
|---|---|
| 1 | Embed query |
| 2 | Vector similarity search |
| 3 | Return top-k chunks |
| 4 | Generate response |
Single retrieval strategy. No state between queries.
Hindsight
| Step | Operation |
|---|---|
| 1 | Parse query (extract temporal expressions, entities) |
| 2 | Execute 4 parallel retrievals: semantic, BM25, graph, temporal |
| 3 | Fuse results with RRF |
| 4 | Rerank with cross-encoder |
| 5 | Apply disposition traits |
| 6 | Generate response |
Multiple retrieval strategies. Persistent state across sessions.
Example Scenarios
Multi-Hop Reasoning
Stored facts:
- "Alice is the tech lead on Project Atlas"
- "Project Atlas uses Kubernetes"
- "Kubernetes cluster had an outage Tuesday"
Query: "Was Alice affected by recent issues?"
| System | Result |
|---|---|
| RAG | Retrieves facts about Alice only (no semantic similarity to "issues") |
| Hindsight | Traverses Alice → Project Atlas → Kubernetes → outage via entity links |
Temporal Queries
Stored facts with timestamps:
- March: "Alice started microservices migration"
- April: "Alice completed auth service"
- October: "Alice focusing on performance"
Query: "What did Alice do last spring?"
| System | Result |
|---|---|
| RAG | Returns all Alice facts regardless of date |
| Hindsight | Parses "last spring" → March-May, filters to that range |
Entity Understanding
Stored facts about a user across sessions:
- "Pro subscription"
- "Mobile app crashes in settings"
- "Switched to annual billing"
- "Desktop app working fine"
Query: "What do you know about my account?"
| System | Result |
|---|---|
| RAG | Lists disconnected facts |
| Hindsight | Returns connected facts via entity graph: subscription status, billing, known issues |
Knowledge Evolution
Week 1: User struggles with async Python, succeeds with threads
Week 3: User asks about asyncio, implements async database calls
| System | Behavior |
|---|---|
| RAG | No memory of progression |
| Hindsight | Consolidates mental model "user prefers sync" → refines to "user growing comfortable with async" |
When to Use Each
| Use Case | Recommended |
|---|---|
| Document Q&A over static corpus | RAG |
| Search with no temporal requirements | RAG |
| AI assistants with persistent memory | Hindsight |
| Applications requiring entity tracking | Hindsight |
| Systems needing consistent disposition | Hindsight |
| Temporal queries ("last month", "in 2023") | Hindsight |
Hindsight Docs Docs Developer Reflect (`hindsight-docs/docs/developer/reflect.mdx`)
sidebar_position: 4
import Figure from '@site/src/components/Figure';
import reflectLoop from '@site/figures/reflect.json';
import CodeSnippet from '@site/src/components/CodeSnippet';
import memoryBanksPy from '!!raw-loader!@site/examples/api/memory-banks.py';
Reflect: Agentic Reasoning with Disposition
When you call reflect(), Hindsight runs an agentic loop that autonomously gathers evidence and reasons through the lens of the bank's disposition to generate contextual responses.
How It Works
Unlike simple retrieval, reflect is an agentic system that:
- Autonomously gathers evidence — The agent decides what information it needs and calls appropriate tools
- Uses hierarchical retrieval — Checks mental models first, then observations, then raw facts
- Applies disposition — Shapes reasoning based on the bank's personality traits
- Enforces applicable directives — Global rules and rules matching the reflect tag scope
- Cites sources — Returns which memories and observations were used
The Agentic Loop
The reflect agent runs in a loop with access to these tools:
| Tool | Purpose | Priority |
|---|---|---|
search_mental_models |
User-curated summaries — best match in full, a snippet of the others | Highest (check first) |
read_mental_models |
Full text of the pages returned as snippets | When a snippet looks relevant |
search_observations |
Consolidated knowledge | High |
recall |
Raw facts (ground truth) | Fallback |
expand |
Get more context for a memory | As needed |
done |
Complete with final answer | When ready |
The agent:
- Must gather evidence before answering (guardrail prevents empty responses)
- Runs up to 10 iterations to find relevant information
- Validates citations — only IDs that were actually retrieved can be cited
Hierarchical Retrieval Strategy
The agent uses a smart retrieval hierarchy:
- Mental Models — User-curated summaries you've pre-computed for common queries
- Observations — Consolidated knowledge with freshness awareness
- Raw Facts — Ground truth for verification when observations are stale
Mental models are saved reflect responses that you create for frequently asked questions. They're checked first because they represent explicitly curated knowledge. See the Mental Models API for how to create and manage them.
If an observation is marked as stale, the agent automatically verifies it against current facts.
Why Reflect?
Most AI systems can retrieve facts, but they can't reason about them in a consistent way.
The Problem
Without reflect:
- No consistent character: Same question gets different answers each time
- No knowledge synthesis: System never connects related facts
- No reasoning context: Responses don't reflect accumulated knowledge
- Generic responses: Every AI sounds the same
The Value
With reflect:
- Consistent character: A "detail-oriented, cautious" bank emphasizes risks and thorough planning
- Evolving knowledge: Observations strengthen and adapt as evidence accumulates
- Contextual reasoning: "Based on what I know about your team's remote work success..."
- Differentiated behavior: Support bots sound diplomatic, code reviewers sound direct
When to Use Reflect
Use recall() when... |
Use reflect() when... |
|---|---|
| You need raw facts | You need reasoned interpretation |
| You're building your own reasoning | You want disposition-consistent responses |
| You need maximum control | You want the bank to "think" for itself |
| Simple fact lookup | Forming recommendations |
Example:
recall("Alice")→ Returns all Alice facts and relevant mental modelsreflect("Should we hire Alice?")→ Agent gathers evidence about Alice, reasons about fit, returns answer with citations
Disposition Traits
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and reasons during reflect():
| Trait | Scale | Low (1) | High (5) |
|---|---|---|---|
| Skepticism | 1-5 | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
| Literalism | 1-5 | Flexible interpretation, reads between the lines | Literal interpretation, takes things at face value |
| Empathy | 1-5 | Detached, focuses on facts | Empathetic, considers emotional context |
Mission: Natural Language Identity
Beyond numeric traits, you can provide a natural language mission that describes the bank's identity and reasoning context:
The reflect mission frames how the agent reasons and responds:
- Provides identity context: who the agent is and what it cares about
- Shapes how disposition traits are applied in practice
- Keeps reasoning consistent across conversations
:::info Per-operation missions
The reflect mission only affects reflect(). To steer what gets extracted during retain(), use retain_mission. To control what gets synthesised into observations, use observations_mission.
:::
Disposition Shapes Reasoning
Two banks with different dispositions, given identical facts about remote work:
Bank A (low skepticism, high empathy):
"Remote work enables flexibility and work-life balance. The team seems happier and more productive when they can choose their environment."
Bank B (high skepticism, low empathy):
"Remote work claims need verification. What are the actual productivity metrics? The anecdotal benefits may not translate to measurable outcomes."
Same facts → Different conclusions because disposition shapes interpretation.
Disposition Presets by Use Case
Different use cases benefit from different disposition configurations:
| Use Case | Recommended Traits | Why |
|---|---|---|
| Customer Support | skepticism: 2, literalism: 2, empathy: 5 | Trusting, flexible, understanding |
| Code Review | skepticism: 4, literalism: 5, empathy: 2 | Questions assumptions, precise, direct |
| Legal Analysis | skepticism: 5, literalism: 5, empathy: 2 | Highly skeptical, exact interpretation |
| Therapist/Coach | skepticism: 2, literalism: 2, empathy: 5 | Supportive, reads between lines |
| Research Assistant | skepticism: 4, literalism: 3, empathy: 3 | Questions claims, balanced interpretation |
Directives: Hard Rules
While disposition traits influence reasoning style, directives are hard
rules that the agent must follow when they apply to the current reflect scope.
Untagged directives are global; tagged directives require matching reflect
tags. See Memory Banks: Directives for
the full matching and default-behavior tables.
When to Use Directives
Use directives for constraints that must never be violated:
- Compliance rules: "Never recommend specific stocks or financial products"
- Privacy constraints: "Never share personal data with third parties"
- Style requirements: "Always respond in formal English"
- Domain guardrails: "Always cite sources when making factual claims"
Directive Scope and Tags
Directive tags scope when a directive applies, just like they scope memories. Untagged directives always apply; tagged directives apply only when the reflect request carries matching tags. A reflect with no tags therefore applies only the untagged directives. Set apply_all_directives: true on the request to enforce every active directive regardless of tags. See Memory Banks: Directive Scope and Tags.
Directives vs Disposition
| Aspect | Disposition | Directives |
|---|---|---|
| Nature | Soft influence | Hard rules |
| Effect | Shapes interpretation and tone | Must be followed exactly |
| Violation | Acceptable (it's a tendency) | Not acceptable |
| Example | High skepticism → questions claims | "Never make medical diagnoses" |
:::tip
Use disposition for personality and character. Use directives for compliance and guardrails.
:::
See Memory Banks: Directives for how to create and manage directives.
What You Get from Reflect
When you call reflect():
Returns:
- Response text — Disposition-influenced answer from the agent
- based_on — Evidence used: memories, mental models, and directives that grounded the response
- trace — Tool calls, LLM calls, and observations accessed (when
include.tool_calls=True) - structured_output — Parsed response if
response_schemawas provided - usage — Token usage metrics
Example:
{
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit for the research team lead position...",
"based_on": {
"memories": [
{"id": "mem-123", "text": "Alice has 5 years of ML experience", "type": "world"},
{"id": "mem-456", "text": "Alice worked at Google on search ranking", "type": "experience"}
],
"mental_models": [],
"directives": [
{"id": "dir-001", "name": "Formal Language", "rules": ["Always respond in formal English"]}
]
},
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000}
}The agent automatically gathers evidence, validates citations, and generates a grounded response.
Structured Output
Reflect returns prose by default. Pass a response_schema (a JSON Schema object with aproperties map) to also get a machine-readable version of the same answer:
{
"query": "How does Alice fit the research lead role?",
"response_schema": {
"type": "object",
"properties": {
"recommendation": { "type": "string" },
"strengths": { "type": "array", "items": { "type": "string" } },
"confidence": { "type": "number" }
},
"required": ["recommendation"]
}
}The response then carries both the prose and the structured view:
{
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit...",
"structured_output": {
"recommendation": "Strong fit for research lead",
"strengths": ["5 years of ML experience", "Search ranking at Google"],
"confidence": 0.9
}
}How it works. The agent first reasons to a natural-language answer, then a second pass
extracts that answer into JSON matching your schema. structured_output is therefore always a
faithful projection of text — you get the readable answer and a typed object to program
against, never one instead of the other.
When extraction fails. The reflect still returns 200 with the prose answer, nostructured_output, and a structured_output_error field saying why (a provider error, a timeout,
unparseable output). A missing structured_output without that field means the answer simply held
nothing matching your schema — so you can retry the broken case without retrying the ordinary one.
Schema rules. The schema must be an object with at least one property. Each property's type
is one of string, number, integer, boolean, array, or object, and required lists
property names. Invalid schemas are rejected before the call runs, so a malformed schema fails
fast instead of silently returning nothing.
Structured output for mental models
A pinned mental model can carry its own response_schema in its trigger config. Every refresh
then stores a structured_output next to the markdown content — extracted from the final
stored document. This matters for delta-mode (surgical) refreshes: the structured view reflects
the whole merged document, not just the facts that changed in that refresh.
Building a schema (no code)
In the control plane, the reflect view and the mental-model dialogs include a Build schema
editor with a Visual mode (add fields — name, type, required, description) and a Code mode
(raw JSON). The page itself shows only whether a schema is set; all editing happens in the
builder, so you never hand-write JSON unless you want to.
Why Disposition Matters
Without disposition, all AI assistants sound the same. With disposition:
- Customer support bots can be diplomatic and empathetic
- Code review assistants can be direct and thorough
- Creative assistants can be open to unconventional ideas
- Risk analysts can be appropriately cautious
Disposition creates consistent character across conversations while observations evolve with evidence.
Next Steps
- Observations — How knowledge is consolidated
- Retain — How rich facts are stored
- Recall — How multi-strategy search works
- Reflect API — Code examples and parameters
Hindsight Docs Docs Developer Retain (`hindsight-docs/docs/developer/retain.md`)
sidebar_position: 2
import Figure from '@site/src/components/Figure';
import retainFigure from '@site/figures/retain.json';
Retain: How Hindsight Stores Memories
When you call retain(), Hindsight transforms conversations and documents into structured, searchable memories that preserve meaning and context.
What Retain Does
Rich Fact Extraction
Hindsight doesn't just store what was said — it captures why, how, and what it means.
What Gets Captured
When you retain "Alice joined Google last spring and was thrilled about the research opportunities", Hindsight extracts:
The core facts:
- Alice joined Google
- This happened last spring
The emotions and meaning:
- She was thrilled
- It represented an important opportunity
The reasoning:
- She chose it for the research opportunities
This rich extraction means you can later ask "Why did Alice join Google?" and get a meaningful answer, not just "she joined Google."
Preserving Context
Traditional systems fragment information:
- "Bob suggested Summer Vibes"
- "Alice wanted something unique"
- "They chose Beach Beats"
Hindsight preserves the full narrative:
- "Alice and Bob discussed naming their summer party playlist. Bob suggested 'Summer Vibes' because it's catchy, but Alice wanted something unique. They ultimately decided on 'Beach Beats' for its playful tone."
This means search results include the full context, not disconnected fragments.
Two Types of Facts
Every fact is classified by whose perspective it captures — the agent that owns the bank, or the outside world:
| Type | What it captures | Example |
|---|---|---|
| experience | The bank's own agent acting, observing, or interacting — its first-person history | "I recommended Python to Alice" |
| world | Facts about other people, places, things, and events | "Alice works at Google" |
The split is decided by who is speaking, not by grammar. A first-person statement is an experience only when the speaker is the bank's agent. The same words said by someone else are a world fact about that person:
- Agent's own log — "I patched the auth bug" → experience (the agent did it).
- A user talking to the agent — "I bought a Tesla" → world (a fact about the user, not the agent).
Describe the speaker in each item's context to steer this correctly. When retaining transcripts or third-party content, a context like "Customer Maria is speaking" ensures her first-person statements are stored as world facts about Maria rather than mistaken for the agent's own experiences. For the agent's own logs, a context like "The assistant is speaking" attributes its first-person statements to the agent as experience facts.
Note: Observations are consolidated automatically in the background after retain() operations complete. This consolidation process synthesizes patterns from new facts into the bank's knowledge base.
Entity Recognition
Hindsight automatically identifies and tracks entities — the people, organizations, and concepts that matter.
What Gets Recognized
- People: "Alice", "Dr. Smith", "Bob Chen"
- Organizations: "Google", "MIT", "OpenAI"
- Places: "Paris", "Central Park", "California"
- Products & Concepts: "Python", "TensorFlow", "machine learning"
Entity Resolution
The same entity mentioned different ways gets unified through fuzzy name matching, reinforced by co-occurrence and temporal proximity:
- "Alice" + "Alice Chen" + "Alice C." → one person
Because resolution keys off name similarity, close variants merge automatically. Names that do not resemble each other (a nickname and an unrelated formal name, for example) are not unified on the name alone, though shared co-occurring entities can still link them.
Why it matters: You can ask "What do I know about Alice?" and get everything, even if she was mentioned as "Alice Chen" in some conversations.
Resolution is a judgement call, so it can go the other way too: on a bank with a lot of history, a short new name that resembles an existing entity — and that turns up alongside entities the existing one is already associated with — can be absorbed into it instead of becoming its own entity. If you see facts about a new person attached to an unrelated entity, How entity resolution decides explains what is being compared and which setting makes matching stricter.
Context-Aware Disambiguation
If "Alice" appears with "Google" and "Stanford" multiple times, a new "Alice" mentioning those is likely the same person. Hindsight uses co-occurrence patterns to disambiguate common names.
Entity Labels
You can define a controlled vocabulary of key:value classification labels (e.g. pedagogy:scaffolding, engagement:active) that are extracted at retain time and stored as entities. Because labels become entities, they automatically link related memories in the knowledge graph and improve both semantic and keyword retrieval. Labels can optionally also write to the memory unit's tags, enabling standard tag-based filtering during recall and reflect.
Unlike regular entities, label entities never merge by name similarity — distinct label values must stay distinct, so they resolve by exact match only and are excluded from fuzzy name matching altogether.
See entity_labels in the bank config for full configuration details.
Building Connections
Memories aren't isolated — Hindsight creates a knowledge graph with four types of connections:
Entity Connections
All facts mentioning the same entity are linked together.
Enables: "Tell me everything about Alice" → retrieves all Alice-related facts
Time-Based Connections
Facts close in time are connected, with stronger links for closer dates.
Enables: "What else happened around then?" → finds contextually related events
Meaning-Based Connections
Semantically similar facts are linked, even if they use different words.
Enables: "Tell me about similar topics" → finds thematically related information
Causal Connections
Cause-effect relationships are explicitly tracked.
Enables: "Why did this happen?" → trace reasoning chains
Example: "Alice felt burned out" ← caused by ← "She worked 80-hour weeks"
Understanding Time
Hindsight tracks two temporal dimensions:
When It Happened
For events (meetings, trips, milestones), Hindsight records when they occurred.
- "Alice got married in June 2024" → occurred in June 2024
For general facts (preferences, characteristics), there's no specific occurrence time.
- "Alice prefers Python" → ongoing preference
When You Learned It
Hindsight also tracks when you told it each fact.
Why both?
Imagine in January 2025, someone tells you "Alice got married in June 2024":
- Historical queries work: "What did Alice do in 2024?" → finds the marriage
- Recency ranking works: Recent mentions get priority in search
- Temporal reasoning works: "What happened before her marriage?" → finds earlier events
Without this distinction, old information would either be unsearchable by date or treated as irrelevant.
Tagging Memories
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
- Item tags: Tag individual memories with specific scopes
- Document tags: Apply tags to all items in a batch
- Tag filtering: Filter during recall/reflect by tags
See Retain API for code examples and Recall API for filtering options.
Images and Files in Your Content
A lot of what a document actually says lives in its pictures: the screenshot of
the button an instruction refers to, the diagram holding an escalation path, the
chart that is the data. content accepts an ordered list of blocks so those sit
where they belong, instead of being summarised into a caption beforehand:
{
"content": [
{"type": "text", "text": "To reset the VPN, click the button shown:"},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}},
{"type": "text", "text": "...then reconnect."}
]
}A plain string still works exactly as before — nothing about text-only retain
changes.
The point is position. Extraction sends the text and the pictures together,
in order, so the model reads the screenshot beside the sentence that introduces
it. "Click the button shown below" is meaningless on its own; next to the
picture it becomes a fact that names the button.
What comes back
Facts read naturally — [image: image/png] marks where an attachment was, never
a content hash — and each memory carries the attachments it was drawn from,
with a URL you can fetch:
{
"text": "The escalation path for a stuck sync begins by contacting Tier 3 Platform.",
"attachments": [
{"id": "c414cd0e204d", "kind": "image", "media_type": "image/png",
"url": "/v1/default/banks/my-bank/attachments/c414cd0e204d"}
]
}Facts that came from the prose have no attachments, even when the same document
is full of pictures. So an attachment shown next to a memory means the model
looked at it to produce that memory — it is evidence, not decoration.
An observation carries the attachments of the facts it was consolidated from, so
a screenshot still reaches you when recall or reflect answers from the
observation rather than the raw fact. Reflect's based_on (withinclude.facts) returns each cited memory with its attachments, plus thedocument_id, chunk_id, tags and metadata it was stored with.
What to expect from charts and tables
An attachment carrying structured data is transcribed rather than summarised:
each row, bar or labelled value becomes its own fact, carrying its label, its
figure, and how it is drawn. A chart therefore produces many more memories than a
screenshot does — that is deliberate, since a summary of a twenty-bar chart keeps
three values and silently drops seventeen, and later questions ask about the ones
it dropped.
Very dense pages are still sampled rather than exhausted: a single extraction pass
over an infographic listing a hundred entries will capture a large share of them,
not all of them.
Requirements
- A model that can read images. By default that is the bank's retain model, but
it does not have to be:HINDSIGHT_API_VLM_MODELnames a vision slot used
only for the chunks that actually carry an attachment, leaving every text-only
chunk on the retain LLM. So a bank whose documents are mostly prose can keep a
cheap text model and pay for vision only where there is something to look at. - If that model cannot read images — or if Hindsight cannot tell, which is the
case for gateway backends serving mixed catalogues — the retain is refused
with422rather than dropping the attachment silently. SeeHINDSIGHT_API_LLM_VISION. - Batch retain (
HINDSIGHT_API_RETAIN_BATCH_ENABLED) cannot carry attachments,
and also refuses with422.
This is different from POST /files/retain,
which converts a whole file to markdown as its own document. That is still the
right tool for a scanned report you want parsed — but it separates the file from
the prose that referred to it, which is exactly what inline attachments avoid.
For storage, size limits and accepted media types, see
Inline attachments in retain.
What You Get
After retain() completes:
- Structured facts that preserve meaning, emotions, and reasoning
- Unified entities that resolve different name variations
- Knowledge graph with entity, temporal, semantic, and causal links
- Temporal grounding for both historical and recency-based queries
- Optional tags for filtering during recall
All stored in your isolated memory bank, ready for recall() and reflect().
Steering Extraction with a Mission
By default, retain() extracts all significant facts from the content. You can narrow this focus with a retain mission (retain_mission) — a plain-language description of what this bank should pay attention to.
e.g. Always include technical decisions, API design choices, and architectural trade-offs.
Ignore meeting logistics, greetings, and social exchanges.The mission is injected into the extraction prompt alongside the built-in rules — it steers the LLM without replacing the extraction logic. It works with the LLM-based extraction modes (concise, verbose, verbatim, custom) and is ignored in chunks mode.
For finer control, you can also change the extraction mode:
| Mode | When to use |
|---|---|
concise (default) |
General-purpose — selective, fast |
verbose |
When you need richer facts with full context and relationships |
custom |
When you want to write your own extraction rules entirely |
verbatim |
When you need the original chunk text preserved, with LLM-extracted metadata such as entities and dates |
chunks |
When you want to store chunks as-is with no LLM call or extracted metadata |
Set retain_mission and retain_extraction_mode via the bank config API, or with the HINDSIGHT_API_RETAIN_MISSION and HINDSIGHT_API_RETAIN_EXTRACTION_MODE environment variables.
When a mission excludes everything in a document
A mission narrows what becomes a memory — and content that produces no facts produces no memories at all. The document itself is still stored, but recall and reflect search memories, so a document with zero memories cannot be found by either. Tightening a mission therefore trades away retrieval of the raw source, not just fact creation.
This is a normal outcome, not an error: the retain succeeds and the operation is reported as completed. Two signals tell you it happened:
| Where | What to look for |
|---|---|
retain.completed webhook |
data.memory_unit_count: 0 |
| Metrics | hindsight.retain.documents.total{outcome="no_facts"} |
You can also audit after the fact: GET /documents returns memory_unit_count per document, so filtering for 0 lists everything currently unreachable.
Extraction is not fully deterministic — a borderline document can yield facts on one run and none on the next. Treat a zero as "this document needs another pass" rather than as a permanent verdict.
To recover a document, widen the mission and reprocess it — the stored text is re-extracted, and no re-upload is needed:
POST /v1/default/banks/{bank_id}/documents/{document_id}/reprocessObservation Consolidation
After retain() completes, Hindsight automatically triggers observation consolidation in the background. This process:
- Analyzes new facts against existing observations
- Creates new observations when patterns emerge
- Refines existing observations with new evidence
- Tracks which facts support each observation
This happens asynchronously — your retain() call returns immediately while consolidation runs in the background.
See Observations for details on how consolidation works.
Memory Defense and Source Provenance
receipt_uri (optional)
Type: string.
Optional pointer into an external receipt or co-signature system. Stored as-is and surfaced in security_events.receipt_uri for any Memory Defense decision on this item.
422 — Memory Defense violation
When Memory Defense is enabled on the target bank and every item in the batch is blocked by policy, the request returns 422 with a violation list:
{
"detail": {
"violations": [
{ "index": 0, "detector": "prompt_injection", "severity": "high", "message": "..." }
]
}
}Partial-block batches return 200 with the un-blocked items processed; blocked items are silently dropped from the result with their decisions recorded in security_events.
See Memory Defense for the full guide.
Next Steps
- Observations — How knowledge is consolidated after retain
- Recall — How multi-strategy search retrieves relevant memories
- Reflect — How the agentic loop uses observations
- Retain API — Code examples and parameters
Hindsight Docs Docs Developer Retrieval (`hindsight-docs/docs/developer/retrieval.md`)
sidebar_position: 3
import Figure from '@site/src/components/Figure';
import tempr from '@site/figures/tempr.json';
Recall: How Hindsight Retrieves Memories
When you call recall(), Hindsight uses multiple search strategies in parallel to find the most relevant memories, regardless of how you phrase your query.
The Challenge of Memory Recall
Different queries need different search approaches:
- "Alice works at Google" → needs exact name matching
- "Where does Alice work?" → needs semantic understanding
- "What did Alice do last spring?" → needs temporal reasoning
- "Why did Alice leave?" → needs causal relationship tracing
No single search method handles all these well. Hindsight runs four complementary strategies in parallel and fuses their results.
Four Search Strategies
Semantic Search
What it does: Understands the meaning behind words, not just the words themselves.
Best for:
- Conceptual matches: "Alice's job" → "Alice works as a software engineer"
- Paraphrasing: "Bob's expertise" → "Bob specializes in machine learning"
- Synonyms: "meeting" matches "conference", "discussion", "gathering"
Why it matters: You can ask questions naturally without matching exact keywords.
Keyword Search
What it does: Finds exact terms and names, even when they're spelled uniquely.
Best for:
- Proper nouns: "Google", "Alice Chen", "MIT"
- Technical terms: "PostgreSQL", "HNSW", "TensorFlow"
- Unique identifiers: URLs, product names, specific phrases
Why it matters: Ensures you never miss results that mention specific names or terms, even if they're semantically distant from your query.
Backends: Hindsight ships five pluggable BM25 backends, selected viaHINDSIGHT_API_TEXT_SEARCH_EXTENSION:
| Backend | What it uses | Citus-compatible? |
|---|---|---|
native |
PostgreSQL tsvector + ts_rank_cd (TF-IDF, not true BM25) |
Yes |
vchord |
vchord_bm25 extension |
No |
pg_textsearch |
Timescale pg_textsearch extension |
No |
pgroonga |
PGroonga (Groonga) full-text extension, TokenBigram polyglot tokenizer |
No |
pg_search |
ParadeDB pg_search extension, configurable tokenizer (e.g. jieba, chinese_compatible, ngram) via HINDSIGHT_API_TEXT_SEARCH_EXTENSION_PG_SEARCH_TOKENIZER |
Yes |
If you need true BM25 ranking on a horizontally scaled Postgres (Citus) cluster,pg_search is the only option. See the pg_search docker-compose example.
Graph Traversal
What it does: Follows connections between entities to find indirectly related information.
Best for:
- Indirect relationships: "What does Alice do?" → Alice → Google → Google's products
- Entity exploration: "Bob's colleagues" → Bob → co-workers → shared projects
- Multi-hop reasoning: "Alice's team's achievements"
Why it matters: Retrieves facts that aren't semantically or lexically similar but are structurally connected through the knowledge graph.
Example: Even if Alice and her manager are never mentioned together, graph traversal can find the manager through shared projects or team relationships.
Temporal Search
What it does: Understands time expressions and filters by when events occurred.
Best for:
- Historical queries: "What did Alice do in 2023?"
- Time ranges: "What happened last spring?"
- Relative time: "What did Bob work on last year?"
- Before/after: "What happened before Alice joined Google?"
How it works: When the query contains a time reference, Hindsight parses it into a date window, then retrieves the memories whose time overlaps that window. Within the window it selects candidates by semantic relevance to the query — not by recency — so the most relevant in-window memory is never dropped just because other memories happen to be more recent. It then spreads the selection across the window's range: the window is divided into time-buckets and the strongest match from each populated bucket is taken first, so a "what happened in 2023?" query surfaces memories from across the whole year rather than clustering on whichever stretch is densest. Time is also used as a scoring signal — memories closer to the center of the window get a small boost (see Temporal proximity signal).
Why it matters: Enables precise historical queries that stay relevant and representative of the whole period. It also stays fast and meaningful on memory banks whose timestamps are densely clustered (for example when a large batch is ingested with one date): because selection is relevance-first, a time window that happens to match most of the bank still returns the best matches rather than an arbitrary slice.
Result Fusion
After the four strategies run, results are fused together:
- Memories appearing in multiple strategies rank higher (consensus)
- Rank matters more than score (robust across different scoring systems)
- Final results are re-ranked using a neural model that considers query-memory interaction
Why fusion matters: A fact that's both semantically similar AND mentions the right entity will rank higher than one that's only semantically similar.
Why Multiple Strategies?
Consider the query: "What did Alice say about Python last spring?"
- Semantic finds facts about Alice's views on programming
- Keyword ensures "Python" is actually mentioned
- Graph connects Alice → programming languages → related entities
- Temporal filters to "last spring" timeframe
The fusion of all four gives you exactly what you're looking for, even though no single strategy would suffice.
Token Budget Management
Hindsight is built for AI agents, not humans. Traditional search systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
How it works:
- Top-ranked memories selected first
- Stops when token budget is exhausted
- You specify context budget, Hindsight fills it with the most relevant memories
Parameters you control:
max_tokens: How much memory content to return (default: 4096 tokens)budget: Search depth level (low, mid, high)types: Filter by world, experience, observation, or alltags: Filter memories by visibility tagstags_match: How to match tags (see Recall API for all options)
Expanding Context: Chunks
Memories are distilled facts—concise but sometimes missing nuance. When your agent needs deeper context, you can optionally retrieve the source material:
Chunks return the raw text that generated each memory—useful when the distilled fact loses important nuance:
Memory: "Alice prefers Python over JavaScript"
Chunk: "Alice mentioned she prefers Python over JavaScript, mainly because
of its data science ecosystem, though she admits JS is better for
frontend work and she's been learning TypeScript lately."Use include_chunks=True with max_chunk_tokens to control the token budget for chunks. This is useful when generating responses that need verbatim quotes or when context matters (e.g., "What exactly did Alice say about the project?").
Tuning Recall: Quality vs Latency
Different use cases require different trade-offs between recall quality and response speed. Two parameters control this:
Budget: Search Depth
Controls how thoroughly Hindsight explores the memory bank—affecting graph traversal depth, candidate pool size, and cross-encoder re-ranking:
| Budget | Best For | Trade-off |
|---|---|---|
| low | Quick lookups, simple queries | Fast, may miss indirect connections |
| mid | Most queries, balanced | Good coverage, reasonable speed |
| high | Complex queries requiring deep exploration | Thorough, slower |
Example: "What did Alice's manager's team work on?" benefits from high budget to traverse multiple hops (Alice → manager → team → projects) and evaluate more candidates.
Max Tokens: Context Window Size
Controls how much memory content to return:
| Max Tokens | ~Pages of Text | Best For | Trade-off |
|---|---|---|---|
| 2048 | ~2 pages | Focused answers, fast LLM | Fewer memories, faster |
| 4096 (default) | ~4 pages | Balanced context | Good coverage, standard |
| 8192 | ~8 pages | Comprehensive context | More memories, slower LLM |
Example: "Summarize everything about Alice" benefits from higher max_tokens to include more facts.
Two Independent Dimensions
Budget and max_tokens control different aspects of recall:
| Parameter | What it controls | Latency impact | Example |
|---|---|---|---|
| Budget | How thoroughly to explore memories | Search time | High budget finds Alice → manager → team → projects |
| Max Tokens | How much context to return | LLM processing time | High tokens returns more memories to the agent |
They're independent. Common combinations:
| Budget | Max Tokens | Use Case |
|---|---|---|
| high | low | Deep search, return only the best results |
| low | high | Quick search, return everything found |
| high | high | Comprehensive research queries |
| low | low | Fast chatbot responses |
Recommended Configurations
| Use Case | Budget | Max Tokens | Why |
|---|---|---|---|
| Chatbot replies | low | 2048 | Fast responses, focused context |
| Document Q&A | mid | 4096 | Balanced coverage and speed |
| Research queries | high | 8192 | Comprehensive, multi-hop reasoning |
| Real-time search | low | 2048 | Minimize latency |
Scoring & Ranking Deep Dive
This section explains exactly how Hindsight turns raw retrieval results into a final ranked list. The pipeline has three stages: RRF fusion, cross-encoder reranking, and combined scoring.
Stage 1: Reciprocal Rank Fusion (RRF)
After all strategies run in parallel, their results are merged using Reciprocal Rank Fusion. RRF combines ranked lists by rewarding items that appear highly ranked across multiple strategies, without relying on raw scores (which aren't comparable across different retrieval methods).
Formula:
score(d) = Σ 1 / (k + rank_i(d))
iWhere:
- k = 60 (smoothing constant — prevents top-ranked items from dominating)
- rank_i(d) = position of document d in strategy i (1-indexed)
- The sum runs over all strategies where d appears
Within RRF, all four strategies are weighted equally — fusion uses rank position, not the source, so no strategy gets an implicit multiplier. You can, however, deliberately bias a source with HINDSIGHT_API_RECALL_STRATEGY_BOOSTS: that boost is applied at a separate stage — before the reranking pre-filter cap, and again after a cross-encoder rerank — not inside the RRF fusion above. The pre-cap step runs only when the merged pool exceeds the cap, and it does not rewrite RRF scores. A passthrough reranker (mode rrf, an rrf provider, or a failover chain that has degraded to rrf) skips the post-rerank nudge: within the cap the setting then does nothing, and above the cap it only changes who is kept.
Why RRF over raw score merging? Each retrieval strategy produces scores on a different scale (cosine similarity, BM25 tf-idf, graph activation). These scores aren't comparable — a BM25 score of 12.5 and a cosine similarity of 0.85 don't mean the same thing. RRF sidesteps this by using only rank positions, making it robust across any scoring system without requiring calibration.
Example: A memory ranked #1 in semantic and #5 in BM25:
RRF score = 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318A memory ranked #1 in semantic only:
RRF score = 1/(60+1) = 0.0164The first memory ranks higher because it has consensus across strategies.
Stage 2: Cross-Encoder Reranking
RRF gives a good initial ranking, but it's based on positions, not on deep query-document understanding. The cross-encoder evaluates each candidate against the query as a pair, producing a relevance score.
Pre-filtering: Before reranking, candidates are trimmed to the top 300 (by RRF score) to limit computational cost. This is configurable via HINDSIGHT_API_RERANKER_MAX_CANDIDATES. If HINDSIGHT_API_RECALL_STRATEGY_BOOSTS is set, the boost is applied before this cut, so candidates from a favoured source are more likely to survive it. The boost promotes the favoured arm in rank space (its rank is divided by the level's divisor before the RRF contribution is computed) rather than scaling its score, so it reaches deeper into that arm without evicting the top-ranked hits of the others — including on banks whose merged pool is many times the cap. When trace: true is requested, the rerank_prefilter phase reports how many candidates were kept and dropped, the cap in force, the active boosts, and the per-arm composition of the survivors.
Why rerank after RRF? RRF is position-based — it knows a memory ranked well across strategies, but it never actually reads the query and the memory together. The cross-encoder does: it takes the query and each candidate as a pair and produces a relevance score based on their full interaction. This catches nuances that position-based fusion misses, like a memory that ranked #1 in keyword search because it matched a common term but is actually irrelevant to the query's intent.
Score normalization: Cross-encoders output raw logits (which can be negative). Scores that already fall within [0, 1] — as returned by calibrated external API rerankers (e.g. Cohere, SiliconFlow, ZeroEntropy, Alibaba, Jina) — are passed through unchanged to preserve their absolute confidence. Raw logits outside [0, 1] are normalized to [0, 1] using the sigmoid function:
CE_normalized = 1 / (1 + e^(-raw_logit))Batch processing: Candidates are scored in batches — 32 pairs for the local reranker, 128 pairs for TEI.
:::tip No cross-encoder?
When running without a cross-encoder (e.g., slim image with no external reranker), the system falls back to RRF-derived scores: candidates are assigned synthetic scores spread across [0.1, 1.0] based on their RRF rank, so the combined scoring boosts below still work meaningfully. HINDSIGHT_API_RECALL_STRATEGY_BOOSTS does not add its post-rerank nudge on this path. If the merged pool is within the reranker cap, the setting changes nothing; if the pool is larger, it only changes which candidates are kept, and their order still follows these RRF-derived scores plus recency, temporal and proof.
:::
Stage 3: Combined Scoring (Boosts)
The normalized cross-encoder score is adjusted by three multiplicative boosts that incorporate signals the cross-encoder can't see: recency, temporal proximity, and evidence strength.
Why multiplicative instead of additive? Additive boosts (e.g., CE + 0.1 × recency) would give the same absolute bonus to every candidate regardless of relevance. A barely-relevant memory could leapfrog a highly-relevant one just by being recent. Multiplicative boosts keep adjustments proportional to the base relevance score — a +10% nudge on a high-relevance memory is a bigger absolute change than +10% on a low-relevance one. This ensures secondary signals never overpower the primary relevance judgment.
Formula:
final_score = CE_normalized × recency_boost × temporal_boost × proof_count_boostEach boost is centered at 1.0 (neutral) and controlled by an alpha that caps how much it can swing:
boost = 1 + α × (signal - 0.5)| Boost | α | Max adjustment | What it rewards |
|---|---|---|---|
| Recency | 0.2 | ±10% | Recent memories over older ones |
| Temporal proximity | 0.2 | ±10% | Memories close to a queried time window |
| Proof count | 0.1 | ±5% | Observations backed by more evidence |
Recency signal
Linear decay over 365 days from the memory's occurrence date:
recency = clamp(1.0 - days_ago / 365, 0.1, 1.0)A memory from the query timestamp has recency 1.0 (+10% boost). A memory from 6 months before the query timestamp has recency ~0.5 (neutral). A memory more than a year before the query timestamp has recency 0.1 (-8% penalty). If no query_timestamp is provided, the server's current time is used. Memories without dates get 0.5 (neutral — no boost or penalty).
Temporal proximity signal
Only active when the query contains a time reference (e.g., "last spring", "in 2023"). Measures how close a memory's date is to the center of the queried time window:
temporal_proximity = 1.0 - min(days_from_center / (window_days / 2), 1.0)A memory at the center of the window gets 1.0 (+10% boost). A memory at the edge gets 0.0 (-10% penalty). For non-temporal queries, all memories get 0.5 (neutral).
Proof count signal
For observation-type memories, rewards those backed by more evidence using a logarithmic curve:
proof_norm = clamp(0.5 + ln(proof_count) / 10, 0.0, 1.0)| Proof count | proof_norm | Boost |
|---|---|---|
| 1 | 0.5 | Neutral |
| 3 | 0.61 | +1.1% |
| 10 | 0.73 | +2.3% |
| 150+ | 1.0 | +5% (max) |
Maximum combined range
With all boosts at their extremes:
- Best case: ×1.10 × 1.10 × 1.05 ≈ +27%
- Worst case: ×0.90 × 0.90 × 0.95 ≈ -23%
The boosts are intentionally conservative — they nudge the ranking without overriding cross-encoder relevance.
Stage 4: Token Truncation
After scoring, results are sorted by final_score and selected top-down until the max_tokens budget is exhausted. Only the memory text counts toward the budget — metadata is free. A result too long for what is left of the budget is skipped and the packing continues with the next one, so a single long fact does not cost you the shorter ones ranked behind it. If nothing fits at all, the top-ranked result is still returned whole, so a query that matched something is never answered with an empty list.
How Budget Maps to Pipeline Parameters
The budget parameter (low/mid/high) controls search depth — how many candidates each strategy considers. Each level maps to a recall budget number that flows through every pipeline stage:
| Budget | Recall budget (fixed mode) | Env var override |
|---|---|---|
| low | 100 | HINDSIGHT_API_RECALL_BUDGET_FIXED_LOW |
| mid | 300 (default) | HINDSIGHT_API_RECALL_BUDGET_FIXED_MID |
| high | 1000 | HINDSIGHT_API_RECALL_BUDGET_FIXED_HIGH |
This recall budget flows through the pipeline as follows:
| Pipeline stage | How the recall budget is used |
|---|---|
| Semantic search | LIMIT recall_budget in SQL, with the ANN candidate list sized to match for the query (on pgvector, hnsw.ef_search, capped at that setting's maximum of 1000) |
| BM25 search | LIMIT recall_budget in SQL |
| Graph traversal | Explores up to recall_budget nodes |
| Temporal spreading | Activates up to recall_budget nodes via links |
| Result consideration | Top recall_budget × 2 results considered for token filtering |
Reranking pre-filter (300 candidates) is independent of budget — it's a separate knob (HINDSIGHT_API_RERANKER_MAX_CANDIDATES).
:::info Adaptive budgeting
An alternative budget mode scales the recall budget with max_tokens instead of using fixed values:
recall_budget = clamp(max_tokens × ratio, min, max)| Budget | Ratio | Env var override |
|---|---|---|
| low | 2.5% of max_tokens | HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_LOW |
| mid | 7.5% of max_tokens | HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_MID |
| high | 25% of max_tokens | HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_HIGH |
The result is clamped to a floor of 20 (HINDSIGHT_API_RECALL_BUDGET_MIN) and a ceiling of 2000 (HINDSIGHT_API_RECALL_BUDGET_MAX).
Enable with HINDSIGHT_API_RECALL_BUDGET_FUNCTION=adaptive.
:::
Graph Scoring Detail
The graph traversal (link expansion) combines three independent signals additively for each candidate:
| Signal | Score formula | Range |
|---|---|---|
| Entity overlap | tanh(shared_entity_count × 0.5) | [0, ~1.0] |
| Semantic link | Precomputed kNN link weight | [0.7, 1.0] |
| Causal link | Causal link weight | [0, 1.0] |
graph_score = entity_score + semantic_score + causal_score ∈ [0, 3]The additive combination rewards convergent evidence — a memory connected to the query through multiple signal types ranks higher than one connected through a single strong signal.
Why tanh for entity scores? Raw shared-entity count is unbounded — a high-fanout entity like "user" could produce counts of 50+, drowning out the other two signals. tanh(count × 0.5) saturates naturally: the first few shared entities matter a lot (1→0.46, 2→0.76, 3→0.91), but additional ones contribute diminishing returns, keeping the entity signal in [0, 1] alongside semantic and causal scores.
Why additive instead of multiplicative here? Unlike the combined scoring boosts, graph signals are independent evidence channels, not adjustments to a base score. A memory might be connected only through causal links (no shared entities, no semantic similarity) — multiplicative combination would zero it out. Additive scoring lets each signal contribute independently, and the outer RRF fusion handles ranking across strategies.
Entity signal example: A memory sharing 1 entity with the query scores tanh(0.5) ≈ 0.46. Two shared entities score tanh(1.0) ≈ 0.76. Three or more saturate near 0.91+.
Next Steps
- Retain — How memories are stored with rich context
- Reflect — How disposition influences reasoning
- Recall API — Code examples, parameters, and tag filtering
Hindsight Docs Docs Developer Services (`hindsight-docs/docs/developer/services.md`)
import Figure from '@site/src/components/Figure';
import services from '@site/figures/services.json';
Services
Hindsight consists of three services that can run together or separately depending on your deployment needs.
API Service
The core memory engine. Handles all memory operations:
- Retain: Ingests content, extracts facts, builds knowledge graph
- Recall: Semantic search across memories
- Reflect: Disposition-aware answer generation
hindsight-api # Default port: 8888The API service is stateless and can be horizontally scaled behind a load balancer. All state is stored in PostgreSQL.
By default, the API also processes background tasks (mental model consolidation) internally. For high-throughput deployments, you can disable this and run dedicated workers instead.
Worker Service
Dedicated task processor for background operations. Uses the same package and Docker image as the API service, just with a different entry point.
hindsight-worker # Default metrics port: 8889Workers use PostgreSQL as a task broker, polling for pending tasks. Multiple workers can run simultaneously without conflicts.
| Deployment | Internal Worker | Dedicated Workers |
|---|---|---|
| Development | ✅ Simple, all-in-one | ❌ Overkill |
| Small production | ✅ Less infrastructure | ❌ Overkill |
| High throughput | ❌ API bottleneck | ✅ Scale independently |
| Long-running tasks | ❌ Blocks API resources | ✅ Isolated processing |
To use dedicated workers, disable the internal worker in the API and start worker processes:
# Disable internal worker in API
HINDSIGHT_API_WORKER_ENABLED=false hindsight-api
# Start dedicated workers (run multiple instances)
hindsight-worker --worker-id worker-1
hindsight-worker --worker-id worker-2Each worker exposes /health/live (liveness, no database access), /health and/health/ready (readiness, checks the database), and /metrics for monitoring.
See Monitoring - Health Endpoints.
Before scaling down or removing workers, release their tasks with hindsight-admin decommission-worker <worker-id>.
See Configuration - Distributed Workers for all worker settings and Installation - Helm for Kubernetes deployment.
Control Plane
Web UI for managing and exploring your memory banks:
- Browse agents and memory banks
- Explore entities and relationships
- View ingestion history and operations
- Test recall queries interactively
The Control Plane connects to the API service and provides a visual interface for development and debugging.
For bare metal deployments, you can run the Control Plane standalone using npx. See Installation - Bare Metal for details.
Hindsight Docs Docs Developer Storage (`hindsight-docs/docs/developer/storage.md`)
Storage
Hindsight uses PostgreSQL as its primary storage backend, with Oracle AI Database available as an alternative for enterprise deployments.
Why PostgreSQL?
PostgreSQL provides all capabilities required for a semantic memory system in a single database:
| Capability | Implementation |
|---|---|
| Vector search | pgvector extension with HNSW indexes |
| Full-text search | Built-in tsvector with GIN indexes |
| Relational data | Native PostgreSQL |
| JSON documents | JSONB with indexing |
| Graph queries | Recursive CTEs |
Reduced System Dependencies
Building exclusively for PostgreSQL simplifies deployment and operations:
- Single connection string to configure
- Single backup and restore strategy
- Single monitoring target
- ACID transactions across all data types
- Single upgrade path
No Storage Abstraction
Hindsight does not abstract storage behind a generic interface. This is a deliberate trade-off.
We believe PostgreSQL is becoming the standard database API. Its popularity, extension ecosystem, and modularity mean that PostgreSQL-compatible interfaces are appearing everywhere—from serverless offerings to distributed databases. Building for PostgreSQL today means compatibility with a growing ecosystem tomorrow.
Supporting multiple databases would increase flexibility but conflict with our core goals: Hindsight is fully open source and designed to be as simple as possible to run and use. Adding database abstractions introduces complexity in code, testing, documentation, and operations—complexity that we pass on to users.
By building on PostgreSQL, we keep the system simple:
- One set of deployment instructions
- One set of performance characteristics to understand
- One codebase optimized for one backend
- No configuration decisions about which database to use
Oracle AI Database Support
For enterprise deployments, Hindsight also supports Oracle AI Database with full feature parity. All memory operations—retain, recall, and reflect—work identically on Oracle, making it a drop-in option for organizations that standardize on Oracle infrastructure.
See the Oracle Database guide for setup: prerequisites, provisioning, connection URLs, migrations, and the differences from PostgreSQL.
Development with pg0
For local development, Hindsight uses pg0—an embedded PostgreSQL distribution.
What is pg0?
pg0 is a single binary containing:
- PostgreSQL server
- pgvector extension (pre-installed)
- Automatic initialization
Behavior
When no HINDSIGHT_API_DATABASE_URL is configured, Hindsight:
- Starts an embedded PostgreSQL instance on port 5555
- Initializes the schema
- Stores data in
~/.hindsight/pg0/
Environments
| Environment | Database | Configuration |
|---|---|---|
| Development | pg0 (embedded) | Automatic |
| Production | PostgreSQL 15+ | HINDSIGHT_API_DATABASE_URL environment variable |
Requirements
- PostgreSQL 15 or later
- pgvector 0.5.0 or later
Any PostgreSQL instance that satisfies these requirements should work. If you encounter issues with a specific setup, open a GitHub issue.
Tested Managed Services
- AWS RDS (PostgreSQL 15+)
- Google Cloud SQL
- Azure Database for PostgreSQL
- Supabase
- Neon
Hindsight Docs Docs Sdks Cli (`hindsight-docs/docs/sdks/cli.mdx`)
sidebar_position: 4
CLI Reference
import CodeSnippet from '@site/src/components/CodeSnippet';
import cliSh from '!!raw-loader!@site/examples/api/cli-reference.sh';
The Hindsight CLI provides command-line access to memory operations and bank management. All commands follow the OpenAPI specification, so you can use --help on any command to see all available options.
Installation
curl -fsSL https://hindsight.vectorize.io/get-cli | bashConfiguration
Configure the API URL:
You can also run hindsight configure with no flags to be prompted interactively, or use environment variables (highest priority):
export HINDSIGHT_API_URL=http://localhost:8888
export HINDSIGHT_API_KEY=your-api-keyNamed Profiles
When you need to switch between multiple Hindsight deployments (e.g. local,
staging, production) without constantly rewriting ~/.hindsight/config, use
named profiles. Each profile is a TOML file at~/.hindsight/cli-profiles/<name>.toml and is selected per-invocation with-p/--profile (or by setting $HINDSIGHT_PROFILE).
Profile files are written with 0600 permissions on Unix so the API key is
only readable by the owner.
Configuration precedence (highest first):
- Environment variables (
HINDSIGHT_API_URL,HINDSIGHT_API_KEY) - Named profile — explicit
-p <name>, otherwise$HINDSIGHT_PROFILE - Shared config file (
~/.hindsight/config, written byhindsight configure) - Default (
http://localhost:8888)
HINDSIGHT_API_URL / HINDSIGHT_API_KEY always override profile values, which
makes it safe to use -p in scripts while letting CI inject credentials via
environment.
Core Commands
Retain (Store Memory)
Store a single memory:
Retain Files
Bulk import from files:
Recall (Search)
Search memories using semantic similarity:
Reflect (Generate Response)
Generate a response using memories and bank disposition:
Memory History
View the observation history for a specific memory unit:
Clear Observations
Remove all observations for a memory unit, keeping the core fact:
Bank Management
List Banks
View Disposition
Set Disposition
View Statistics
Set Bank Name
Set Mission
Clear Observations (Bank-wide)
Remove all observations across the entire bank:
Recover Consolidation
Recover from a failed or stuck consolidation:
Document Management
Entity Management
Operation Management
Track and manage async operations (retain-files, consolidation, etc.):
Webhook Management
Configure event delivery hooks for bank activity:
Knowledge Base
Manage a bank's knowledge pages — living documents organized in a folder tree. See Knowledge Pages for what they are and how they refresh.
:::tiphindsight fs mount --bank <bank_id> mirrors the same knowledge base onto disk as
real markdown files, kept current by a background refresh loop — handy when you'd
rather use grep, rg, or your editor than the commands above.
:::
Audit Logs
Inspect the audit trail for a bank:
The list shows each entry without its request and response, which can be large. To see them, pass an entry's id from the list to hindsight audit get <bank_id> <log_id>.
Output Formats
Global Options
| Flag | Description |
|---|---|
-v, --verbose |
Show detailed output including request/response |
-o, --output <format> |
Output format: pretty, json, yaml |
-p, --profile <name> |
Use a named profile (see Named Profiles) |
--help |
Show help |
--version |
Show version |
Control Plane UI
Launch the web-based Control Plane UI directly from the CLI:
hindsight uiThis runs the Control Plane locally on port 9999 using the API URL from your configuration. The UI provides:
- Memory bank management — Browse and manage all your banks
- Entity explorer — Visualize the knowledge graph
- Query testing — Interactive recall and reflect testing
- Operation history — View ingestion and processing logs
:::tip
The UI command requires Node.js to be installed. It automatically downloads and runs the @vectorize-io/hindsight-control-plane package via npx.
:::
Interactive Explorer
Launch the TUI explorer for visual navigation of your memory banks:
hindsight exploreThe explorer provides an interactive terminal interface to:
- Browse memory banks — View all banks and their statistics
- Search memories — Run recall queries with real-time results
- Inspect memory details — Open a memory to view its text, metadata, entities, and full JSON fields
- Inspect entities — Explore the knowledge graph and entity relationships
- View facts — Browse world facts, experiences, and observations
- Navigate documents — See source documents and their extracted memories
Keyboard Shortcuts
| Key | Action |
|---|---|
↑/↓ |
Navigate items |
Enter |
Select item / view details |
Tab |
Switch panels |
/ |
Search |
q |
Quit |
{/* Screenshot placeholder: explore command TUI */}
Example Workflow
Hindsight Docs Docs Sdks Embed (`hindsight-docs/docs/sdks/embed.md`)
sidebar_position: 5
Daemon CLI (hindsight-embed)
Zero-configuration local memory system with automatic daemon management. Perfect for development, prototyping, and single-user applications.
Overview
hindsight-embed is a zero-configuration SDK that wraps the Hindsight API and PostgreSQL database into a single auto-managed local daemon. It's designed for development, prototyping, and single-user applications where you want memory capabilities without infrastructure overhead.
How it works:
- First command triggers startup: When you run any
hindsight-embedcommand, it checks if a local daemon is running - Auto-daemon management: If no daemon exists, it automatically spawns
hindsight-api --daemonin the background - Embedded database: The daemon uses
pg0(embedded PostgreSQL) — no separate database installation required - Command forwarding: Your command is forwarded to the local daemon via HTTP (localhost:8888)
- Auto-shutdown: After 5 minutes of inactivity (configurable), the daemon gracefully shuts down to free resources
Key features:
- Zero setup — One
configurecommand and you're ready - Automatic lifecycle — Daemon starts on-demand and is shared by every client
- Isolated storage — Each bank gets its own embedded PostgreSQL database
- Local-only — Binds to
127.0.0.1:8888, not accessible from network - Production-grade engine — Uses the same memory engine as the full API service
Think of it as SQLite for long-term memory — all the power of Hindsight without managing servers.
Installation
Install via uvx (recommended - always latest version):
# Run directly without installation
uvx hindsight-embed@latest configure
# Or use pipx for persistent installation
pipx install hindsight-embedQuick Start
1. Configure
# Interactive configuration
hindsight-embed configure
# Or non-interactive via environment variables
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
hindsight-embed configureConfiguration is saved to ~/.hindsight/embed:
HINDSIGHT_API_LLM_PROVIDER=openai
HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
# Daemon settings (macOS: force CPU to avoid MPS/XPC issues)
HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1
HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=12. Use Memory Operations
# Store a memory
hindsight-embed memory retain default "User prefers dark mode"
# Query memories
hindsight-embed memory recall default "user preferences"
# Reasoning with memory
hindsight-embed memory reflect default "What color scheme should I use?"The daemon starts automatically on first use!
3. Open the Control Center (optional)
Use the local control center when you want a browser-based configuration wizard and daemon supervisor:
# Launch the control center and open the browser
hindsight-embed control start
# Or use the browser wizard instead of terminal prompts during setup
hindsight-embed configure --uiThe control center listens on localhost only (http://localhost:7878 by default) and prints a tokenized URL. It stores the local access token at ~/.hindsight/control.token and writes logs to ~/.hindsight/control.log.
# Pick a different control-center port for this launch
hindsight-embed control start --port 7879
# Start without opening a browser automatically
hindsight-embed control start --no-open
# Check, inspect, or stop the control center
hindsight-embed control status
hindsight-embed control logs -f
hindsight-embed control stopThe control center runs as a separate process from the memory daemon. Stopping or restarting the control center does not stop a running daemon.
Environment Variables
| Variable | Description | Default |
|---|---|---|
HINDSIGHT_API_LLM_API_KEY |
Required. API key for LLM provider | - |
HINDSIGHT_API_LLM_PROVIDER |
LLM provider: openai, anthropic, gemini, groq, minimax, ollama |
openai |
HINDSIGHT_API_LLM_MODEL |
Model name | gpt-4o-mini |
HINDSIGHT_EMBED_DAEMON_LOG_MAX_BYTES |
Rotate the daemon log at startup when it reaches this size; 0 disables rotation |
10485760 (10 MiB) |
HINDSIGHT_EMBED_DAEMON_LOG_BACKUP_COUNT |
Retained backups; 0 truncates a full log at startup |
3 |
HINDSIGHT_EMBED_CONTROL_PORT |
Default port for hindsight-embed control start |
7878 |
The size is checked only when a daemon starts, so a single uninterrupted run is never truncated and can
grow past MAX_BYTES — and at the next start that whole file is kept as the first backup. Retained size
is therefore around MAX_BYTES × (BACKUP_COUNT + 1) (40 MiB by default) only for daemons that restart
regularly; a daemon left running for weeks keeps whatever it wrote. Restart it to keep the bound
meaningful.
Provider Examples:
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Groq (fast inference)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=llama-3.3-70b-versatile
# Anthropic
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514Daemon Management
The daemon runs until you stop it — it never exits on its own, so a long retain, reflect or
consolidation is never cut short.
Daemon Commands
# Check daemon status
hindsight-embed daemon status
# View daemon logs in real-time
hindsight-embed daemon logs -f
# Stop daemon manually
hindsight-embed daemon stopReclaiming Disk Space
When the daemon is launched with uvx (the default for the npm package and the editor plugins), every Hindsight version it has run keeps its own cached Python environment — around 1.5 GB each — and these are not removed automatically. If disk space gets tight, stop the daemon before clearing the cache: unused entries can only be reclaimed while no daemon is running.
hindsight-embed daemon stop
uv cache prune
hindsight-embed daemon status # starts fresh on the current versionControl Center Commands
# Start or reuse the local browser control center
hindsight-embed control start
# Check whether it is running
hindsight-embed control status
# View control-center logs
hindsight-embed control logs -f
# Stop the control-center process
hindsight-embed control stopCommands
All memory operations follow the same interface as the CLI:
Retain (Store Memory)
hindsight-embed memory retain "content"
# With context
hindsight-embed memory retain "content" --context "source information"
# Background processing
hindsight-embed memory retain "content" --asyncRecall (Search)
hindsight-embed memory recall "query"
# With budget control
hindsight-embed memory recall "query" --budget high
# Show trace
hindsight-embed memory recall "query" --traceReflect (Generate Response)
hindsight-embed memory reflect "prompt"
# With additional context
hindsight-embed memory reflect "prompt" --context "additional info"Bank Management
# List all banks
hindsight-embed bank list
# View bank stats
hindsight-embed bank stats
# Set bank name
hindsight-embed bank name "My Assistant"
# Set bank mission
hindsight-embed bank mission "I am a helpful AI assistant"Troubleshooting
Daemon Won't Start
Check the daemon logs:
hindsight-embed daemon logs
# Or watch in real-time
hindsight-embed daemon logs -fCommon issues:
- Missing API key: Set
HINDSIGHT_API_LLM_API_KEY - Port conflict: Another service using port 8888
- Permissions: Check
~/.hindsight/directory permissions
Daemon Exits Immediately
The daemon never stops itself, so an exit is a crash or a failed startup. Check the log:
hindsight-embed daemon status
tail -n 100 ~/.hindsight/daemon.logReset Configuration
# Remove config file and reconfigure
rm ~/.hindsight/embed
hindsight-embed configureWhen to Use
Perfect for:
- Development and prototyping
- Single-user applications
- Local-first tools
- Quick experiments with Hindsight
Not suitable for:
- Production multi-user deployments
- Network-accessible services
- High-availability requirements
- Multi-tenant applications
For production deployments, use the API Service with external PostgreSQL instead.
Hindsight Docs Docs Sdks Go (`hindsight-docs/docs/sdks/go.md`)
sidebar_position: 3
Go Client
Official Go client for the Hindsight API, generated from the OpenAPI 3.1 spec using OpenAPI Generator.
import CodeSnippet from '@site/src/components/CodeSnippet';
import quickstartGo from '!!raw-loader!@site/examples/api/quickstart.go';
Installation
go get github.com/vectorize-io/hindsight/hindsight-clients/goRequires Go 1.23+.
Quick Start
API Structure
The Go client provides access to all Hindsight API operations through structured namespaces:
client.MemoryAPI- Retain, recall, reflect operationsclient.BanksAPI- Bank managementclient.DirectivesAPI- Directive managementclient.MentalModelsAPI- Mental model managementclient.DocumentsAPI- Document operationsclient.EntitiesAPI- Entity operationsclient.OperationsAPI- Async operation monitoring
Working with Nullable Fields
The Go client uses NullableString, NullableTime, and similar types for optional fields:
Error Handling
More Examples
For detailed examples of all operations, see:
- Python SDK documentation - API concepts are the same
- Node.js SDK documentation - API concepts are the same
- OpenAPI specification - Complete API reference
Hindsight Docs Docs Sdks Hindsight All Npm (`hindsight-docs/docs/sdks/hindsight-all-npm.md`)
sidebar_position: 6
Programmatic API (Node.js)
The @vectorize-io/hindsight-all npm package is the Node.js equivalent of the Python hindsight-all package. It lets your Node code spawn and supervise a local Hindsight daemon without deploying any server infrastructure — pair it with @vectorize-io/hindsight-client for memory operations.
The daemon runs as a separate OS process on 127.0.0.1 (not in your Node process). Your code talks to it over HTTP via HindsightClient.
This package does not ship an HTTP client — it only owns the server process. Once the daemon is running, talk to it with @vectorize-io/hindsight-client against server.getBaseUrl(). The two packages compose: one owns the process, the other owns the API surface.
How it works
server.start()resolves the underlyinghindsight-embedcommand (viauvxfrom PyPI, oruv run --directory <path>for a local checkout).- Runs
profile create <name> --merge --port <port> [--env KEY=VALUE ...]with every entry fromoptions.envforwarded as--env. - Runs
daemon --profile <name> start. - Polls
http://host:port/healthuntil it returns200or thereadyTimeoutMsbudget is exhausted. server.stop()runsdaemon --profile <name> stop.
The server is intentionally transparent: new daemon env vars or CLI flags never require a wrapper release — pass them through env, extraProfileCreateArgs, or extraDaemonStartArgs.
Requirements
- Node.js ≥ 22 — uses global
fetchandAbortSignal.timeout. uv/uvxonPATH— used to download and run the Hindsight daemon. Install via docs.astral.sh/uv.
Install
npm install @vectorize-io/hindsight-all @vectorize-io/hindsight-clientExample
import { HindsightServer, consoleLogger } from '@vectorize-io/hindsight-all';
import { HindsightClient } from '@vectorize-io/hindsight-client';
const server = new HindsightServer({
profile: 'my-app',
port: 9077,
env: {
HINDSIGHT_API_LLM_PROVIDER: 'anthropic',
HINDSIGHT_API_LLM_API_KEY: process.env.ANTHROPIC_API_KEY,
HINDSIGHT_API_LLM_MODEL: 'claude-sonnet-4-20250514',
},
logger: consoleLogger,
});
await server.start();
const client = new HindsightClient({ baseUrl: server.getBaseUrl() });
await client.retain('user-123', 'User prefers dark mode.');
const recall = await client.recall('user-123', 'what are the user preferences?');
await server.stop();For a remote Hindsight API, skip the server entirely and point HindsightClient directly at the remote URL.
`HindsightServerOptions`
| Option | Type | Default | Description |
|---|---|---|---|
profile |
string |
"default" |
Profile name passed to --profile on every sub-command. |
port |
number |
8888 |
TCP port the daemon listens on. |
host |
string |
"127.0.0.1" |
Hostname the daemon binds to (used for health checks). |
embedVersion |
string |
"latest" |
Version of the underlying hindsight-embed package to run via uvx. |
embedPackagePath |
string |
— | Local checkout path — takes precedence over embedVersion. Uses uv run --directory instead of uvx. |
env |
Record<string, string | undefined> |
{} |
Environment variables passed to the daemon process and written into the profile config via --env KEY=VALUE. The preferred way to surface any HINDSIGHT_API_* / HINDSIGHT_EMBED_* setting. |
extraProfileCreateArgs |
string[] |
[] |
Extra args appended verbatim to profile create. |
extraDaemonStartArgs |
string[] |
[] |
Extra args appended verbatim to daemon start. |
platformCpuWorkaround |
boolean |
true on macOS |
Auto-set HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1 and HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=1 to avoid Metal/MPS crashes. Caller-supplied env values win over the auto-applied ones. |
readyTimeoutMs |
number |
30000 |
Max time to wait for /health to return 200. |
readyPollIntervalMs |
number |
1000 |
Polling interval while waiting for /health. |
logger |
Logger |
silent | Pluggable logger (debug/info/warn/error). consoleLogger and silentLogger helpers are exported. |
Server methods
| Method | Returns | Description |
|---|---|---|
start() |
Promise<void> |
Configure profile, spawn the daemon, wait for /health. Idempotent — safe to re-run. |
stop() |
Promise<void> |
Stop the daemon. Never throws; logs and resolves even on failure. |
checkHealth() |
Promise<boolean> |
One-shot /health probe with a 2 s timeout. |
getBaseUrl() |
string |
http://host:port — pass this straight to HindsightClient. |
getProfile() |
string |
The profile name this server operates on. |
For memory operations (retain, recall, reflect, bank management) use @vectorize-io/hindsight-client.
Hindsight Docs Docs Sdks Hindsight All (`hindsight-docs/docs/sdks/hindsight-all.md`)
sidebar_position: 2
Programmatic API (Python)
The hindsight-all Python package lets your code spawn and manage a local Hindsight daemon without deploying any server infrastructure. It bundles the Hindsight API server, embedded PostgreSQL, and the Python client into one install — pip install hindsight-all and you can start a fully-functional Hindsight instance from a few lines of Python.
The daemon runs as a separate OS process on 127.0.0.1 (not in your Python process memory). Your code talks to it over HTTP via the bundled HindsightClient.
If you already have a Hindsight server running and just need a client, use Python Client (hindsight-client) instead.
How it works
hindsight-all exposes two primary APIs:
HindsightServer— explicit lifecycle. Use it as a context manager when you want deterministic startup/shutdown (e.g. in tests).HindsightEmbedded— auto-managed. Starts a daemon on first use and reuses it across calls. Easiest for application code that doesn't want to think about lifecycle.
Both end up talking to the same underlying daemon via the same HindsightClient HTTP interface — the difference is only how the server process is managed.
Installation
pip install hindsight-allThe hindsight-all wheel bundles hindsight-api-slim, hindsight-client, and hindsight-embed as dependencies, so one pip install gets you everything.
On Intel (x86_64) Macs, install hindsight-all-slim instead — the full bundle's local ML models have no Intel-Mac wheels. See Supported Platforms.
`HindsightServer` — explicit lifecycle
Use HindsightServer as a context manager when you want the server to start immediately, run for the duration of a block, and shut down cleanly afterwards. Ideal for tests and short-lived scripts.
import os
from hindsight import HindsightServer, HindsightClient
with HindsightServer(
llm_provider="openai",
llm_model="gpt-4o-mini",
llm_api_key=os.environ["OPENAI_API_KEY"],
) as server:
client = HindsightClient(base_url=server.url)
client.retain(bank_id="my-bank", content="Alice works at Google")
results = client.recall(bank_id="my-bank", query="What does Alice do?")
for r in results:
print(r.text)
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
print(answer.text)
# Server is stopped here`HindsightEmbedded` — auto-managed
HindsightEmbedded is the simplest way to use Hindsight in Python. It automatically manages a background daemon for you — starts on first use and stays alive across calls.
from hindsight import HindsightEmbedded
import os
# Server starts automatically on first call
client = HindsightEmbedded(
profile="myapp", # Profile for data isolation
llm_provider="openai",
llm_model="gpt-4o-mini",
llm_api_key=os.environ["OPENAI_API_KEY"],
)
# Use immediately - no manual server management needed
client.retain(bank_id="my-bank", content="Alice works at Google")
results = client.recall(bank_id="my-bank", query="What does Alice do?")
# Server continues running (stop it with `hindsight-embed daemon stop`)
# Or explicitly stop it:
client.close(stop_daemon=True)What's a Profile?
A profile is an isolated Hindsight environment. Each profile gets its own embedded PostgreSQL database (stored in ~/.pg0/instances/hindsight-embed-{profile}/) and its own API server. Use different profiles to separate environments (dev/prod), applications, or users.
When to use which
| Use case | Pick |
|---|---|
| Tests, short-lived scripts, deterministic startup/shutdown | HindsightServer (context manager) |
| Long-running application, auto-start on first use, don't want to manage lifecycle | HindsightEmbedded |
| Existing Hindsight server running elsewhere | hindsight-client directly |
API namespaces
Both HindsightEmbedded and HindsightClient expose organized API namespaces for bank management, mental models, directives, and memories:
from hindsight import HindsightEmbedded
import os
embedded = HindsightEmbedded(
profile="myapp",
llm_provider="openai",
llm_api_key=os.environ["OPENAI_API_KEY"],
)
# Core operations
embedded.retain(bank_id="test", content="Hello")
results = embedded.recall(bank_id="test", query="Hello")
# Bank management
embedded.banks.create(bank_id="test", name="Test Bank", mission="Help users")
embedded.banks.set_mission(bank_id="test", mission="Updated mission")
embedded.banks.delete(bank_id="test")
# Mental models
embedded.mental_models.create(
bank_id="test",
name="User Preferences",
content="User prefers dark mode"
)
models = embedded.mental_models.list(bank_id="test")
# Directives
embedded.directives.create(
bank_id="test",
name="Response Style",
content="Be concise and friendly"
)
directives = embedded.directives.list(bank_id="test")
# List memories
memories = embedded.memories.list(bank_id="test", type="world", limit=50)API namespaces ensure the daemon is running before each call, so daemon crashes are handled gracefully:
# ✅ GOOD - Uses API namespace (daemon restarts handled)
embedded.banks.create(bank_id="test", name="Test")
# ❌ BAD - Direct client access (daemon crashes NOT handled)
client = embedded.client
client.create_bank(bank_id="test", name="Test") # Fails if daemon crashedFor the full reference of retain/recall/reflect methods and their options (which work the same regardless of how you obtain the client) see the Python Client page.
Hindsight Docs Docs Sdks Nodejs (`hindsight-docs/docs/sdks/nodejs.mdx`)
sidebar_position: 2
import CodeSnippet from '@site/src/components/CodeSnippet';
import nodejsSdkMjs from '!!raw-loader!@site/examples/api/nodejs-sdk.mjs';
TypeScript / JavaScript Client
Official TypeScript/JavaScript client for the Hindsight API. Supports Node.js and Deno.
Installation
Node.js
npm install @vectorize-io/hindsight-clientDeno
No installation needed — import directly via the npm: specifier:
import { HindsightClient } from "npm:@vectorize-io/hindsight-client";Quick Start
Client Initialization
Core Operations
Version and Feature Checks
Retain (Store Memory)
Retain Batch
Recall (Search)
Reflect (Generate Response)
Bank Management
Create Bank
List Memories
Document Management
Get Document
List Documents
Update Document
Delete Document
Hindsight Docs Docs Sdks Python (`hindsight-docs/docs/sdks/python.mdx`)
sidebar_position: 1
import CodeSnippet from '@site/src/components/CodeSnippet';
import pythonSdkPy from '!!raw-loader!@site/examples/api/python-sdk.py';
Python Client
Official HTTP client for the Hindsight API. Use this when you have a Hindsight server already running — locally, in Docker, or as a managed service — and you want a typed Python client to talk to it.
If you want to embed and run a Hindsight server in your Python process (no external server required), see Embedded Python (hindsight-all) instead.
Installation
pip install hindsight-clientQuick Start
Client Initialization
For operations the helper methods don't cover, the client also exposes the full generated API as async namespaces (client.banks, client.memory, client.documents, client.mental_models, client.directives, ...). Their methods must be awaited.
Core Operations
Version and Feature Checks
The async client method is available as await client.aget_version().
Retain (Store Memory)
Retain Batch
Recall (Search)
Recall with Chunks
Reflect (Generate Response)
Bank Management
Create Bank
List Memories
Async Support
All methods have async versions prefixed with a:
Context Manager
Hindsight Docs Docs Developer Api Bank Templates (`hindsight-docs/docs/developer/api/bank-templates.mdx`)
sidebar_position: 9
Bank Templates
Declarative JSON manifests for creating pre-configured memory banks with a single API call.
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeSnippet from '@site/src/components/CodeSnippet';
{/* Import raw source files */}
import templatesPy from '!!raw-loader!@site/examples/api/bank-templates.py';
import templatesMjs from '!!raw-loader!@site/examples/api/bank-templates.mjs';
import templatesSh from '!!raw-loader!@site/examples/api/bank-templates.sh';
import templatesGo from '!!raw-loader!@site/examples/api/bank-templates.go';
Overview
A bank template is a JSON manifest that describes a bank's full setup: configuration overrides, mental models, directives, and more. Instead of making multiple API calls to configure a bank, you submit one manifest and the API provisions everything.
Templates are useful for:
- Replication — stamp out identically-configured banks for multiple users or agents
- Onboarding — new users start with a known-good configuration instead of configuring from scratch
- Sharing — distribute recommended setups as portable JSON files
- Framework integrations — ship a recommended template alongside your integration
Browse the Bank Templates Hub for ready-to-use templates.
Manifest Schema
{
"version": "1",
"bank": {
"reflect_mission": "...",
"retain_mission": "...",
"retain_extraction_mode": "concise | verbose | custom | verbatim | chunks",
"retain_custom_instructions": "...",
"retain_chunk_size": 2048,
"retain_structured_chunk_size": 8192,
"disposition_skepticism": 3,
"disposition_literalism": 3,
"disposition_empathy": 3,
"enable_observations": true,
"observations_mission": "...",
"entity_labels": [{ "key": "sentiment", "type": "value", "values": [{ "value": "positive" }, { "value": "negative" }] }],
"entities_allow_free_form": true
},
"mental_models": [
{
"id": "unique-lowercase-id",
"name": "Human-Readable Name",
"source_query": "The query that generates this mental model's content",
"tags": ["optional", "tags"],
"max_tokens": 2048,
"trigger": {
"refresh_after_consolidation": false,
"fact_types": ["world", "experience", "observation"],
"exclude_mental_models": false,
"exclude_mental_model_ids": []
}
}
],
"directives": [
{
"name": "directive-name",
"content": "The directive instruction text",
"priority": 0,
"is_active": true,
"tags": ["optional", "tags"]
}
]
}Fields
| Field | Required | Description |
|---|---|---|
version |
Yes | Schema version. Currently "1". |
bank |
No | Bank configuration overrides. Omit to leave config unchanged. |
mental_models |
No | Mental models to create or update. Omit to leave unchanged. |
directives |
No | Directives to create or update. Omit to leave unchanged. |
All of bank, mental_models, and directives are optional. Omit any section to leave that part of the bank unchanged.
Bank Config Fields
Every per-bank configuration setting can be carried in a template, so an exported
bank reproduces its full configuration when imported elsewhere. All fields inbank are optional — only the fields you include are set as per-bank overrides,
and everything else inherits from the server/tenant defaults.
The complete, always-current list is the template JSON
Schema; each field means the same thing it does in
Configuration. The most commonly used ones:
| Field | Type | Description |
|---|---|---|
reflect_mission |
string | Mission/context for reflect operations |
retain_mission |
string | Steers what gets extracted during retain |
retain_extraction_mode |
string | concise, verbose, custom, verbatim, or chunks |
retain_custom_instructions |
string | Custom extraction prompt (requires mode=custom) |
retain_chunk_size |
integer | Target max characters per content chunk |
retain_structured_chunk_size |
integer | Max characters for a single JSONL line or conversation turn to keep whole; defaults to retain_chunk_size when unset |
disposition_skepticism |
integer (1-5) | How skeptical the disposition is |
disposition_literalism |
integer (1-5) | How literal the disposition is |
disposition_empathy |
integer (1-5) | How empathetic the disposition is |
enable_observations |
boolean | Toggle observation consolidation |
observations_mission |
string | Controls what gets synthesised into observations |
entity_labels |
object[] | Controlled vocabulary as label groups — see Memory Banks → entity_labels |
entities_allow_free_form |
boolean | Allow entities outside the label vocabulary |
Mental Model Fields
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique ID (lowercase alphanumeric with hyphens). Used to match on re-import. |
name |
Yes | Human-readable name |
source_query |
Yes | The query that generates this model's content via reflect |
tags |
No | Tags for scoped visibility. Default: [] |
max_tokens |
No | Max tokens for generated content (256-8192). Default: 2048 |
trigger |
No | Trigger settings for auto-refresh |
Directive Fields
| Field | Required | Description |
|---|---|---|
name |
Yes | Directive name. Used as th |