# Read Pipeline

When you call `memory.retrieve()`, the SDK searches everything it knows about a user and returns the facts most relevant to your query — ranked, scored, and formatted as a string you can paste directly into an LLM prompt.

**You don't need to understand the internals to use it.** Just call `retrieve()` and use `result.context`. This page explains what happens under the hood for when you want to tune behavior or debug results.

```mermaid
flowchart LR
    A["Query"] --> B["Plan"]
    B --> C["Retrieve\n(3 signals)"]
    C --> D["Enhance"]
    D --> E["Rerank"]
    E --> F["RetrieveResult"]
```

## Overview

Every `memory.retrieve(user_id, query)` call runs five stages:

1. **Plan** — Figures out *what* to search for. Reformulates your query, identifies which people/places/things are relevant.
2. **Retrieve** — Searches for matching facts using three methods in parallel: meaning similarity, keyword matching, and relationship graph traversal.
3. **Enhance** — Expands context by following entity relationships to find related facts that weren't directly matched.
4. **Rerank** — An LLM re-evaluates the top results and reorders them by actual relevance to your query.
5. **Format** — Compresses the ranked facts into a token-budgeted context string, organized by relevance tiers.

---

## Stage 1: Retrieval Agent (Planner)

**In plain English:** Before searching, the pipeline asks an LLM: "What is this person looking for?" The LLM reformulates the query for better search results, identifies which entities (people, places, companies) are relevant, and decides the strategy.

The retrieval agent is an LLM-powered planner that decides **how** to retrieve, not just **what** to retrieve. It analyzes the query and produces a `RetrievalPlan`.

### What the Planner Decides

| Field | Description | Example |
|-------|-------------|---------|
| `strategy` | Retrieval strategy | `"multi_signal"` (default) or `"skip"` (for greetings) |
| `similarity_query` | Reformulated query for semantic search | "user location city" (from "where do I live?") |
| `entities` | Entity keys for graph signal | `["person:ana", "organization:acme"]` |
| `as_of_range` | Time-travel window (optional) | `{"start": "2024-01-01", "end": "2024-06-30"}` |
| `broad_query` | Whether to expand graph scope | `true` for "tell me everything about..." |
| `reason` | Explanation of the strategy | For debugging and observability |

### Query Reformulation

The planner doesn't just pass the user's query to semantic search. It **reformulates** it to improve vector similarity matching:

- `"where do I live?"` → `"user location city residence"`
- `"what does Ana do for work?"` → `"Ana profession occupation job role"`

This bridges the vocabulary gap between how users ask questions and how facts are stored.

### Schema-Aware Planning

The planner inspects the actual memory schema for this user:

- Which entity types exist (persons, organizations, places...)
- Which entities have the most facts
- What attributes are stored

This grounds the plan in reality — the planner won't search for entities that don't exist.

### Hybrid Entity Resolution

When you ask "Where does Carlos live?", the pipeline needs to figure out that "Carlos" means the entity `person:carlos` in the database. It uses three methods to do this — if one fails, the others cover it:


1. **Deterministic resolution (primary)** — Matches words in the query against known entity aliases (`MemoryEntityAlias`), display names (`MemoryEntity.display_name`), and entity_key slugs. Fast (< 10ms), reliable, zero LLM cost. For example, "Onde o Carlos mora?" deterministically resolves to `person:carlos` via slug match.

2. **LLM planner (supplementary)** — The retrieval agent extracts entities from its understanding of the query. Catches indirect references the deterministic layer misses (e.g., "my boss" → `person:kevin`).

3. **Query expansion (alias priming)** — `expand_query()` resolves aliases and fetches 1-hop KG neighbors, adding related entities.

All three sources are **unified** before the graph gate. If any source identifies an entity, the graph traversal runs. This makes the graph signal fault-tolerant: if the LLM fails to extract entities (LLM variance), the deterministic layer covers it.

The trace step `"retrieval"` includes an `entities_sources` breakdown showing which entities came from each source (`llm`, `deterministic`, `expansion`).

### Skip Strategy

For greetings and casual messages ("hi", "how are you?"), the planner returns `strategy: "skip"`, short-circuiting the pipeline. No database queries, no LLM calls, instant response.

