froog / Integrations

LangChain and LangGraph

Give a LangChain agent a Linux machine of its own. Three @tool functions run commands and read and write files in a froog sandbox, a full Ubuntu VM with sudo, Docker and internet. create_agent builds a LangGraph graph, so the same tools work with threads and checkpointers.

Install

Python 3.11 or newer. The froog SDK is one file with no dependencies: download it next to your script, as the SDK docs say.

$ python3 -m venv .venv && source .venv/bin/activate
$ pip install langchain langchain-openai
$ curl -fsSLO https://froog.tech/sdk/froog.py   # the froog client: one file, standard library only
$ export OPENAI_API_KEY=...        # your model provider's key
$ export FROOG_API_KEY=froog_...   # dashboard, API keys; or: froog api-key create agents --preset ci

A complete example

Save as langchain_froog.py next to froog.py and run python langchain_froog.py. Any chat model works: change the model string and install its package.

from langchain.agents import create_agent
from langchain.tools import tool

from froog import Froog, FroogError

froog = Froog()  # reads FROOG_API_KEY from the environment
LIMIT = 20_000   # characters of output handed back to the model


def open_sandbox(name: str) -> str:
    """Make sure the sandbox `name` is running: create it, or start it if it was stopped."""
    try:
        sb = froog.get_sandbox(name)
    except FroogError as e:
        if e.status != 404:
            raise
        froog.create_sandboxes({
            "name": name,
            "size": "small",          # 2 vCPU, 4 GB
            "ttl_secs": 1800,         # auto-stop 30 minutes after it starts; the disk is kept
            "environment": False,     # none of your account's variables or files
            "labels": {"app": "langchain-demo"},
        })
        sb = {"state": "creating"}
    if sb["state"] == "stopping":
        froog.wait_until(name, "stopped")
        sb["state"] = "stopped"
    if sb["state"] == "stopped":
        froog.start_sandbox(name, {})
    froog.wait_until(name, "running")
    return name


def clip(text: str) -> str:
    return text if len(text) <= LIMIT else "[...cut...]\n" + text[-LIMIT:]


def sandbox_tools(sandbox: str) -> list:
    """Tools bound to one froog sandbox."""

    @tool(parse_docstring=True)
    def run_command(command: str, timeout_secs: int = 300) -> str:
        """Run a bash command in your Linux sandbox (Ubuntu 24.04, passwordless sudo, Docker, internet).
        The working directory is /home/user. Returns the exit code, stdout and stderr.

        Args:
            command: The bash command to run, e.g. "python3 app.py" or "pip install requests".
            timeout_secs: Seconds before the command is killed (1 to 3600).
        """
        r = froog.exec(sandbox, {"command": command, "timeout_secs": timeout_secs})
        out = f"exit code {r['exit_code']}" + (" (timed out)" if r.get("timed_out") else "")
        if r["stdout"]:
            out += "\nstdout:\n" + clip(r["stdout"])
        if r["stderr"]:
            out += "\nstderr:\n" + clip(r["stderr"])
        return out

    @tool(parse_docstring=True)
    def write_file(path: str, content: str) -> str:
        """Create or overwrite a text file in the sandbox. Parent folders are created.

        Args:
            path: Absolute, or relative to /home/user.
            content: The whole file.
        """
        froog.write_file(sandbox, content.encode(), path)
        return f"wrote {len(content)} characters to {path}"

    @tool(parse_docstring=True)
    def read_file(path: str) -> str:
        """Read a text file from the sandbox.

        Args:
            path: Absolute, or relative to /home/user.
        """
        return clip(froog.read_file(sandbox, path).decode("utf-8", errors="replace"))

    return [run_command, write_file, read_file]


def main() -> None:
    sandbox = open_sandbox("chat-demo")  # one sandbox per conversation (or per LangGraph thread)
    try:
        agent = create_agent(
            model="openai:gpt-6.1-sol",
            tools=sandbox_tools(sandbox),
            system_prompt="You have a Linux sandbox. Use your tools to write and run code, and check results by running them.",
        )
        result = agent.invoke(
            {"messages": [{"role": "user", "content": "Write primes.py that prints the first 20 primes, run it, and tell me the output."}]},
            {"recursion_limit": 40},
        )
        print(result["messages"][-1].text)
    finally:
        froog.delete_sandbox(sandbox)  # or froog.stop_sandbox(sandbox) to keep the disk


