<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The AI evolution]]></title><description><![CDATA[Artificial Intelligence is evolving rapidly, but most people still confuse LLMs, RAG, AI Agents, and MCP. In this article, we break down the complete AI evolution in simple terms and explain how modern AI systems actually work.]]></description><link>https://aievolution.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>The AI evolution</title><link>https://aievolution.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 21 Jun 2026 13:34:46 GMT</lastBuildDate><atom:link href="https://aievolution.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[

Why Most Multi-Agent AI Systems Waste 90% of Their Time (And How to Fix It)]]></title><description><![CDATA[I got five AI agents running in parallel, each analyzing a codebase. Total wall-clock time barely improved over sequential.
The code was clean. The concurrency was correct. And the number barely moved]]></description><link>https://aievolution.hashnode.dev/why-most-multi-agent-ai-systems-waste-90-of-their-time-and-how-to-fix-it</link><guid isPermaLink="true">https://aievolution.hashnode.dev/why-most-multi-agent-ai-systems-waste-90-of-their-time-and-how-to-fix-it</guid><category><![CDATA[AI]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[technology]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Divy]]></dc:creator><pubDate>Mon, 15 Jun 2026 12:26:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699c941340e1f055acc5dded/cf0efb0b-09e3-4c08-8a87-8eed3baedc52.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I got five AI agents running in parallel, each analyzing a codebase. Total wall-clock time barely improved over sequential.</p>
<p>The code was clean. The concurrency was correct. And the number barely moved. Each agent had spent the first 90 seconds installing analysis tools before running a single line of actual work</p>
<p>You can solve the concurrency problem perfectly and still lose on setup time.</p>
<p>Five agents running in parallel still take only 90 seconds to set up. But across all five VMs, that's 450 seconds of compute spent repeating the same work. The clock doesn't slow down, but the infrastructure cost does.</p>
<p>The fix was not more threads.</p>
<p>It was a memory snapshot. Build the environment once, checkpoint the entire VM state (filesystem, memory, running processes), and fork all five agents from that single frozen moment. Each fork warm-restores rapidly with the tools already loaded. No re-installs. No cold boots.</p>
<p>Here is what that looks like, what took me three iterations to get right, and where it still has rough edges.</p>
<p>Let's get the mental model first.</p>
<hr />
<h2>What This Does (30 Seconds)</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/723qzlf8hzaaojpd9bef.png" alt="Mental Model" /></p>
<p><strong>The idea is straightforward:</strong> instead of five agents each spending 90 seconds installing the same tools, install them once, freeze that environment, and stamp out five identical copies.</p>
<p>Each copy runs a different analysis in parallel. A lead LLM reads all five results and tells you what to fix first.</p>
<p>In code:</p>
<ol>
<li><p>Creates one Linux VM, installs code analysis tools (bandit, radon) and writes a sample Python project</p>
</li>
<li><p>Freezes the entire VM state into a memory snapshot (filesystem, memory, running processes included)</p>
</li>
<li><p>Forks 5 independent copies, each agent assigned a different analysis task (Security, Complexity, Docstrings, Tests, Structure)</p>
</li>
<li><p>Runs all 5 in parallel via <code>asyncio.gather</code>, finishing in seconds instead of minutes</p>
</li>
<li><p>Feeds all results to a lead LLM that produces a single prioritized fix list</p>
</li>
</ol>
<p>Setup time is paid once, upfront, before any agent runs. The rest of this article explains how.</p>
<hr />
<h2>Why Sandboxes Matter for Agent Workloads</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxnabbx4p5f54wx7rfy8.png" alt="Sandboxes" /></p>
<p>If you have not worked with sandboxes before: think of one as a disposable computer that lives in the cloud.</p>
<p>You spin it up, run whatever code you need inside it, and throw it away when you're done. It has its own filesystem, its own processes, its own network. Nothing it does can touch your machine or any other sandbox running at the same time.</p>
<p>In short: Sandboxes provide the agent with a secure and isolated enviornement</p>
<p>That isolation is the whole point. Your agent can install packages, write files, crash badly, or spin up a browser, and none of it bleeds out. When the task is done, you terminate the VM and it is gone.</p>
<p>The next agent starts clean.</p>
<p>Most agent frameworks treat the execution environment as an afterthought. The LLM call is the interesting part. The environment is just "wherever the code runs."</p>
<p>That works fine for single-turn tasks. It breaks down fast for anything multi-step.</p>
<p>When an agent needs to install packages, write intermediate files, maintain a browser session across multiple pages, or resume a task from a different machine, you need the execution environment to behave like a persistent object, not a function call that resets on every invocation.</p>
<p>Tensorlake gives each agent a MicroVM backed by Firecracker and CloudHypervisor, optimized for fast boot times and strong isolation. Each sandbox is a full Linux VM. It boots in hundreds of milliseconds, persists filesystem and memory state across sessions, and can be snapshotted at any point in its lifecycle.</p>
<p>Tensorlake also lets you spin up multiple sandboxes in parallel for concurrent agent execution, and honestly it is one of my favourite things about it.</p>
<p>it also ranks in the top 5 of <a href="https://www.computesdk.com/benchmarks/sandboxes/">SandboxBenchmarks</a>.</p>
<p><strong>What changes the math is a single question:</strong> what does the snapshot actually capture?</p>
<hr />
<h2>Two Kinds of Snapshots. Very Different Behavior.</h2>
<p>Quick vocabulary before the details. Tensorlake sandboxes have four lifecycle modes.</p>
<ul>
<li><p>An <strong>ephemeral</strong> sandbox runs a task and disappears when done, with no name and no persistence between runs.</p>
</li>
<li><p>A <strong>named</strong> sandbox outlives the process that created it and can be suspended then reconnected to from any machine. <strong>Suspend</strong> freezes the VM exactly as it is and <strong>resume</strong> brings it back to that same state.</p>
</li>
<li><p>A <strong>snapshot</strong> is that frozen moment saved as a reusable artifact.</p>
</li>
<li><p>A <strong>fork</strong> is a snapshot restored into a fresh, independent VM.</p>
</li>
</ul>
<p>This project uses the last two.</p>
<p><strong>Suspend</strong> and <strong>Snapshot</strong> both preserve state, but serve different purposes : Suspend is for pausing <em>this</em> sandbox to resume later, while a snapshot is a reusable artifact for retrying from a checkpoint or cloning an environment.</p>
<p>Tensorlake supports two checkpoint types. Most tutorials only mention one.</p>
<ul>
<li><p><code>CheckpointType.FILESYSTEM</code> captures disk state only. Restore from it and the new sandbox does a full cold boot: processes restart from scratch, packages get re-imported. Your pip installs survive. Nothing that was in memory does.</p>
</li>
<li><p><code>CheckpointType.MEMORY</code> is different. It captures disk state, VM memory, and all running processes. The restored VM resumes mid-stride, exactly as the source was at checkpoint time. No boot sequence. No re-initialization. If Python had already imported bandit, the fork starts with it loaded. The environment is not rebuilt. It is copied.</p>
</li>
</ul>
<blockquote>
<p>The checkpoint type is not a performance detail. It determines whether your fork is a clone or a restart.</p>
</blockquote>
<p>The default when you call <code>sandbox.checkpoint()</code> with no arguments is filesystem. That is the wrong choice for a parallel swarm where agents share a prepared environment. You want memory.</p>
<p>One more constraint worth knowing upfront: for memory snapshots, resources (CPUs, RAM) are baked into the snapshot at checkpoint time. You cannot override them when creating forks. Set the right <code>cpus</code> and <code>memory_mb</code> on the base sandbox before you checkpoint. Every fork inherits them automatically.</p>
<hr />
<h2>The Architecture</h2>
<p>The pattern has five distinct phases. Each one has a single responsibility.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lg2978l9dp9f37mi2mb6.png" alt="Architecture" /></p>
<p><strong>Phase 1 — Base Snapshot:</strong> Spins up a single baseline sandbox, installs analysis tools (<code>bandit</code>, <code>radon</code>), writes the target code, and checkpoints the entire running VM state using <code>CheckpointType.MEMORY</code>. The base sandbox is then terminated, leaving behind the reusable snapshot ID.</p>
<p><strong>Phase 2 — Agent Forking:</strong> Restores 5 independent sandboxes concurrently from the base snapshot using <code>sandbox.fork(...)</code>. Each fork is a warm start that inherits all installed tools, environment settings, and target files.</p>
<p><strong>Phase 3 — Sequential Baseline (Timing):</strong> Runs each agent's analysis script (<code>analyze.py</code>) one-by-one inside its respective sandbox to measure sequential time as a benchmark denominator.</p>
<p><strong>Phase 4 — Parallel Swarm:</strong> Executes all 5 agents concurrently using <code>asyncio.gather(...)</code>. Each agent runs the same analysis script inside its isolated sandbox but with a different focus configuration passed via the <code>PERSPECTIVE</code> environment variable.</p>
<p><strong>Phase 5 — LLM Aggregation:</strong> Collects the individual reports (Security, Complexity, Docstrings, Tests, Structure) alongside the timing data, and passes them to the lead LLM (GPT) to synthesize a single prioritized fix list.</p>
<p>Phase 1 runs once. Phases 2 through 4 run every time you want results. The fork is cheap. The base environment build is not, but you only pay that cost once per snapshot.</p>
<hr />
<h2>Phase 1: Build and Snapshot</h2>
<p>The base sandbox installs the analysis tools, writes the target codebase into the VM, then snapshots the entire state. Every fork inherits both the tools and the target project automatically.</p>
<pre><code class="language-python">from tensorlake.sandbox import AsyncSandbox, CheckpointType