!!! info "Neuroscience parallel"
    The retrieval agent mirrors **retrieval cues** in cognitive psychology. When you try to remember something, your brain doesn't do an exhaustive search — it uses contextual cues to narrow down the search space. The planner identifies entities and reformulates queries as cues that guide the retrieval signals.

---

## Stage 2: Multi-Signal Retrieval

**In plain English:** The pipeline searches for relevant facts using three different methods at the same time — like searching by meaning, by exact words, and by connections between entities. This catches facts that any single method alone would miss.

Three independent signals run **in parallel** via `asyncio.gather()`, each finding candidates from a different angle:

```mermaid
flowchart TD
    P["RetrievalPlan"] --> S["Semantic Search\n(pgvector cosine)"]
    P --> K["Keyword Search\n(SQL ILIKE)"]
    P --> G["Graph Traversal\n(BFS 2-hop)"]
    S --> M["Merge & Rank\n(RRF + weighted scoring)"]
    K --> M
    G --> M
```

### Signal 1: Semantic Search

Uses pgvector cosine similarity to find facts whose embeddings are close to the query embedding.

- Embeds the reformulated query (from the planner)
- Searches the `MemoryFact` table with HNSW index
- Returns top-N candidates above `min_similarity` threshold
- Filters: `user_id`, active facts (`valid_to IS NULL`), confidence ≥ `min_confidence`

This is the primary signal — it finds facts that are **semantically similar** to the query, even if they don't share exact keywords.

### Signal 2: Keyword Search

SQL ILIKE matching on `fact_text` for exact or partial keyword hits.

- Extracts significant words (> 2 characters) from the query
- Matches against fact text (up to 5 keywords)
- Score = fraction of query words found in the fact

This complements semantic search by catching exact matches that embedding similarity might miss (e.g., proper nouns, technical terms, abbreviations).

### Signal 3: Graph Retrieval

Traverses entity relationships to find facts connected to the query entities.

- Starts from entities identified by the planner
- BFS traversal up to 2 hops through `MemoryEntityRelationship`
- Facts are fetched via **entity links** (`MemoryFactEntityLink`), not just the primary `entity_key`. This means a fact "Clara left Vertix" (primary subject: Clara) is also found when querying about Vertix — because the fact has a secondary entity link to Vertix.
- Scoring formula: `edge_strength × recency_factor × edge_recency_factor × query_bonus`
- `query_bonus`: 1.5× when the entity name appears in the query text
- **Fallback**: if the entity links table is empty (pre-migration), retrieval falls back to direct `entity_key` matching

Graph retrieval excels at finding **contextual** facts. When you ask about a person, it also finds facts about their workplace, their relationships, and their projects.

### Merge & Rank

After all three signals return, results are merged:

1. **Deduplicate** by fact ID (same fact may appear in multiple signals)
2. **Apply recency decay** — Exponential decay with configurable half-life (`recency_half_life_days`, default 14)
3. **Apply confidence decay** — Older facts with lower confidence are penalized
4. **Compute combined score** — Weighted sum:

!!! warning "Reranker overrides these weights"
    By default, `enable_reranker=True` — the LLM reranker determines the **final** ranking, not these weights. The weights below only affect candidate pre-selection and pre-reranking order. To use these weights for final ranking, set `enable_reranker=False`.

```python
score = (
    score_weights["semantic"]   * semantic_score +    # default 0.70
    score_weights["recency"]    * recency_score +     # default 0.20
    score_weights["importance"] * importance_score     # default 0.10
)
```

### Complete Score Breakdown

Each fact gets scored on multiple dimensions. You can inspect these in `fact.scores` to understand **why** a fact ranked where it did:

| Key | Source | Range | Description |
|-----|--------|-------|-------------|
| `semantic` | Semantic search | 0.0–1.0 | Cosine similarity between query and fact embeddings. Primary retrieval signal. |
| `keyword` | Keyword search | 0.0–1.0 | Fraction of query words found in the fact text. Complements semantic for exact matches. |
| `recency` | Merge & Rank | 0.0–1.0 | Exponential decay from `created_at`, half-life = `recency_half_life_days` (default 14). |
| `importance` | Dynamic importance | 0.05–3.0 | Computed from retrieval frequency, recency of use, user corrections, and pattern membership. Starts at 0.5 for new facts and evolves as the fact is retrieved and confirmed. Requires the background importance job to produce non-default values. |
| `confidence` | Merge & Rank | 0.0–1.0 | Effective confidence after temporal decay. The base confidence is assigned by the LLM during extraction (typically 0.95 for assertive statements). It decays over time and is used as a filter (`min_confidence`) and scoring signal. |
| `reranker` | Reranking | 0.0, 0.3, 0.5, 0.8, 1.0 | LLM-based relevance score. Only present when `enable_reranker=True`. Discrete values assigned by the reranker LLM. |

