---
title: 'Long-Term Memory'
source: 'https://academia.sh/en/courses/ai-agents/long-term-memory'
course: 'AI Agents'
language: en
updated: '2026-10-21T05:02:27+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines long-term memory not as a retrieval job but as a write decision, and builds three write policies with the standard library.'
    - 'Measures, for each policy, the number of records stored next to the steps saved in later tasks.'
    - 'Ties recall hits to the write unit by counting how many fields a read returns and how many steps it spares.'
    - 'Tests with the run-to-run difference why the policy that saves the most steps gives the most wrong answers.'
---

# Long-Term Memory

Measuring three write policies on the same second set of 48 tasks: making the observation of every step a separate record accumulates 74 records and saves 44 steps, writing a single answer record per task saves 199 steps with 24 records but drops task success from 0.9167 to 0.5833, and the selective policy, which merges the observations in the plan into a single record, saves 118 steps, again with 24 records. What pays off is not the number of records but the unit of the record.

Short-term memory is bounded by the window, but the real loss is not at the bound: **the moment the
loop ends, that entire context is erased.** The agent reads the meter, computes the index difference,
applies the tariff, and gives the answer. When the same subscriber comes back with another question,
the meter is read **again**.

Long-term memory fills this gap: a set of records that lives independently of the loop and that later
tasks can read. The question here is not **what will be brought back**. Chunking, ranking, and retrieval
hits were measured in the Embeddings and Retrieval-Augmented Generation course and are not repeated.
The question here is **what will be written**: three write policies are run with the same recall
mechanism, and for each one the number of stored records is printed next to the steps saved in later
tasks.

- **TM61.** The code is a **simulator**; no real model endpoint, tool server, or memory service is
  called. The environment, the tools, and the tasks are **fictional**, seed **20260218**.
- **TM62.** Memory is a **list of records**; each record is a searchable text and its fields.
  Observation and answer records are keyed by the **subscriber code**, document records by the
  **question text**.
- **TM63. Recall is a single read:** the record that shares the most tokens with the request is
  returned, and on a tie the one written last.
- **TM64.** Three write policies: **every step** (the observation of every step is a separate record),
  **result only** (a single answer record per task), **selective** (the observations that enter the
  steps of the plan are **merged into a single record**).
- **TM65.** Recalled fields are written into the initial state and a step whose output is known is
  **skipped**; if the plan is fully covered from memory, the loop never starts.
- **TM66.** Records are written from the **first set of 24 tasks**; the measurement is made on **48 new
  questions asked to the same subscribers**. The step budget is 8, and the baseline is the **loop
  without memory**.

## What Is Left When the Loop Ends

The block below sets up the shared definition and asks a single subscriber two separate questions. The tool and field names in the code are the `read_meter`, `consumption`, `tariff`, `search_docs`, and `write_record` tools of the previous lessons; they are counted in tokens here, so they stay Turkish.