async def build_base_snapshot() -&gt; str:
    async with await AsyncSandbox.create(
        name="base-swarm-env",
        cpus=2.0,
        memory_mb=2048,
        timeout_secs=600,
    ) as sandbox:

        # Install analysis tools. These are baked into the snapshot
        # and available to every forked agent at no extra install cost.
        result = await sandbox.run(
            "pip",
            ["install", "bandit", "radon", "--user", "--break-system-packages", "-q"],
            timeout=180,
        )
        if result.exit_code != 0:
            raise RuntimeError(f"pip install failed:\n{result.stderr}")

        # Write a sample Python project with intentional issues for agents to find.
        # All forks inherit this from the snapshot; no need to write per-agent.
        target_files = {
            "/workspace/target/auth.py": b'''
import subprocess
DB_PASSWORD = "hardcoded_secret_123"

def authenticate(user_input):
    return eval(user_input)

def run_command(cmd):
    return subprocess.call(cmd, shell=True)
''',
            "/workspace/target/logic.py": b'''
def classify(a, b, c, d, e, f, g, h):
    if a and b:
        if c or d:
            if not e and f:
                return "path_a"
            elif e and not f:
                return "path_b"
            elif g and h:
                return "path_c"
            else:
                return "path_d"
        elif g:
            return "path_e"
    return "path_f"
''',
        }
        for path, content in target_files.items():
            parent = "/".join(path.split("/")[:-1])
            await sandbox.run("mkdir", ["-p", parent])
            await sandbox.write_file(path, content)

        # Verify tools work before snapshotting.
        # A broken tool in the snapshot means broken forks.
        verify = await sandbox.run(
            "python3", ["-m", "bandit", "--version"]
        )
        if verify.exit_code != 0:
            raise RuntimeError(f"Tool verification failed:\n{verify.stderr}")

        snapshot = await sandbox.checkpoint(
            checkpoint_type=CheckpointType.MEMORY
        )

    # Context manager terminates the base sandbox here.
    if snapshot.status.value != "completed":
        raise RuntimeError(f"Snapshot failed: {snapshot.status.value}")

    return snapshot.snapshot_id
</code></pre>
<p>The <code>async with</code> pattern guarantees <code>terminate()</code> is called on exit, including on exceptions. Without it, any exception before a manual <code>terminate()</code> call leaves an orphaned VM running in the background. TensorLake's async documentation shows this pattern explicitly.</p>
<p><code>result.exit_code</code> comes from <code>CommandResult</code>, the SDK's return type for <code>run()</code>. It has <code>stdout: str</code>, <code>stderr: str</code>, and <code>exit_code: int</code>. Note that <code>stdout</code> is already a string, not bytes, so no <code>.decode()</code> is needed anywhere.</p>
<p>The status check after <code>checkpoint()</code>: <code>SnapshotStatus</code> is an enum, so <code>.value</code> gives you <code>"completed"</code>, <code>"in_progress"</code>, or <code>"failed"</code>. The documentation shows <code>checkpoint()</code> returns a <code>SnapshotInfo</code> with a <code>status</code> field. Checking that status before proceeding is a useful defensive practice. I learned this after a failed snapshot left me debugging downstream agent failures.</p>
<hr />
<h2>Phase 2: Fork and Run an Agent</h2>
<p>This is the actual fork. The call is <code>AsyncSandbox.create(snapshot_id=snapshot_id)</code>. No special <code>fork()</code> method. No copy-on-write API. Just <code>create()</code> with a snapshot ID. Every call produces a fully independent VM starting from that snapshot's frozen state.</p>
<pre><code class="language-python">PERSPECTIVES = ["Security", "Complexity", "Docstrings", "Tests", "Structure"]

async def run_agent(agent_id: int, snapshot_id: str) -&gt; AgentReport:
    perspective = PERSPECTIVES[agent_id % len(PERSPECTIVES)]
    t_start = time.time()

    # cpus and memory_mb intentionally omitted.
    # For MEMORY snapshots, resources are inherited from the snapshot
    # and cannot be overridden at restore time.
    async with await AsyncSandbox.create(
        snapshot_id=snapshot_id,
        allow_internet_access=False,  # code analysis is offline; no outbound needed
        timeout_secs=120,
    ) as sandbox:

        await sandbox.write_file(
            "/workspace/analyze.py",
            ANALYSIS_SCRIPT.encode("utf-8")
        )

        result = await sandbox.run(
            "python3",
            ["/workspace/analyze.py"],
            env={"PERSPECTIVE": perspective},
            timeout=60,
        )

    elapsed = time.time() - t_start

    if result.exit_code != 0:
        raise RuntimeError(f"Agent {agent_id} failed:\n{result.stderr}")

    output = json.loads(result.stdout.strip())
    return AgentReport(
        agent_id=agent_id,
        perspective=perspective,
        score=output["score"],
        finding=output["finding"],
        execution_time_s=elapsed,
    )
</code></pre>
<p><code>allow_internet_access=False</code> is safe here because bandit and radon analyze source code and do not make network calls. This parameter is not locked by MEMORY snapshots. TensorLake's networking documentation recommends disabling outbound internet access for untrusted code.</p>
<p>The dispatch script gets written fresh into each forked VM via <code>sandbox.write_file()</code>. Each agent's VM is fully isolated: writing to <code>/workspace/analyze.py</code> in fork 0 has no effect on fork 1. The target project files are already there, inherited from the snapshot.</p>
<p>Since <code>result.stdout</code> is already a Python string, <code>json.loads(result.stdout.strip())</code> works directly. The <code>.strip()</code> handles the trailing newline from <code>print()</code> inside the sandbox.</p>
<hr />
<h2>Phase 3: Sequential First, Then Parallel</h2>
<p>The sequential baseline exists for one reason: to give the speedup calculation a real denominator. Without it, you have a time with no context.</p>
<pre><code class="language-python">async def run_sequential(snapshot_id: str, count: int) -&gt; SwarmResult:
    reports = []
    for i in range(count):
        reports.append(await run_agent(i, snapshot_id))
    return SwarmResult(mode="sequential", ...)

