Concepts

Memory Tools

Two-layer tool architecture — memory subagent for most agents, raw file tools for direct control.

MemexAI exposes two layers of tooling. The memory subagent (memory_remember + memory_context) is the right default for assistants and copilots — the model stores and retrieves memory without managing paths. The raw file tools give you direct file-level control when your application needs deterministic writes or has a file plan.

┌─────────────────────────────────────────────────────┐
│              Memory subagent (default)               │
│  memory_remember          memory_context             │
│  (LLM decides what/       (ranked retrieval +        │
│   where to write)          link expansion)           │
└────────────────┬──────────────────┬─────────────────┘
                 │                  │
┌────────────────▼──────────────────▼─────────────────┐
│              Raw file tools                          │
│  memory_list   memory_read   memory_write            │
│  memory_patch  memory_find                           │
└─────────────────────────────────────────────────────┘

              ┌──────────▼──────────┐
              │  Postgres (mx_file) │
              │  BM25 · pgvector    │
              │  revisions · audit  │
              └─────────────────────┘

memory_find is the search primitive in the raw layer — it returns ranked file metadata and snippets and is also what memory_context calls internally during retrieval.


memory_remember

Takes raw text — conversation snippets, user statements, observations — and autonomously decides what durable facts to write and where to store them.

How it works

An LLM reads the incoming text alongside existing memory files, extracts stable facts (ignoring transient conversational noise), and issues a minimal set of memory_write or memory_patch operations. It avoids duplicate writes by reading what is already stored before deciding.

Every write produces a revision snapshot and an access log row, linking back to the originating tool call ID when available.

When to use

Use memory_remember after a completed user turn or at natural breakpoints in a conversation — not on every message. Calling it too frequently creates noise and near-duplicate memory. A good pattern:

// Most common: user message after a completed turn
await memory.remember({ text: userMessage })

// Tool result: pass the durable fragment, not a raw dump
await memory.remember({ text: 'run_sql failed: ILIKE is not supported in this SQL dialect' })

// Assistant observation or stated fact
await memory.remember({ text: assistantResponse })

Pass dryRun: true to preview what would be written without committing:

{ "text": "I prefer 2BHK in Indiranagar, budget 1.2 crore", "dryRun": true }
{
  "writes": [
    { "path": "user/preferences.md", "operation": "patch", "reason": "Added apartment preferences", "dryRun": true }
  ]
}

Write routing

memory_remember always pre-fetches two files before writing: user/index.md and shared/index.md. Everything else is on demand. This means: to reliably route a fact to the right file, put the routing table in shared/index.md — that file is guaranteed to be read before every write decision.

The agent also receives the full list of visible files and has a small read budget (3 reads) to fetch any additional files it deems relevant. You can place a routing table in any shared/ file — for example shared/conventions.md — but for the agent to reliably read it, cite it in shared/index.md. A reference like - shared/conventions.md — file routing rules tells the agent the file exists and is worth reading before writing.

## Memory File Structure
| File                | What goes here                              |
|---------------------|---------------------------------------------|
| user/preferences.md | Apartment type, locations, floor, budget    |
| user/family.md      | Family members, relationships, pets         |
| user/health.md      | Allergies, constraints                      |
| user/visits.md      | Property visits, prices, observations       |

For routing guidance that should be reliably followed, put it in shared/index.md — that file is always pre-fetched before any write decision.

Routing systemic insights to shared/

Facts that benefit every user — tool dialect quirks, API limitations, product conventions — belong in shared/ rather than user/ when the deployment is trusted. Enable this with MEMEX_SHARED_WRITE_MODE=rw on the service, then add a routing table in shared/index.md:

## Memory file structure
| File | What goes here |
|------|----------------|
| user/preferences.md | User preferences and private stated facts |
| shared/tool-quirks.md | SQL dialect errors, API limitations, unsupported syntax |

With shared writable mode on, memory_remember can route a systemic fact to shared/tool-quirks.md instead of a user file. Shared writable mode is off by default; only enable it for trusted agents. See Memory Scopes for the full trust model.


memory_context

Assembles a context block — a formatted, character-budgeted digest of the most relevant memory files — ready to inject directly into a system prompt.

How it works

memory_context is an agentic ReAct loop. It spins up a model call with three tools — memory_list, memory_find, and memory_read — and lets the model decide which files to fetch before assembling the final context block.

memory_context("apartment requirements and family")

  ▼  LLM step 1
  ├── memory_read("user/index.md")           ← catalog of all user files

  ▼  LLM step 2
  ├── memory_find("apartment preferences family")
  │     BM25 + optional pgvector hybrid      ← see memory_find for details
  │     → [user/preferences.md, user/family.md, user/visits.md]

  ▼  LLM step 3
  ├── memory_read("user/preferences.md")
  ├── memory_read("user/family.md")

  ▼  LLM final step
  └── text response  →  context block returned to caller