```python
# FICTIONAL environment and FICTIONAL agent. No real model endpoint, tool server, or memory
# service is called. The subscriber records, documents, and tasks are fictional, seed 20260218.
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


def tokenize(m):
    b = []
    for s in str(m).lower().replace(",", " , ").split():
        while len(s) > 5:
            b.append(s[:5])
            s = s[5:]
        b.append(s)
    return b


def trace_id(m):
    h = 2166136261
    for c in str(m):
        h = ((h ^ ord(c)) * 16777619) & M32
    return h


# ---- FICTIONAL environment: records of the measurement network
# wire words stay Turkish: they are counted in tokens (same rule as the MCP course)
# onceki/simdiki/tip/bolge = previous/current/type/region; mesken/isyeri = residential/commercial
REGION = ["dogu", "bati", "kuzey", "guney", "merkez"]
TYPE = ["mesken", "isyeri"]
SUBSCRIBERS = {}
for i in range(60):
    r = rng(SEED + 31 * i)
    previous = 10000 + int(r() * 80000)
    SUBSCRIBERS[f"A{i + 1:03d}"] = {"bolge": REGION[int(r() * 5)], "tip": TYPE[int(r() * 2)],
                                    "onceki": previous, "simdiki": previous + int(r() * 70)}
TIERS, ABOVE = ((10, 4), (25, 7), (40, 11)), 16
COMMERCIAL_FACTOR = 2
DOCUMENTS = {"TR01": "tarife basamaklari 10 25 40 metrekup ve ustu",
             "TR02": "isyeri abonesinde birim ucret iki katidir",
             "SY01": "endeks farki tuketimi verir sayac donusu 100000 eklenir",
             "AR01": "ariza kaydi acildiginda saha ekibi iki gun icinde gider",
             "KS01": "planli kesinti duyurusu bir hafta once yayimlanir"}

CALLS = [0]                              # tool call counter


def _charge(m3, type_):
    remaining, lower, total = m3, 0, 0
    for upper, unit in TIERS:
        band = min(remaining, upper - lower)
        if band <= 0:
            break
        total += band * unit
        remaining -= band
        lower = upper
    total += remaining * ABOVE
    return total * (COMMERCIAL_FACTOR if type_ == "isyeri" else 1)


def t_read_meter(abone=None, **_):
    a = SUBSCRIBERS.get(abone)
    if a is None:
        return {"hata": "abone yok"}
    return {"onceki": a["onceki"], "simdiki": a["simdiki"], "tip": a["tip"],
            "bolge": a["bolge"]}


def t_consumption(onceki=None, simdiki=None, **_):
    if onceki is None or simdiki is None:
        return {"hata": "eksik alan"}
    f = simdiki - onceki
    return {"m3": f + 100000 if f < 0 else f}


def t_tariff(m3=None, tip="mesken", **_):
    if m3 is None:
        return {"hata": "eksik alan"}
    return {"tutar": _charge(m3, tip)}


def t_document(anahtar=None, **_):
    if not anahtar:
        return {"hata": "eksik alan"}
    q = set(tokenize(anahtar))
    best, code = -1, None
    for k, m in DOCUMENTS.items():
        o = len(q & set(tokenize(m)))
        if o > best:
            best, code = o, k
    return {"kod": code, "metin": DOCUMENTS[code]} if best > 0 else {"hata": "bulunamadi"}


def t_record(abone=None, sutun=None, deger=None, **_):
    if not (abone and sutun and deger is not None):
        return {"hata": "eksik alan"}
    return {"yazildi": True, "abone": abone, "sutun": sutun, "deger": deger}


TOOLS = {
    "sayac_oku":  {"function": t_read_meter, "fields": ("abone",), "side_effect": False},
    "tuketim":    {"function": t_consumption, "fields": ("onceki", "simdiki"),
                      "side_effect": False},
    "tarife":     {"function": t_tariff, "fields": ("m3", "tip"), "side_effect": False},
    "belge_ara":  {"function": t_document, "fields": ("anahtar",), "side_effect": False},
    "kayit_yaz":  {"function": t_record, "fields": ("abone", "sutun", "deger"),
                      "side_effect": True},
}
TOOL_NAMES = list(TOOLS)


def call_tool(name, state):
    """Fields outside the schema are filtered out; a missing field is the tool's own error."""
    CALLS[0] += 1
    if name not in TOOLS:
        return {"hata": "arac yok"}
    kw = {a: state[a] for a in TOOLS[name]["fields"] if a in state}
    return TOOLS[name]["function"](**kw)


# ---- FICTIONAL tasks: each one is solved by a known tool sequence
TASKS = []
for i in range(24):
    r = rng(SEED + 9973 * i)
    sub = f"A{1 + int(r() * 60):03d}"
    t = pick(r(), [0.30, 0.22, 0.20, 0.28])
    if t == 0:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "charge", "subscriber": sub,
                      "request": f"{sub} abonesinin bu donem bedeli nedir",
                      "sequence": ("sayac_oku", "tuketim", "tarife")})
    elif t == 1:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "consumption", "subscriber": sub,
                      "request": f"{sub} abonesinin bu donem tuketimi kac metrekup",
                      "sequence": ("sayac_oku", "tuketim")})
    elif t == 2:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "document", "subscriber": sub,
                      "request": "sayac donusunde endeks farki nasil hesaplanir",
                      "sequence": ("belge_ara",)})
    else:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "record", "subscriber": sub,
                      "request": f"{sub} abonesinin bedelini hesapla ve kayda yaz",
                      "sequence": ("sayac_oku", "tuketim", "tarife", "kayit_yaz")})


def correct_answer(g):
    a = SUBSCRIBERS[g["subscriber"]]
    f = a["simdiki"] - a["onceki"]
    m3 = f + 100000 if f < 0 else f
    if g["kind"] == "consumption":
        return m3
    if g["kind"] == "document":
        return "SY01"
    return _charge(m3, a["tip"])


# ---- FICTIONAL agent: at each step it picks a tool or finishes
def selection(d):
    return max(0.34, 0.80 - 0.09 * (d - 1))       # share of picking the right tool, falls with depth


def agent(g, max_steps=8, seed=SEED, self_critique=False, approval=False):
    """FICTIONAL agent loop. No real model is called; tool selection
    comes from a single draw that depends on the task's sequence and the depth."""
    r = rng(seed ^ trace_id(g["name"]))
    state = {"abone": g["subscriber"], "anahtar": g["request"], "sutun": "bedel"}
    trace, step, tok, errors = [], 0, 0, 0
    seq, k = g["sequence"], 0
    while step < max_steps:
        step += 1
        tok += 60 + 22 * len(trace)
        target = seq[k] if k < len(seq) else None
        if target is None:                         # the job is done , the agent should stop
            if r() < 0.88:
                return {"done": True, "steps": step, "trace": trace, "tokens": tok,
                        "errors": errors, "answer": state.get("sonuc"),
                        "approvals": state.get("onay", 0)}
            choice = TOOL_NAMES[int(r() * len(TOOL_NAMES))]
        else:
            choice = target if r() < selection(k + 1) else TOOL_NAMES[int(r() * len(TOOL_NAMES))]
        if TOOLS[choice]["side_effect"] and approval:
            state["onay"] = state.get("onay", 0) + 1
        if choice == "kayit_yaz":
            state["deger"] = state.get("sonuc")
        s = call_tool(choice, state)
        trace.append((choice, "hata" in s))
        tok += 30
        if "hata" in s:
            errors += 1
            if self_critique:
                tok += 40
            continue
        state.update({kk: vv for kk, vv in s.items() if kk != "hata"})
        if choice == "tuketim":
            state["sonuc"] = s["m3"]
        if choice == "tarife":
            state["sonuc"] = s["tutar"]
        if choice == "belge_ara":
            state["sonuc"] = s["kod"]
        if choice == target:
            k += 1
    return {"done": False, "steps": step, "trace": trace, "tokens": tok, "errors": errors,
            "answer": state.get("sonuc"), "approvals": state.get("onay", 0)}



CALLS[0] = 0
g1 = TASKS[4]
g2 = dict(g1, name="G05b", kind="consumption", sequence=("sayac_oku", "tuketim"),
          request=f"{g1['subscriber']} abonesinin bu donem tuketimi kac metrekup")
print("task set:", {t: sum(g["kind"] == t for g in TASKS)
                    for t in ("charge", "consumption", "document", "record")})
print(g1["subscriber"], "two separate questions:", [x[0] for x in agent(g1)["trace"]],
      [x[0] for x in agent(g2)["trace"]], "; tool calls", CALLS[0])
```