async def run_parallel(snapshot_id: str, count: int) -&gt; SwarmResult:
    # asyncio.gather returns a list of results when awaited.
    reports = await asyncio.gather(
        *(run_agent(i, snapshot_id) for i in range(count))
    )
    reports.sort(key=lambda r: r.agent_id)
    return SwarmResult(mode="parallel", ...)
</code></pre>
<p><code>asyncio.gather</code> is what TensorLake's async documentation recommends for concurrent sandbox fan-out. The <code>ThreadPoolExecutor</code> approach works too (the sync Sandbox API supports it), but if you are already in an async context, <code>gather</code> is cleaner.</p>
<hr />
<h2>Phase 4:What the Analysis Script Does</h2>
<p>The dispatch script runs inside each forked sandbox. It reads the <code>PERSPECTIVE</code> environment variable, routes to the right analysis function, and prints one JSON line to stdout. All five analyses are fully offline, with no network calls needed.</p>
<pre><code class="language-python"># ANALYSIS_SCRIPT — runs INSIDE each forked sandbox
import json, os, subprocess, ast, pathlib, sys

PERSPECTIVE = os.environ["PERSPECTIVE"]
TARGET = "/workspace/target"

def run_security():
    """bandit: find hardcoded secrets, unsafe eval, shell injection."""
    r = subprocess.run(
        ["python3", "-m", "bandit", "-r", TARGET, "-f", "json", "-q"],
        capture_output=True, text=True
    )
    try:
        data = json.loads(r.stdout)
    except json.JSONDecodeError:
        return {"score": 0, "finding": "bandit parse error"}
    issues = data.get("results", [])
    high = [i for i in issues if i.get("issue_severity") == "HIGH"]
    return {
        "issues": len(issues), "high": len(high),
        "score": max(0, 100 - len(issues) * 10),
        "finding": high[0]["issue_text"] if high else ("Minor issues" if issues else "Clean"),
    }

def run_complexity():
    """radon: cyclomatic complexity per function."""
    r = subprocess.run(
        ["python3", "-m", "radon", "cc", TARGET, "-j"],
        capture_output=True, text=True
    )
    try:
        data = json.loads(r.stdout)
    except json.JSONDecodeError:
        return {"score": 0, "finding": "radon parse error"}
    blocks = [b for file_blocks in data.values() for b in file_blocks]
    complex_blocks = [b for b in blocks if b.get("complexity", 0) &gt; 5]
    avg = sum(b["complexity"] for b in blocks) / len(blocks) if blocks else 0
    top = f"{complex_blocks[0]['name']} (cc={complex_blocks[0]['complexity']})" if complex_blocks else "All within threshold"
    return {
        "functions": len(blocks), "complex_count": len(complex_blocks),
        "avg_cc": round(avg, 2),
        "score": max(0, 100 - len(complex_blocks) * 15),
        "finding": top,
    }

def run_docstrings():
    """ast: count functions and classes that lack docstrings."""
    total, documented = 0, 0
    for path in pathlib.Path(TARGET).rglob("*.py"):
        tree = ast.parse(path.read_text())
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
                total += 1
                if ast.get_docstring(node):
                    documented += 1
    pct = int(documented / total * 100) if total else 100
    return {"total": total, "documented": documented, "score": pct,
            "finding": f"{documented}/{total} documented ({pct}%)"}

def run_tests():
    """Count test files relative to source files."""
    all_py = list(pathlib.Path(TARGET).rglob("*.py"))
    test_files = [f for f in all_py if f.stem.startswith("test_") or f.stem.endswith("_test")]
    ratio = len(test_files) / len(all_py) * 100 if all_py else 0
    return {
        "source_files": len(all_py), "test_files": len(test_files),
        "score": min(100, int(ratio * 2)),
        "finding": f"{len(test_files)}/{len(all_py)} files are tests ({ratio:.0f}%)",
    }

def run_structure():
    """ast: count functions, classes, imports across the codebase."""
    stats = {"functions": 0, "classes": 0, "imports": 0, "files": 0}
    for path in pathlib.Path(TARGET).rglob("*.py"):
        stats["files"] += 1
        tree = ast.parse(path.read_text())
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):          stats["functions"] += 1
            elif isinstance(node, ast.ClassDef):           stats["classes"] += 1
            elif isinstance(node, (ast.Import, ast.ImportFrom)): stats["imports"] += 1
    fpr = stats["functions"] / stats["files"] if stats["files"] else 0
    return {**stats, "functions_per_file": round(fpr, 1),
            "score": min(100, int(fpr * 20)),
            "finding": f"{stats['functions']} functions across {stats['files']} files"}

dispatch = {
    "Security":   run_security,
    "Complexity": run_complexity,
    "Docstrings": run_docstrings,
    "Tests":      run_tests,
    "Structure":  run_structure,
}

fn = dispatch.get(PERSPECTIVE)

if fn is None:
    print(json.dumps({"error": f"Unknown perspective: {PERSPECTIVE}"}))
    sys.exit(1)

result = fn()
result["perspective"] = PERSPECTIVE
print(json.dumps(result))
</code></pre>
<p>Two things worth keeping when you adapt this.</p>
<p>Parameters via environment variables: <code>sandbox.run(env={"KEY": "val"})</code> passes per-command variables and avoids shell escaping issues when values contain spaces or special characters. It also keeps the dispatch script stateless, with no hardcoded perspective names inside the script itself.</p>
<p>JSON to stdout: the orchestrator reads <code>result.stdout.strip()</code> and passes it directly to <code>json.loads()</code>. The script has one job: print exactly one valid JSON line. Any other stdout output (debug prints, progress bars) breaks the parse. Keep it strict.</p>
<hr />
<h2>Phase 5: Lead Agent Synthesis</h2>
<p>After all five agents return, a single GPT-4o call synthesizes their findings into a prioritized action list.</p>
<pre><code class="language-python">def aggregate_with_llm(parallel: SwarmResult, sequential: SwarmResult) -&gt; str:
    client = OpenAI()
    speedup = sequential.total_time_s / parallel.total_time_s

    reports_block = "\n".join(
        f"[{r.perspective}] Score: {r.score}/100 | {r.finding}"
        for r in parallel.reports
    )

    prompt = (
        "You are a senior engineering lead reviewing a parallel code analysis report.\n\n"
        f"Agent Findings:\n{reports_block}\n\n"
        "Benchmark:\n"
        f"  Sequential : {sequential.total_time_s:.2f}s\n"
        f"  Parallel   : {parallel.total_time_s:.2f}s\n"
        f"  Speedup    : {speedup:.2f}x\n\n"
        "Provide: overall codebase health score, top three issues to fix immediately "
        "(with file and severity), recommended next actions, and one sentence on what "
        "the parallel speedup means for running this at scale."
    )

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
    )
    return response.choices[0].message.content
</code></pre>
<p>The lead agent sees both the analysis findings and the timing benchmark in the same context. That is the reduce step in a map-reduce agent pattern: give the aggregator everything the workers produced, not just the domain data. The call is synchronous because there is nothing left to concurrently await at this point.</p>
<hr />
<h2>Where the Time Actually Goes</h2>
<p>Both timelines contain the same agents doing the same work. What changes is when setup happens. These numbers are structural projections based on typical pip install times and sandbox warm-restore behavior, not measured results. Your numbers will vary by workload and network conditions. Run the demo to measure your case.</p>
<p>Without memory snapshots:</p>
<pre><code class="language-other">Agent 0: [setup ~90s][work ~8s]
Agent 1: [setup ~90s][work ~9s]
Agent 2: [setup ~90s][work ~8s]
Agent 3: [setup ~90s][work ~9s]
Agent 4: [setup ~90s][work ~8s]

Sequential total: ~490s
Parallel total:   ~100s  (setup still paid by each fork separately)
</code></pre>
<p>With memory snapshots (MEMORY type):</p>
<pre><code class="language-other">Base build:  [setup ~90s][checkpoint ~3s]  ← paid once, outside the loop
Agent 0: [warm fork ~1s][work ~8s]
Agent 1: [warm fork ~1s][work ~9s]
Agent 2: [warm fork ~1s][work ~8s]
Agent 3: [warm fork ~1s][work ~9s]
Agent 4: [warm fork ~1s][work ~8s]

Sequential total: ~48s
Parallel total:   ~10s
</code></pre>
<p>The speedup ratio looks similar on paper. The absolute time is not. At five agents the gap is 450 seconds versus 5 seconds of overhead. At fifty agents it is 4,500 seconds versus 50 seconds.</p>
<blockquote>
<p>Setup time does not scale down with parallelism. It multiplies. The snapshot moves it outside the loop entirely.</p>
</blockquote>
<p><strong>The benchmark captures four numbers:</strong> sequential total time (the denominator), parallel total time (wall-clock from first fork to last return), speedup (sequential divided by parallel), and efficiency (speedup divided by agent count, multiplied by 100).</p>
<p>Efficiency is the one most benchmarks skip. A 4.2x speedup across five agents is 84% parallel efficiency: 16% is lost to fork startup, scheduling, and I/O contention. That number matters when you scale from five agents to fifty.</p>
<hr />
<h2>What the Code Does Not Handle</h2>
<p>The demo covers the happy path. Three things to add before production:</p>
<ul>
<li><p><strong>LLM rate limits.</strong> Twenty or thirty concurrent agents all hitting the OpenAI API will trigger rate limit errors. The demo has no retry logic. Add exponential backoff before you scale.</p>
</li>
<li><p><strong>Snapshot storage.</strong> Snapshots may incur charges depending on your plan. Use <code>Sandbox.delete_snapshot(snapshot_id)</code> when done. The demo has a <code>CLEANUP_SNAPSHOT_ON_EXIT</code> flag at the top of the file.</p>
</li>
<li><p><strong>Agent error isolation.</strong> If one <code>run_agent()</code> coroutine raises inside <code>asyncio.gather</code>, the whole batch fails. In production, wrap each coroutine with <code>asyncio.create_task()</code> and handle errors per-agent.</p>
</li>
</ul>
<hr />
<h2>When to Use This Pattern (And When Not To)</h2>
<p>Use it when:</p>
<ul>
<li><p>Multiple agents need the same environment</p>
</li>
<li><p>Their tasks are independent (no inter-agent communication mid-run)</p>
</li>
<li><p>Setup time is a meaningful fraction of total runtime</p>
</li>
<li><p>Reproducibility matters: every fork starts from an identical state</p>
</li>
</ul>
<p>Skip it when:</p>
<ul>
<li><p>Agents need to share state during execution. Forks are fully isolated. If agent 2 needs to react to what agent 1 found, use shared storage or message queues instead.</p>
</li>
<li><p>The task is fast enough for a single agent. Forking five sandboxes for a 3-second job adds overhead, not speed.</p>
</li>
<li><p>Environment setup takes under 5 seconds. The snapshot overhead only pays off when setup is the actual bottleneck.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th><strong>Your situation</strong></th>
<th><strong>Right choice</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Multiple agents, shared dependencies, independent outputs</td>
<td>Memory snapshot, fork N copies</td>
</tr>
<tr>
<td>Single agent, long task, needs to pause and resume</td>
<td>Named sandbox with suspend/resume</td>
</tr>
<tr>
<td>Pure browser automation, no code execution</td>
<td>Stagehand or BrowserBase</td>
</tr>
<tr>
<td>Stateless task, resets every run</td>
<td>Ephemeral sandbox, no snapshot needed</td>
</tr>
<tr>
<td>Environment setup under 5 seconds</td>
<td>Filesystem snapshot or skip snapshots</td>
</tr>
</tbody></table>
<p>On filesystem performance: Tensorlake publishes performance benchmarks on their GitHub comparing sandbox execution times across providers. Refer to their repository for current numbers.</p>
<hr />
<h2>Running This</h2>
<pre><code class="language-bash">pip install tensorlake openai
export TENSORLAKE_API_KEY="your-key"
export OPENAI_API_KEY="your-key"
python3 agent.py
</code></pre>
<p>Free tier at cloud.tensorlake.ai, no credit card required. The demo takes 3-5 minutes end to end. After it runs, <code>benchmark_results.json</code> has the full per-agent timing data.</p>
<p>Phase 1 (base build and snapshot) runs once. If you want to run the benchmark multiple times, pass your existing snapshot ID directly and skip Phase 1. The snapshot persists between runs until you delete it.</p>
<hr />
<h2>What Actually Took Three Iterations</h2>
<p>The first version had plain <code>await sandbox.terminate()</code> at the end of each function. Two exceptions during testing left sandboxes running and billing for idle compute. Switched to <code>async with await AsyncSandbox.create(...) as sandbox:</code> and that stopped.</p>
<p>The second version called <code>sandbox.checkpoint(sandbox.sandbox_id)</code>. I had copied the pattern from a CLI reference (<code>tl sbx checkpoint &lt;sandbox-id&gt;</code>) and assumed the Python SDK matched. It does not. The Python instance method takes no positional arguments: <code>sandbox.checkpoint(checkpoint_type=CheckpointType.MEMORY)</code>. That is it.</p>
<p>The third version was the first one that ran end to end, but with <code>CheckpointType.FILESYSTEM</code> by default because I had not read the snapshots documentation carefully. The benchmark looked reasonable. The forks were doing full cold boots and I was measuring them alongside the actual work. Switching to <code>CheckpointType.MEMORY</code> was the change that made setup time disappear from per-fork timing.</p>
<p>Small mistakes individually. What they share: Tensorlake's API is well documented, but the snapshot docs, the SDK reference, and the async docs are three separate pages. Read only the quickstart and you miss two of the three things that matter most for this pattern.</p>
<hr />
<p>You can also check the complete project on my github here:</p>
<p><a href="https://github.com/dvy246/Tensorlake-Agentic-Swarm.git">click_here</a></p>
<hr />
<h2>The Thing That Changes</h2>
<p>Running the same five agents sequentially and then in parallel is one of those moments where the architecture becomes legible in a way that documentation does not fully convey.</p>
<p>The snapshot moves setup cost from inside the loop to outside it. The agents still do the same work on the same hardware. The savings come from not rebuilding an environment five times when it only needed to be built once.</p>
<p>Most multi-agent optimization advice focuses on LLM calls: batching, caching, cheaper models. That advice is right. But if you have five agents each spending 90 seconds on pip installs before making a single inference call, no amount of LLM optimization helps until you address setup time first.</p>
<p>The bottleneck was never the agents. It was rebuilding the same environment on every run. Snapshot it once, fork cheaply, and parallel execution finally delivers what you expected when you first wrote <code>asyncio.gather</code>.</p>
<hr />
<p>References:</p>
<ul>
<li><p><a href="https://docs.tensorlake.ai/sandboxes/introduction">Tensorlake Sandboxes</a></p>
</li>
<li><p><a href="https://docs.tensorlake.ai/sandboxes/sdk-reference">SDK Reference</a></p>
</li>
<li><p><a href="https://docs.tensorlake.ai/sandboxes/snapshots">Snapshots</a></p>
</li>
<li><p><a href="https://docs.tensorlake.ai/sandboxes/async">Async SDK</a></p>
</li>
<li><p><a href="https://www.computesdk.com/benchmarks/sandboxes/">https://www.computesdk.com/benchmarks/sandboxes/</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[MCP Is Dead. The Downloads Just Don't Know It Yet.]]></title><description><![CDATA[Your AI agent ran a query on a fake database last month.
It got real results. The tool worked perfectly. Your SSH keys left in the background.
The agent didn't flag it. The registry didn't catch it. N]]></description><link>https://aievolution.hashnode.dev/mcp-is-dead-the-downloads-just-don-t-know-it-yet</link><guid isPermaLink="true">https://aievolution.hashnode.dev/mcp-is-dead-the-downloads-just-don-t-know-it-yet</guid><dc:creator><![CDATA[Divy]]></dc:creator><pubDate>Fri, 05 Jun 2026 13:04:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699c941340e1f055acc5dded/b7996749-48f4-4877-9efb-6f4808aaca80.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>Your AI agent ran a query on a fake database last month.</p>
<p>It got real results. The tool worked perfectly. Your SSH keys left in the background.</p>
<p>The agent didn't flag it. The registry didn't catch it. Nobody warned you.</p>
<p>That's not a hypothetical. That's MCP in 2026, with 97 million monthly downloads and a Linux Foundation home.</p>
<p>The hype was real. So are the cracks.</p>
<hr />
<h2>First: what is MCP, and why should you care</h2>
<p>If you've never built AI agents before, this matters. Skip it if you have.</p>
<p>Say you're building an AI assistant that needs to do real work:</p>
<ul>
<li>Look up customer records in a database</li>
<li>Create tickets in Jira</li>
<li>Send a Slack message</li>
<li>Pull a file from Google Drive</li>
</ul>
<p>Each of those lives in a different system. Different API, different auth, different data format.</p>
<p>To connect your AI to all of them, you'd write a custom integration for each one. Fine for two tools. Painful for ten. Then you switch models and rewrite everything.</p>
<p>This is the <strong>N×M problem</strong>: N tools multiplied by M AI models equals a mountain of glue code nobody wants to maintain.</p>
<p><strong>MCP — the Model Context Protocol</strong> — solves that. Released by Anthropic in November 2024, it's an open standard that gives AI models one universal way to talk to external tools. You build an MCP server once around a tool, and any MCP-compatible AI can use it.</p>
<pre><code>Your agent  →  MCP Client  →  MCP Server  →  Real Tool (Slack, Postgres, GitHub)
</code></pre>
<p>Three pieces:</p>
<ul>
<li><strong>MCP Host</strong>: your app (Claude Desktop, VS Code, a custom agent)</li>
<li><strong>MCP Client</strong>: the component inside your app that speaks MCP, discovers tools, calls them</li>
<li><strong>MCP Server</strong>: a small process wrapping a real tool, exposing it in a format any MCP client can use</li>
</ul>
<p>That's it. The N×M problem disappears. One integration per tool, works with every AI.</p>
<p>The pitch was real. The adoption proved it.</p>
<hr />
<h2>How MCP went from zero to everywhere in 14 months</h2>
<p>The adoption happened fast. Unusually fast.</p>
<ul>
<li><strong>Nov 2024</strong> — Anthropic launches MCP. ~2M monthly SDK downloads.</li>
<li><strong>Apr 2025</strong> — OpenAI adopts it. Downloads jump to 22M.</li>
<li><strong>Jul 2025</strong> — Microsoft integrates it into Copilot Studio. 45M.</li>
<li><strong>Nov 2025</strong> — AWS adds support. 68M.</li>
<li><strong>Mar 2026</strong> — Every major AI vendor on board. 97M downloads. 10,000+ public MCP servers.</li>
</ul>
<p>In December 2025, Anthropic donated MCP to the Agentic AI Foundation under the Linux Foundation, co-founded with OpenAI and Block. It stopped being Anthropic's protocol and became the industry's.</p>
<p>The "USB-C for AI" comparison spread everywhere. Everyone plugged in.</p>
<p>And then engineers started running it in production.</p>
<hr />
<h2>Why developers hate MCP</h2>
<p>The complaints have been building in forums, GitHub issues, and private Slack channels for months. Not from people who misunderstood MCP. From people who ran it in production and got surprised by the same things.</p>
<p><strong>"It's unauthenticated by default."</strong></p>
<p>Out of the box, an MCP server trusts whatever connects to it.</p>
<p>No built-in check that the server is who it claims. No built-in check that the client is who it claims. You're responsible for adding that layer.</p>
<p>Most tutorials don't mention this.</p>
<p><strong>"The STDIO transport executes arbitrary OS commands."</strong></p>
<p>The official MCP STDIO transport runs any OS command you point at it to launch a server. Even when the server startup fails. No sanitization warnings. Nothing in the developer toolchain flags it.</p>
<p>OX Security documented this in April 2026.</p>
<p>Anthropic's response: expected behavior, sanitization is the developer's responsibility. LangChain said the same. Microsoft said the same.</p>
<p>Three major vendors. Same answer: your problem.</p>
<p><strong>"The spec moves, community servers don't."</strong>
MCP servers published in community registries frequently fall behind spec updates. A server that worked last month may behave differently after the protocol updates. The registry has no enforcement mechanism. You find out in production.</p>
<p><strong>"Every REST API I already have needs a new wrapper process."</strong></p>
<p>Adding MCP to a tool that already has a clean REST API means building an entire MCP server around it.</p>
<p>That server needs to be:</p>
<ul>
<li>Deployed and monitored</li>
<li>Updated when the underlying API changes</li>
<li>Secured separately from both the agent and the tool</li>
</ul>
<p>For ten existing APIs, that's ten new processes to own. Month three of production, you feel every one of them.</p>
<p><strong>"The registries are basically npm circa 2015."</strong></p>
<p>In early 2026, OX Security cloned <code>mcp-server-postgres</code> and named it <code>mcp-server-postgress</code> (extra 's'). Functionally identical. Same queries, same responses.</p>
<p>Hidden inside: a payload that silently pulled SSH keys and environment files to an outside server.</p>
<p>They submitted it to eleven major MCP registries.</p>
<p>Nine published it. No automated security review. No source code analysis. Nothing.</p>
<p><strong>"It eats my context window before the user says anything."</strong></p>
<p>When your MCP client connects to a server, it loads the full tool schema into your context window — names, descriptions, parameters for every tool.</p>
<ul>
<li>One server, five tools: ~500 tokens gone before the first message</li>
<li>Ten servers: 2,000–3,000 tokens gone before the first message</li>
</ul>
<p>The model is already reasoning over a smaller budget. Before your user typed a word.</p>
<p>These aren't edge cases. They're the standard experience for anyone who's moved past a local demo.</p>
<hr />
<h2>The 3 problems that actually break production systems</h2>
<h3>The trust problem</h3>
<p>Your agent has no way to verify an MCP server is who it claims to be.</p>
<p>The OX Security incident made this real: nine out of eleven registries accepted a typosquatted credential-stealing package. The malicious server functioned correctly. Ran database queries. Returned results. And silently pulled your SSH keys in the background. Nothing in the protocol flagged it.</p>
<p>Since January 2026, researchers have filed 30 CVEs against the MCP ecosystem in 60 days. Prompt injection through tool descriptions. Credential theft via config file reads. "Tool poisoning," where a server description manipulates the agent's next decision. These aren't exotic attack vectors.</p>
<blockquote>
<p>Your agent can't tell your Postgres server from an attacker's. That's not a code bug. It's a design gap.</p>
</blockquote>
<p>MCP was built for a trusted local environment. Production isn't that.</p>
<hr />
<h3>The wrapper tax</h3>
<p>Every tool you connect to MCP needs its own MCP server. Ten tools means ten additional processes to own.</p>
<p>Each one needs to:</p>
<ul>
<li>Stay in sync when the underlying tool's API changes</li>
<li>Be monitored for failures in production</li>
<li>Be secured separately from the agent and the tool itself</li>
<li>Be deployed as part of your infrastructure</li>
</ul>
<p>For the first two tools, manageable. Month three with fifteen integrations, it's a job.</p>
<p>The N×M problem is solved. The "N new processes" problem quietly replaced it.</p>
<hr />
<h3>The context window bill</h3>
<p>Tool schemas are not free. They're tokens. And they arrive before your user's message does.</p>
<p>A team building a customer service agent connected to ten MCP servers found their available reasoning budget had shrunk by 30% before the first user question arrived. Same model. Same prompts. Just more tools.</p>
<p>In a long multi-step agent session, schema tokens compound. Quality drifts. Costs climb. Most teams don't trace this back to tool schema overhead until they look at what's actually in the context window.</p>
<hr />
<h2>What engineers are using instead</h2>
<p>A few patterns have emerged for teams that ran into the problems above.</p>
<p><strong>Direct REST API calls</strong></p>
<p>For tools with a clean existing API, skip MCP entirely. Call the API directly from your agent. No new server to maintain, no schema overhead, existing auth covers it.</p>
<p>Works well when you control the tool and the API is stable. Doesn't scale when you need multiple AI systems to share the same integrations.</p>
<hr />
<p><strong>Native provider tool use</strong></p>
<p>Anthropic and OpenAI both have built-in tool calling that needs no MCP infrastructure. You define the tool schema inline, pass it with the request, the model calls it.</p>
<ul>
<li>No server process</li>
<li>No registry</li>
<li>Your auth sits directly on the call</li>
</ul>
<p>Most teams running focused single-purpose agents in 2026 are doing this. Simpler to reason about, harder to share across systems.</p>
<hr />
<p><strong>UTCP (Universal Tool Calling Protocol)</strong></p>
<p>UTCP skips the wrapper entirely. Instead of wrapping a tool in an MCP server, it calls the tool's existing HTTP endpoints directly, with a discovery layer on top.</p>
<p>As of early 2026:</p>
<ul>
<li>1,000+ GitHub stars</li>
<li>Implementations in Python, Go, and TypeScript</li>
<li>Growing community from teams that wanted lower latency and less infrastructure overhead</li>
</ul>
<p>Best for teams with well-designed existing APIs who don't want a separate server layer. Not a full MCP replacement if you need the ecosystem breadth — but for many production use cases, materially simpler.</p>
<hr />
<p><strong>MCP with a gateway layer</strong></p>
<p>For teams committed to MCP, the answer to most of the problems above is an MCP gateway — a controlled layer between your agent and your servers.</p>
<pre><code>Your agent  →  MCP Gateway  →  MCP Server 1  →  Tool
                            →  MCP Server 2  →  Tool
                            →  MCP Server N  →  Tool
</code></pre>
<p>A gateway handles:</p>
<ul>
<li><strong>Authentication</strong> — verifies server identity before your agent calls anything</li>
<li><strong>Tool filtering</strong> — loads only schemas relevant to the current task, not all of them</li>
<li><strong>Audit logging</strong> — records every tool call for compliance and debugging</li>
<li><strong>Rate limiting</strong> — stops runaway tool calls from blowing your budget</li>
</ul>
<p>As of April 2026, 86–89% of AI agent pilots fail before reaching production. Governance gaps and audit visibility are the two most common reasons. A gateway is what closes both.</p>
<hr />
<h2>So do we actually need MCP?</h2>
<p>Yes. With caveats that matter.</p>
<p><strong>Use MCP when:</strong></p>
<ul>
<li>Multiple AI systems need to share the same tools</li>
<li>You're a SaaS company giving AI agents access to your product</li>
<li>You need dynamic tool discovery across a large integration ecosystem</li>
</ul>
<p><strong>Skip MCP when:</strong></p>
<ul>
<li>You're building a focused agent with two or three tools you already own</li>
<li>Your tools have clean REST APIs you control</li>
<li>You need low latency and minimal infrastructure overhead</li>
</ul>
<p>The "MCP for everything" era is over. It's the right call when standardization pays off at scale. When you just need your agent to hit an API you already control, MCP is overhead pretending to be infrastructure.</p>
<hr />
<h2>Cheat sheet: what to actually do</h2>
<table>
<thead>
<tr>
<th>Your situation</th>
<th>What makes sense</th>
</tr>
</thead>
<tbody><tr>
<td>Local dev, one or two tools, just exploring</td>
<td>Bare MCP or native tool calls. Don't over-engineer.</td>
</tr>
<tr>
<td>Agent using tools you own, clean REST APIs</td>
<td>Direct API calls or native tool use. Skip MCP overhead.</td>
</tr>
<tr>
<td>Production agent, 5+ tools, or external users</td>
<td>MCP with a gateway. Authentication is not optional.</td>
</tr>
<tr>
<td>Enterprise, compliance, or regulated industry</td>
<td>MCP gateway with audit logs and SSO. Non-negotiable.</td>
</tr>
<tr>
<td>Pulling from community MCP registries</td>
<td>Treat every server as untrusted. Verify before deploying.</td>
</tr>
<tr>
<td>Need to share tools across multiple AI systems</td>
<td>MCP is the right call. This is exactly what it's for.</td>
</tr>
</tbody></table>
<hr />
<h2>The actual state of things</h2>
<p>MCP isn't going away. The downloads are real. The Linux Foundation governance is serious. Multi-vendor adoption means the protocol has institutional staying power.</p>
<p>But the MCP of early tutorials — install a community server, plug it in, done — that version is dead.</p>
<p>It was never safe for production. It was never meant to be.</p>
<p>The engineers moving to UTCP or direct API calls aren't abandoning MCP because it failed. They're routing around the parts that weren't built for what they're building.</p>
<p>I keep coming back to the OX Security test. Nine out of eleven registries. No automated review. The agent called the fake server, ran its queries, and handed over credentials it didn't know it was handing over.</p>
<p>Your agent does what it's told by the tools it trusts.</p>
<p>MCP hasn't fully answered how it decides what to trust. Until it does, treat every community MCP server the way you'd treat a random npm package in 2015.</p>
<p>You know how that era ended.</p>
]]></content:encoded></item><item><title><![CDATA[I Built a Stateful Research Agent Inside a Sandbox. Here's What the Numbers Actually Looked Like.]]></title><description><![CDATA[Three steps into a multi-page research task, the agent lost everything.
Not a crash. Not a thrown exception. 
The function returned, context reset, and the pricing data it had just collected vanished.]]></description><link>https://aievolution.hashnode.dev/i-built-a-stateful-research-agent-inside-a-sandbox-here-s-what-the-numbers-actually-looked-like</link><guid isPermaLink="true">https://aievolution.hashnode.dev/i-built-a-stateful-research-agent-inside-a-sandbox-here-s-what-the-numbers-actually-looked-like</guid><category><![CDATA[ai agents]]></category><category><![CDATA[AI]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[technology]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[software development]]></category><category><![CDATA[#ai-tools]]></category><dc:creator><![CDATA[Divy]]></dc:creator><pubDate>Wed, 27 May 2026 04:51:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699c941340e1f055acc5dded/b1440f51-ab40-45f5-bd2b-58d1d0e6d163.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>Three steps into a multi-page research task, the agent lost everything.</p>
<p>Not a crash. Not a thrown exception. </p>
<p>The function returned, context reset, and the pricing data it had just collected vanished. </p>
<p><strong>This failure is predictable:</strong> stateless execution environments were never built to hold state across browser sessions that run for twenty minutes. </p>
<p>You hit it eventually, usually at the worst moment.</p>
<p>The two standard workarounds are both annoying. Stuffing state into the prompt works until token costs starts becoming an issue. An external state store solves the problem but now you are maintaining another service.</p>
<p>I had been using E2B for short-lived code execution. It handles that well, and they have added persistence features over time, including early-stage snapshot support. But for agents that need to pause mid-task and resume from a different process, state management is still mostly on you.</p>
<p>Someone in my Discord mentioned Tensorlake. I opened the docs and decided to build against this specific problem.</p>
<p>In this article i will walk you through the steps using which you can build a desktop using agent using sandbox.</p>
<p>Let's start with setting up.</p>
<hr />
<h2>Visual Explanation First</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z3835yst9bjk3nelqmz8.png" alt="Image description" /></p>
<hr />
<h2>Setup</h2>
<p>What caught my attention first: named sandboxes with <code>suspend()</code> and <code>resume()</code> that preserve the full VM state, not just files, but running processes and open browser sessions. Sub-second resume, according to their docs.</p>
<p>Ten minutes from zero to running:</p>
<pre><code class="language-bash">pip install tensorlake
tl login   # or TENSORLAKE_API_KEY env var
</code></pre>
<p>Free tier, no credit card.</p>
<pre><code class="language-python">from tensorlake.sandbox import Sandbox

