A transactional sandbox runtime for AI coding agents. Run agent-generated code in an isolated container, checkpoint filesystem and conversation state together, and roll both back atomically when something breaks.
Agents that write and execute code need somewhere to do that safely, and a way to recover when they fail. There are three specific instances of this:
- State that can't be rolled back with git. An agent's mistakes aren't just bad diffs: a corrupted SQLite file, a half-run DB migration, or a deleted binary asset. There's nothing to revert, even with version control.
- Filesystem and memory drift apart after a manual rollback. Even if files are snapshotted manually, reverting them doesn't update the agent's belief about what it did. Its next turn reasons against a file that no longer exists.
- No reliable signal for whether a change actually worked. Agents commonly declare success without real verification, or crash mid-test, leaving an ambiguous result.
Rewind addresses all three by tying filesystem snapshots, conversation history, and a structured verification contract to the same checkpoint.
Rewind boots an Alpine Linux Docker container, mounts your host workspace into it read-only, and gives the agent a writable OverlayFS layer to work in. Checkpointing is a layer-stacking operation (fast, with no file copying), and rollback discards layers back to a chosen point. Separately, it keeps a parallel in-memory record of your conversation messages, snapshotted at the same checkpoint label, so a filesystem rollback and a memory rollback always happen together via one call.
from rewind_sdk import session
with session("agent", workspace="./src", auto_commit=True) as sess:
sess.checkpoint("stable")
sess.write_file("auth.py", new_implementation)
try:
sess.run_tests("pytest")
except RuntimeError:
sess.rollback("stable")
# If this block exits without raising and auto_commit=True,
# the workspace is streamed back to ./src on the host.Filesystem and message history are restored together, with one call. This is the mechanic everything in this SDK is built around, making that pairing convenient.
These are implemented and covered by the test suite or directly traceable in source:
- OverlayFS checkpoints — instant, layer-based snapshots of the sandbox filesystem (
engine.py) - Paired memory rollback — message history is truncated to match a filesystem checkpoint in one call
- Auto-checkpoint before tool calls — automatically snapshots state before tool calls via
on_tool_call()or the@session.tooldecorator - Auto-rollback mechanisms — triggers rollbacks inside
run(),run_tests(), and invocation workflows on exceptions or test failures - JSON verifier contract — parses
{"status": "pass"|"fail"|"unknown"}from stdout, handles retries, and returns structured summaries - Verification ledger — append-only audit log of verification, escalation, and rollback events; survives
rollback()calls - Escalation on UNKNOWN — after retries are exhausted,
mode="interactive"prompts continue/rollback/stop on stdin;mode="agent"halts withVerificationHaltErrorand preserves the container - LangGraph adapter — dedicated
wrap_langraph()utility to keep memory in sync with graph states @session.tooldecorator — LangChain-compatible tools with automatic checkpointing, scoped exception rollback, andRuntimeError→ error-string conversion for the LLM- Two-phase commit to host — host files are only touched if a session block exits without raising and
auto_commit=Trueis set; halt-aware__exit__skips commit and optionally preserves the container - Dangling tool-call cleanup — automatically drops trailing assistant messages that initiated a failed tool call, preventing strict-schema providers from rejecting your history on the next turn
- CLI and MCP server —
rewind_cli.pyandmcp_server.pyexpose session operations including verification ledger history
Requirements: Python 3.9+, Docker running locally.
pip install rewind-sdkNote: the PyPI package name is
rewind-sdk(hyphen), but the importable Python module isrewind_sdk(underscore); this is not a typo.
from rewind_sdk import session For LangGraph integration:
pip install "rewind-sdk[langgraph]"git clone https://github.com/rahulb0802/rewind-sdk.git
cd rewind-sdk
pip install -e .Copy and paste this example to watch the rollback execute live on your machine. On first run, Docker needs to pull the base Alpine image, which can take 10–30 seconds with no visible output; this is normal.
from rewind_sdk import session, Verifier
GOOD = "def authenticate(user, pw):\n return user == 'admin' and pw == 'correct-horse'\n"
BROKEN = "def authenticate(user, pw):\n return user == 'admin' and pw ==\n" # syntax error
# verifier contract for auto-rollback initiation
VERIFY = (
"import py_compile, json\n"
"try:\n"
" py_compile.compile('auth.py', doraise=True)\n"
" print(json.dumps({'status': 'pass'}))\n"
"except py_compile.PyCompileError:\n"
" print(json.dumps({'status': 'fail'}))\n"
)
with session("demo", workspace="./demo_workspace", auto_commit=True) as sess:
print("Sandbox started, pulling image on first run if needed...")
sess.write_file("auth.py", GOOD)
sess.write_file("verify.py", VERIFY)
sess.checkpoint("stable")
sess.auto_rollback("exception", "test_failure", to="stable",
verifier=Verifier(command="python3 verify.py"))
# simulate an agent mid-tool-call, then a broken write
dangling_msg = [{"role": "assistant", "content": "", "tool_calls": [{"id": "1"}]}]
sess.on_tool_call(dangling_msg, tool_name="write_file")
sess.write_file("auth.py", BROKEN)
try:
sess.run_tests() # fails -> auto_rollback fires here, no manual call
except RuntimeError:
pass
restored = sess.get_messages()
print("File reverted:", sess.read_file("auth.py") == GOOD)
print("Dangling msg dropped:", not any(m.get("tool_calls") for m in restored))
print("Agent gets a resume cue:", restored)with session("agent", workspace="./src") as sess:
messages = [
{"role": "user", "content": "Find the bug"},
{"role": "assistant", "content": "Found it in auth.py"},
]
sess.sync_memory(messages)
restored = sess.get_messages()sess.auto_checkpoint(trigger="before_tool_call", keep_last=10)trigger="before_tool_call" is the only trigger currently implemented. keep_last trims the SDK's own convenience label history (_auto_labels), but does not delete the underlying OverlayFS checkpoint layers, which remain on disk regardless of this setting. If you're watching container disk usage, this parameter won't help; there's currently no automatic checkpoint-layer pruning.
Auto-checkpoints fire when you call sess.on_tool_call(...) or use @session.tool, which calls on_tool_call() automatically before each decorated tool runs. You can also wire the LangGraph adapter's before_tool_node hook into a custom graph. It is not a global hook that activates on every tool call without one of these integrations.
from rewind_sdk import Verifier
sess.checkpoint("known_good") # create this BEFORE risky work begins
sess.auto_rollback(
"exception",
"test_failure",
to="known_good",
verifier=Verifier(command="pytest", retries=2, timeout=30.0),
)"test_failure" rollback requires a Verifier whose command prints JSON
({"status": "pass"|"fail"|"unknown", ...}) to stdout. "exception" fires
rollback on run() / run_tests() errors and on failures inside
@session.tool-decorated tools. For decorated tools, rollback is scoped per
tool via rollback_on_error (see below); only tools with
rollback_on_error=True (the default) participate.
Important:
to=should almost always be an explicit checkpoint label created withsess.checkpoint(...)before the risky operation, not the default"latest". Auto-checkpoints are taken immediately before each tool call, meaning the most recent auto-checkpoint can already contain the very change that caused the failure you're trying to recover from.to="latest"rolls back to that checkpoint, not to a known-good state.
if sess.last_auto_rollback:
print(sess.last_auto_rollback["event"], sess.last_auto_rollback["to"])The @session.tool decorator wraps a function as a LangChain-compatible tool with
automatic on_tool_call() bookkeeping. When a tool raises RuntimeError, the
decorator converts it to an error string the LLM can read; if a rollback fired,
the string includes a [REWIND] notice naming the checkpoint that was restored.
@sandbox.tool(rollback_on_error=False)
def run_sql(query: str) -> str:
"""Read-only query — failure should not roll back filesystem changes."""
return sandbox.run(f"sqlite3 db.sqlite '{query}'")
@sandbox.tool(rollback_on_error=True) # default; can be omitted
def run_script(path: str) -> str:
"""State-changing script — failure rolls back to the last checkpoint."""
return sandbox.run(f"python3 {path}")rollback_on_error controls whether "exception" auto-rollback applies to
failures inside that tool:
rollback_on_error=True(default) — failures (including fromsandbox.run()inside the tool) trigger auto-rollback when"exception"is configured.rollback_on_error=False— suppresses auto-rollback for that tool; use for read-only or side-effect-free operations where a failure should not discard other work in the sandbox.
VerificationHaltError (raised when a verifier returns UNKNOWN and escalation
resolves STOP) always propagates uncaught through @session.tool — it is a
session-level halt signal, not a tool error string.
Duplicate rollbacks to the same checkpoint are skipped automatically and
recorded in the ledger as skipped_noop.
When a Verifier is configured via auto_rollback(...), run_tests() runs
verifier.command in the container and treats JSON stdout as the sole
verdict (process exit code is ignored):
{"status": "pass", "summary": "All checks passed."}
{"status": "fail", "summary": "2 check(s) failed", "errors": ["..."]}
{"status": "unknown", "summary": "Verifier crashed", "error": "..."}| Status | Behavior |
|---|---|
| pass | No rollback; ledger records verification; run_tests() returns a formatted summary string |
| fail | Auto-rollback to to= checkpoint; run_tests() raises RuntimeError (or @session.tool converts it to an error string the LLM can act on) |
| unknown | Retried up to verifier.retries times; if still unknown, escalation runs (see below) |
from rewind_sdk import Verifier
sess.auto_rollback(
"exception",
"test_failure",
to="known_good",
verifier=Verifier(command="python3 verify.py", retries=2, retry_delay=1.0, timeout=30.0),
)
summary = sess.run_tests() # human-readable string on PASSsession.ledger is an append-only record of verification, escalation, and
rollback events. It lives outside the rollback scope — rollback() never
touches it.
for entry in sess.ledger.history():
print(entry.event_type, entry.status, entry.resolution, entry.checkpoint)# Interactive (default): prompts [c]ontinue / [r]ollback / [s]top on stdin after UNKNOWN
sess = session("dev", workspace="./src")
# Agent / headless: UNKNOWN → STOP automatically; container preserved on halt
sess = session("agent", workspace="./src", mode="agent")Override the default handler explicitly if needed:
from rewind_sdk import session, stdin_escalation_handler, stop_escalation_handler
sess = session("custom", workspace="./src", escalation_handler=stdin_escalation_handler)Escalation resolutions:
- continue — proceed without trustworthy verification; ledger records the decision;
run_tests()still raises (UNKNOWN is never treated as PASS) - rollback — revert to the
to=checkpoint - stop — raise
VerificationHaltError; inmode="agent",__exit__preserves the container and skipsauto_commit
Catch halt outside the with session(...) block so __exit__ can run
preserve-on-halt logic. Catching inside with and returning makes Python call
__exit__(None, None, None), which skips container preservation.
from rewind_sdk import session, VerificationHaltError, wrap_langgraph
sandbox = session("agent", workspace="./src", mode="agent", auto_commit=True)
try:
with sandbox:
sandbox.checkpoint("known_good")
sandbox.auto_rollback("exception", "test_failure", to="known_good", verifier=...)
@sandbox.tool
def run_verify() -> str:
return sandbox.run_tests()
safe_agent = wrap_langgraph(agent, session=sandbox)
for event in safe_agent.stream({"messages": messages}):
...
except VerificationHaltError as exc:
print(exc) # halt details
print(exc.checkpoint) # checkpoint at time of halt
print(sandbox.engine.container_name) # preserved container name
print(sandbox.ledger.history()) # audit trailInstall with the LangGraph extra: pip install "rewind-sdk[langgraph]"
import threading
from rewind_sdk import session, Verifier, VerificationHaltError, wrap_langgraph
tool_lock = threading.Lock()
sandbox = session("agent_sandbox", workspace="./my_codebase", mode="agent", auto_commit=True)
try:
with sandbox:
sandbox.auto_checkpoint(trigger="before_tool_call")
sandbox.checkpoint("known_good")
sandbox.auto_rollback(
"exception",
"test_failure",
to="known_good",
verifier=Verifier(command="pytest", retries=2, timeout=30.0),
)
@sandbox.tool(rollback_on_error=False)
def read_file(path: str) -> str:
"""Read-only — query failures should not roll back other work."""
with tool_lock:
return sandbox.read_file(path)
@sandbox.tool
def write_file(path: str, content: str) -> str:
"""State-changing — failures trigger exception rollback."""
with tool_lock:
sandbox.write_file(path, content)
return f"Wrote to {path}"
@sandbox.tool
def run_verify() -> str:
"""Run the configured verifier; FAIL rolls back, UNKNOWN halts."""
with tool_lock:
return sandbox.run_tests()
agent = create_react_agent(llm, tools=[read_file, write_file, run_verify])
safe_agent = wrap_langgraph(agent, session=sandbox)
for event in safe_agent.stream({"messages": messages}):
...
except VerificationHaltError as exc:
# sandbox container is preserved in mode="agent"
...@sandbox.tool injects on_tool_call() before each tool run, so you do not
need to call it manually in decorated tools. wrap_langgraph keeps memory
synced and re-raises VerificationHaltError uncaught; other unhandled
exceptions trigger "exception" rollback via on_tool_result.
Your system prompt doesn't need to mention rollbacks, checkpoints, or recovery,
as the message-history correction happens in memory.py, not in the prompt.
A thread lock around tool execution is recommended because the sandbox is a single container; concurrent writes from parallel tool calls aren't serialized for you.
rewind_cli.pyis included in the GitHub repo, not the PyPI package. Clone the repo (see Install from source) to use it.
python rewind_cli.py init ./my-project
python rewind_cli.py write src/app.py "print('hi')"
python rewind_cli.py checkpoint stable
python rewind_cli.py exec "pytest"
python rewind_cli.py rollback stable
python rewind_cli.py status
python rewind_cli.py ledger
python rewind_cli.py ledger --checkpoint stable
python rewind_cli.py destroyAdd --json for machine-readable output and --quiet to suppress stderr logging, which is useful if another agent is driving the CLI directly.
mcp_server.pyis included in the GitHub repo, not the PyPI package. Clone the repo to use it.
mcp_server.py exposes session operations (init_sandbox, execute_sandbox_command, write_sandbox_file, read_sandbox_file, sync_agent_memory, create_sandbox_checkpoint, rollback_sandbox_state, configure_auto_checkpoint, configure_auto_rollback, get_sandbox_status, get_ledger_history) as MCP tools, for clients that want to drive a Rewind sandbox without writing Python. The MCP server uses stop_escalation_handler by default. Install with MCP extra: pip install "rewind-sdk[mcp]".
Being direct and transparent (as this is still an early prototype):
- Containers run
--privileged. This is required for the current OverlayFS mounting approach, but it means the sandbox container has broad host-kernel access, and it is not a hardened security boundary against a determined adversary. Treat it as protection against an agent's accidental mistakes (bad refactors, destructive commands), not as isolation against malicious code. - One framework integration. Only LangGraph is supported today. The adapter pattern (
messages_to_dicts/dicts_to_messages) is framework-agnostic in design, but no LangChain-only or CrewAI adapter exists yet. - No automatic concurrency control inside the SDK. If you call sandbox methods from multiple threads, you need your own lock (see the LangGraph example above); the SDK does not serialize for you.
- Exception rollback is opt-in per tool. With
@session.tool, you choose per tool whether failures trigger"exception"rollback viarollback_on_error. Read-only tools should setrollback_on_error=Falseso a query failure does not roll back unrelated filesystem changes. Outside decorated tools,sess.run()still rolls back on failure when configured. VerificationHaltErrormust escapewithuncaught. Catch it outside thewith session(...)block so__exit__can preserve the container inmode="agent". Catching insidewithand returning treats the exit as clean.keep_lastdoesn't free disk space. It trims label bookkeeping, not the underlying checkpoint layers.- Default behavior discards work. With default arguments (
destroy_on_exit=True,auto_commit=False), exiting awith session(...)block destroys the container and writes nothing back to the host. Passauto_commit=Trueexplicitly if you want results persisted. Halt inmode="agent"is the exception: the container is preserved even withdestroy_on_exit=True. - Untested against multi-agent/complex tool calls. The dangling-tool-call cleanup handles the single-message case (one assistant tool-call message immediately before the checkpoint). Behavior under deeper crash scenarios hasn't been verified.
session(name="rewind_sandbox", workspace=".", *, container_name=None,
engine=None, memory=None, destroy_on_exit=True, auto_commit=False,
mode="interactive", escalation_handler=None)
sess.write_file(path, content)
sess.read_file(path) -> str
sess.run(cmd) -> str # raises RuntimeError on non-zero exit
sess.run_tests(cmd=None) -> str # uses verifier.command when cmd omitted;
# returns formatted summary on PASS
sess.sync_memory(messages, message_format="auto")
sess.get_messages(message_format="auto") -> list
sess.checkpoint(label, messages=None) -> str
sess.rollback(label="latest", patch_notes=None, message_format="auto") -> list
sess.auto_checkpoint(trigger="before_tool_call", keep_last=None)
sess.auto_rollback(*events, to=None, verifier=None)
sess.tool(fn=None, *, name=None, rollback_on_error=True) # decorator
sess.on_tool_call(messages=None, tool_name=None)
sess.on_tool_result(messages=None, error=None)
sess.start(workspace=None, force=False)
sess.attach()
sess.destroy()
sess.status() -> dict
sess.commit() # manual host export; auto_commit calls this on clean exit
sess.ledger # VerificationLedger (survives rollback)
sess.get_ledger() -> VerificationLedger
sess.last_auto_rollback # dict after most recent auto-rollback, or Nonefrom rewind_sdk import (
Verifier,
VerificationHaltError,
VerificationStatus,
VerificationResult,
VerificationLedger,
LedgerEntry,
EscalationContext,
EscalationResolution,
format_verification_result,
parse_verifier_output,
stdin_escalation_handler,
stop_escalation_handler,
wrap_langgraph,
)| Issue | Solution |
|---|---|
| Docker not running | docker version should return cleanly, or start Docker Desktop |
RuntimeError: Session not started |
Use with session(...) or call .start() first |
Work disappeared after the with block |
Default auto_commit=False, pass auto_commit=True |
"Checkpoint X already exists" |
Checkpoint labels must be unique per session; pick a new label |
| Container destroyed after UNKNOWN halt | Catch VerificationHaltError outside with session(...), not inside; use mode="agent" |
run_tests() raises but agent keeps going |
FAIL inside @session.tool becomes an error string (by design); UNKNOWN raises VerificationHaltError at the session boundary |
| Verifier always returns unknown | Ensure stdout is a single JSON object with a "status" field; use stderr for debug logs |
Built by a solo developer. Feedback and bug reports welcome.
Email: rewind.sdk.dev@protonmail.com
GitHub Issues: https://github.com/rahulb0802/rewind-sdk/issues
MIT: see LICENSE.