The system prompt instructs the model to:

  1. Start with user/index.md as a catalog of all available files
  2. Use memory_find to rank candidates by relevance (see memory_find for how search works)
  3. Use memory_read to pull full content for files worth including
  4. Fall back to memory_list only when the index doesn't give enough signal
  5. For broad queries (full profile, summary), read every file referenced in the index

The loop is capped at 12 steps. The model's final text response becomes the returned context field.

Why agentic instead of deterministic?

The model can reason about which files matter in ways pure ranking cannot. A query about "apartment requirements" might need user/health.md (dust allergy → ventilation) even if neither word appears in the query. The model reads user/index.md, sees a health file exists, and decides whether to include it.

memory_remember is also prompted to write ## See also sections with [[user/related.md]] wikilinks when creating related files. The retrieval agent uses these as signals — seeing [[user/preferences.md]] in a health file is a hint that preferences are relevant to the same query.

Example

{ "query": "apartment requirements and family" }
{
  "context": "## user/preferences.md\n(updated 2025-06-08)\n\n# Preferences\n- 2BHK in Indiranagar or Koramangala\n- Budget: 1.2 crore\n- Floor 4+\n\n## user/family.md\n(updated 2025-06-08)\n\n# Family\n- Wife: Meera (dust allergy)\n- Pet: Max (golden retriever)",
  "filesRead": ["user/index.md", "user/preferences.md", "user/family.md"]
}

Parameters

FieldTypeDefaultDescription
querystringWhat to retrieve. Passed to the model as the retrieval goal.
maxCharsnumber24 000Character budget hint passed to the model.

Raw file tools

Raw tools expose the file layer directly. Use them when your application has a file plan, needs deterministic writes, or is building tooling on top of MemexAI.

const tools = memory.createRawToolset()

memory_list

Lists all files visible to the current user — their own user/ files plus the shared/ namespace.

{}
{
  "files": [
    { "path": "user/preferences.md", "updatedAt": "2025-06-08T10:00:00Z" },
    { "path": "user/family.md",      "updatedAt": "2025-06-08T10:01:00Z" },
    { "path": "shared/index.md",     "updatedAt": "2025-06-01T09:00:00Z" }
  ]
}

memory_read

Reads a single file by its virtual path.

{ "path": "user/preferences.md" }
{
  "path": "user/preferences.md",
  "content": "# Preferences\n- 2BHK in Indiranagar or Koramangala\n- Budget: 1.2 crore\n- Floor 4+",
  "updatedAt": "2025-06-08T10:00:00Z"
}

memory_write and memory_patch

memory_write creates or fully overwrites a file. Use it when you are constructing a file from scratch or want a clean replacement.

memory_patch makes targeted edits without rewriting the full file:

  • Append under a heading — add a new bullet point under ## Visits without touching the rest of the file.
  • Replace exact text — swap a specific substring in place.

Prefer memory_patch when updating a section of a larger file. It produces a smaller revision diff and reduces the risk of accidentally losing adjacent content.

{
  "path": "user/visits.md",
  "operation": "append_under_heading",
  "heading": "## Visited",
  "content": "- Prestige Tranquility — 1.45 Cr, above budget, good light"
}

memory_find

Searches memory by keyword or semantic query and returns ranked file metadata and snippets. This is the search primitive — it does not synthesize an answer or expand links. Use memory_context when you want a ready-to-inject context block; use memory_find when you need ranked results to drive your own logic.

Search modes

Query string

    ├── BM25 (always)
    │     Postgres tsvector GIN index
    │     Fast, no API call needed
    │     matchReason: "lexical"

    └── pgvector (when hybrid mode is configured)
          Gemini text-embedding-004 (or configured provider)
          Cosine similarity over stored embeddings
          matchReason: "semantic"


          Reciprocal Rank Fusion
          score = Σ 1 / (k + rank_i)   where k = 60
          matchReason: "hybrid"

Hybrid mode activates automatically when GEMINI_API_KEY is set on the service and MEMEX_SEARCH_MODE is auto (the default). Embeddings are stored at write time. If an embedding could not be stored (for example, due to a transient rate-limit), that file falls back to BM25-only ranking for that query.

Reciprocal Rank Fusion

RRF merges two independent ranked lists without needing their scores to be on the same scale. For each candidate file, its RRF score is the sum of 1 / (k + rank) across both lists, where k = 60 is a stability constant. A file ranked #1 by BM25 and #3 by vector gets a higher combined score than a file that appears in only one list. This rewards files that are relevant by both signal types.

BM25 results          Vector results
  #1 user/prefs.md       #1 user/family.md
  #2 user/family.md      #2 user/prefs.md
  #3 user/health.md      #3 user/visits.md

