DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

Persist Your Agent's Conversation History Without Rolling Your Own JSON

Every agent session starts from scratch. The user asks a question. The agent answers. The session ends. The next session has no memory of the first.

Most teams solve this by adding a database. Postgres, Redis, DynamoDB — all fine choices, but all requiring infrastructure setup, connection management, schema migrations, and operational overhead for what is essentially a list of messages.

conversation-codec takes a simpler path: JSONL files with optional encryption. No database. No dependencies in the default case. Just a file that persists between sessions.


The Shape of the Fix

from conversation_codec import ConversationCodec

codec = ConversationCodec(path="./sessions/user-123.jsonl")

# Save after each session
messages = [
    {"role": "user", "content": "What is the capital of France?"},
    {"role": "assistant", "content": "Paris."},
]
codec.save(messages)

# Load in the next session
history = codec.load()
# history == [{"role": "user", ...}, {"role": "assistant", ...}]

# Pass to your LLM
response = client.messages.create(
    model="claude-sonnet-4-6",
    messages=history + [{"role": "user", "content": "And Germany?"}],
    max_tokens=256,
)
Enter fullscreen mode Exit fullscreen mode

Load, extend, save. The full conversation history persists between process restarts.


What It Does NOT Do

conversation-codec does not implement conversation summarization or context window management. If your history grows past the model's context limit, you need agent-message-window or agentfit to manage that.

It does not sync across processes or machines. The file is local. For multi-process or distributed scenarios, you need a shared storage backend.

It does not validate that the messages conform to any provider's schema. It stores and loads whatever dict list you give it. Validation is your responsibility or handled by agentvet.


Inside the Library

JSONL format: one JSON object per line. save() rewrites the file from scratch (not append-only, because the full conversation is the unit). load() reads all lines and parses each.

def save(self, messages: list[dict]) -> None:
    with open(self._path, "w") as f:
        for msg in messages:
            f.write(json.dumps(msg) + "\n")

def load(self) -> list[dict]:
    if not self._path.exists():
        return []
    with open(self._path) as f:
        return [json.loads(line) for line in f if line.strip()]
Enter fullscreen mode Exit fullscreen mode

The redact callable: ConversationCodec(path=..., redact=my_fn). Before saving, save() runs each message through my_fn(msg) -> msg. Use this to strip PII or tokens before the file is written. The loaded messages are already redacted; there is no restore path from the file.

Encryption: ConversationCodec(path=..., fernet_key=key). When a key is provided, save() encrypts the full JSONL with Fernet before writing. load() decrypts on read. Requires cryptography package. Install with pip install conversation-codec[crypto].

The 14 tests cover: empty file returns empty list, round-trip fidelity, redact callback invocation, Fernet encrypt/decrypt round-trip, and missing file returns empty list.


When to Use It

Use it for single-user agents where conversation history should persist between sessions. Personal assistants. Support bots where you want context across interactions. Any agent where "remember what we talked about" is a feature.

The file-per-session model works well up to thousands of sessions. For tens of thousands of sessions, you will want indexed storage (a database) so you can look up sessions by user ID without scanning the filesystem.

Skip it if your conversations are short (under 5 turns) and rebuilding context from scratch each time is acceptable. And skip it if you already have a persistence layer — you do not need another one.


Install

# Default (no encryption)
pip install git+https://github.com/MukundaKatta/conversation-codec

# With Fernet encryption support
pip install "git+https://github.com/MukundaKatta/conversation-codec[crypto]"
Enter fullscreen mode Exit fullscreen mode
from conversation_codec import ConversationCodec
from cryptography.fernet import Fernet
from llm_pii_redact import Redactor

# Full setup: PII redaction + encryption
redactor = Redactor()
key = Fernet.generate_key()

codec = ConversationCodec(
    path=f"./sessions/{user_id}.jsonl",
    redact=lambda msg: {**msg, "content": redactor.redact(msg["content"])[0]},
    fernet_key=key,
)
Enter fullscreen mode Exit fullscreen mode

Sibling Libraries

Library What it solves
agent-message-window Sliding context window with tool_use/tool_result pairing
agentfit Fit message history into a token budget
llm-pii-redact PII redaction before storage
agent-resume Checkpoint/resume long-running agent jobs
agent-state-checkpoint Durable JSON checkpoint for full agent state

The natural pairing: conversation-codec stores the message list, agent-message-window trims it to fit the context window, llm-pii-redact cleans PII before it goes to disk.


What's Next

Append mode would be useful for very long conversations where rewriting the whole file on each save is wasteful. An append_turn() method that adds one turn at a time would fix that at the cost of making load() slightly more complex.

A migration helper for existing storage formats would reduce adoption friction. If you already store conversations in Postgres or Redis, a from_postgres() class method that loads and converts them to the JSONL format would make migration easy.

Compression is another option: gzip the JSONL before writing. Conversation files compress extremely well (LLM responses are repetitive text). A compressed=True flag would halve file sizes with minimal code.


Built as part of the agent-stack family: composable Python primitives for production LLM agents.

Top comments (0)