Versioned memory and state management for AI agents. Git-like history over event-sourced memory units.
Project description
StateForge
Versioned memory and state management for AI agents. Git-like history (snapshot, diff, rollback) and queryable provenance over event-sourced memory units. SQLite-backed, async-first, optionally encrypted at rest.
Agent memory is mutable, opaque, and ephemeral by default. StateForge makes it versioned, inspectable, and reproducible. Every memory unit is written once, never mutated, and addressable forever. Every snapshot is an immutable set of references to those units. You can always answer: what did this agent know, when did it know it, and where did that knowledge come from.
Install
pip install stateforge-llm
Optional extras:
| Extra | Adds | When to use |
|---|---|---|
[langchain] |
langchain-core>=0.2 |
StateForgeMessageHistory adapter |
[langgraph] |
langgraph>=0.1 |
StateForgeCheckpointer adapter |
[encryption] |
sqlcipher3>=0.6 |
DB-level encryption at rest (SQLCipher) |
[dev] |
pytest, pytest-asyncio | Run the test suite |
pip install "stateforge-llm[langchain,langgraph,encryption]"
Quickstart
import asyncio
from stateforge import StateForge, units
async def main():
sf = StateForge(db_path="agent.db")
session = await sf.create_session(label="my-agent-run")
# Take a snapshot of agent state
snap1 = await sf.snapshot(
session.id,
units=[
units.message(session.id, key="msg:0", value="hello", source="user"),
units.kv(session.id, key="goal", value={"task": "summarize"}, source="agent"),
],
label="initial",
)
# Move state forward
snap2 = await sf.snapshot(
session.id,
units=[
units.message(session.id, key="msg:0", value="hello", source="user"),
units.message(session.id, key="msg:1", value="working on it", source="agent"),
units.kv(session.id, key="goal",
value={"task": "summarize", "progress": 0.5}, source="agent"),
],
label="after-step-1",
)
# See what changed
d = await sf.diff(snap1.id, snap2.id)
print(f"added: {len(d.added)} modified: {len(d.modified)} removed: {len(d.removed)}")
# → added: 1 modified: 1 removed: 0
# Roll back. Non-destructive: snap1 + snap2 stay readable, head moves to snap3.
snap3 = await sf.rollback(session.id, to_snapshot_id=snap1.id, label="undo")
head = await sf.head(session.id)
assert head.id == snap3.id
await sf.close()
asyncio.run(main())
How it works (one minute)
Three first-class concepts:
| What | Lifetime | |
|---|---|---|
| MemoryUnit | An atomic piece of memory: a message, a KV fact, an embedding, a tool result, a summary. Has id, key, type, value, source. |
Write-once. Immutable. |
| Snapshot | A named, immutable set of references to MemoryUnits at a point in time. Snapshots form a linked list via parent_id. |
Write-once. Immutable. |
| Session | A logical grouping. Tracks the current head_snapshot_id. |
Mutable head pointer; everything else is append-only. |
A snapshot does not copy unit content — it references it. Two snapshots that share a unit pay the storage cost once. This matches the Git object model honestly.
Diff identity
Two units are "the same logical thing" iff (session_id, key, type) matches.
- added = identity present in
to, absent infrom - removed = identity present in
from, absent into - modified = same identity, different
value,metadata, orembedding
If a snapshot accidentally contains two units with the same identity, the latest by created_at wins.
Rollback
sf.rollback(session_id, to=snap) creates a new snapshot whose unit set equals snap's, with parent_id = previous head. The previous head and every prior snapshot stay intact. Rollback is itself a versioned event.
Value contract
value and metadata must be JSON-safe: str | int | float | bool | None | list | dict. bytes, datetime, UUID, Decimal, and custom classes raise ValueTypeError at write time. No silent corruption, no pickle attack surface. Base64-encode bytes yourself if you need them.
Encryption at rest (opt-in)
Threat model: disk theft, leaked backup, misconfigured cloud volume. DB-level encryption via SQLCipher defends against all three — an attacker with the file and without the key learns nothing, not even index keys or session labels.
import os
from stateforge import StateForge
sf = StateForge(
db_path="agent.db",
encryption_key=os.environ["STATEFORGE_KEY"], # 64-char hex string
)
# All schema, data, indexes, and WAL pages are encrypted on disk.
Generate a key once and stash it in your secrets manager:
python -c "import secrets; print(secrets.token_hex(32))"
Then pip install "stateforge-llm[encryption]".
What encryption does NOT protect against: a malicious process running as the same user (the key lives in memory while StateForge is open); coredumps or swap leaks; an attacker who obtains the key (env var leak, compromised secrets manager); side-channel observation of access patterns (file size, timestamps). Defend those layers separately.
Lifecycle caveats:
- Switching an existing plain DB to encrypted (or vice versa) is not supported in v0 — the DB is one mode or the other from creation.
- Key rotation is not supported in v0. Lost key = lost data; no recovery.
- A wrong key for an existing encrypted DB raises
EncryptionKeyErroron the first read afterPRAGMA key.
CLI
stateforge sessions list
stateforge snapshots list --session run-1
stateforge snapshot show head --session run-1
stateforge snapshot show head~1 --session run-1
stateforge diff head~1 head --session run-1
stateforge rollback --session run-1 --to initial
stateforge history --session run-1
stateforge provenance <unit_id>
stateforge vacuum
Snapshot refs accept head, head~N, label, or uuid (full or 8+ char prefix). Add --json to any command for machine-readable output. Set STATEFORGE_DB to skip --db on every call.
Example:
$ stateforge --db agent.db history --session demo
History for session demo (2 snapshots, head → root)
head 568e3c3d step-1 2026-05-23T17:53:37+00:00
head~1 d505aedf initial 2026-05-23T17:53:37+00:00
Framework adapters
LangChain
from stateforge import StateForge
from stateforge.adapters.langchain import StateForgeMessageHistory
from langchain_core.messages import HumanMessage, AIMessage
sf = StateForge("agent.db")
session = await sf.create_session(label="chat-1")
history = StateForgeMessageHistory(
session_id=session.id,
stateforge=sf,
auto_snapshot=True, # snapshot every N add_messages calls
snapshot_every=5,
)
await history.aadd_messages([
HumanMessage(content="Hi"),
AIMessage(content="Hello!"),
])
auto_snapshot=False (the default) means you snapshot explicitly via await history.snapshot(). from_snapshot(...) lets you resume after a process restart.
LangGraph
from stateforge import StateForge
from stateforge.adapters.langgraph import StateForgeCheckpointer
from langgraph.graph import StateGraph
sf = StateForge("agent.db")
checkpointer = StateForgeCheckpointer(sf)
graph = StateGraph(AgentState)
# ... add nodes ...
app = graph.compile(checkpointer=checkpointer)
Key feature: per-field shredding. Each top-level field in checkpoint["channel_values"] becomes its own MemoryUnit(KV). This means sf.diff(prev_checkpoint, curr_checkpoint) reports which fields changed in the agent's state, not just "1 unit modified". This is the difference between a useful audit trail and an opaque blob store.
Security & limitations (read before deploying)
StateForge v0 is a single-tenant, single-process Python library. The DB file is the trust boundary.
| Concern | v0 stance |
|---|---|
| Disk theft / leaked backup | ✅ Defended by opt-in SQLCipher |
| JSON-safe value contract | ✅ Enforced at write boundary |
| Snapshot atomicity (crash mid-write) | ✅ Single-transaction guarantee |
| Multi-tenant isolation | ❌ Out of scope. Run one instance per tenant. |
| Field-level / per-row encryption | ❌ v1+ (DB-level is supported now) |
| Key rotation | ❌ v1+. Lost key = lost data; no recovery. |
| Cryptographically signed provenance | ❌ v1+. Provenance is descriptive, not attestable. |
| Retention / TTL | ❌ v1+. Snapshots accumulate; manage at app level. |
| Vector similarity search | ❌ v1+. Embeddings stored faithfully but inert. |
Integrator checklist before deploying with sensitive data:
- Is the data sensitive enough that disk theft is a concern? → use
encryption_key=.... - Is the key sourced from a hardened location (KMS, secrets manager, OS keychain) and not hardcoded?
- Is the process running in a trust boundary you control (no untrusted code in-process)?
- Are OS-level controls in place (swap encryption, no coredumps, restricted file permissions on the
.db)? - Is there exactly one tenant per
StateForgeinstance / DB file? - If using backups: are backups encrypted (either by encrypting at rest here, or by the backup tool)?
Development
git clone https://github.com/Danultimate/stateforge-llm
cd stateforge-llm
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,langchain,langgraph,encryption]"
pytest -q
# → 265 passed in <1s
Built through a multi-agent design review process: an internal technical specification (v0.4.2) is the source of truth for every design decision — data model, schema, API surface, reliability contract, security threat model, deferred v1+ items.
Project status
v0.4.2 (2026-05) — implementation complete. 265 tests passing, 0 skipped. All in-scope items from the spec are shipped. The library has not been published to PyPI yet; install from source.
See the spec for the v1+ roadmap.
License
MIT — see pyproject.toml.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stateforge_llm-0.4.1.tar.gz.
File metadata
- Download URL: stateforge_llm-0.4.1.tar.gz
- Upload date:
- Size: 51.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b1a79973b030c89fa7c6093243b3c2f651b6b93321bbd25854e940879b497ae
|
|
| MD5 |
ae000830852562a5cf141df9ad8bfc96
|
|
| BLAKE2b-256 |
36b0b8c49fcf68337c98bc86ab71201c5f5d4eca4e1b784d5d956c9a750e094b
|
Provenance
The following attestation bundles were made for stateforge_llm-0.4.1.tar.gz:
Publisher:
release.yml on Danultimate/stateforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stateforge_llm-0.4.1.tar.gz -
Subject digest:
6b1a79973b030c89fa7c6093243b3c2f651b6b93321bbd25854e940879b497ae - Sigstore transparency entry: 1630713437
- Sigstore integration time:
-
Permalink:
Danultimate/stateforge@4ba9a4b0fb33e4824df71f235b5bb18e503bb1da -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/Danultimate
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4ba9a4b0fb33e4824df71f235b5bb18e503bb1da -
Trigger Event:
push
-
Statement type:
File details
Details for the file stateforge_llm-0.4.1-py3-none-any.whl.
File metadata
- Download URL: stateforge_llm-0.4.1-py3-none-any.whl
- Upload date:
- Size: 38.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ecf7f856847cbe56ae96df2005397e157571970aa7cef6319f9f3b9b0cd751e2
|
|
| MD5 |
c049cc6251a92c945198086297b237c5
|
|
| BLAKE2b-256 |
7906b8f90964b4318d9bfb0721410e87958f67a1af5ee61d988b16df06c4ee37
|
Provenance
The following attestation bundles were made for stateforge_llm-0.4.1-py3-none-any.whl:
Publisher:
release.yml on Danultimate/stateforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stateforge_llm-0.4.1-py3-none-any.whl -
Subject digest:
ecf7f856847cbe56ae96df2005397e157571970aa7cef6319f9f3b9b0cd751e2 - Sigstore transparency entry: 1630713444
- Sigstore integration time:
-
Permalink:
Danultimate/stateforge@4ba9a4b0fb33e4824df71f235b5bb18e503bb1da -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/Danultimate
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4ba9a4b0fb33e4824df71f235b5bb18e503bb1da -
Trigger Event:
push
-
Statement type: