Install
# mix.exs
def deps do
[{:foresight, "~> 0.1", hex: :foresight_memory}]
endThat is the whole dependency line. The hex: option is there because the package is
published as foresight_memory — the plain name belongs to an unrelated library on
hex. Nothing else is affected: the application is :foresight, the modules are
Foresight.*, and you write alias Foresight as you would expect.
Foresight compiles and boots with no configuration at all — you can verify that before writing any config:
{:ok, _pid} = Foresight.Supervisor.start_link(name: :foresight)This is worth stating explicitly because for a long time it was not true. The
HTTP modules referenced plug while declaring it optional, so a bare install
failed with a CompileError, and the default reranker demanded the whole
Nx/Bumblebee stack, so even after compiling it would not start. Both are fixed,
and there is a test in the default suite that fails if either regresses.
With no configuration you get an in-memory-free, LLM-free skeleton: it starts, but it cannot embed, rerank, or store anything. Useful for confirming the wiring and not much else.
A working configuration
Memory needs somewhere to live. Foresight uses PostgreSQL with pgvector.
# config/config.exs
config :foresight,
repo: [enabled: true],
engine: [
embedder: Foresight.Embedders.Bumblebee,
reranker: Foresight.Rerankers.Passthrough,
llm: Foresight.LLMs.LlmCore
]
config :foresight, Foresight.Repo,
url: "postgresql://postgres:postgres@localhost:5432/my_app_memory"# mix.exs — capabilities you enable, you must also depend on
{:bumblebee, "~> 0.7"}, {:nx, "~> 0.12"}, {:exla, "~> 0.12"}, # local embeddings
{:llm_core, "~> 0.5"} # LLM accessThen run the migrations:
mix ecto.create
mix ecto.migrate
The one trap
embedder and reranker live under engine:. Set them at the top level and
nothing happens — silently. No error, no warning; your configuration is simply
ignored and the defaults stand.
config :foresight, embedder: MyEmbedder # ✗ silently ignored
config :foresight, engine: [embedder: MyEmbedder] # ✓The failure surfaces much later and points somewhere else entirely — usually as a
preflight complaint about :nx, because the ignored setting left the default in
place. If a config change appears to have had no effect, check the nesting first.
The preflight is on your side
Enable a capability without its dependency and Foresight refuses to start, by name:
** (RuntimeError) Foresight capability preflight failed:
optional dependency :nx is not available.
Add/fetch the dependency before enabling the related capability.This is deliberate. The alternative is booting successfully and failing on the first request that happens to need embeddings — at which point the error appears somewhere far from its cause.
Your first memories
alias Foresight.Context
ctx = %Context{tenant_id: "default", mode: :mode_a, bank: "notes"}
{:ok, _} = Foresight.put_bank(ctx, "notes", %{"name" => "Notes"})
{:ok, _} =
Foresight.retain(ctx, %{
"items" => [
%{"content" => "Deployed v2.3 on Tuesday. The migration took 40 minutes.",
"context" => "ops log"}
]
})
{:ok, result} = Foresight.recall(ctx, %{"query" => "how long did the migration take?"})A bank is an isolated namespace of memory — one per agent, per user, per project, whatever your unit of "a memory" is. Banks do not share retrieval; searching one never returns another's contents.
The context carries tenant, isolation mode and bank on every call. There is no ambient state and no "current bank": if a call touches memory, it says whose. This is tedious for exactly as long as it takes to appreciate that no request can accidentally read another tenant's data.
Retain is not an insert
retain extracts. One paragraph typically becomes several dated facts, each
independently retrievable, with entities pulled out and linked.
That means it costs an LLM call and takes real time — and it is where quality is
won or lost. When fact extraction was subtly wrong in this library (it dropped
the When: clause from every stored fact), end-to-end answer accuracy sat at
0.467. Fixing extraction alone took it to 0.711. Nothing about retrieval
or reasoning changed.
If you are debugging poor answers, look at what retain actually stored before
you look at anything else. Foresight.list_memories/3 shows you.
Then reflect
{:ok, answer} = Foresight.reflect(ctx, %{"query" => "what happened with v2.3?"})reflect needs an LLM configured. See Reflect for the loop it
runs and how to confirm it did.