sandbox = Sandbox.create(
    name="research-agent",
    cpus=2.0,
    memory_mb=4096,
    secret_names=["OPENAI_API_KEY"],
    image="tensorlake/ubuntu-vnc",
)
</code></pre>
<p>The <code>tensorlake/ubuntu-vnc</code> image is what gives you a real desktop and Firefox inside the VM. You need an actual browser because modern pricing pages heavily use client-side rendering and bot detection that stops headless scrapers cold. Firefox inside a sandbox just looks like a person browsing.</p>
<p><strong>Important:</strong> Playwright is not pre-installed in <code>ubuntu-vnc</code>. Install it before the agent runs:</p>
<pre><code class="language-python">sandbox.run("pip", ["install", "playwright"])
sandbox.run("playwright", ["install", "chromium"])
</code></pre>
<p>Two to three minutes on first setup. After that, packages persist across suspend/resume so you pay the cost once.</p>
<hr />
<h2>Latency: What I Actually Measured</h2>
<p>First sandbox was running in roughly 800-900ms from the <code>Sandbox.create()</code> call to status <code>running</code>.</p>
<p>Here is where time actually goes:</p>
<pre><code class="language-other">Sandbox creation:        ~800ms          (named sandbox, first time)
Sandbox resume:          ~400ms          (from suspended state)
LLM call (GPT-4o):       2,000-4,000ms   (per step, dominates everything)
Browser screenshot:      ~300ms          (capture + transfer)
Page load in sandbox:    1,000-2,000ms   (varies by site)
File read/write:         &lt;50ms           (block-based storage)
Sandbox suspend:         ~200ms
</code></pre>
<p>The LLM calls dominate by a large margin. Sandbox overhead is not the bottleneck. The main optimization is batching browser operations before each model call rather than interleaving individual round trips.</p>
<p>Tensorlake publishes a SQLite filesystem benchmark claiming 1.6-1.9x faster I/O than E2B and Modal. Self-reported numbers. I could not independently verify them. What I can say is that the block-based storage felt responsive for frequent small writes, which is exactly the pattern a research agent uses when checkpointing after every step.</p>
<hr />
<h2>Computer Use: What Worked and What Didn't</h2>
<p>The desktop API itself is clean:</p>
<pre><code class="language-python">with sandbox.connect_desktop(password="tensorlake") as desktop:
    png_bytes = desktop.screenshot()
    desktop.move_mouse(640, 400)
    desktop.click()
    desktop.type_text("pinecone.io")
    desktop.press("Return")
