MemoryEntry
On this page
Purpose
MemoryEntry is the single universal record type used by every memory tier in this package — ShortTermMemory, WorkingMemory, LongTermMemory, and SemanticMemory all store and return MemoryEntry instances. It carries content, timing, importance/decay bookkeeping, tags, and metadata in one dataclass.
Constructor
@dataclass
class MemoryEntry:
content: Any
timestamp: float
memory_type: MemoryType
importance: float = 0.5
access_count: int = 0
tags: List[str] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
embedding: Optional[List[float]] = None
last_access: float = field(default_factory=time.time)
decay_factor: float = 1.0
original_importance: Optional[float] = None
__post_init__
- If
original_importanceisNone, sets it to the currentimportance. - Validates
0 <= importance <= 1, raisingValueError(f"importance must be between 0 and 1, got {self.importance}")otherwise. - If
"id"is not already a key inmetadata, generates one via_generate_id()and stores it inmetadata["id"].
ID generation
def _generate_id(self) -> str:
data = f"{self.content}_{self.timestamp}_{self.memory_type.value}"
return hashlib.md5(data.encode('utf-8')).hexdigest()[:16]
The ID is an MD5 hash (truncated to 16 hex chars) of str(content), timestamp, and memory_type.value. Because timestamp is normally a high-resolution time.time() float, collisions between two distinct entries are unlikely in practice, but this is not a cryptographic or collision-resistant ID scheme — if you construct two entries with the identical content and an identical (e.g. manually-supplied, rounded) timestamp, they will get the same ID and be treated as equal by __eq__/__hash__.
Properties
Methods
access() -> None
Records an access: increments access_count, resets last_access to now, and applies a small importance boost:
boost = 0.05 * (1 - self.importance) # smaller boost the more important it already is
self.importance = min(1.0, self.importance + boost)
Called automatically by LongTermMemory.get() and SemanticMemory.search() whenever an entry is retrieved.
apply_decay(decay_rate: float = 0.1) -> None
days_since_access = self.time_since_access_seconds / 86400
decay = decay_rate * days_since_access
self.decay_factor = max(0.1, 1.0 - decay)
self.importance = max(0.1, self.importance * self.decay_factor)
Linear (not exponential) decay based on days since last access, floored at 0.1 for both decay_factor and importance. Mutates the entry in place. Note this is a different decay formula than the exponential model used by ForgettingMechanism and LongTermMemory.apply_global_decay — three different decay implementations coexist in this package (see Known Issues).
add_tag(tag: str) / add_tags(tags: List[str]) / has_tag(tag: str) -> bool
add_tag lowercases and strips the tag before appending (skips empty strings and duplicates). has_tag lowercases only the query argument, not the stored tags — since add_tag always stores lowercase, this is consistent as long as tags was never populated by direct list mutation bypassing add_tag.
update_metadata(key, value) / merge_metadata(dict)
Simple dict.__setitem__ / dict.update() wrappers.
to_dict(include_embedding: bool = False) -> Dict[str, Any]
Returns a dict with id, content, timestamp, memory_type (as .value string), importance, effective_importance, access_count, tags, metadata, last_access, decay_factor, age_seconds, priority (as .name string). Does not include embedding unless include_embedding=True, and never includes original_importance.
from_dict(data: Dict[str, Any]) -> MemoryEntry (classmethod)
Reconstructs an entry. Requires data['content'] and data['timestamp'] (raises KeyError if absent); every other field has a .get() fallback. memory_type is parsed via MemoryType.from_string(data.get('memory_type', 'short_term')) — meaning an unrecognized memory_type string silently becomes SHORT_TERM rather than raising (see MemoryType.from_string).
[!WARNING] to_dict → from_dict is lossy for embeddings
entry.to_json()thenMemoryEntry.from_json(...)(orto_dict()/from_dict()) drops theembeddingfield unless you explicitly passinclude_embedding=Truetoto_dict(). If you persist entries via this path and later reload them intoSemanticMemory, the embedding will need to be recomputed.
to_json(include_embedding=False) -> str / from_json(json_str) -> MemoryEntry (classmethod)
Thin json.dumps/json.loads wrappers around to_dict/from_dict.
__repr__, __eq__, __hash__, __lt__
__repr__truncatescontentto 50 characters with"..."suffix if longer.__eq__/__hash__are based solely on.id— two entries with the same generated (or manually supplied) ID are considered equal and hash identically, regardless of differing content.__lt__compares byeffective_importance, enablingsorted(entries)to rank by importance ascending. Comparing against a non-MemoryEntryreturnsNotImplemented(Python then falls back to the other operand's comparison, or raisesTypeErrorif neither side implements it).
Thread safety
MemoryEntry itself performs no locking. It is safe to read from multiple threads, but concurrent .access() / .apply_decay() calls on the same instance from different threads are a plain unsynchronized read-modify-write race (e.g. access_count += 1 is not atomic).
Complexity
All operations are O(1) except _generate_id (O(len(str(content)))) and dict/JSON serialization (O(size of content + metadata)).
Examples
import time
from zerda.memory import MemoryEntry, MemoryType
# 1. Basic construction
entry = MemoryEntry(
content={"input": "What's the capital of Japan?", "output": "Tokyo."},
timestamp=time.time(),
memory_type=MemoryType.SHORT_TERM,
importance=0.6,
)
print(entry.id) # 16-char hex string
# 2. Recording an access
entry.access()
print(entry.access_count, entry.importance) # 1, slightly boosted
# 3. Applying decay after a week without access
entry.last_access -= 7 * 86400
entry.apply_decay(decay_rate=0.1)
print(entry.decay_factor, entry.effective_importance)
# 4. Error handling — invalid importance
try:
MemoryEntry(content="x", timestamp=time.time(),
memory_type=MemoryType.SHORT_TERM, importance=1.5)
except ValueError as e:
print(e) # "importance must be between 0 and 1, got 1.5"
# 5. Production-style round trip through JSON (note: embedding dropped)
json_str = entry.to_json()
restored = MemoryEntry.from_json(json_str)
assert restored.id == entry.id
Best practices
- Always construct with a real
time.time()timestamp unless you have a specific reason to backdate an entry (e.g. tests). - Use
.access()when your application logic "reads" a memory to keep decay/priority bookkeeping accurate — don't just read.contentdirectly if you want recency tracking to reflect real usage. - If you need embeddings preserved across serialization, always pass
include_embedding=Trueexplicitly.
Common mistakes
- Constructing with
importanceoutside[0, 1](e.g. passing a raw LLM confidence score on a 0–100 scale) — raisesValueErrorimmediately. - Assuming
tagsstores case-preserved values —add_tagalways lowercases. - Comparing two entries with
==and expecting content equality — it's ID equality only.
Related APIs
MemoryType/MemoryPriorityShortTermMemory,WorkingMemory,LongTermMemory,SemanticMemory— all consume/produceMemoryEntry.ForgettingMechanism— an alternative decay implementation.