hindsight Documentation

Hindsight: Agent Memory That Learns

52,467 stars Python Python 46 files · ~190,632 tokens #agentic-ai#agents#ai-memory#memory
RAW Doc

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

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:

bash
npx skills add https://github.com/vectorize-io/hindsight --skill hindsight-docs

Works with Claude Code, Cursor, and other AI coding assistants.


Quick Start

1. Start a server

bash
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:latest

API: http://localhost:8888
UI: http://localhost:9999

Hindsight 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)

bash
export OPENAI_API_KEY=sk-xxx
export HINDSIGHT_DB_PASSWORD=choose-a-password
cd docker/docker-compose
docker compose up

Oracle AI Database is also supported for enterprise deployments with full feature parity. See the storage documentation for details.

Bare metal (pip)

bash
pip install hindsight-api
export HINDSIGHT_API_LLM_API_KEY=sk-xxx

hindsight-api

Kubernetes (Helm)

bash
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
  --set api.llm.provider=openai \
  --set api.llm.apiKey=sk-xxx \
  --set postgresql.enabled=true

Managed (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

bash
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          # CLI

Python

python
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

javascript
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)

bash
pip install hindsight-all -U

On Intel (x86_64) Macs, install hindsight-all-slim instead — see Supported Platforms.

python
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.

bash
pip install hindsight-litellm
python
from 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

👉 Browse all integrations

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.

bash
npx @vectorize-io/hindsight-coding-agents install all          # every detected agent, wired natively
npx @vectorize-io/hindsight-coding-agents install claude-code  # or just one

Supports 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:

text
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.

python
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.

Retain docs →

Recall

The recall operation is used to retrieve memories. These memories can come from any of the memory types (world, experiences, etc.)

python
client.recall(bank_id="my-bank", query="What does Alice do?")
client.recall(bank_id="my-bank", query="What happened in June?")   # temporal

Recall 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.

Recall docs →

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.

python
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.

Reflect docs →

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.

Observations docs →

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:

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`)


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, reflect treats 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 mount projects the tree onto disk as ordinary markdown, so grep, 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


Hindsight Docs Docs Developer Api Quickstart (`hindsight-docs/docs/developer/api/quickstart.mdx`)


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

bash
pip install hindsight-api
export OPENAI_API_KEY=sk-xxx
export HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY

hindsight-api

API available at http://localhost:8888

bash
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

:::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

bash
pip install hindsight-client
bash
npm install @vectorize-io/hindsight-client
bash
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
bash
go get github.com/vectorize-io/hindsight/hindsight-clients/go

What'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`)


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 returns 422.

A minimal policy:

json
{
  "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 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


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:

bash
pip install hindsight-api
# or
uv add hindsight-api

Running 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:

bash
# 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-migration

Use --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).

bash
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:

bash
# 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.

bash
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:

sql
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:

bash
# 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-prod

rename-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.

bash
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:

bash
# 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-prod

backup

Create a backup of all Hindsight data to a zip file.

bash
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:

bash
# 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_acme

The 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.

bash
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:

bash
# 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.

bash
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:

bash
# 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_acme

When 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.

bash
hindsight-admin decommission-workers [OPTIONS]

Options:

Option Description Default
--schema, -s Database schema public
--yes, -y Skip confirmation prompt false

Examples:

bash
# 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_acme

When 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.

bash
hindsight-admin worker-status [OPTIONS]

Options:

Option Description Default
--schema, -s Database schema public

Examples:

bash
# 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_acme

When 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_ago keeps 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.

bash
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:

bash
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-history

Read-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.

bash
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:

bash
hindsight-admin import-bank --archive my-bank.zip

Run 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:

  1. Stand up a new instance on a fresh database, configured with the new embedding model / vector extension / text-search backend.
  2. Quiesce writes to the source bank (maintenance window) and run hindsight-admin backup for safety.
  3. 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
  4. Verify on the target: run representative recall queries and compare results.
  5. 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:

bash
# 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>/stats

How to recover:

bash
# 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-workers

Both 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:

bash
# Use a specific database
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
hindsight-admin backup /backups/mybackup.zip

Hindsight 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 public schema is reserved or shared
  • Organizational preferences for schema naming conventions
bash
# Example: Using a custom schema
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname
export HINDSIGHT_API_DATABASE_SCHEMA=hindsight

Migrations 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 and
schemas_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:

bash
export HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES=mental_models_with_cron,banks_needing_consolidation

Read from the environment of whatever process runs the migration, so it applies equally to
hindsight-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:

bash
# 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_acme

Vector 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
  • 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;

**vchord** (vchordrq)

  • Alternative high-performance vector index
  • Optimized for high-dimensional embeddings (3000+ dimensions)
  • Includes integrated BM25 search capabilities
  • Requires vchord extension

**scann** (AlloyDB ScaNN)

  • Google's ScaNN index, available on AlloyDB and AlloyDB Omni
  • Uses a single global vector index in AUTO mode (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.yaml for 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 AUTO mode 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:

bash
hindsight-admin repair-bank --all

This 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:

  1. Set HINDSIGHT_API_VECTOR_EXTENSION to your desired extension (pgvector, vchord, pgvectorscale, or scann)
  2. If your database has existing data, you'll get an error with migration instructions (note: switching to scann is allowed even with data — the existing index is dropped and rebuilt as ScaNN once the table has at least 10,000 embedding rows)
  3. 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 llmlingua2 multilingual 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. */
bash
# 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. */

bash
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. */

bash
# 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:

bash
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. */

bash
# 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. */

bash
# 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-versatile

Example: Tuning Retry Behavior for Rate-Limited APIs

bash
# 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. */

bash
pip install 'hindsight-api-slim[local-onnx]'
# or, in this repository:
uv sync --project hindsight-api-slim --extra local-onnx

CUDA 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:

bash
docker compose -f docker/docker-compose/cuda-onnx/docker-compose.yaml up --build

The 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.

bash
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",
    ],
)
PY

