Back to Theory
Tutorial16 min read · May 15, 2026

How to Build a Living Context Engine in Python (Step-by-Step, 2026)

A working Living Context Engine in 50 lines of Python. This tutorial walks through every architectural primitive — adaptive scoring, typed edges, two-phase retrieval, write-back — with runnable code and a real example workflow.

F
Feather DB Engineering
Engineering Team

How to Build a Living Context Engine in Python (Step-by-Step, 2026)

Tutorial · Python 3.10+ · Updated May 2026


What You'll Build

By the end of this tutorial you'll have a working Living Context Engine in Python — composite scoring, typed edges, two-phase retrieval, closed feedback loop, all of it. The code is runnable, the architecture is real, and the example workflow at the end exercises every phase of the engine.

We'll use Feather DB as the underlying engine (it has the fused vector+graph+decay kernel) and wire the orchestration layer in plain Python.

Prerequisites

pip install feather-db numpy

You'll also want an embedding function. For this tutorial we'll use a stub embed() that returns a random 768-dim vector — substitute in your real model (OpenAI, Gemini, sentence-transformers).

Step 1: Open the Store

from feather_db import DB
import numpy as np
import time
import math

db = DB.open("agent.feather", dim=768)

The file agent.feather is your engine's persistent state. It contains the HNSW index, the graph, and the decay metadata. It's portable — copy it anywhere and the agent's memory comes with it.

Step 2: Define the Node Insertion Helper

Every context node carries decay state. Wrap insertions in a helper that stamps the timestamp and initializes counters.

def add_node(db, text, modality="text", importance=1.0, half_life=90):
    vec = embed(text)
    node_id = db.next_id()
    db.add(
        id=node_id,
        vec=vec,
        modality=modality,
        metadata={
            "text": text,
            "inserted_at": int(time.time()),
            "recall_count": 0,
            "importance": importance,
            "half_life_days": half_life,
        },
    )
    return node_id

Step 3: Define the Composite Scoring Function

This is the kernel of "living". It turns a similarity score into a composite score that respects time, recall, and importance.

def composite_score(similarity, meta, time_weight=0.3, now=None):
    now = now or int(time.time())
    age_days = (now - meta["inserted_at"]) / 86400
    stickiness = 1 + math.log(1 + meta["recall_count"])
    effective_age = age_days / stickiness
    half_life = meta.get("half_life_days", 90)
    recency = 0.5 ** (effective_age / half_life)
    return (
        (1 - time_weight) * similarity
        + time_weight * recency
    ) * meta["importance"]

Step 4: Two-Phase Retrieval (read)

Phase 1: ANN search for seeds. Phase 2: traverse typed edges from each seed, scoring each hop.

def read_context(db, query_text, k=5, hops=2, edge_types=None):
    query_vec = embed(query_text)

    # Phase 1 — ANN seeds
    raw_seeds = db.search(query_vec, k=k * 2)
    seeds = []
    for sid, sim in raw_seeds:
        meta = db.get_metadata(sid)
        score = composite_score(sim, meta)
        seeds.append((sid, score, meta, 0))

    seeds.sort(key=lambda x: -x[1])
    seeds = seeds[:k]

    # Phase 2 — bounded BFS on typed edges
    visited = {sid for sid, *_ in seeds}
    frontier = list(seeds)
    results = list(seeds)

    for hop in range(1, hops + 1):
        next_frontier = []
        for sid, _, _, _ in frontier:
            for nid, edge_type in db.neighbors(sid, types=edge_types):
                if nid in visited:
                    continue
                visited.add(nid)
                nmeta = db.get_metadata(nid)
                nvec = db.get_vector(nid)
                sim = float(np.dot(query_vec, nvec))
                score = composite_score(sim, nmeta) * (0.8 ** hop)
                results.append((nid, score, nmeta, hop))
                next_frontier.append((nid, score, nmeta, hop))
        frontier = next_frontier

    results.sort(key=lambda x: -x[1])
    return results

