ConversationBufferMemory

On this page

Purpose

Stores the entire conversation history with no size limit. Use this for short conversations, or when you deliberately need full, unabridged context (e.g. debugging, small automated scripts). For anything long-running, prefer ConversationBufferWindowMemory or ConversationSummaryMemory to avoid unbounded memory growth and unbounded prompt size.

Constructor

Python · 7 lines
def __init__(
    self,
    return_messages: bool = False,
    input_key: str = "input",
    output_key: str = "output",
    memory_key: str = "history",
)
ParameterTypeRequiredDefaultNotes
return_messagesboolNoFalseIf True, load_memory_variables returns the raw list of message dicts instead of a formatted string.
input_keystrNo"input"Key read from the inputs dict passed to save_context.
output_keystrNo"output"Key read from the outputs dict passed to save_context.
memory_keystrNo"history"Key under which the result is returned from load_memory_variables.

Initializes self.chat_memory = [] (a plain list).

Public methods

save_context(inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None

Appends {"input": inputs.get(self.input_key), "output": outputs.get(self.output_key), "timestamp": time.time()} to self.chat_memory. If input_key/output_key are missing from the respective dicts, the stored value is simply None — no exception is raised.

load_memory_variables(inputs: Dict[str, Any]) -> Dict[str, Any]

If return_messages=True: returns {self.memory_key: self.chat_memory} (the raw list, by reference — mutating the returned list mutates internal state).

Otherwise: returns {self.memory_key: text} where text is built by _format_messages_as_text(), joining "user: {input}\nassistant: {output}" per message with "\n".

The inputs parameter passed to this method is entirely unused — it exists to satisfy the BaseMemory abstract signature.

clear() -> None

self.chat_memory.clear().

get_memory_stats() -> Dict[str, Any]

Overrides BaseMemory's default. Returns type, message_count, total_characters (sum of len(str(input)) + len(str(output)) per message), and estimated_tokens (total_characters // 4).

get_messages(limit: int = None) -> List[Dict[str, Any]]

Python · 3 lines
if limit:
    return self.chat_memory[-limit:]
return self.chat_memory.copy()

Returns the last limit messages, or all messages if limit is None/falsy/zero. Note: because the condition is if limit: (truthiness, not is not None), passing limit=0 returns all messages (falls through to .copy()), not zero — a different (arguably more defensible) behavior than the [-0:] slicing trap seen elsewhere in this package, since here 0 never even reaches the slice expression.

[!DANGER] get_messages(limit=N) breaks on ConversationBufferWindowMemory This method is inherited unmodified by ConversationBufferWindowMemory, where self.chat_memory is a collections.deque, not a list. Calling get_messages(limit=5) on a window memory instance raises TypeError: sequence index must be integer, not 'slice', because deque does not support slice indexing. get_messages() (no limit, or limit=None) works fine since it only calls .copy(). See Known Issues.

Async methods

asave_context / aload_memory_variables — thin asyncio.to_thread(...) wrappers, defined locally on this class (see Architecture → Concurrency Model for why this is duplicated per-class rather than inherited).

Internal workflow

Thread safety

Not thread-safe. self.chat_memory.append(...) from save_context is not protected by any lock; concurrent writers from multiple threads can interleave, though Python's GIL makes a single list.append call itself atomic (no corruption), so the worst outcome is non-deterministic ordering, not data corruption.

Complexity

  • save_context: O(1) amortized.
  • load_memory_variables (text mode): O(n) in number of messages.
  • get_memory_stats: O(n) — iterates every message to sum character counts.

Examples

Python · 27 lines
from zerda.memory import ConversationBufferMemory

# 1. Basic
mem = ConversationBufferMemory()
mem.save_context({"input": "Hi"}, {"output": "Hello!"})
print(mem.load_memory_variables({})["history"])
# "user: Hi\nassistant: Hello!"

# 2. Intermediate — return_messages mode
mem2 = ConversationBufferMemory(return_messages=True)
mem2.save_context({"input": "2+2?"}, {"output": "4"})
print(mem2.load_memory_variables({})["history"])
# [{"input": "2+2?", "output": "4", "timestamp": ...}]

# 3. Advanced — custom keys to match a different LLM wrapper's conventions
mem3 = ConversationBufferMemory(input_key="question", output_key="answer")
mem3.save_context({"question": "Capital of Italy?"}, {"answer": "Rome"})

# 4. Error handling — missing key silently stores None
mem4 = ConversationBufferMemory()
mem4.save_context({}, {"output": "..."})   # inputs.get("input") -> None, no exception
print(mem4.get_messages()[0]["input"])     # None

# 5. Production — periodic stats logging
stats = mem.get_memory_stats()
if stats["estimated_tokens"] > 3000:
    print("Consider switching to ConversationSummaryMemory")

Best practices

  • Use for short-lived sessions only; there is no cap, so memory and prompt size grow linearly with conversation length.
  • Prefer get_messages() (no limit) over get_messages(limit=N) if there's any chance this instance is (or will become) a ConversationBufferWindowMemory — see the crash caveat above.

Common mistakes

  • Passing limit=0 to get_messages expecting zero results back — you get everything.
  • Assuming load_memory_variables's inputs argument affects the output — it's ignored entirely.

Related pages