Then start Hindsight with paths relative to the repository root:

bash
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:

yaml
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:

bash
# 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. */

bash
# 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. */

bash
# 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=rrf

Ending 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 *_TIMEOUT settings).
  • 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 at WARNING.
  • 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 least
HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT × HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE
slots — 1024 for the defaults above, versus TEI's own default of 512:

bash
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.

:::

bash
# 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. */

bash
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. */

bash
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. */

bash
# 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-key

When enabled, all requests must include the API key in the Authorization header:

bash
curl -H "Authorization: Bearer your-secret-api-key" \
  http://localhost:8888/v1/default/banks

Requests 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:

bash
export HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertion,x-request-origin

/* Detailed source-code truncated for AI context efficiency. */

json
{
  "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. */

bash
# 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. */

bash
export HINDSIGHT_API_STORE_DOCUMENT_TEXT=false

/* Detailed source-code truncated for AI context efficiency. */

bash
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.

bash
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbatim

retain_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:

json
{
  "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.

bash
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=chunks

HINDSIGHT_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.

bash
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. */

bash
# 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
json
// 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. */

bash
# 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.

bash
# 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,markitdown

Parser: 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.

bash
# 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
bash
# 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=false

File 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.

bash
# Native storage is the default — no additional configuration needed
export HINDSIGHT_API_FILE_STORAGE_TYPE=native

S3 / 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.

bash
# 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_key

Google 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
bash
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.json

Azure 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 -
bash
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. */

bash
HINDSIGHT_API_WORKER_ENABLED=false

/* Detailed source-code truncated for AI context efficiency. */

bash
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

bash
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

bash
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. */

bash
# 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,reflect

Available 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:

bash
export HINDSIGHT_API_MCP_AUTH_TOKEN=your-secret-token

Clients must then include the token in the Authorization header. See MCP Server documentation for details.

Local MCP instructions:

bash
# 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. */

text
HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY="shadow-*:10,staging-*:5,*:1"

/* Detailed source-code truncated for AI context efficiency. */

python
from hindsight_api import MemoryEngine

memory = MemoryEngine.from_env()
await memory.initialize()

/* Detailed source-code truncated for AI context efficiency. */

bash
# 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=production

Local Development:

For local development, we recommend the Grafana LGTM stack which provides traces, metrics, and logs in a single container:

bash
./scripts/dev/start-grafana.sh

/* Detailed source-code truncated for AI context efficiency. */

text
HINDSIGHT_API_PROFILE='{"every": 60, "top": 20}'
text
[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. */

bash
# 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-key

Hierarchical 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:

python
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:

text
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:

python
# 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. */

bash
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():

python
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/):

  1. Set both environment variables to the same path:

    bash
    HINDSIGHT_API_BASE_PATH=/hindsight
    NEXT_PUBLIC_BASE_PATH=/hindsight
  2. Configure 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)

Example: Nginx Configuration

nginx
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

yaml
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

bash
# 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:8888

For configuration issues not covered here, please open an issue on GitHub.


Hindsight Docs Docs Developer Development (`hindsight-docs/docs/developer/development.md`)


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

bash
git clone https://github.com/vectorize-io/hindsight.git
cd hindsight

2. Install Dependencies

bash
uv sync

3. Start PostgreSQL

Start only the database via Docker:

bash
cd docker && docker-compose up -d postgres

4. Configure Environment

bash
cp .env.example .env

Edit .env with your LLM API key:

bash
# 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-versatile

5. Start the API Server

bash
./scripts/start-server.sh --env local

The server will be available at http://localhost:8888.

Running Tests

bash
# Run all tests
uv run pytest

# Run specific test file
uv run pytest tests/test_retrieval.py

# Run with verbose output
uv run pytest -v

Code Generation

Regenerate API Clients

When you modify the OpenAPI spec, regenerate the clients:

bash
./scripts/generate-clients.sh

This generates:

  • Python client in hindsight-clients/python/
  • TypeScript client in hindsight-clients/typescript/

Export OpenAPI Schema

bash
./scripts/export-openapi.sh

Project Structure

text
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 scripts

Contributing

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:

bash
docker-compose ps

Check database connectivity:

bash
psql postgresql://hindsight:hindsight_dev@localhost:5432/hindsight

ML 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:

bash
HINDSIGHT_API_PORT=8889 ./scripts/start-server.sh --env local

Hindsight 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.

bash
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
HINDSIGHT_API_TENANT_API_KEY=your-secret-key

No 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). Returns None by default.

No built-in implementation - implement your own to add custom endpoints.

bash
HINDSIGHT_API_HTTP_EXTENSION=mypackage.ext:MyHttpExtension

OperationValidatorExtension

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).

bash
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator

MCPExtension

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.

bash
HINDSIGHT_API_MCP_EXTENSION=mypackage.mcp:MyMCPExtension

