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 # TypeScriptThe 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 nowimport { 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 nowReturn-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 turnimport { 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 turnWhat survives a restore
| Layer | Captured? | Example of what carries over |
|---|---|---|
| Memory (RAM) | yes | live Python variables, the loaded df, an in-RAM model |
| Kernel state | yes | the running IPython kernel, open sockets at snapshot time |
| Rootfs (disk) | yes | pip installed packages, files the agent wrote |
| New network identity | n/a | the 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:
- First turn — create, do setup,
snapshot_id = sandbox.snapshot()(TS:(await sandbox.snapshot()).id), persist the ID. - Every later turn —
Sandbox.create(from_snapshot=snapshot_id), run the model's code, return the result. - 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
| Use | Reach for | Why |
|---|---|---|
| Keep a kernel warm within one running process | a long-lived TTL sandbox | no restore cost; state lives in the open CodeContext |
| Resume across processes / restarts / hours-apart turns | snapshot() → from_snapshot | the ID is durable and portable; survives orchestrator death |
| Branch the same warmed state many ways, fast | fork() | ~400ms same-host CoW clone; cheap, parallel, disposable |
Next steps
- Persistent agent sessions — the full agent-loop backbone these primitives plug into.
- Install packages in a sandbox — what to run before you snapshot.
- Snapshots and forks — capture/restore internals and CoW mechanics.
- Code interpreter — stateful
CodeContextkernels and rich results.
Install Python packages inside a sandbox on the fly
Install Python packages inside a sandbox on the fly with pip or uv — via exec or a code-interpreter kernel — and reuse them in the next run.
Build a data-analyst agent that returns charts
An LLM agent that writes pandas code, runs it in a PandaStack code-interpreter sandbox, and hands back charts as objects — code execution with charts, no stdout parsing.