Additional signals computed during enhancement (not in `score_weights` but affect final score):

| Key | Source | Description |
|-----|--------|-------------|
| `pattern` | Enhancement | Additive boost for recently confirmed facts (up to +0.10). |
| `graph` | Graph traversal | Score from BFS 2-hop entity relationship traversal. |

### Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `topk_facts` | `20` | Maximum facts to return |
| `topk_events` | `8` | Maximum events to consider |
| `min_similarity` | `0.20` | Minimum cosine similarity for semantic results |
| `min_confidence` | `0.55` | Minimum fact confidence |
| `recency_half_life_days` | `14` | Half-life for recency decay |
| `score_weights` | See above | Weights for each scoring signal |
| `enable_reranker` | `True` | Whether to use LLM reranking |

!!! info "Neuroscience parallel"
    Multi-signal retrieval mirrors **spreading activation** in semantic networks (Collins & Loftus, 1975). When you think of "doctor", activation spreads to related concepts ("hospital", "medicine", "appointment") through associative links. Similarly, graph retrieval spreads from query entities along relationship edges, while semantic search activates facts through embedding proximity.

---

## Stage 3: Enhancement

**In plain English:** After finding the initial results, the pipeline follows connections to discover related facts. If you ask about a person, it might also pull in facts about their workplace, projects, or team — things you didn't directly ask about but that add useful context.

### Spreading Activation

Starting from the top-K seed facts, the pipeline expands context by following entity relationships:

- For each seed fact, find its entity's relationships
- Traverse relationships for N hops (`spreading_activation_hops`, default 2). Set to `0` to disable spreading entirely.
- Apply decay factor per hop (`spreading_decay_factor`, default 0.50). Hop 1 uses the factor directly; Hop 2 uses the factor squared (compounded decay).
- Return up to `spreading_facts_per_entity` additional facts per entity (default 3), applied in both Hop 1 and Hop 2.

This catches important context that wasn't directly matched. If you ask "what does Rafael do?", spreading activation might surface facts about his workplace, team, and projects.

!!! tip "When does spreading activation matter?"
    Spreading has the most impact with **20+ entities** and cross-domain relationships (e.g., people → projects → clients → technologies). With small datasets (< 15 entities), the semantic, keyword, and graph signals already cover the full fact space — spreading may return candidates but they'll be deduplicated against existing results. The trace fields `spreading_candidates_returned` and `spreading_candidates_unique` let you confirm whether spreading is contributing new facts for your dataset.

### Pattern Signal

Facts that have been recently confirmed (NOOP decisions in write update `last_confirmed_at`) get an additive score boost:

- Recently confirmed facts → up to 0.10 extra score
- Captures frequently mentioned, well-established facts

### Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `spreading_activation_hops` | `2` | Maximum hops from seed facts. Set to `0` to disable spreading. |
| `spreading_decay_factor` | `0.50` | Score decay per hop. Hop 1 = factor, Hop 2 = factor² |
| `spreading_facts_per_entity` | `3` | Max facts fetched per entity in both Hop 1 and Hop 2 |
| `spreading_max_related_entities` | `5` | Max KG-related entities to explore in Hop 1 |

---

## Stage 4: Reranking (Optional)

**In plain English:** The previous stages find relevant facts, but their ranking is based on math (similarity scores, keyword overlap). The reranker asks an LLM: "Given what this person is asking, which of these facts are actually most useful?" This produces a smarter final ranking.

When `enable_reranker=True`, the top candidates are reranked by an LLM that considers query intent:

- Respects the semantic meaning of the query (not just keyword overlap)
- Can promote facts that are indirectly relevant but important
- Graceful degradation: if the reranker fails or exceeds `reranker_timeout_sec` (default 5.0s), the original ranking is preserved
- Timeout is enforced via `asyncio.wait_for` — the LLM call is cancelled if it exceeds the configured timeout
- Uses a separate `reranker_llm` provider if configured (typically a cheaper model like gpt-4o-mini)

