Every coding agent I have used in the last year had the same problem. It would edit a file, run a test, find a bug, and then I'd close my laptop. When I came back, none of it existed. Shell history gone. In-progress diff gone. Running REPL gone. The agent was effectively stateless between sessions, and that made it useless for the kind of work I actually do: debugging something that takes longer than a single sitting.
I spent months trying to work around it. Rethinking prompts to include the full session transcript. Wiring up external state stores. Scripting the agent to checkpoint its own working directory. Every fix was fragile, and none of them preserved what I actually needed: a running process I could walk away from and come back to. That's when I found a different approach. Instead of working around stateless containers, I switched to something that was never stateless in the first place. The agent I built isn't a model. It's a harness: a thin Python layer over Tensorlake sandboxes and an OpenAI-compatible tool-calling loop. The defining feature is that the harness outlives the agent's individual runs. The sandbox stays around. The files stay around. The memory stays around. Come back hours or days later, and the agent is exactly where you left it.
Most agent frameworks run on containers. Fast to spin up, easy to orchestrate, familiar to everyone. But containers share a kernel, and that matters when your agent runs untrusted code from a model.
An LLM generates Python code. The agent executes it. If that code runs in a container alongside other workloads, a vulnerability or malicious payload can reach across the shared kernel. For a personal side project, maybe that's fine. For production systems handling customer data, it's not.
There's a second problem too. Containers are ephemeral by design. Stop the container, lose everything inside it. That works for stateless API calls. It breaks down when your agent needs to maintain context across hours or days of work.
Tensorlake sandboxes aren't containers. They're MicroVMs backed by Firecracker and CloudHypervisor, each with its own kernel. That's the isolation story.
Speed is the other half. The default image, tensorlake/ubuntu-minimal, boots in around 460 milliseconds. The tensorlake/ubuntu-systemd image takes about a second. Cold starts clock in at 84 milliseconds. Fast enough to spin up a fresh sandbox per tool call without thinking about cost.
In a SQLite insert benchmark on a 2 vCPU / 4 GB RAM sandbox, Tensorlake completed 100,000 inserts in 2.45 seconds. The filesystem measured 4.1x faster on fsync, 2.8x faster on sequential write, and 1.9x faster on random read. These aren't synthetic numbers. They're real-world measurements on the kind of workloads agents actually do.
The practical effect: your agent doesn't wait around for infrastructure. It runs commands, edits files, executes tests, and moves on. The bottleneck shifts from "how fast can the sandbox boot" to "how fast can the model think."
What sold me on Tensorlake was suspend and resume.
A named sandbox gets a stable ID that never changes across suspend and resume. When I suspend, the filesystem, memory, and running processes all freeze in place. The meter stops. Hours later, I resume. Everything's exactly where it was.
from tensorlake.sandbox import Sandboxsandbox = Sandbox.create(name="debugging-agent")sandbox.run("pip", ["install", "flask", "pytest"])sandbox.suspend()sandbox.resume()sandbox.run("pytest", ["-v"])
A named sandbox ID stays valid across suspend and resume for weeks. Your SSH alias keeps working. VS Code Remote-SSH picks up where it left off. The tmux session you started three days ago? Still there.
Most agent harnesses lose state because the underlying container dies when you stop paying attention. Tensorlake's suspend isn't docker stop. It snapshots VM memory and processes. The Python REPL that was mid-expression stays mid-expression. The debugger attached to a d process stays attached. That's what makes multi-day debugging actually work: the agent doesn't have to re-derive any of its intermediate state every time you sit back down.
For a small team the cost shape matters too. A named sandbox that suspends on idle costs almost nothing between runs. My bill runs closer to "two cups of coffee a month" than to "always-on VM."
The tool-calling loop in my agent is small. Model returns tool calls. The agent maps each tool name to a sandbox operation, runs it, and sends the result back to the model. Every call goes through the sandbox proxy. The agent never touches the host.
Three tools cover most work: read a file, write a file, execute code.
from tensorlake.sandbox import Sandboxsandbox = Sandbox.create(name="agent-session")content = sandbox.read_file("/home/tl-user/workspace/app.py")sandbox.write_file("/home/tl-user/workspace/fix.py", new_code.encode())result = sandbox.run("python", ["-c", "import pytest; pytest.main(['-v'])"])
The agent can handle a production codebase without the model leaking it into training context or exfiltrating it to a remote endpoint. And the same harness works against a toy repo in development and a real repo in production. It only cares about the sandbox ID, not what code lives inside it.
Once you have stateful sandboxes, snapshots become the unit of memory.
When the agent finishes a task, I call checkpoint(checkpoint_type=CheckpointType.MEMORY). This captures the filesystem, memory, and running processes all at once. The snapshot ID is the durable handle. Next time the agent hits a similar problem, that snapshot context feeds into the system prompt as learned behavior.
from tensorlake.sandbox import CheckpointType, Sandboxsnapshot = sandbox.checkpoint(checkpoint_type=CheckpointType.MEMORY)restored = Sandbox.create(snapshot_id=snapshot.snapshot_id)
The qualitative effect is what you'd hope for. First time the agent sees "write a binary search function," it takes three iterations. Second time, two. No retraining. Just starting from the right prior.
I think this is the most underappreciated property of the platform. Snapshots aren't a deployment artifact. They're the unit of memory. A filesystem-only snapshot won't capture the running pytest debug session. A process-restart-only snapshot won't capture the uncommitted files. Both are required, and both come for free with CheckpointType.MEMORY.
Once you have snapshots, forking becomes the unit of parallelism. tl sbx clone creates a memory checkpoint and immediately boots a new sandbox from it. The clone warm-restores filesystem, memory, and running processes from the source.
tl sbx clone build-basesnapshot = sandbox.checkpoint(checkpoint_type=CheckpointType.MEMORY)workers = [Sandbox.create(snapshot_id=snapshot.snapshot_id) for _ in range(8)]
The obvious use case is parallel evaluation. Three candidate solutions, three sandboxes, same held-out task, score each, keep the winner. The new sandbox boots in hundreds of milliseconds with the warmed environment already loaded, including whatever files the agent had already edited.
The less obvious use case is just running two unrelated agents in parallel from the same warmed state. Forking isn't the same as creating from scratch. Dependencies don't reinstall. Files don't recopy. The environment's already warm.
Tensorlake's Harbor docs describe a pattern as "fresh isolation per trial, pre-warmed snapshots for expensive environments, and independent test verification." I built a verifier primitive around it.
The shape is simple. A fresh sandbox, no snapshot restore, internet blocked, metadata endpoints on the deny-out list. The candidate code can't reach into the verifier's filesystem, can't reach out to a remote endpoint, and can't read the evaluation script before it runs.
from tensorlake.sandbox import Sandboxverifier = Sandbox.create( image="tensorlake/ubuntu-minimal", allow_internet_access=False)verifier.write_file("/home/tl-user/workspace/candidate.py", candidate_code.encode())verifier.write_file("/home/tl-user/workspace/eval.py", eval_script.encode())result = verifier.run("python", ["/home/tl-user/workspace/eval.py"])score = parse_loss(result.stdout)verifier.terminate()
That last property matters. The candidate can't access the eval script before it runs, which makes the score trustworthy.
This pattern works for flaky tests too. Run the test ten times in ten fresh verifier sandboxes. See how many pass. You can't do that with a long-lived container because state leaks between runs. Each fresh sandbox is independent.
Most of the time the agent's three tools are enough. Read a file, write a file, run some code. But once in a while it gets stuck on something a search engine would resolve in seconds. A stack trace it can't Google on its behalf. A deprecation warning for an obscure API.
Tensorlake provides tensorlake/ubuntu-vnc, a managed desktop image with XFCE, TigerVNC, and Firefox. The SDK connects through the authenticated sandbox proxy so you can drive the desktop without manually exposing port 5901.
from tensorlake.sandbox import Sandboxsandbox = Sandbox.create(image="tensorlake/ubuntu-vnc")with sandbox.connect_desktop(password="tensorlake") as desktop: desktop.screenshot() desktop.press(["ctrl", "alt", "t"]) desktop.type_text("firefox https://stackoverflow.com") desktop.press("enter") desktop.move_mouse(640, 400) desktop.click()
I don't use this on every run. The cost is higher (2 GB+ RAM, longer boot), and it's unnecessary for tasks that fit inside the sandbox's filesystem. But it's the difference between "agent works on problems it already knows" and "agent works on problems a human would Google."
For pure browser automation, Tensorlake also supports the Chrome DevTools Protocol. Drive Chrome directly with Playwright or Puppeteer through a tunnel. Faster than VNC for that use case.
The default tensorlake/ubuntu-minimal image is fine for a few experiments and painful for production work. Every cold start pays the cost of pip install flask, pip install torch, apt-get install libpq-dev. Over a hundred runs that adds up.
The fix is baking a custom OCI image. Define it once. Push it to Tensorlake. Every fresh sandbox that references it boots pre-configured.
from tensorlake import Imagefrom tensorlake.sandbox import Sandboximage = ( Image(name="ml-environment", base_image="tensorlake/ubuntu-minimal") .run("apt-get update && apt-get install -y python3-pip libpq-dev") .run("python3 -m pip install --break-system-packages torch transformers flask pytest pandas") .run("mkdir -p /home/tl-user/workspace/cache") .workdir("/home/tl-user/workspace"))image.build(registered_name="ml-environment")sandbox = Sandbox.create(image="ml-environment")
My own setup runs roughly twenty pip installs deep (Flask, SQLAlchemy, pytest, requests, pandas, the standard data stack), and cold starts dropped from around 12 seconds to around 600 milliseconds after I baked the image. The cost saving compounds because every parallel fork boots pre-configured too.
Sometimes you don't want to write a Dockerfile. You just want an existing image as a sandbox. Tensorlake supports that with a one-line import.
tl sbx image import pytorch/pytorch:2.4.1-cuda12.1-cudnn9-runtime --registered-name pytorch-runtime
Or from Python:
from tensorlake import import_sandbox_imageimport_sandbox_image( "pytorch/pytorch:2.4.1-cuda12.1-cudnn9-runtime", registered_name="pytorch-runtime",)
Tensorlake pulls the image layers directly and writes them into the sandbox root filesystem. No Docker daemon involved. The reference is always pulled fresh from the registry.
Any OCI image works. Python base images. Node.js. Your company's private registry. If you can docker pull it, you can import it. Private registries work too, just run docker login first and Tensorlake picks up the credentials from your local Docker config.
I used this to bring in a PyTorch image with CUDA already configured. One command instead of a Dockerfile that installs CUDA, cuDNN, and PyTorch in the right order. If you need extra packages on top, write a Dockerfile that uses the imported image as a base. But for a lot of cases, the import is enough.
The closest I've gotten to a real RL primitive is a ranking pattern. Fork each candidate solution into its own sandbox. Run a score function inside each. Parse a floating-point score from stdout. Return the lowest-loss winner.
from tensorlake.sandbox import Sandboxdef rl_select(snapshot_ids, score_fn): scores = [] for snap_id in snapshot_ids: snapshot = Sandbox.get_snapshot(snap_id) eval_sandbox = Sandbox.create(snapshot_id=snapshot.snapshot_id) result = eval_sandbox.run("bash", ["-c", score_fn]) scores.append((snap_id, float(result.stdout.strip()))) eval_sandbox.terminate() return min(scores, key=lambda x: x[1])[0]winner = rl_select( snapshot_ids=["snap-001", "snap-002", "snap-003"], score_fn="cd /home/tl-user/workspace && pytest -q --durations=0 2>&1 | tail -1")
This isn't a full RL training loop. There's no policy gradient, no advantage estimation, no reward model. What it is: a clean primitive for the "group sequence policy optimization" pattern. Rank a batch of candidates by an evaluator, keep the winner, let the next round start from the better prior. For a coding agent, the evaluator is the test suite. For a research agent, it would be a held-out benchmark.
Tensorlake's Harbor integration makes this practical at scale. Snapshot a warmed environment. Clone it thousands of times in parallel. Pay once for setup. Every rollout starts from a known, reproducible state.
tl sbx describe prints an SSH config block you can paste into ~/.ssh/config. One copy-paste and you're in the sandbox. Open VS Code, run code --remote, and you're editing the agent's actual workspace.
Here's what the setup actually looks like:
tl sbx ssh keys add --name laptop ~/.ssh/id_ed25519.pubtl sbx describe my-devssh my-dev
The sandbox ID is stable across suspend and resume, so the SSH alias keeps working for weeks. I haven't had to update my ~/.ssh/config since I started using this, and I've been using it on and off for months.
One non-obvious detail: ServerAliveInterval 30 and ServerAliveCountMax 3 keep the connection healthy across short network gaps. Without them, a brief laptop sleep kills the session and the agent's running tmux. With them, the connection just resumes.
scp, rsync, and full port-forwarding all ride the same connection. Named sandboxes auto-suspend after the idle timeout. The clock s while you're connected, so you only pay while you work. Filesystem, memory, running processes, tmux sessions, and ~/.vscode-server, exactly where you left them. Resume by name, weeks later.
Tensorlake sandboxes aren't a fancier container runtime. They're a different unit of compute: boots in milliseconds, suspends cleanly, forks from snapshots, runs your agent's code in the same place your agent's data lives.
For an independent developer the economics matter. A named sandbox that suspends on idle costs almost nothing between runs. The bill maps to work done, not time elapsed.
That's what I keep coming back to. Not the benchmarks or the architecture diagrams. Just the fact that when I close my laptop and come back three days later, the agent is still there, mid-bug, mid-thought, exactly where I left it. That changes how you build.
The docs are worth a read if any of this sounds useful. The sandbox quickstart walks through the first sandbox in a few minutes. The remote-dev page covers the workstation workflow in more detail. And the cookbooks on GitHub have real examples if you prefer learning from code.
Building Stateful AI Agents That Survive Session Kills was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.