Skip to content
Changelog

Keep sandbox checkpoints in the Postgres your agents already use

October 21, 20261 min read

Agno's PostgresCheckpointStore writes each sandbox checkpoint into two PostgreSQL tables, so any replica that reaches your database can restore a session's workspace on a new computer. Sandboxed agents already coordinate their turns through Postgres, and now their checkpoints can sit in the same place with no S3 bucket or shared disk.

from agno.agents.claude import ClaudeAgent
from agno.db.postgres import PostgresDb
from agno.sandbox.docker import DockerSandbox
from agno.sandbox.postgres_checkpoints import PostgresCheckpointStore
 
db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")
 
agent = ClaudeAgent(
    id="coder",
    db=db,
    sandbox=DockerSandbox(
        "agno-sandbox:local",
        checkpoints=PostgresCheckpointStore(db),
    ),
)

We tried it as two replicas would see it: two separate PostgresDb, PostgresCheckpointStore, DockerSandbox and agent instances on one Postgres, with real Docker containers. The agent is Agno's FixtureAgent, a credential-free runtime that echoes the input and records each run in a workspace file. Replica A ran the first turn, its warm container was removed, and replica B ran the second.

replica A turn 1: RunStatus.completed 'Write the first draft'
checkpoint: CheckpointRef(sha256='d5de630b5548...', size=622)
# warm container removed (generation 3209530595559429)
replica B turn 2: RunStatus.completed 'Revise the draft' resumed warm: False
fixture-effects.txt: ['0011382e-...', '4c33fb30-...']
run ids: ['0011382e-...', '4c33fb30-...']

Replica B started a new container, pulled the checkpoint out of Postgres and found turn 1's run already listed in the workspace file. The store had created its tables on first use, in the ai schema of the db:

-- tables
('ai', 'agno_sandbox_checkpoints')
('ai', 'agno_sandbox_checkpoints_chunks')
-- header rows (sha256, size, created_at)
d5de630b5548 622 1793218480
d9f427f87d85 648 1793218482
-- chunk rows (sha256, seq, bytes)
d5de630b5548 0 622
d9f427f87d85 0 648

Each checkpoint is a header row named by the SHA-256 of its bytes, plus chunk rows of up to chunk_bytes, which defaults to 8 MiB. These archives fit in one chunk; with chunk_bytes=128 the same 622 bytes became 5 rows. The header and every chunk go in one transaction, so an interrupted upload leaves nothing readable. When replica B stored the exact bytes replica A had stored, put returned the same reference and the tables still held 2 headers and 2 chunks.

Reads check what they get. After one stored byte was overwritten, and again after the object was deleted:

tampered read: SandboxError Checkpoint checksum mismatch
after delete: CheckpointObjectNotFound Checkpoint object is missing
chunk rows left for it: 0

Deleting a header removes its chunks through a cascade. A reader that already holds its private copy keeps working after the delete. Pass a separate PostgresDb to keep checkpoint bytes out of your system of record, and set table_name, max_bytes or scratch when the defaults don't fit.

See the cookbook, and learn more about sandboxes and checkpoints in the documentation.

Frequently asked questions

No. It creates agno_sandbox_checkpoints and agno_sandbox_checkpoints_chunks in the schema of the PostgresDb you pass it. Give it a second PostgresDb if you want checkpoint bytes kept apart from your sessions and runs.

Each read copies the chunks into a private temporary file and compares its size and SHA-256 with the reference. A mismatch raises SandboxError with "Checkpoint checksum mismatch", and no files reach the new computer.

Its async methods do. The sync methods need PostgresDb, so with AsyncPostgresDb call aopen_run_checkpoint, because open_run_checkpoint raises TypeError.

It is opt-in. A sandbox with no checkpoints= argument still writes to a directory on the local host, so pass checkpoints=PostgresCheckpointStore(db) to switch.

Shipped around the same time