multi-agent-lab / docs /architecture /long-running.md
agharsallah
feat: update model endpoint URLs and enhance documentation with mermaid diagrams
2a8eab2
|
Raw
History Blame Contribute Delete
4.05 kB
# 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
```mermaid
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:
```python
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 selection
- `scripts/resume_run.py` β€” resume/long-run demo entrypoint