LLMRequestExtension

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.

bash
HINDSIGHT_API_LLM_REQUEST_EXTENSION=mypackage.llms:MyLLMs

An 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 llm cost 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 as base_url, ...), anthropic and litellm routed 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 whole llm instead. 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.provider before 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 llm answer 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 and api_key instead.
  • No batch retain. HINDSIGHT_API_RETAIN_BATCH_ENABLED=true with 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, and on_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:

bash
HINDSIGHT_API_<TYPE>_EXTENSION=mypackage.module:MyExtensionClass

Configuration is passed via prefixed environment variables:

bash
HINDSIGHT_API_<TYPE>_SOME_CONFIG=value
# Extension receives: {"some_config": "value"}

All extensions support lifecycle hooks:

  • on_startup() - Called when the application starts
  • on_shutdown() - Called when the application shuts down

Extensions have access to an ExtensionContext that provides:

  • run_migration(schema) - Run database migrations for a schema
  • get_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:

python
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:

python
depends_on = ("a1b2c3d4e5f6",)   # a Hindsight revision id

A 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

python
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:

python
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:

bash
HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS=x-user-assertion

They are available as context.extra_headers, keyed by lower-cased name:

python
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 operations
extra_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

python
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 router

Routes 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

python
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.
        pass

precheck runs before the request body is read or deserialized. Its
PrecheckContext.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 the
validate_bank_write access check that runs first:

  • validate_memory_update(ctx: MemoryUpdateContext) runs before any work. The
    context carries the requested text (when editing it), the requested state,
    and edits_fields. Reject here to refuse the curation; the returned
    status_code is passed through to the HTTP response.
  • on_memory_update_complete(result: MemoryUpdateResult) runs once the change
    has committed. result.action is edit, invalidate, revert, or reason,
    and result.reembedded_tokens is 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.documents lists the archive's documents
    (ImportArchiveDocument: document_id and the original content), 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_complete with
    result.source == RetainSource.IMPORT, zero LLM token counts, and
    processed_content_tokens=0. Use source to tell an import apart from a
    re-retain whose chunks were all deduplicated, which also reports
    processed_content_tokens=0; ordinary retains report RetainSource.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), or
None for no restriction:

python
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 — answer 404 when 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, a kind:rule fact 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 (403 otherwise — 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):

python
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
    with tag: true could add (key:value for each allowed value, key:* for an
    open vocabulary), and explicit observation_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 through
validate_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 raising
DeferOperation. 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.

python
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 from
validate_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 with
self.context.get_memory_engine().cancel_operation(...) and
then raise DeferOperation: the row stays cancelled and no failure is
recorded.

Example: Custom MCPExtension

python
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

python
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:

dockerfile
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:

bash
# 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-api

Contributing 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:

bash
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

:::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:

bash
sudo chown -R 1000:1000 $HOME/.hindsight-docker

Do 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:

text
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/:

bash
docker compose -f docker/docker-compose/cuda/docker-compose.yaml up --build

The 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

bash
# 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:latest

Verifying image signatures

Images are signed with Cosign keyless OIDC. To verify any tag:

bash
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.com

Helm / Kubernetes

Best for: Production deployments, auto-scaling, cloud environments

bash
# 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/hindsight

Requirements:

  • 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:

bash
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
  --set worker.enabled=true \
  --set worker.replicaCount=3

The 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

bash
pip install hindsight-api        # Full — works out of the box
pip install hindsight-api-slim   # Slim — requires external services for embeddings, reranking, and the database

When 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):

bash
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx

hindsight-api

This 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:

bash
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-api

Note: The database must exist and have pgvector enabled (CREATE EXTENSION vector;).

CLI Options

bash
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 logging

Control Plane

The Control Plane (Web UI) can be run standalone using npx:

bash
npx @vectorize-io/hindsight-control-plane --api-url http://localhost:8888

This 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

bash
# 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-plane

Windows

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:

powershell
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-api

Using External PostgreSQL (optional)

If you prefer to use your own PostgreSQL instance instead of the embedded database:

powershell
# 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:

powershell
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

:::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:

  1. DeepSeek works well for HINDSIGHT_API_LLM_PROVIDER, but DeepSeek does not provide an embeddings endpoint.
  2. Use local embeddings (recommended for privacy and reliability in restricted networks).
  3. Set HF_ENDPOINT=https://hf-mirror.com before starting Hindsight so Hugging Face model downloads use a China-accessible mirror.
powershell
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-api

The 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.

bash
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 database

On 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.

python
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.

python
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


Hindsight Docs Docs Developer Knowledge Pages (`hindsight-docs/docs/developer/knowledge-pages.mdx`)


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:

bash
hindsight fs mount --bank my-bank