```
task set: {'charge': 6, 'consumption': 8, 'document': 7, 'record': 3}
A026 two separate questions: ['sayac_oku', 'tuketim', 'tarife'] ['sayac_oku', 'tuketim'] ; tool calls 5
```

The same subscriber's meter was read twice. The first two steps of the second question repeat the first
exactly; two of the five tool calls bring no new information. The loss is not a flaw of the loop but
**the absence of a lasting place to write**.

## Three Write Policies

The only difference of the agent loop is that the initial state comes from memory and a step whose
output is known is skipped.

```python
PLAN = {"charge": (("sayac_oku", "tuketim", "tarife"),
                   "{sub} abonesinin bu donem bedeli nedir"),
        "consumption": (("sayac_oku", "tuketim"),
                        "{sub} abonesinin bu donem tuketimi kac metrekup"),
        "document": (("belge_ara",), "sayac donusunde endeks farki nasil hesaplanir"),
        "record": (("sayac_oku", "tuketim", "tarife", "kayit_yaz"),
                   "{sub} abonesinin bedelini hesapla ve kayda yaz")}
KINDS = ("charge", "consumption", "document", "record")
OUTPUTS = {"sayac_oku": ("onceki", "simdiki", "tip", "bolge"), "tuketim": ("m3",),
           "tarife": ("tutar",), "belge_ara": ("kod",)}
RESULT_KEY = {"tuketim": "m3", "tarife": "tutar", "belge_ara": "kod"}


def next_set(n=48):
    """NEW questions asked to the same subscribers; the recipe is identical to TASKS."""
    k = []
    for i in range(n):
        r = rng(SEED + 7919 * (i + 101))
        sub = TASKS[int(r() * 24)]["subscriber"]
        t = KINDS[pick(r(), [0.30, 0.22, 0.20, 0.28])]
        k.append({"name": f"H{i + 1:02d}", "kind": t, "subscriber": sub,
                  "sequence": PLAN[t][0], "request": PLAN[t][1].format(sub=sub)})
    return k


def recall(memory, request):
    """Single read: the record sharing the most tokens, on a tie the one written last."""
    b, best, record = set(tokenize(request)), 0, None
    for text, fields in memory:
        o = len(b & set(tokenize(text)))
        if o >= best and o > 0:
            best, record = o, fields
    return record


def agent_b(g, memory, seed=SEED, max_steps=8):
    """The only difference from the shared loop: the initial state comes from memory
    and a step whose output is already known is skipped."""
    r = rng(seed ^ trace_id(g["name"]))
    record = recall(memory, g["request"])
    c = {"steps": 0, "read": record is not None, "fields": len(record or {}),
         "observations": [], "on_path": {}}
    state = {"abone": g["subscriber"], "anahtar": g["request"], "sutun": "bedel"}
    state.update(record or {})
    if "sonuc" in state:
        return dict(c, answer=state["sonuc"])
    step, k, seq = 0, 0, g["sequence"]
    while k < len(seq) and seq[k] in OUTPUTS and all(x in state for x in OUTPUTS[seq[k]]):
        state["sonuc"] = state.get(RESULT_KEY.get(seq[k]), state.get("sonuc"))
        k += 1
    if k == len(seq):
        return dict(c, answer=state.get("sonuc"))
    while step < max_steps:
        step += 1
        target = seq[k] if k < len(seq) else None
        if target is None:
            if r() < 0.88:
                break
            choice = TOOL_NAMES[int(r() * len(TOOL_NAMES))]
        else:
            choice = target if r() < selection(k + 1) else TOOL_NAMES[int(r() * len(TOOL_NAMES))]
        if choice == "kayit_yaz":
            state["deger"] = state.get("sonuc")
        s = call_tool(choice, state)
        observation = {x: s[x] for x in OUTPUTS.get(choice, ()) if x in s}
        c["observations"].append(observation)
        if "hata" in s:
            continue
        state.update({kk: vv for kk, vv in s.items() if kk != "hata"})
        if choice in RESULT_KEY:
            state["sonuc"] = s[RESULT_KEY[choice]]
        if choice == target:
            c["on_path"].update(observation)
            k += 1
    return dict(c, steps=step, answer=state.get("sonuc"))


def run_policy(policy, seed=SEED, n=48):
    """First the initial set is run and written according to the policy, then the new set."""
    memory = []
    for g in TASKS:
        s = agent_b(g, [], seed)
        key = g["request"] if g["kind"] == "document" else g["subscriber"]
        write = (s["observations"] if policy == "every step" else [s["on_path"]]
                 if policy == "selective" else [{"sonuc": s["answer"]}]
                 if policy == "result only" else [])
        memory += [(key + " " + " ".join(f"{x} {v}" for x, v in a.items()), a)
                   for a in write if None not in a.values()]
    CALLS[0] = 0
    steps = correct = reads = hits = fields = 0
    for g in next_set(n):
        s = agent_b(g, memory, seed)
        d = s["answer"] == correct_answer(g)
        steps, correct, reads = steps + s["steps"], correct + d, reads + s["read"]
        fields, hits = fields + s["fields"], hits + (d and s["read"])
    return (len(memory), reads, fields, hits / max(reads, 1), steps, CALLS[0], correct / n)


POLICIES = ("memoryless", "every step", "result only", "selective")
print("policy         records reads fields hit rate steps calls  correct | 2nd run")
for p in POLICIES:
    a, b = run_policy(p), run_policy(p, SEED + 1)
    print(f"{p:<14} {a[0]:>7} {a[1]:>5} {a[2]:>6} {a[3]:>8.4f} {a[4]:>5} {a[5]:>5}"
          f" {a[6]:>8.4f} | {b[4]:>4} steps  {b[6]:.4f}")
```