RRF scores:
  user/prefs.md   = 1/(60+1) + 1/(60+2) = 0.0164 + 0.0161 = 0.0325  ← highest
  user/family.md  = 1/(60+2) + 1/(60+1) = 0.0161 + 0.0164 = 0.0325
  user/health.md  = 1/(60+3) + 0        = 0.0159
  user/visits.md  = 0        + 1/(60+3) = 0.0159

memory_find is a pure search primitive — it returns what Postgres ranked, nothing more. Following [[...]] wikilinks embedded in file content is deliberate reasoning: it requires reading a file, parsing its links, and deciding whether those targets are relevant. That reasoning happens in memory_context, where the LLM can make that judgment call. Keeping them separate means you can use memory_find results to drive your own retrieval logic without an LLM in the loop.

Example

{ "query": "apartment budget and location", "limit": 5 }
{
  "query": "apartment budget and location",
  "results": [
    {
      "path": "user/preferences.md",
      "snippet": "Budget: 1.2 crore. Preferred: Indiranagar or Koramangala.",
      "rank": 0.91,
      "matchReason": "hybrid"
    },
    {
      "path": "user/visits.md",
      "snippet": "Prestige Tranquility — 1.45 Cr, above budget",
      "rank": 0.74,
      "matchReason": "lexical"
    }
  ],
  "truncated": false
}

Parameters

FieldTypeDefaultDescription
querystringrequiredKeyword or semantic query.
limitnumber10Max number of results to return.
prefixstringFilter by virtual path prefix, e.g. user/.

Which tool set to use

SituationUse
Conversational assistant that should remember preferencesmemory_remember + memory_context
Application with a known file plan, deterministic writesRaw file tools
Building a custom retrieval pipeline on top of MemexAImemory_find + memory_read
Debugging what a model storedAdmin console or memory_list + memory_read
Checking what a model would write before committingmemory_remember with dryRun: true

Start with the subagent. Add raw file tools only when your use case requires direct file control.

// Typical assistant setup
const system = await memory.getSystemPrompt('You are a helpful assistant with durable user memory.')
const tools = memory.createMemorySubagentToolset()
// → exposes memory_remember and memory_context to the model

Extraction patterns

memory_remember is a primitive. The question of when to call it and what text to pass belongs to your application. MemexAI does not watch tool results or conversation history by itself.

Hot path vs background path

Both are valid:

PathUse whenTradeoff
Hot pathThe current answer depends on saved memory, or the user expects an immediate durable saveMore latency, simpler correctness
Background pathLatency matters, or you want batching, dedupe, review, or a cheaper extractorMemory may affect the next turn, not the current one

Do not treat background extraction as the only architecture. A direct await memory.remember(...) is right when durability is part of the current interaction. A queued job is right when learning can happen after the response.

Post-turn user input

The simplest pattern is to run memory after a completed turn:

const result = await generateText({ model, system, prompt: userMessage, tools, stopWhen: stepCountIs(5) })

// Hot path: await when the user explicitly asked to save something.
await memory.remember({ text: userMessage })

// Background path: enqueue or fire-and-forget when latency matters.
memory.remember({ text: userMessage }).catch(console.error)

Post-tool-result extraction

Tool results are often noisy. Extract a compact durable fact first, then pass that to MemexAI:

for (const step of result.steps) {
  for (const toolResult of step.toolResults ?? []) {
    if (!isPermanentError(toolResult.result)) continue

    const insight = await extractInsight(toolResult) // app-owned policy
    if (insight) {
      await memory.remember({ text: insight })
    }
  }
}

The extractor can be a cheap model call, deterministic code, or a human-reviewed queue. The important part is to avoid passing raw logs, transient failures, secrets, or routine success payloads.

Framework-owned extraction points

Use the hook that matches the framework:

FrameworkRecommended extraction point
Vercel AI SDKafter result.steps or stream step events
OpenAI / Anthropic SDKinside the manual tool-call loop after app tool execution
LangChain / LangGraphcallback, runnable wrapper, or graph node after accepted turns/tool nodes
LlamaIndexpost-response hook or workflow step
CrewAItask-output extraction after kickoff()
Google ADKnative memory service lifecycle via MemexAdkMemoryService

When to trigger extraction

TriggerWorth remembering?
Tool failed with a permanent errorYes — dialect quirk, API limitation, unsupported feature
Tool failed transientlyNo — timeout, rate limit, or network blip
Tool succeeded with a surprising reusable constraintSometimes
User stated a preference explicitlyYes
Assistant inferred a stable factSometimes, preferably after confirmation
Routine turn with no new informationNo

For systemic insights that should help every user, route the extracted text to shared/ through shared/index.md and MEMEX_SHARED_WRITE_MODE=rw.


Tool calls and auditability

Every tool call produces an access log row in mx_access_log. Every write or patch also produces a revision snapshot in mx_revision, making it possible to time-travel back to any prior state of a memory file.

When your framework provides tool call IDs (Vercel AI SDK, Anthropic SDK, LangChain), pass them through adapter handlers — the revision rows link back to the originating model tool call.


On this page