The 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 fs mirrors 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`)


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:

text
http://localhost:8888/mcp/{bank_id}/

For example, to connect to the memory bank alice:

text
http://localhost:8888/mcp/alice/

To disable the MCP server, set the environment variable:

bash
export HINDSIGHT_API_MCP_ENABLED=false

Authentication

By default, the MCP endpoint is open (no authentication required).

To enable authentication, configure the API key tenant extension:

bash
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
export HINDSIGHT_API_TENANT_API_KEY=your-secret-key

When authentication is enabled, include your API key in the Authorization header:

Claude Code

bash
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:

json
{
  "mcpServers": {
    "hindsight": {
      "url": "http://localhost:8888/mcp",
      "headers": {
        "Authorization": "Bearer your-secret-key",
        "X-Bank-Id": "my-bank"
      }
    }
  }
}

Direct HTTP Request

bash
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:

  1. URL path (highest priority): http://localhost:8888/mcp/my-bank/
  2. X-Bank-Id header: --header "X-Bank-Id: my-bank"
  3. Default: Uses HINDSIGHT_MCP_BANK_ID env 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_id with 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.

bash
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_*, and get_* are marked with readOnlyHint: true.
  • Delete, clear, and invalidate operations are marked with destructiveHint: true.
  • openWorldHint is false for 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, and cancel_operation are 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:

json
{
  "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:

json
{
  "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:

json
{
  "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. Pass
tags: [] 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:

json
{
  "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 and
trigger_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:

json
{
  "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:

json
{
  "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 explicit
null to clear a nullable setting. Read the current policy back with
get_mental_model (detail content or full) if you want to inspect before
changing.

json
{
  "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 operations
  • retain_mission — steers what gets extracted during retain()
  • retain_extraction_mode — concise (default), verbose, custom, verbatim, or chunks
  • retain_custom_instructions — custom extraction prompt (active when mode is custom)
  • retain_chunk_size — target maximum characters for each content chunk
  • retain_structured_chunk_size — maximum characters for a single JSONL line or conversation turn to keep whole
  • retain_chunk_batch_size — number of chunks to process in parallel
  • enable_observations — toggle observation consolidation after retain()
  • observations_mission — controls observation synthesis rules
  • disposition_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 classification
  • entities_allow_free_form — allow labels outside entity_labels
  • recall_include_chunks — include raw chunks in recall results
  • recall_prefer_observations — default prefer_observations for recall calls that omit it
  • recall_max_tokens — max tokens for recall results
  • mcp_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 endpoint
GET /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-Id header

Hindsight Docs Docs Developer Mental Models (`hindsight-docs/docs/developer/mental-models.mdx`)


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 CachedContent API. On by default; disable with HINDSIGHT_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:

bash
# Uses claude-haiku-4-5 automatically
export HINDSIGHT_API_LLM_PROVIDER=anthropic
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx

You can override the default by explicitly setting HINDSIGHT_API_LLM_MODEL:

bash
# 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-20250929

This also applies to per-operation overrides:

bash
# 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=anthropic

Using 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:

bash
# 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=16000

Important: 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

text
/* 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:

bash
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_URL optionally points to an existing headless Copilot runtime, such as http://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 public
https://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 private
chatgpt.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):

  1. Sign in:

    bash
    hindsight-admin chatgpt login

    This opens your browser and waits for the callback on http://127.0.0.1:1455/auth/callback
    (--port changes the port; --no-browser prints 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 (mode 0600) next to this host's host_id.
    If you declined plan usage, run login --enable-plan-usage to be asked again.

  2. Configure Hindsight:

    bash
    export 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.effort
  3. Check the stored session (prints account, client id, scopes and expiry, never token values):

    bash
    hindsight-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.

  1. On the laptop: run login as above.
  2. Copy the file over SSH and install it on the server with login --from-file, as the user that runs Hindsight (add
    --home <dir> before login if the server sets HINDSIGHT_API_LLM_CHATGPT_HOME).
    --from-file keeps the server's own host_id (creating one on first use) instead of the laptop's:
    bash
    scp ~/.hindsight/chatgpt/credentials.json server:/tmp/chatgpt.json
    ssh server 'hindsight-admin chatgpt login --from-file /tmp/chatgpt.json && rm /tmp/chatgpt.json'
  3. On the laptop, stop using that copy without revoking it:
    bash
    hindsight-admin chatgpt logout --local-only
    Refresh tokens rotate, so after the copy only the server may use and refresh this session. A plain
    logout would revoke it and end the server's session too. --local-only clears the laptop's
    tokens but keeps its client registration for a later login. --from-file refuses 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 login command 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: false and stream: true, omits fields it rejects
    (temperature, max_output_tokens, user, and others), and sends system prompts as developer
    messages.
  • logout revokes the session and clears local tokens but keeps the client registration and host id
    for the next login. logout --local-only clears the tokens without revoking.
  • HINDSIGHT_API_LLM_CHATGPT_HOME relocates 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:

  1. Install Codex CLI:

    bash
    npm install @openai/codex
  2. Login with ChatGPT credentials:

    bash
    codex auth login

    This opens a browser window to authenticate with your ChatGPT account and saves OAuth tokens to ~/.codex/auth.json.

  3. Verify authentication:

    bash
    ls ~/.codex/auth.json  # Should show the auth file exists
  4. Configure Hindsight:

    bash
    export 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 automatically
  5. Start Hindsight:

    bash
    hindsight-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 /health keep working (the database and API are fine), but
    /reflect fails with an error such as:
    text
    Codex 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.

bash
# 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-api

For a systemd unit, set it in the service definition so it never shares auth
with an interactive Codex session:

ini
[Service]
Environment=CODEX_HOME=/var/lib/hindsight/codex

After 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 with
HINDSIGHT_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.

bash
# 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:

bash
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

The base URL defaults to https://api.meta.ai/v1. Available models are
muse-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
    whichever HINDSIGHT_API_EMBEDDINGS_PROVIDER you 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:

  1. Log in to Nous Portal:

    bash
    hermes portal

    This opens a browser to authenticate with Nous Portal and saves OAuth credentials to ~/.hermes/auth.json.

  2. Verify authentication:

    bash
    hermes portal status  # should show "Auth: ✓ logged in"
  3. Configure Hindsight:

    bash
    export 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 automatically
  4. Start Hindsight:

    bash
    hindsight-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 with HINDSIGHT_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 via claude 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:

  1. Install Claude Code CLI:

    bash
    npm install @anthropics/claude-code
    # Or via Homebrew
    brew install anthropics/claude-code/claude-code
  2. Login with Claude credentials:

    bash
    claude auth login

    This opens a browser window to authenticate with your Claude account. Authentication is automatically managed by the Claude Agent SDK.

  3. Verify authentication:

    bash
    claude --version
    # Should show version without errors
  4. Configure Hindsight:

    bash
    export HINDSIGHT_API_LLM_PROVIDER=claude-code
    # No API key needed - uses claude auth login credentials

    Server-held token (optional, for a personal machine without a browser login): instead of
    relying on the host's claude auth login session, 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 the anthropic provider with an API key.

    bash
    claude 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.

  5. Start Hindsight:

    bash
    hindsight-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-agent installed and signed in under the same OS user that runs Hindsight

Setup Steps:

  1. Install the CLI:

    bash
    curl https://cursor.com/install -fsS | bash
  2. Log in:

    bash
    cursor-agent login
  3. Verify:

    bash
    cursor-agent --version
    cursor-agent --list-models
  4. Configure Hindsight:

    bash
    export 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-agent turn, which takes 15-30s
    against auto — slower than a chat-completions request and past the 30s
    HINDSIGHT_API_REFLECT_LLM_TIMEOUT default. 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 over auto for reflect, the operation that leans hardest on tools.
  • Free plans are limited to auto. A named --model returns
    Named models unavailable Free plans can only use Auto. Paid plans can use any id from
    cursor-agent --list-models.
  • The agent's own tools are turned off. cursor-agent is an agent, not a completions
    endpoint: it ships Shell, Read, Write, Delete, web fetch and more, and --mode ask is
    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 own CURSOR_CONFIG_DIR (so it never sees your ~/.cursor config, hooks or session
    history), and writes a cli-config.json there that denies every tool by name. That is
    the equivalent of the tools=[] the claude-code provider 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 if HINDSIGHT_API_LLM_REASONING_EFFORT is 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 with
HINDSIGHT_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:

  1. Log in once, on the host that will own the credential:

    bash
    python -m hindsight_api.engine.providers.xai_oauth_auth login

    The command prints a verification URL and a user code, waits for approval,
    then writes ~/.hindsight/xai_oauth.json with owner-only permissions.

  2. Configure Hindsight:

    bash
    export HINDSIGHT_API_LLM_PROVIDER=xai-oauth
    # export HINDSIGHT_API_LLM_MODEL=grok-4.5   # defaults to grok-4.5
    # No API key needed
  3. Start Hindsight:

    bash
    hindsight-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.ai through provider: openai, this provider sends the
    member's configured reasoning_effort, so that setting takes effect here.
    The one value not forwarded is none: 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 with
Invalid 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:

bash
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/v1

Or the deployment-scoped form. Keep the api-version query string — Hindsight
parses it out of the base URL and passes it to the SDK:

bash
export HINDSIGHT_API_LLM_BASE_URL=https://<resource>.openai.azure.com/openai/deployments/<deployment>?api-version=2025-01-01-preview

Important notes:

  • HINDSIGHT_API_LLM_MODEL is your deployment name, not the model name. A
    gpt-4o deployed as my-gpt4o is configured as my-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 the api-key header. Test against the Azure endpoint
    directly first to isolate which layer is failing.
  • Gateways and proxies must preserve the same path shape (/openai/v1 or
    /openai/deployments/...?api-version=).
  • Azure OpenAI accepts prompt_cache_key on GPT deployments (it improves cache
    routing on GPT-5.6 and later), but the same *.openai.azure.com endpoint also
    serves non-OpenAI Foundry models (DeepSeek, Llama, Mistral) that reject it with
    unrecognized_request_argument. The host alone can't tell the two apart, so
    HINDSIGHT_API_LLM_CACHE_AFFINITY resolves
    auto to none for Azure hosts. If your deployment serves an OpenAI model,
    set it explicitly to openai_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.user for 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:

  1. 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-id
  2. Service 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:

text
/* 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:

bash
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/abc123

The 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:

text
/* 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:

bash
./scripts/dev/start-monitoring.sh

This starts a single Docker container providing:

Enable tracing in your API:

bash
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:

bash
curl http://localhost:8888/metrics

Health 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:

json
{ "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:

json
{ "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-in
bank_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 with
HINDSIGHT_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 as consolidation)
  • bank_id: Memory bank identifier
  • source: Where the operation was triggered from (api, reflect, internal, worker)
  • budget: Budget level if specified (low, mid, high)
  • max_tokens: Max tokens if specified
  • success: 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 on tenant. Recall phase metrics recorded inside the operation carry it too.

The source label allows distinguishing between:

  • api: Direct API calls from clients
  • reflect: Internal recall calls made during reflect operations
  • internal: Other internal operations
  • worker: 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; use
hindsight_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: facts when the document has memory units after the retain, no_facts when it has none
  • bank_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:

promql
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:

promql
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 a base_path / root-path prefix. Requests that match no route (404s) are labelled unmatched; a request matched by a route that has no path template is labelled unknown_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): user or system
  • type (Memory): rss_max (maximum resident set size)

Histogram Buckets

Custom bucket boundaries are configured for better percentile accuracy:

Operation Duration Buckets (seconds):

text
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.0

LLM Duration Buckets (seconds):

text
0.1, 0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0

HTTP Duration Buckets (seconds):

text
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0

Prometheus Configuration

yaml
scrape_configs:
  - job_name: 'hindsight'
    static_configs:
      - targets: ['localhost:8888']

Example Queries

Average operation latency by type

promql
rate(hindsight_operation_duration_sum[5m]) / rate(hindsight_operation_duration_count[5m])

LLM calls per minute by provider

promql
rate(hindsight_llm_calls_total[1m]) * 60

P95 LLM latency

promql
histogram_quantile(0.95, rate(hindsight_llm_duration_bucket[5m]))

Total tokens consumed by model

promql
sum by (model) (hindsight_llm_tokens_input_total + hindsight_llm_tokens_output_total)

Internal vs API recall operations

promql
sum by (source) (rate(hindsight_operation_total{operation="recall"}[5m]))

HTTP requests per second by endpoint

promql
sum by (endpoint) (rate(hindsight_http_requests_total[1m]))

HTTP error rate (5xx)

promql
sum(rate(hindsight_http_requests_total{status_class="5xx"}[5m])) / sum(rate(hindsight_http_requests_total[5m]))

P95 HTTP latency

promql
histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))

Database pool utilization

promql
hindsight_db_pool_size / hindsight_db_pool_max

Active database connections

promql
hindsight_db_pool_size - hindsight_db_pool_idle

CPU usage rate

promql
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:

bash
# 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 → Tempo

Supports any OTLP-compatible backend (Grafana LGTM, Langfuse, OpenLIT, DataDog, New Relic, Honeycomb, Pydantic Logfire, etc.).

Span Hierarchy

Parent Spans (Operations):

  • hindsight.retain - Memory ingestion
  • hindsight.recall - Memory retrieval
    • hindsight.recall_embedding - Query embedding
    • hindsight.recall_retrieval - Parallel search (semantic, BM25, graph, temporal)
    • hindsight.recall_fusion - Reciprocal Rank Fusion
    • hindsight.recall_rerank - Cross-encoder reranking
  • hindsight.reflect - Agentic reasoning
    • hindsight.reflect_tool_call - Tool execution (recall, lookup, etc.)
  • hindsight.consolidation - Observation synthesis
  • hindsight.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 type
  • hindsight.bank_id - Memory bank ID
  • hindsight.query - Query text (truncated to 100 chars)
  • hindsight.fact_types - Fact types for recall
  • hindsight.thinking_budget - Budget allocation
  • hindsight.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 name
  • gen_ai.usage.input_tokens - Input tokens
  • gen_ai.usage.output_tokens - Output tokens
  • hindsight.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`)


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:

  1. Detects the input language automatically from the content
  2. Extracts facts in the original language - preserving nuance and meaning
  3. Stores entities in their native script - 张伟 stays 张伟, not "Zhang Wei"
  4. 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.