```
policy         records reads fields hit rate steps calls  correct | 2nd run
memoryless           0     0      0   0.0000   212   166   0.9167 |  222 steps  0.8958
every step          74    47     99   0.8936   168   132   0.8958 |  176 steps  0.8542
result only         24    45     45   0.5556    13    10   0.5833 |   14 steps  0.6250
selective           24    47    166   0.8511    94    74   0.8542 |   96 steps  0.8958
```

The baseline finishes the 48 tasks with 212 steps and 166 tool calls, and task success is 0.9167. The
three policies are built on top of this row.

**Result only** is the most striking row. With 24 records it brings 212 steps down to **13**, which saves
199 steps; tool calls fall from 166 to 10. But task success drops to **0.5833**. The reason is this: the
record is an **answer** keyed by the subscriber code, whereas an answer belongs to a question. The
subscriber's consumption answer is stored, the next task asks for the same subscriber's **charge**; the
value is right, the question is different.

The **every step** policy accumulates 74 records, three times as many, and saves only 44 steps (from 212
to 168). The reason is visible in the reads column: the 47 reads bring a total of **99** fields, two
fields per read. Because the observation of each step is a separate record, a single read brings the
output of **a single step**. The **selective** policy saves 118 steps with the same 24 records (from 212
to 94) and brings **166 fields** in 47 reads; because the observations that enter the plan merge into a
single record, one read carries all the preconditions.