</code></pre>
<p>Screenshot as PNG bytes, decode it, figure out where to click, send coordinates. Each browser interaction takes 1-3 seconds depending on page load. Slow compared to an API call. But it works on pages that block scrapers, because from the server's side it is just a person using Firefox.</p>
<p><strong>The problem:</strong> coordinates assume a fixed layout, and layouts do not stay fixed.</p>
<p>Weaviate's pricing page ran an A/B test between two of my agent's steps. The toggle moved 30px down. The agent clicked empty space. No error, no exception. Just a screenshot showing nothing happened, and twenty minutes of debugging before I identified the offset.</p>
<p>The fix: pass screenshots to GPT-4o Vision to identify element positions dynamically rather than hardcoding coordinates. Adds about 2 seconds per interaction, handles layout drift reliably. Worth it for reliability; too slow for high-frequency operations.</p>
<p>When the DOM is accessible, Playwright inside the sandbox is the better path:</p>
<pre><code class="language-python">result = sandbox.run(
    "python",
    ["-c", """
import asyncio
from playwright.async_api import async_playwright

async def get_pricing():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://pinecone.io/pricing")
        pricing_text = await page.inner_text(".pricing-section")
        print(pricing_text)
        await browser.close()

asyncio.run(get_pricing())
"""]
)
</code></pre>
<p>The hybrid strategy I landed on:</p>
<table>
<thead>
<tr>
<th><strong>Situation</strong></th>
<th><strong>Approach</strong></th>
<th><strong>Why</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Site with bot detection</td>
<td>Vision + coordinates</td>
<td>Playwright gets blocked</td>
</tr>
<tr>
<td>Accessible DOM</td>
<td>Playwright directly</td>
<td>Faster, no coordinate drift</td>
</tr>
<tr>
<td>Unknown or variable layout</td>
<td>Screenshot + GPT-4o Vision</td>
<td>Resolves position dynamically</td>
</tr>
<tr>
<td>High-frequency operations</td>
<td>Playwright only</td>
<td>Vision adds ~2s per call</td>
</tr>
</tbody></table>
<p>Use vision as a fallback, not a first tool. Vision handles layout variation. Playwright handles speed. Neither does both well.</p>
<hr />
<h2>Statefulness: The Part That Actually Mattered</h2>
<p>After three steps (Pinecone free tier limits noted, $70/mo Starter plan recorded, Weaviate docs started), I called <code>sandbox.suspend()</code>.</p>
<p>The sandbox froze. Filesystem, memory, running browser: all paused. Twelve minutes later, from a different terminal:</p>
<pre><code class="language-python">sandbox = Sandbox.connect("research-agent")
sandbox.resume()
</code></pre>
<p>About 400ms. The Weaviate pricing tab was still open. Tensorlake's suspend/resume preserves the full VM state, including memory and running processes. </p>
<p>Everything written to <code>/workspace/research_notes.json</code> was intact.</p>
<p>The workflow I settled on: write state explicitly after each meaningful step, then suspend.</p>
<pre><code class="language-python"># After each step, before suspending:
sandbox.write_file(
    "/workspace/state.json",
    json.dumps({
        "pinecone_pricing": pinecone_data,
        "weaviate_started": True,
        "next_url": "https://weaviate.io/pricing"
    }).encode()
)
sandbox.suspend()

# On next invocation, from any process:
sandbox = Sandbox.connect("research-agent")
sandbox.resume()
state = json.loads(bytes(sandbox.read_file("/workspace/state.json")))
# picks up from state["next_url"]
</code></pre>
<p>The state file is the continuity mechanism. Not elegant, but it removes the need for an external database and the filesystem is fast, durable across suspend, and readable from any reconnecting process.</p>
<hr />
<h2>Scaling and Failure Handling</h2>
<p><code>Sandbox.create()</code> is a blocking synchronous call. For parallel workloads, wrap in <code>concurrent.futures</code>:</p>
<pre><code class="language-python">from tensorlake.sandbox import Sandbox
from concurrent.futures import ThreadPoolExecutor

def research_competitor(name, url):
    sandbox = Sandbox.create(
        name=f"research-{name}",
        cpus=1.0,
        memory_mb=2048,
        secret_names=["OPENAI_API_KEY"],
        image="ubuntu-vnc",
    )
    # ... agent logic ...
    result = sandbox.read_file("/workspace/report.json")
    sandbox.terminate()
    return result

competitors = [
    ("pinecone", "pinecone.io/pricing"),
    ("weaviate", "weaviate.io/pricing"),
    ("qdrant", "qdrant.tech/pricing"),
]

with ThreadPoolExecutor(max_workers=5) as executor:
    reports = list(executor.map(lambda c: research_competitor(*c), competitors))
</code></pre>
<p>Three concurrent sandboxes ran without delay. I have not tested at twenty or fifty. Their docs mention hundreds per second. Take that at face value until you have load data.</p>
<p><strong>Note:</strong> Tensorlake's Python SDK v0.5.8 introduced native async APIs that offer a cleaner alternative to threading for I/O-bound orchestration. If you are on v0.5.8 or later, those are worth reaching for before wrapping synchronous calls in a thread pool.</p>
<p><strong>Patterns worth building from day one:</strong></p>
<p><strong>Idempotent state writes.</strong> Write state after each meaningful step. If the agent fails mid-run, the next invocation reads the file and skips completed work. This does not happen automatically.</p>
<p><strong>Checkpoint before risky operations.</strong> <code>sandbox.checkpoint()</code> creates a restorable snapshot. By default, snapshots preserve the filesystem state. Preserving full memory state is supported as an explicit option. Either way, you can restore into a fresh sandbox if an operation goes wrong:</p>
<pre><code class="language-python"># Filesystem snapshot (default)
snapshot = sandbox.checkpoint()

