Runtime backends
Three runtimes ship with the framework. They differ only in where the
journal lives: nowhere (InProcRuntime), a local SQLite file
(SqliteRuntime), or Postgres (PostgresRuntime). Pick SQLite when
one process must survive restarts, and Postgres when several
instances share runs.
InProcRuntime: the default
No journal, no durability. Crashes lose everything. Used implicitly
when you don’t pass runtime=.
from loomflow import Agent
agent = Agent("...", model="claude-opus-4-7") # InProcRuntime under the hoodHot-path overhead is zero. The agent loop’s runtime.step(name, fn, *args) collapses to await fn(*args) directly.
SqliteRuntime: single file, no infra
from loomflow import Agent
from loomflow.runtime import SqliteRuntime
agent = Agent(
"...",
model="claude-opus-4-7",
runtime=SqliteRuntime("./journal.db"),
)Each call to runtime.step(...) writes a row to the journal, keyed by
the session and user, the run, the step name and a fingerprint of its
inputs. On a fresh SqliteRuntime pointing at the same DB file,
re-running a crashed run with the same session and inputs reads the
cached rows instead of re-executing. A run that finished is never
replayed into a later one; see
Runs and epochs.
The DB file (and any missing parent directories) is created when you
construct the runtime, with three tables: journal_steps,
journal_streams and checkpoints. No migrations are needed.
Multiple agents can share the same file: the connection runs in WAL
mode with a 5-second busy timeout, so concurrent writers wait their
turn instead of failing immediately with database is locked.
Streaming steps
Architectures that stream model responses (runtime.stream_step(...))
journal the full list of chunks once the stream finishes. On replay,
the chunks are emitted in order without hitting the network.
PostgresRuntime: multi-instance durable
from loomflow import Agent
from loomflow.runtime import PostgresRuntime
runtime = await PostgresRuntime.connect("postgres://user:pw@host/db")
await runtime.init_schema() # creates the tables; safe to re-run
agent = Agent(
"...",
model="claude-opus-4-7",
runtime=runtime,
)Install: pip install 'loomflow[postgres]'.
connect() opens the connection pool (min_size / max_size tune
it) but doesn’t create tables. init_schema() creates
journal_steps, journal_streams and checkpoints with
CREATE TABLE IF NOT EXISTS, so call it on every start. Close the
pool with await runtime.aclose() on shutdown.
When two instances of your service share the same Postgres journal,
both can resume any session. That’s useful for blue/green deploys
and distributed worker pools. Runs on one (session, user) take turns
only within a process, so route a session’s requests to one instance
at a time.
JournalStore: the lower-level primitive
SqliteRuntime and PostgresRuntime are convenience wrappers over
JournaledRuntime + a JournalStore:
from loomflow import Agent
from loomflow.runtime import JournaledRuntime, SqliteJournalStore
store = SqliteJournalStore("./journal.db")
runtime = JournaledRuntime(store=store)
agent = Agent("...", model="claude-opus-4-7", runtime=runtime)Implementations:
| Class | Backing store |
|---|---|
InMemoryJournalStore | Python dict (lost on exit) |
SqliteJournalStore | Single-file SQLite (constructed directly) |
PostgresJournalStore | Postgres (await PostgresJournalStore.connect(dsn), then init_schema()) |
Useful when you want to share a JournalStore across multiple
runtimes (rare), or when you want to swap the backing store without
re-wrapping the runtime.
Per-backend feature matrix
| Feature | InProc | SQLite | Postgres |
|---|---|---|---|
| Survives a process restart | ❌ | ✅ | ✅ |
| Multi-instance shared journal | ❌ | partial† | ✅ |
agent.resume(session_id=...) from a checkpoint | same process only | ✅ | ✅ |
| Async network setup | n/a | none | yes |
| Hot-path overhead | zero | small | network roundtrip |
| Production-recommended | dev only | single instance | multi-instance |
† SQLite supports multiple readers and writers via WAL mode but isn’t ideal for high-write workloads across many instances.
Journal format
Journal step values are stored as tagged JSON. JSON types are
stored as themselves; tuples, sets, bytes, datetimes, enums, Pydantic
models and dataclasses are stored as tagged objects. Decoding never
imports a module or runs a constructor: a model, dataclass or enum is
rebuilt only from a class whose module is already loaded (models via
model_construct, so validators don’t run). A step that returns
something the codec can’t store (an arbitrary object) isn’t journaled:
it re-executes on resume, with a one-time warning.
Values written by releases before 0.17 were pickled. With the default serializer those rows are skipped with a one-time warning, and their steps re-execute. To read them, or to let steps return arbitrary objects, opt into pickle:
from loomflow.runtime import PostgresRuntime, SqliteJournalStore, SqliteRuntime
runtime = SqliteRuntime("./journal.db", serializer="pickle")
# or
runtime = await PostgresRuntime.connect("postgres://...", serializer="pickle")
# or, on a store you wrap yourself
store = SqliteJournalStore("./journal.db", serializer="pickle")Only use serializer="pickle" on a store no untrusted party can
write. Loading a pickle can run arbitrary code: anyone who can write
to the SQLite file or the Postgres tables could then run code in every
process that replays from them. The JSON default has no such hole.
Checkpoints are always JSON, whichever serializer you pick.
Don’t share InMemoryJournalStore across processes. It’s a
Python dict, so there’s nothing to share. The class exists for tests
and for the in-proc default; don’t reach for it in production
workflows that need durability.
Retention
Nothing in the journal is pruned automatically except the
per-session checkpoint cap. Purge finished sessions or old entries
with await runtime.store.prune(session_id=...) or
await runtime.store.prune(before=...). Runs with a user_id are
stored under journal_scope(session_id, user_id) (from
loomflow.runtime); pass that as session_id to prune one user’s
entries. For audit and compliance,
keep the audit log separately; that’s
the permanent record.