The 0.8 ** hop factor is a hop penalty — neighbors two edges away count less than direct neighbors. Tune to taste.

Step 5: Reason (call the LLM)

Format the retrieved subgraph as a context block; preserve the graph structure in the prompt.

def format_context(results):
    by_hop = {}
    for nid, score, meta, hop in results:
        by_hop.setdefault(hop, []).append((nid, score, meta))

    lines = []
    for hop in sorted(by_hop):
        label = "Directly relevant" if hop == 0 else f"Connected (hop {hop})"
        lines.append(f"\n## {label}")
        for nid, score, meta in by_hop[hop]:
            lines.append(f"- [{score:.3f}] {meta['text']}")
    return "\n".join(lines)

def reason(llm, query, results):
    ctx = format_context(results)
    prompt = f"Context:\n{ctx}\n\nQuestion: {query}\nAnswer:"
    return llm.generate(prompt)

Step 6: Write Back (close the loop)

Persist the agent's output as a new node with edges to the inputs.

def write_back(db, output_text, input_ids, edge_type="derived_from"):
    out_id = add_node(db, output_text, modality="text")
    for src_id in input_ids:
        db.link(src_id, out_id, edge_type=edge_type)
        # bump recall_count on inputs that contributed
        meta = db.get_metadata(src_id)
        meta["recall_count"] += 1
        db.update_metadata(src_id, meta)
    db.save()
    return out_id

Step 7: Apply Decay (silent + signal-driven)

Time-based decay happens automatically inside composite_score. Signal-driven adjustments are explicit:

def reinforce(db, node_ids, signal_strength=1.0):
    for nid in node_ids:
        meta = db.get_metadata(nid)
        meta["importance"] = min(3.0, meta["importance"] + 0.1 * signal_strength)
        meta["recall_count"] += int(signal_strength)
        db.update_metadata(nid, meta)
    db.save()

The Full Loop in One Function

def run_loop(db, llm, query, edge_types=None, signal_fn=None):
    # 1. Read
    results = read_context(db, query, k=5, hops=2, edge_types=edge_types)
    input_ids = [r[0] for r in results]

    # 2. Reason
    output = reason(llm, query, results)

    # 3. Update
    out_id = write_back(db, output, input_ids)

    # 4. Decay — signal capture if a feedback function is given
    if signal_fn is not None:
        strength = signal_fn(output)
        reinforce(db, input_ids, signal_strength=strength)
        reinforce(db, [out_id], signal_strength=strength)

    return output, out_id

End-to-End Example

db = DB.open("marketing.feather", dim=768)

# Seed the store
brief_id  = add_node(db, "Q3 brand-x campaign brief: emphasize sustainability", importance=2.0)
brand_id  = add_node(db, "brand-x guidelines: warm tone, professional voice")
db.link(brief_id, brand_id, edge_type="references")

# Run the loop
output, out_id = run_loop(
    db, llm,
    query="draft a launch headline for the brand-x campaign",
    edge_types=["references", "responds_to", "derived_from"],
    signal_fn=lambda o: 1.5 if "sustain" in o.lower() else 1.0,
)

print(output)

Each subsequent call benefits from the previous one. The output node persists with edges to brief and brand. Future drafts retrieve the context graph, see the previous draft, and write decisions that build on it.

What This Buys You

You now have a Living Context Engine running locally in Python:

  • Adaptive scoring is applied to every retrieval automatically.
  • The graph topology densifies as the system runs.
  • Every agent output becomes context for future calls.
  • Decay suppresses stale entries automatically; importance multipliers protect cross-cutting material.

The full source for a more polished version of this tutorial is in the Feather DB cookbook. For production deployment patterns — sharding, multi-agent isolation, observability — see the documentation.


Related: Closing the Loop in Feather DB · The Four Phases Explained.