Skip to content
academia.sh

Lesson 14 / 22

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.

Published Updated

In this lesson

  • 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.
Contents

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.

# 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.

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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close