The reranker is the most expensive stage but provides the highest quality improvement for complex queries.

!!! info "Weighted blend scoring"
    The reranker does NOT replace the formula score. It **blends** with it:

    `final_score = (1 - reranker_weight) × formula_score + reranker_weight × reranker_score`

    With the default `reranker_weight=0.60`, the reranker has strong influence but cannot zero out a fact with good retrieval signals. A fact with formula=0.9 and reranker=0.0 gets final=0.36 (not 0.0). The `scores` dict preserves both `formula` (pre-reranker) and `reranker` (LLM score) for debugging.

---

## Stage 5: Formatting

**In plain English:** The pipeline takes the ranked facts and organizes them into a ready-to-use string for your LLM prompt. The most important facts go first (CORE MEMORY), supporting facts next (EXTENDED CONTEXT), and background history last (RELEVANT_EVENTS) — all within a token budget so you don't blow up your prompt.

### Context Compression

Facts are divided into three tiers within a token budget (`context_max_tokens`):

!!! note "`context_max_tokens` is a proportional budget, not a hard cap"
    The `context_max_tokens` parameter controls the **relative** size of the output context, but the actual token count may exceed the configured value. The pipeline guarantees a minimum context for core facts and uses the parameter as a proportional budget across tiers. Treat it as a target, not a strict limit. For example, setting `context_max_tokens=100` may produce ~240 tokens due to minimum guarantees for the hot tier.

| Tier | Config Key | Output Label | Budget Share | Content |
|------|-----------|-------------|-------------|---------|
| **Hot** | `hot_tier_ratio` | `CORE MEMORY` | 50% | Most relevant facts (highest scores) |
| **Warm** | `warm_tier_ratio` | `EXTENDED CONTEXT` | 30% | Supporting context |
| **Cold** | (remainder) | `RELEVANT_EVENTS` | 20% | Background facts and event history |

### Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `context_max_tokens` | `2000` | Maximum tokens in formatted context |
| `hot_tier_ratio` | `0.50` | Share of budget for top facts |
| `warm_tier_ratio` | `0.30` | Share of budget for supporting facts |

### Output Format

The `context` string is formatted for direct injection into LLM prompts:

```
## Known facts about the user:
- Lives in São Paulo (confidence: 0.95)
- Works at Acme Corp as a backend engineer (confidence: 0.90)
- Wife's name is Ana (confidence: 0.92)
```

---

## RetrieveResult

```python
result = await memory.retrieve(user_id="user_123", query="...")

# Pre-formatted context (ready for LLM prompts)
print(result.context)

# Individual facts with scores
for fact in result.facts:
    print(f"[{fact.score:.2f}] {fact.entity_name}: {fact.value}")
    print(f"  Scores: {fact.scores}")  # {"semantic": 0.85, "recency": 0.72, ...}

# Pipeline stats
print(f"Candidates evaluated: {result.total_candidates}")
print(f"Duration: {result.duration_ms:.0f}ms")
```

---

## Pipeline Diagram (Complete)

```mermaid
flowchart TD
    Q["User query"] --> AG["Retrieval Agent\n(LLM planner)"]
    AG -->|skip| SKIP["Return empty\n(greeting/casual)"]
    AG -->|multi_signal| PAR["Parallel retrieval"]
    PAR --> SEM["Semantic Search\n(pgvector cosine)"]
    PAR --> KW["Keyword Search\n(SQL ILIKE)"]
    PAR --> GR["Graph Traversal\n(BFS 2-hop)"]
    SEM --> MERGE["Merge & Rank\n(dedup + weighted scoring)"]
    KW --> MERGE
    GR --> MERGE
    MERGE --> SA["Spreading Activation\n(expand context along edges)"]
    SA --> RR{"Reranker\nenabled?"}
    RR -->|yes| RERANK["LLM Rerank"]
    RR -->|no| FMT["Format & Compress"]
    RERANK --> FMT
    FMT --> RES["RetrieveResult"]
```

!!! info "Neuroscience parallel"
    The tiered compression (hot/warm/cold) mirrors **levels of activation** in working memory. In Cowan's embedded-process model, a small number of items are in the focus of attention (hot tier), surrounded by activated long-term memory (warm tier), with the rest of long-term memory available but not active (cold tier). The token budget acts as the capacity limit of working memory.