## Not the Number of Records but the Unit of the Record

The two rankings in the table do not coincide. By the number of records the most generous policy is
every step, by steps saved it is result only; the selective policy saves **2.68 times** as many steps as
every step with a third of its records. What decides is not how many records are written but **which
unit** a record covers: one step, one task, or all the fields of one entity.

The second run says which difference counts as a finding. The baseline gives 0.9167 and 0.8958, a
run-to-run difference of **0.0417**. The loss of result only is 0.3334 and 0.2708 in the two runs, both
far above the band and **measured**. The loss of the selective policy is 0.0625 in one run and **0.0000**
in the other; it oscillates around the band and **counts as unmeasured**. That the gain of 118 steps has
a cost in task success could not be shown in this setup.

The rule is plain: writing **an answer that depends on the question** into memory makes that answer
wrong when the question changes. What should be written is the **observation**, which is independent of
the question: a meter reading, a subscriber type, a document code. Reproducing the answer at every task
is cheaper than a wrong answer.

## Summary

- When the loop ends, short-term memory is erased; the same subscriber's meter is read again in the next
  question.
- What is measured is not retrieval but **the write decision**: with the same recall mechanism, three
  policies give three different results.
- Result only saves 199 steps with 24 records but drops task success from 0.9167 to 0.5833; the loss is
  eight times the run-to-run difference and is measured.
- Every step saves 44 steps with 74 records, selective saves 118 steps with 24; the fields brought per
  read are 99 against 166.
- What decides is the unit of the record, not the number of records; writing an observation instead of
  an answer that depends on the question removes the wrong answer.

## Next Step

This lesson treated memory **between** tasks. **Within** a task the problem reverses: the context is not
erased, it grows, and when the window fills something has to be thrown out. The decision to discard is
the mirror of the decision to write. The next lesson measures two ways of compression: summarizing the
observations and keeping only the relevant ones. As the compression ratio rises, the tokens carried fall,
but every field that drops may be the input of a next step. These two are what will be measured: the
tokens gained and **the number of tasks that need the discarded field**.
