Skip to Content
RuntimeIn-proc · SQLite · Postgres

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 hood

Hot-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:

ClassBacking store
InMemoryJournalStorePython dict (lost on exit)
SqliteJournalStoreSingle-file SQLite (constructed directly)
PostgresJournalStorePostgres (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

FeatureInProcSQLitePostgres
Survives a process restart❌✅✅
Multi-instance shared journal❌partial†✅
agent.resume(session_id=...) from a checkpointsame process only✅✅
Async network setupn/anoneyes
Hot-path overheadzerosmallnetwork roundtrip
Production-recommendeddev onlysingle instancemulti-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.

Last updated on