The default embedding model (BAAI/bge-small-en-v1.5) is English-only. For multilingual content, use a multilingual embedding model:

bash
# In your .env file
HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-m3

Recommended 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

The default reranker (cross-encoder/ms-marco-MiniLM-L-6-v2) is English-only. For multilingual content, use a multilingual reranker:

bash
# In your .env file
HINDSIGHT_API_RERANKER_LOCAL_MODEL=BAAI/bge-reranker-v2-m3

Recommended 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, or pg_search).
  • HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE — selects the PostgreSQL dictionary used by the native backend (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):

bash
HINDSIGHT_API_TEXT_SEARCH_EXTENSION=native
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE=spanish

Choosing for a CJK or mixed-language bank:

bash
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.
bash
# Every LLM call (retain, consolidation, reflect) emits Spanish regardless of source language.
HINDSIGHT_API_LLM_OUTPUT_LANGUAGE=Spanish

Common 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 + leave HINDSIGHT_API_LLM_OUTPUT_LANGUAGE unset — 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:

  1. Detect the input language
  2. Extract all facts, entities, and descriptions in that same language
  3. Never translate to English unless the input is in English

Hindsight Docs Docs Developer Observations (`hindsight-docs/docs/developer/observations.mdx`)


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, reflect treats 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:

  1. New facts analyzed — Each new fact is compared against existing observations
  2. Pattern detection — Related facts are grouped and synthesized
  3. Observation creation/update — New observations are created or existing ones refined
  4. 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:

  1. Detects the conflict — New fact contradicts existing observation
  2. Preserves history — Incorporates the previous understanding into the new observation
  3. Creates nuanced observation — Synthesizes a richer understanding that captures the change
  4. 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:

  1. Mental Models — User-curated summaries (highest priority)
  2. Observations — Consolidated knowledge with freshness awareness
  3. 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.

