PandaStack

Snapshot & restore sandbox state between agent turns

Snapshot sandbox state between agent turns so a paused agent resumes instantly — kernel, memory, and rootfs intact — without redoing setup.

An agent that installs packages and loads data in turn 1 should not redo that work in turn 2. The expensive part of a turn is almost never the model call — it is the environment: a 20-second pip install, a CSV parsed into a DataFrame, a model checkpoint pulled into RAM. If your orchestrator tears the sandbox down between turns (or restarts entirely), all of that is gone and the next turn pays for it again.

A snapshot freezes the whole VM — running kernel, in-memory state, and rootfs — into an ID you can store. Later you create a fresh sandbox from_snapshot=<id> and the agent wakes up exactly where it left off: imports already done, packages already installed, DataFrames still in memory. This is the right tool when turns are spaced apart, run in different processes, or survive a deploy. For keeping one kernel warm inside a single process, see persistent agent sessions; for the mechanics of capture-and-restore, see snapshots and forks.

Setup

export PANDASTACK_API_KEY=pds_...
pip install pandastack          # Python
npm install @pandastack/sdk     # TypeScript

The SDK reads PANDASTACK_API_KEY from the environment — never hardcode it. These examples use the code interpreter template, which bakes IPython, pandas, matplotlib, and friends.

Turn 1: do the expensive setup, then snapshot

Spin up a sandbox, install whatever the agent needs, load its working data, and call snapshot(). The return value is the only thing you have to persist — stash it next to your conversation state (a DB row, Redis, the run record).

from pandastack import Sandbox

sandbox = Sandbox.create(template="code-interpreter")

# The expensive turn-1 work the agent should never repeat.
sandbox.exec("pip install requests", check=True)
ctx = sandbox.create_code_context()
ctx.run_code("""
import pandas as pd
df = pd.read_csv('https://example.com/sales.csv')   # parsed once
SUMMARY = df.describe()                              # lives in kernel memory
""")

# Freeze kernel + memory + rootfs into an ID. snapshot() returns a str.
snapshot_id = sandbox.snapshot()
save_to_db(conversation_id, snapshot_id)   # persist the ID, your code
sandbox.kill()                              # safe to tear down now
import { Sandbox } from "@pandastack/sdk";

const sandbox = await Sandbox.create({ template: "code-interpreter" });

// The expensive turn-1 work the agent should never repeat.
await sandbox.exec("pip install requests");
const ctx = await sandbox.createCodeContext();
await ctx.runCode(`
import pandas as pd
df = pd.read_csv('https://example.com/sales.csv')   # parsed once
SUMMARY = df.describe()                              # lives in kernel memory
`);

// snapshot() resolves to a SnapshotInfo — take .id.
const snapshotId = (await sandbox.snapshot()).id;
await saveToDb(conversationId, snapshotId);  // persist the ID, your code
await sandbox.kill();                         // safe to tear down now

Return-type asymmetry. Python's sandbox.snapshot() returns the snapshot ID directly as a str. TypeScript's sandbox.snapshot() resolves to a SnapshotInfo object — use (await sandbox.snapshot()).id. Same operation, different shape; store a plain string either way.

Turn 2: restore from the stored ID

When the next turn arrives — seconds or hours later, same process or a fresh worker after a restart — create a sandbox from the saved ID. The new VM resumes the captured kernel and memory, so the previously defined variables and installed packages are already there. No reinstall, no reparse.

from pandastack import Sandbox

snapshot_id = load_from_db(conversation_id)   # the str you saved in turn 1

sandbox = Sandbox.create(template="code-interpreter", from_snapshot=snapshot_id)
ctx = sandbox.create_code_context()

# requests is installed, df is loaded, SUMMARY is in memory — all from turn 1.
ex = ctx.run_code("print(SUMMARY.loc['mean']); int(df.shape[0])")
print(ex.text)   # -> row count, with zero setup this turn
import { Sandbox } from "@pandastack/sdk";

const snapshotId = await loadFromDb(conversationId); // the id you saved in turn 1

const sandbox = await Sandbox.create({
  template: "code-interpreter",
  fromSnapshot: snapshotId,
});
const ctx = await sandbox.createCodeContext();

// requests is installed, df is loaded, SUMMARY is in memory — all from turn 1.
const ex = await ctx.runCode("print(SUMMARY.loc['mean']); int(df.shape[0])");
console.log(ex.text); // -> row count, with zero setup this turn

What survives a restore

LayerCaptured?Example of what carries over
Memory (RAM)yeslive Python variables, the loaded df, an in-RAM model
Kernel stateyesthe running IPython kernel, open sockets at snapshot time
Rootfs (disk)yespip installed packages, files the agent wrote
New network identityn/athe restored VM gets a fresh IP/MAC; long-lived external connections may need re-establishing

The snapshot ID is immutable: every restore starts from the same frozen point, so you can resume the same checkpoint as many times as you like. Restoring does not consume the snapshot.

Branch a turn with fork()

Sometimes you do not want to resume a checkpoint — you want to try several things from it. fork() is a same-host copy-on-write clone of a live sandbox (~400ms): the child starts from the parent's exact memory and disk, then diverges. This is how you fan out speculative tool calls or A/B two agent strategies from one warmed environment without re-running setup for each.

sandbox = Sandbox.create(template="code-interpreter", from_snapshot=snapshot_id)

branch_a = sandbox.fork()   # both inherit the restored kernel + df + packages
branch_b = sandbox.fork()
# run a risky transformation in branch_a, a different one in branch_b; compare.
const sandbox = await Sandbox.create({
  template: "code-interpreter",
  fromSnapshot: snapshotId,
});

const branchA = await sandbox.fork(); // both inherit the restored state
const branchB = await sandbox.fork();
// run a risky transformation in branchA, a different one in branchB; compare.

A useful pattern: snapshot once at a known-good checkpoint, then fork() per speculative branch. The snapshot is your durable resume point across turns; forks are cheap, throwaway explorations within a turn. To branch many at once in TypeScript, sandbox.forkTree({ count }) returns an array of children.

Wiring it into an agent loop

The whole pattern reduces to three lines bolted onto your loop:

  1. First turn — create, do setup, snapshot_id = sandbox.snapshot() (TS: (await sandbox.snapshot()).id), persist the ID.
  2. Every later turnSandbox.create(from_snapshot=snapshot_id), run the model's code, return the result.
  3. Optionally re-snapshot at the end of a turn that changed state worth keeping, and store the new ID — turning each turn into an incremental checkpoint.

Because the resume side is just an ID lookup plus a create, it does not matter whether turn 2 runs in the same process, a different queue worker, or after a redeploy. The agent's environment is portable, and the expensive setup happens exactly once.

Snapshot vs. live session vs. fork

UseReach forWhy
Keep a kernel warm within one running processa long-lived TTL sandboxno restore cost; state lives in the open CodeContext
Resume across processes / restarts / hours-apart turnssnapshot()from_snapshotthe ID is durable and portable; survives orchestrator death
Branch the same warmed state many ways, fastfork()~400ms same-host CoW clone; cheap, parallel, disposable

Next steps

On this page