A newer version of the Gradio SDK is available: 6.20.0
Long-Running Scenarios
An LLM call is request-response; a scenario that runs for hours is not. The engine treats the long-running operation as a durable loop whose checkpoint is the ledger, paced by two clocks, bounded by a token-aware governor.
Two clocks
Wall clock (real time) Sim clock (turns)
"one episode every hour" βββΊ conductor.step(n_ticks=60)
(a cron / scheduler concern) (60 sim-ticks at full inference speed)
step(n_ticks=N) advances N sim-ticks in one call. Wall-clock cadence is the
caller's job (a cron, a durable-execution timer); the conductor only knows sim
time. This is what lets the same scenario "run in realtime" for a demo and
"simulate a day in ten minutes" for testing.
The ledger is the checkpoint
flowchart LR
Run["step(n_ticks=N)"] -->|append| L[("Ledger")]
L --> Snap{"turn % snapshot_every == 0?"}
Snap -->|yes| Backup["snapshot_to(path)"] --> Run
Snap -->|no| Run
Run -.->|crash| Crash([process dies])
Crash --> From["SQLiteLedger.from_file(db)"]
From --> Restore["conductor.restore()<br/>adopt run_id + last turn"]
Restore --> Run
Because all state derives from the append-only ledger, crash recovery is nearly free:
ledger = SQLiteLedger.from_file("runs/village.db") # rehydrate from disk
conductor = Conductor(scenario, ledger=ledger)
conductor.restore() # adopt run_id + last turn from the tail
conductor.step(n_ticks=60) # continue the same run
restore() returns False on an empty ledger (start fresh). snapshot_every +
snapshot_path periodically checkpoint a SQLite ledger via its native backup.
scripts/resume_run.py is the end-to-end demo: run it twice against the same DB
and the turn count climbs. tests/test_long_running.py proves resume + snapshot.
Durable backend (env-gated)
For a hosted, multi-instance deployment the checkpoint can live in managed
Postgres instead of a local file. SqlAlchemyLedger (ADR-0014) is a drop-in
backend with the same idempotency + ordering guarantees, driving both Postgres
(Neon) and SQLite through one SQLAlchemy code path. Selection is env-gated:
make_ledger() returns SqlAlchemyLedger(DATABASE_URL) when DATABASE_URL is
set and the in-memory Ledger otherwise β so the system runs fully offline by
default and never imports SQLAlchemy unless a backend is configured. restore()
works identically over it.
The governor as safety valve
A "many small models posting to a shared board" topology is exactly what produces
runaway cascades and surprise bills. The Governor caps calls (per-turn, total),
tokens (max_total_tokens), and spend (hourly_budget_usd). Providers report
last_usage; the conductor meters real tokens into the governor each turn. All
token/spend limits default to off, so opting in is a config choice.
Hibernation
Agents that aren't scheduled cost nothing β they are rows in the registry with their memory derived from the ledger. Only the handful acting this turn consume inference. That is how a 20-agent village stays affordable.
What layers on top (deferred to docs, not built)
- Wall-clock cadence: a cron job calling
step(n_ticks=β¦)then exporting an episode artifact. - Durable execution: Temporal / Modal / Inngest wrap the loop for retries, timers, and replay β the ledger holds domain state, the engine holds scheduling.
- Cost telemetry: an LLM-observability hook (Langfuse/Helicone/OpenLLMetry)
feeds real per-call cost into
record_call(cost_usd=β¦).
Code
src/core/conductor.pyβstep(n_ticks),restore(),_maybe_snapshot()src/core/governor.pyβ token + spend caps,reset()src/core/sqlite_ledger.pyβ persistence,snapshot_to(),from_file(),tail()src/core/sqlalchemy_ledger.pyβ durable Postgres/SQLite backend (ADR-0014)src/core/ledger_factory.pyβmake_ledger(), env-gated backend selectionscripts/resume_run.pyβ resume/long-run demo entrypoint