text
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:

json
[
  {
    "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:

  • scopes is a list of alternatives. Each is {"tags": [...], "tags_match": "all" | "exact"}, where tags are 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} (extra user: 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 using
    observation_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 on shared.
    :::

    Remember that a strategy matches observation scopes — the tag sets consolidation groups observations under, set by observation_scopes at retain time — not the tags of individual memories. A memory tagged with a user, a team and a company but retained with observation_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_tokens and consolidation_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:

json
[
  {"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 to
observation_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:

  1. Delete all observations that list the memory as a source
  2. Reset consolidated_at on the memory itself and any other source memories that contributed to those observations
  3. 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:

http
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:

bash
# 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 oracledb

If 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:

bash
# 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.sh

A 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:

bash
export HINDSIGHT_API_DATABASE_SCHEMA=HINDSIGHT_TEST

Then 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:

sql
-- 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 privilege
CONNECT 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 for
VECTOR columns) plus the CTXAPP role.
:::

2. Build the connection URL

Hindsight uses SQLAlchemy-style URLs. The Oracle form is:

text
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:

text
oracle+oracledb://hindsight:[email protected]:1521/ORCLPDB1

Full 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 a
dsn query parameter:

text
oracle+oracledb://USER:PASSWORD@/?dsn=DESCRIPTOR_OR_TNS_ALIAS

The descriptor has to be percent-encoded, like the password. For example, the
Autonomous Database descriptor

text
(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

bash
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 with
ssl_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:

bash
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 1

HINDSIGHT_API_DATABASE_BACKEND defaults to postgresql; set it to oracle to
select the Oracle backend.

:::warning Set DATABASE_SCHEMA to your Oracle user
HINDSIGHT_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:

bash
hindsight-admin run-db-migration

This 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

bash
hindsight-api

On 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-admin backup,
    restore, bank export/import, and worker-status use asyncpg binary COPY and
    TRUNCATE, which are PostgreSQL-specific and not available on Oracle.
    Schema migrations (run-db-migration) are supported on Oracle.
  • No embedded database. The pg0 embedded 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's pg_trgm trigram
    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:

  1. Use high-throughput LLM providers: Choose providers with high requests-per-minute (RPM) limits and low latency

    • Fast: Groq with gpt-oss-20b or other openai-oss models, self-hosted models on GPU clusters (vLLM, TGI)
    • Slow: Standard cloud LLM providers with rate limits
  2. 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.

  3. Use async mode for large datasets: Queue operations in the background

  4. Parallel processing: For very large datasets, use multiple concurrent retention requests with different document_id values

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).

bash
export HINDSIGHT_API_LLM_MAX_CONCURRENT=2

A 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:

bash
# 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=1

Timeouts 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:

bash
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 box

A 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:

bash
# 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):

bash
export HINDSIGHT_API_LLM_REASONING_EFFORT=low

Consolidation 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:

bash
export HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=2   # default 8; lower = smaller prompts, more calls

Built-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:

bash
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 output

See 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:

bash
# 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=true

The 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:

bash
export HINDSIGHT_API_RERANKER_MAX_CANDIDATES=100   # default 300; RRF pre-filters the rest

For a pure-CPU box that struggles with the cross-encoder, flashrank is a lighter ONNX-based reranker:

bash
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank

You 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

  1. Appropriate budgets: Use lower budgets for simple queries, higher for comprehensive reasoning
  2. Limit result tokens: Set max_tokens to control response size (default: 4096)
  3. Include chunks: Use include_chunks to 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

  1. Budget selection: Use lower budgets when context is sufficient
  2. Context provision: Provide relevant context to 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 trace parameter 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-20b via Groq for retain — Hindsight doesn't need frontier models
  • Enable provider Batch API: Set HINDSIGHT_API_RETAIN_BATCH_ENABLED=true with 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_tokens for 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`)


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`)


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:

  1. Autonomously gathers evidence — The agent decides what information it needs and calls appropriate tools
  2. Uses hierarchical retrieval — Checks mental models first, then observations, then raw facts
  3. Applies disposition — Shapes reasoning based on the bank's personality traits
  4. Enforces applicable directives — Global rules and rules matching the reflect tag scope
  5. 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:

  1. Mental Models — User-curated summaries you've pre-computed for common queries
  2. Observations — Consolidated knowledge with freshness awareness
  3. 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 models
  • reflect("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_schema was provided
  • usage — Token usage metrics

Example:

json
{
  "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 a
properties map) to also get a machine-readable version of the same answer:

json
{
  "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:

json
{
  "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, no
structured_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


Hindsight Docs Docs Developer Retain (`hindsight-docs/docs/developer/retain.md`)


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:

json
{
  "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:

json
{
  "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 (with
include.facts) returns each cited memory with its attachments, plus the
document_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_MODEL names 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
    with 422 rather than dropping the attachment silently. See
    HINDSIGHT_API_LLM_VISION.
  • Batch retain (HINDSIGHT_API_RETAIN_BATCH_ENABLED) cannot carry attachments,
    and also refuses with 422.

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.

text
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:

text
POST /v1/default/banks/{bank_id}/documents/{document_id}/reprocess

Observation Consolidation

After retain() completes, Hindsight automatically triggers observation consolidation in the background. This process:

  1. Analyzes new facts against existing observations
  2. Creates new observations when patterns emerge
  3. Refines existing observations with new evidence
  4. 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:

json
{
  "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`)


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

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.


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 via
HINDSIGHT_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.


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 all
  • tags: Filter memories by visibility tags
  • tags_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:

text
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
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:

text
score(d) = Σ  1 / (k + rank_i(d))
           i

Where:

  • 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:

text
RRF score = 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318

A memory ranked #1 in semantic only:

text
RRF score = 1/(60+1) = 0.0164

The 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:

text
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:

text
final_score = CE_normalized × recency_boost × temporal_boost × proof_count_boost

Each boost is centered at 1.0 (neutral) and controlled by an alpha that caps how much it can swing:

text
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:

text
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:

text
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:

text
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:

text
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]
text
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
bash
hindsight-api        # Default port: 8888

The 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.

bash
hindsight-worker     # Default metrics port: 8889

Workers 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:

bash
# 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-2

Each 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:

  1. Starts an embedded PostgreSQL instance on port 5555
  2. Initializes the schema
  3. 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`)


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

bash
curl -fsSL https://hindsight.vectorize.io/get-cli | bash

Configuration

Configure the API URL:

You can also run hindsight configure with no flags to be prompted interactively, or use environment variables (highest priority):

bash
export HINDSIGHT_API_URL=http://localhost:8888
export HINDSIGHT_API_KEY=your-api-key

Named 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):

  1. Environment variables (HINDSIGHT_API_URL, HINDSIGHT_API_KEY)
  2. Named profile — explicit -p <name>, otherwise $HINDSIGHT_PROFILE
  3. Shared config file (~/.hindsight/config, written by hindsight configure)
  4. 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:

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.

:::tip
hindsight 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:

bash
hindsight ui

This 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:

bash
hindsight explore

The 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`)


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:

  1. First command triggers startup: When you run any hindsight-embed command, it checks if a local daemon is running
  2. Auto-daemon management: If no daemon exists, it automatically spawns hindsight-api --daemon in the background
  3. Embedded database: The daemon uses pg0 (embedded PostgreSQL) — no separate database installation required
  4. Command forwarding: Your command is forwarded to the local daemon via HTTP (localhost:8888)
  5. Auto-shutdown: After 5 minutes of inactivity (configurable), the daemon gracefully shuts down to free resources

Key features:

  • Zero setup — One configure command 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):

bash
# Run directly without installation
uvx hindsight-embed@latest configure

# Or use pipx for persistent installation
pipx install hindsight-embed

Quick Start

1. Configure

bash
# 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 configure

Configuration is saved to ~/.hindsight/embed:

bash
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=1

2. Use Memory Operations

bash
# 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:

bash
# 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 --ui

The 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.

bash
# 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 stop

The 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:

bash
# 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-20250514

Daemon 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

bash
# Check daemon status
hindsight-embed daemon status

# View daemon logs in real-time
hindsight-embed daemon logs -f

# Stop daemon manually
hindsight-embed daemon stop

Reclaiming 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.

bash
hindsight-embed daemon stop
uv cache prune
hindsight-embed daemon status   # starts fresh on the current version

Control Center Commands

bash
# 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 stop

Commands

All memory operations follow the same interface as the CLI:

Retain (Store Memory)

bash
hindsight-embed memory retain  "content"

# With context
hindsight-embed memory retain  "content" --context "source information"

# Background processing
hindsight-embed memory retain  "content" --async
bash
hindsight-embed memory recall  "query"

# With budget control
hindsight-embed memory recall  "query" --budget high

# Show trace
hindsight-embed memory recall  "query" --trace

Reflect (Generate Response)

bash
hindsight-embed memory reflect  "prompt"

# With additional context
hindsight-embed memory reflect  "prompt" --context "additional info"

Bank Management

bash
# 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:

bash
hindsight-embed daemon logs
# Or watch in real-time
hindsight-embed daemon logs -f

Common 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:

bash
hindsight-embed daemon status
tail -n 100 ~/.hindsight/daemon.log

Reset Configuration

bash
# Remove config file and reconfigure
rm ~/.hindsight/embed
hindsight-embed configure

When 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`)


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

bash
go get github.com/vectorize-io/hindsight/hindsight-clients/go

Requires 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 operations
  • client.BanksAPI - Bank management
  • client.DirectivesAPI - Directive management
  • client.MentalModelsAPI - Mental model management
  • client.DocumentsAPI - Document operations
  • client.EntitiesAPI - Entity operations
  • client.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:


Hindsight Docs Docs Sdks Hindsight All Npm (`hindsight-docs/docs/sdks/hindsight-all-npm.md`)


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

  1. server.start() resolves the underlying hindsight-embed command (via uvx from PyPI, or uv run --directory <path> for a local checkout).
  2. Runs profile create <name> --merge --port <port> [--env KEY=VALUE ...] with every entry from options.env forwarded as --env.
  3. Runs daemon --profile <name> start.
  4. Polls http://host:port/health until it returns 200 or the readyTimeoutMs budget is exhausted.
  5. server.stop() runs daemon --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 fetch and AbortSignal.timeout.
  • uv / uvx on PATH — used to download and run the Hindsight daemon. Install via docs.astral.sh/uv.

Install

bash
npm install @vectorize-io/hindsight-all @vectorize-io/hindsight-client

Example

ts
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`)


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

bash
pip install hindsight-all

The 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.

python
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.

python
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:

python
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:

python
# ✅ 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 crashed

For 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`)


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

bash
npm install @vectorize-io/hindsight-client

Deno

No installation needed — import directly via the npm: specifier:

typescript
import { HindsightClient } from "npm:@vectorize-io/hindsight-client";

Quick Start

Client Initialization

Core Operations

Version and Feature Checks

Retain (Store Memory)

Retain Batch

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`)


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

bash
pip install hindsight-client

Quick 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 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`)


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

json
{
  "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 in
bank 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