if __name__ == "__main__":
    main()

One sandbox per LangGraph thread

With a checkpointer, LangGraph keeps each thread's messages. Key the sandbox by the same thread id and the files stay with the conversation:

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

from langchain_froog import open_sandbox, sandbox_tools  # the example above, saved as langchain_froog.py

checkpointer = InMemorySaver()  # use a database-backed checkpointer in production


def chat(thread_id: str, text: str) -> str:
    """One turn of a conversation. The same thread always gets the same sandbox."""
    sandbox = open_sandbox(f"thread-{thread_id}")  # thread ids: lowercase letters, digits, dashes
    agent = create_agent(
        model="openai:gpt-6.1-sol",
        tools=sandbox_tools(sandbox),
        system_prompt="You have a Linux sandbox. Use your tools to write and run code.",
        checkpointer=checkpointer,
    )
    result = agent.invoke(
        {"messages": [{"role": "user", "content": text}]},
        {"configurable": {"thread_id": thread_id}, "recursion_limit": 40},
    )
    return result["messages"][-1].text

How it works

  • open_sandbox gets the conversation's sandbox running: it creates it (a small Ubuntu VM, ready in under a minute), or starts it if it was stopped, and waits for running.
  • Three tools bound to that sandbox: run a command (POST /v1/sandboxes/{name}/exec, up to an hour, output and exit code back), write a file and read a file (PUT and GET /v1/sandboxes/{name}/files/content). Output is cut to the last 20,000 characters so a noisy build doesn't fill the context window.
  • Cleanup in finally: the example deletes the sandbox. In a real app, stop it or let its auto-stop timer stop it (see the notes).

Notes

  • One sandbox per conversation or user. Name it after the conversation, thread or user id (chat-42, user-7: 1–40 lowercase letters, digits and dashes) so every turn lands in the same machine with the same files. open_sandbox above creates it the first time and starts it again if it was stopped. Labels ("labels": {"user": "7"}) let you find a user's sandboxes later with GET /v1/sandboxes?label=user=7.
  • Stop instead of delete when the user may come back. The example's ttl_secs: 1800 stops the sandbox 30 minutes after it starts, busy or not (PATCH a new ttl_secs to push it back while the conversation goes on). Its disk is kept for 14 days, a start takes about 15–30 seconds (about a second within 10 minutes of the stop), and installed packages and files are still there. Delete it when the conversation is over for good.
  • Cost. Running time is billed per second, only while the sandbox runs: small (2 vCPU, 4 GB) is $0.0137 an hour, default (4 vCPU, 8 GB) $0.0274. A 10-minute conversation on small costs about a quarter of a cent; the idle time before the TTL stops it counts as running time, so pick a TTL that matches how long users pause. A stopped sandbox costs no running time. See pricing.
  • Templates. Install what your agent needs once (Python packages, a repo, a database), then save it: froog snapshot my-box --name agent-base. Create each conversation's sandbox with "from": "agent-base" and it starts with all of it, instead of the agent spending its first turns on pip install. See snapshots.
  • Keep your secrets out. "environment": false starts the sandbox without your account's environment variables, repos and secret files, since the model runs whatever it likes in there. Pass only what that sandbox needs in env. Use a scoped API key for your backend (froog api-key create agents --preset ci can only touch the sandboxes it created) and never send it to a browser. See API keys.
  • More tools. The same pattern covers the rest of the API: POST /sandboxes/{name}/urls (host_port) gives a running web server a public URL the agent can return to the user, start_job runs servers and long builds in the background, exec_stream streams output as it arrives, and fork_sandbox copies a sandbox to try several approaches at once. Every call is in the HTTP API.
  • Your own graph. In a hand-built LangGraph StateGraph, put the same tools in a ToolNode, and keep the sandbox name in the graph's state or config.

Questions

Does this work with LangGraph, not just LangChain?

Yes. create_agent returns a compiled LangGraph graph, and the tools are ordinary LangChain tools, so they also go into a ToolNode in your own StateGraph. Keep one sandbox per thread id.

Is there a froog LangChain package?

Not yet. The tools are about 40 lines on top of froog's single-file Python SDK, shown in full on this page.

7 days free: 25 hours of running time, 2 sandboxes at once. Then from $0.0137 an hour, billed per second.