Skip to main content

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
ApproachPer-agent cold-startIsolationDisk overhead
CrewAI default (threads)~0 msNone — shared GIL, sys.path, envNone
CrewAI + Docker per agent2–5 sStrongFull image × N
CrewAI + forkd~200 msStrong (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
ExecutorPer-call cold-startIsolationMid-state fork
LocalCommandLineCodeExecutor~0 msNoneNo
DockerCommandLineCodeExecutor2–5 sStrongNo
ForkdCommandLineCodeExecutor~200 msStrong (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.
ApproachPer-handoff costState inheritanceIsolation
Swarm + shared process~0 msYes (same process — and same risk)None
Swarm + Docker per agent2–5 s + cold filesystemNo — each container starts freshStrong
Swarm + forkd~200 ms (Diff BRANCH)Yes — full VM state inheritedStrong (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:
pip install forkd-mcp
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:
ToolDescription
spawn_sandboxesFork N children from a parent snapshot
exec_commandRun a shell command inside a sandbox
eval_codeEvaluate a Python expression against the warmed runtime
branch_sandboxBRANCH a running sandbox into a new snapshot
list_snapshotsList registered parent snapshots
list_sandboxesList live sandbox instances
get_sandboxGet metadata for one sandbox
kill_sandboxTerminate a sandbox
create_snapshotRegister a new snapshot from a rootfs + kernel
wait_for_textPoll a sandbox’s output for a string
ping_sandboxHealth-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.

Build docs developers (and LLMs) love