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,
)
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()]
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]"
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,
)
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)