Documentation Index
Fetch the complete documentation index at: https://mintlify.com/deeplethe/forkd/llms.txt
Use this file to discover all available pages before exploring further.
Four agent frameworks, four integration patterns — each one a ~150–250 line Python script you can read in 5 minutes and adapt to your own project. All have a --dry-run mode that exercises the full forkd plumbing (spawn, exec, BRANCH, fan-out) without requiring an LLM API key.
CrewAI fan-out
CrewAI’s Process.parallel runs agents in threads inside one Python process. That works until two agents both import torch with conflicting settings, or one agent’s os.chdir() confuses another, or a runaway while True blocks the GIL. Today you either accept contamination risk or build a Docker-per-agent harness — and pay 2–5 seconds of cold-start per agent.
forkd gives every agent its own microVM, all forked from one warmed parent in milliseconds. You keep CrewAI’s orchestration model and add real KVM isolation.
Pattern: one spawn_sandboxes(n=N) call returns N sandbox handles; each is wrapped in a ForkdRun BaseTool that the LLM routes its computation to. The LLM sees N tools with distinct names and dispatches accordingly.
def provision_sandboxes(
controller: Controller, snapshot_tag: str, n: int, per_child_netns: bool
) -> tuple[list[SandboxHandle], float]:
"""Spawn N sandboxes from one parent snapshot. Returns (handles, seconds).
The single `spawn_sandboxes` call is the v0.3 fast path — all N
children forked from one shared memory image in one daemon round-trip.
"""
t0 = time.monotonic()
raw = controller.spawn_sandboxes(
snapshot_tag=snapshot_tag,
n=n,
per_child_netns=per_child_netns,
)
elapsed = time.monotonic() - t0
return [SandboxHandle(id=sb["id"], snapshot_tag=snapshot_tag) for sb in raw], elapsed
def make_forkd_tool(controller: Controller, sandbox: SandboxHandle):
"""Build a CrewAI BaseTool that exec's code in `sandbox`."""
from crewai.tools import BaseTool
from pydantic import Field
sb_id = sandbox.id
class ForkdRun(BaseTool):
name: str = f"forkd_run_{sb_id[-6:]}"
description: str = (
"Execute a short Python expression inside an isolated forkd "
"microVM. Input: a Python expression as a string. Output: "
"stdout from the sandbox."
)
sandbox_id: str = Field(default=sb_id)
def _run(self, code: str) -> str:
result = controller.exec_command(
self.sandbox_id,
["python3", "-c", code],
timeout_secs=30,
)
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
exit_code = result.get("exit_code", -1)
if exit_code != 0:
return f"[exit={exit_code}] stderr: {stderr}\nstdout: {stdout}"
return stdout.strip() or "(no output)"
return ForkdRun()
Setup and run:
pip install crewai forkd>=0.3.1
sudo bash scripts/netns-setup.sh 3
FORKD_TOKEN=$(sudo cat /etc/forkd/token) \
python3 recipes/crewai-fanout/demo.py --n=3
Expected output (dry-run, no LLM key):
[crewai-fanout] using snapshot 'coding-agent-fork-prewarm-v1'
[crewai-fanout] spawned 3 sandboxes in 612.3ms (204.1ms/child)
- sb-6a0d53e8-0001
- sb-6a0d53e8-0002
- sb-6a0d53e8-0003
[crewai-fanout] skipping CrewAI run (no LLM key in env); plan:
agent[0] → sandbox sb-6a0d53e8-0001 → task: Compute the 25th Fibonacci number...
agent[1] → sandbox sb-6a0d53e8-0002 → task: Check whether 9973 is prime...
agent[2] → sandbox sb-6a0d53e8-0003 → task: Sort the list [4, 1, 8, 2, 9, 3]...
[crewai-fanout] cleaned up 3 sandboxes
| Approach | Per-agent cold-start | Isolation | Disk overhead |
|---|
| CrewAI default (threads) | ~0 ms | None — shared GIL, sys.path, env | None |
| CrewAI + Docker per agent | 2–5 s | Strong | Full image × N |
| CrewAI + forkd | ~200 ms | Strong (microVM) | Diff-snapshot bytes only |
AutoGen BRANCH
AutoGen ships two production CodeExecutor options: LocalCommandLineCodeExecutor (fast, no isolation) and DockerCommandLineCodeExecutor (strong isolation, 2–5 s cold-start). Neither lets you do the forkd-shaped move: branch a conversing agent mid-turn. This recipe adds a third executor option backed by forkd, plus exposes the BRANCH primitive that Docker can’t offer.
Pattern: ForkdCommandLineCodeExecutor implements autogen_core.code_executor.CodeExecutor. Every execute_code_blocks() call exec’s inside the sandbox. After a turn, branch_sandbox(diff=True) snapshots the agent’s accumulated VM state; spawn_sandboxes(branch_tag, n=K) fans out K grandchildren that each inherit everything the parent agent built.
class ForkdCommandLineCodeExecutor(CodeExecutor):
"""Exec each AutoGen code block inside a forkd microVM."""
def __init__(self, controller: Controller, sandbox_id: str) -> None:
self._controller = controller
self._sandbox_id = sandbox_id
async def execute_code_blocks(
self, code_blocks: list[CodeBlock], cancellation_token: Any
) -> CodeResult:
output_chunks: list[str] = []
for block in code_blocks:
if block.language not in ("python", "py"):
raise NotImplementedError(
f"ForkdCommandLineCodeExecutor only supports python "
f"blocks for now; got {block.language!r}."
)
result = self._controller.exec_command(
self._sandbox_id,
["python3", "-c", block.code],
timeout_secs=30,
)
output_chunks.append(result.get("stdout", ""))
stderr = result.get("stderr", "")
if stderr:
output_chunks.append(f"[stderr]\n{stderr}")
if result.get("exit_code", 0) != 0:
return CodeResult(
exit_code=result["exit_code"],
output="\n".join(output_chunks),
)
return CodeResult(exit_code=0, output="\n".join(output_chunks))
The BRANCH + fan-out section after the agent turn:
# 3) BRANCH — capture the agent's accumulated VM state.
branch_tag = f"autogen-branch-{int(time.time() * 1000)}"
t0 = time.monotonic()
branch = controller.branch_sandbox(sb_id, tag=branch_tag, diff=True)
branch_secs = time.monotonic() - t0
print(
f"[autogen-branch] BRANCH (diff=true) → tag={branch['tag']} "
f"(client-observed {branch_secs * 1000:.0f}ms)"
)
# 4) fan out grandchildren from the branch
kids = controller.spawn_sandboxes(
snapshot_tag=branch["tag"],
n=args.fanout,
per_child_netns=args.per_child_netns,
)
Setup and run:
pip install pyautogen forkd>=0.3.1
sudo bash scripts/netns-setup.sh 3
FORKD_TOKEN=$(sudo cat /etc/forkd/token) \
python3 recipes/autogen-branch/demo.py --fanout=3
Expected output (dry-run):
[autogen-branch] using snapshot 'coding-agent-fork-prewarm-v1'
[autogen-branch] source sandbox: sb-6a0d5598-0001
[autogen-branch] dry-run mode (no LLM key)
[autogen-branch] dry-run exec result (exit=0):
hello from autogen-branch
65536
[autogen-branch] BRANCH → tag=autogen-branch-1779242000123 (client-observed 472ms)
[autogen-branch] fanned out 3 grandchildren in 89ms
sb-...-0002: exit=0 stdout="from-forkd-vm\n(3, 12, 1)"
sb-...-0003: exit=0 stdout="from-forkd-vm\n(3, 12, 1)"
sb-...-0004: exit=0 stdout="from-forkd-vm\n(3, 12, 1)"
[autogen-branch] cleaned up 3 grandchildren
[autogen-branch] cleaned up source sandbox sb-6a0d5598-0001
| Executor | Per-call cold-start | Isolation | Mid-state fork |
|---|
LocalCommandLineCodeExecutor | ~0 ms | None | No |
DockerCommandLineCodeExecutor | 2–5 s | Strong | No |
ForkdCommandLineCodeExecutor | ~200 ms | Strong (microVM) | Yes, in ~200 ms |
OpenAI Swarm handoff
Swarm-style frameworks model handoffs as “agent A returns agent B from a tool call”. The next round runs B with the same conversation — but B’s environment is unchanged. In plain Swarm there’s no way for B to “pick up where A left off” in the sandbox; B starts from the same blank image A started from.
With forkd, the handoff is the snapshot point. BRANCH the sandbox at handoff time, spawn one child from the branch, rewire B’s tool to use the child. Now B inherits everything A built: filesystem writes to /tmp, loaded packages, set environment variables.
Pattern: ForkdRunner holds the current sandbox ID. do_handoff() BRANCHes it, spawns one child, and rewires runner.sandbox_id in-place so all subsequent tool calls from agent B land in the branched child without re-wrapping.
@dataclass
class ForkdRunner:
"""Holds a current sandbox handle. The handoff function rewires
`sandbox_id` to point at a new (branched) sandbox in-place."""
controller: Controller
sandbox_id: str
def run_python(self, code: str) -> dict[str, Any]:
"""Tool exposed to the Swarm agent."""
return self.controller.exec_command(
self.sandbox_id, ["python3", "-c", code], timeout_secs=20
)
def do_handoff(
controller: Controller,
runner: ForkdRunner,
*,
diff: bool = True,
per_child_netns: bool = True,
label: str = "handoff",
) -> str:
"""The forkd-specific part. Call from inside a handoff tool to
BRANCH the current sandbox + spawn one child from the branch.
Rewires `runner.sandbox_id` so subsequent tool calls land in the
branched child. Returns the new sandbox id.
`diff=True` is the v0.3 fast path (≈200 ms pause).
"""
tag = f"{label}-{int(time.time() * 1000)}"
t0 = time.monotonic()
branch = controller.branch_sandbox(runner.sandbox_id, tag=tag, diff=diff)
branch_ms = (time.monotonic() - t0) * 1000
t0 = time.monotonic()
child = controller.spawn_sandboxes(
snapshot_tag=branch["tag"], n=1, per_child_netns=per_child_netns
)[0]
spawn_ms = (time.monotonic() - t0) * 1000
print(
f"[handoff] branched + spawned child in {branch_ms:.0f}ms + "
f"{spawn_ms:.0f}ms (diff_physical={branch.get('diff_physical_bytes')}b)"
)
runner.sandbox_id = child["id"]
return child["id"]
The do_handoff call is identical whether you use openai-swarm (the archived reference implementation) or openai-agents (the active SDK). Only the orchestration loop differs between the two libraries.
Setup and run (dry-run):
pip install forkd>=0.3.2
sudo bash scripts/netns-setup.sh 1
FORKD_TOKEN=$(sudo cat /etc/forkd/token) \
python3 recipes/openai-swarm/demo.py --dry-run
Expected output:
[openai-swarm] using snapshot 'coding-agent-fork-prewarm-v1'
[openai-swarm] source sandbox: sb-...-0001
[openai-swarm] dry-run mode (--dry-run)
[dry-run] agent_researcher: write notes
stdout: wrote 78 bytes
[dry-run] handoff: researcher → summarizer (BRANCH inside)
[handoff] branched + spawned child in 270ms + 75ms (diff_physical=...)
[dry-run] agent_summarizer (now on sb-...-0002): read inherited notes
stdout: inherited tag: researcher-pass-1
sum: 31
[dry-run] sanity-check: a non-branched child has no /tmp/notes.json
fresh sandbox sees: CLEAN
✓ confirmed: BRANCH transferred state, fresh spawn did not
[openai-swarm] killed sb-...-0002
[openai-swarm] killed sb-...-0001
The CLEAN check is the proof that BRANCH is actually transferring state and not just spawning a fresh sibling from the same parent.
| Approach | Per-handoff cost | State inheritance | Isolation |
|---|
| Swarm + shared process | ~0 ms | Yes (same process — and same risk) | None |
| Swarm + Docker per agent | 2–5 s + cold filesystem | No — each container starts fresh | Strong |
| Swarm + forkd | ~200 ms (Diff BRANCH) | Yes — full VM state inherited | Strong (microVM) |
MCP server
forkd-mcp exposes the forkd controller as an MCP server for Claude Desktop, Claude Code, Cursor, Cline, and any other MCP-aware client. Once registered, the agent can fork and drive forkd microVMs directly in plain English.
Install:
Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"forkd": {
"command": "forkd-mcp",
"env": {
"FORKD_URL": "http://127.0.0.1:8889",
"FORKD_TOKEN": "<your-token>"
}
}
}
}
Tools exposed by forkd-mcp 0.2.0:
| Tool | Description |
|---|
spawn_sandboxes | Fork N children from a parent snapshot |
exec_command | Run a shell command inside a sandbox |
eval_code | Evaluate a Python expression against the warmed runtime |
branch_sandbox | BRANCH a running sandbox into a new snapshot |
list_snapshots | List registered parent snapshots |
list_sandboxes | List live sandbox instances |
get_sandbox | Get metadata for one sandbox |
kill_sandbox | Terminate a sandbox |
create_snapshot | Register a new snapshot from a rootfs + kernel |
wait_for_text | Poll a sandbox’s output for a string |
ping_sandbox | Health-check a sandbox |
Verify the MCP wiring end-to-end:
FORKD_TOKEN=$(sudo cat /etc/forkd/token) python3 recipes/mcp-agent/demo.py
This script drives forkd-mcp over stdio JSON-RPC exactly as Claude Desktop would — list_tools, spawn_sandboxes, exec_command, branch_sandbox(diff=true), fan-out, cleanup — and prints the BRANCH pause_ms so you can see the v0.3 diff-snapshot numbers in your environment.
See /reference/sdk/mcp for the full MCP server API reference, registration instructions for Cursor and Cline, and how to use the wait: false async BRANCH path from a conversational agent.
Jupyter kernel recipe
The jupyter-kernel/ recipe builds a parent from quay.io/jupyter/scipy-notebook — the canonical Jupyter image with the full SciPy stack (numpy, pandas, scipy, scikit-learn, matplotlib, seaborn, sympy, ipython) pre-imported. A “fresh kernel” goes from ~2 s of import time to ~1 ms per child fork.
from forkd import Sandbox
with Sandbox(tag="jk") as sb:
sb.eval("sklearn.datasets.load_iris().data.shape") # → (150, 4)
sb.eval("numpy.linalg.eigvals([[1,2],[3,4]]).tolist()")
This is the shape Anthropic Claude code-interpreter, OpenAI code-interpreter, and Modal-hosted notebook agents all run on — many short-lived kernel sessions, each needing the SciPy runtime ready immediately.
E2B code-interpreter compatibility
The e2b-codeinterpreter/ recipe builds a parent from E2B’s official code-interpreter image. forkd’s Python SDK is E2B wire-compatible: code using from e2b import Sandbox can switch to from forkd import Sandbox and run against this parent unchanged.
from forkd import Sandbox # drop-in for `from e2b import Sandbox`
with Sandbox() as sb:
r = sb.commands.run("python3 -c 'import pandas; print(pandas.__version__)'")
print(r.stdout)
This is the lightest “agent-ready” parent at ~600 MB, making it the fastest to snapshot and the most CoW-efficient at large N.