try:
    agent.navigate_to_pricing_page()
except Exception:
    # Restore filesystem state into a new sandbox
    sandbox = Sandbox.create(snapshot_id=snapshot.snapshot_id)
</code></pre>
<p><strong>Named sandboxes.</strong> If the orchestration process dies, any other process reconnects with <code>Sandbox.connect("sandbox-name")</code> and resumes from the last written state.</p>
<p><strong>Architectural boundary:</strong> Tensorlake provides the execution environment and runtime for agents: the VM, the filesystem, the process lifecycle, the networking. It is not an agent framework. Retry logic, circuit breakers, and LLM rate-limit backoff belong in the orchestration layer above it: LangChain, LlamaIndex, a custom harness, or whatever you are using to drive the agent. That separation is deliberate, not a gap.</p>
<hr />
<h2>The Mental Model</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qyppsefvza86ochjivid.png" alt="Image description" /></p>
<p>The part that shifted how I thought about the design:</p>
<pre><code class="language-other">┌─────────────────────────────────────────────┐
│                 Your Agent                   │
│    (LLM + tool calling logic)                │
└──────────────────┬──────────────────────────┘
                   │ tool calls
┌──────────────────▼──────────────────────────┐
│           Tensorlake Sandbox                 │
│  ┌──────────────────────────────────────┐   │
│  │ State Layer: /workspace filesystem   │   │
│  │  state.json, research_notes.json     │   │
│  └──────────────────────────────────────┘   │
│  ┌──────────────────────────────────────┐   │
│  │ Execution Layer: processes, scripts  │   │
│  └──────────────────────────────────────┘   │
│  ┌──────────────────────────────────────┐   │
│  │ Computer Use: VNC, screenshots, mouse│   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
</code></pre>
<p>The sandbox is not the agent. It is the stable environment the agent operates in. When it resumes, the environment is exactly where the agent left it. The agent's logic lives outside and reconnects to a world that did not reset.</p>
<p>That changes what you can build. An agent that runs for an hour, navigates fifteen pages, and writes a structured report is feasible when the execution environment outlasts the orchestration session. With purely ephemeral execution, it is not.</p>
<hr />
<h2>How It Compares</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d9jigfr6lz2effe3s3p8.png" alt="Photo by author" /></p>
<p><strong>vs E2B:</strong></p>
<ul>
<li>Both use Firecracker microVMs. E2B markets sub-200ms cold starts; community reports put real-world p50 closer to 400-600ms. Tensorlake named sandbox creation was ~800ms in my testing.</li>
<li>E2B has added snapshot and pause-resume in recent releases. The statefulness gap is narrower than a year ago. Tensorlake's suspend/resume preserves the full running VM state, including open processes, browser sessions, all in under a second. E2B's memory snapshot support is still described as early-stage.</li>
<li>Tensorlake claims 1.6-1.9x faster filesystem I/O on their own benchmarks. Self-reported. For an independent reference: Tensorlake recently ranked top 2 across all three categories in the ComputeSDK sandbox benchmarks.</li>
<li>Neither provides DOM-level element selection at the SDK layer.</li>
</ul>
<p><strong>vs Modal:</strong></p>
<ul>
<li>Modal uses gVisor rather than Firecracker, designed around stateless function execution. Stateful long-running agents work but need more setup. Cold starts are around 1-1.5 seconds per their docs.</li>
</ul>
<p><strong>vs Stagehand (BrowserBase):</strong></p>
<ul>
<li>Stagehand has DOM-level selectors (CSS, XPath, natural language) via <code>locator()</code>. For pure browser automation, this is a real ergonomic advantage.</li>
<li>Tensorlake gives you a full VM. Code execution, file management, package installs, and browser use in the same environment. If that combination is what you need, the full VM model is worth the coordinate complexity.</li>
<li>Browser automation only? Stagehand is the more focused tool.</li>
</ul>
<pre><code class="language-python">from tensorlake.sandbox import SandboxClient

client = SandboxClient()

for sb in client.list():
    print(sb.sandbox_id, sb.status)
</code></pre>
<hr />
<h2>What the Build Produced</h2>
<p>By the end of the session, the agent had produced the comparison: Pinecone versus Weaviate pricing, extracted across seven pages, with notes preserved across two suspensions and a full restart of the orchestrating machine.</p>
<pre><code class="language-python">report_bytes = sandbox.read_file("/workspace/comparison_report.md")
print(bytes(report_bytes).decode("utf-8"))
</code></pre>
<p>Accurate. Correct tier names and numbers.</p>
<p>Tensorlake did not solve the hard parts: the retrieval logic, state schema, hybrid browser strategy. It stayed out of the way while those got built. Most of the infrastructure friction came down to state management, and most of that went away once the sandbox filesystem became the state store.</p>
<hr />
<h2>Three Things to Know Before You Start</h2>
<p><strong>Speed is a systems problem, not a sandbox problem.</strong> LLM calls account for the bulk of per-step latency. Optimize by batching browser operations before each model call, not by chasing sandbox startup time.</p>
<p><strong>Design for interruption from day one.</strong> Write state after every meaningful step. Not because the sandbox will crash, but because resuming from a different process after an unexpected interruption is a real scenario, not an edge case.</p>
<p><strong>Computer use is a primitive.</strong> The coordinate-based API works, but layout drift will break hardcoded positions. Use Playwright when the DOM is accessible. Fall back to vision when you need a real browser session. Do not automate full workflows with raw coordinates.</p>
<hr />
<p><strong>Is the sandbox infrastructure production-ready?</strong> Yes. Suspend/resume held up, filesystem persistence was consistent, and Firecracker isolation did what it was supposed to.</p>
<p><strong>Is the computer use layer production-ready?</strong> Not without additional engineering. The raw coordinate API is a reasonable primitive, but element resolution needs to be built on top of it. A vision-backed <code>click_element()</code> in the SDK would change the story significantly. Until then, budget the time to build that layer yourself.</p>
<p>Worth using? Yes, if you go in with clear expectations about what the platform handles and what it leaves to you. That boundary is sharper than most, which makes it easier to work with once you have internalized it.</p>
<hr />
<p>You can also check the complete project on my github here:</p>
<p><a href="https://github.com/dvy246/Sandboxes.git">click_here</a></p>
<hr />
<p>References</p>
<p><strong>Tensorlake.</strong> <em>Tensorlake Documentation &amp; Sandbox SDK.</em> <a href="https://tensorlake.ai">https://tensorlake.ai</a></p>
<p><strong>E2B.</strong> <em>E2B Sandbox Infrastructure.</em> <a href="https://e2b.dev">https://e2b.dev</a></p>
<p><strong>Modal.</strong> <em>Modal Serverless Infrastructure.</em> <a href="https://modal.com">https://modal.com</a></p>
<p><strong>Stagehand (BrowserBase).</strong> <em>Stagehand Browser Automation.</em> <a href="https://browserbase.com/stagehand">https://browserbase.com/stagehand</a></p>
<p><strong>Amazon Web Services.</strong> <em>Firecracker MicroVMs.</em> <a href="https://firecracker-microvm.github.io/">https://firecracker-microvm.github.io/</a></p>
<p><strong>Microsoft.</strong> <em>Playwright Browser Automation.</em> <a href="https://playwright.dev">https://playwright.dev</a></p>
<p><strong>Benchmark:</strong> <a href="https://www.computesdk.com/benchmarks/sandboxes/">https://www.computesdk.com/benchmarks/sandboxes/</a></p>
]]></content:encoded></item></channel></rss>