> ## Documentation Index
> Fetch the complete documentation index at: https://docs.archil.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Parallel agents

> Fork a prepared sandbox once per agent, let each agent try a different approach, and keep the fork with the best score.

Agents are nondeterministic: give the same task to three agents and you get three different solutions, some better than others. Instead of hoping one run gets it right, you can run several attempts side by side and keep the best one.

This guide does that with [sandbox forks](/compute/sandboxes/introduction#forking-a-sandbox). You prepare a base sandbox once, with the project checked out and its tools installed. Then you fork it three times and run an agent in each fork, each with a different hint or model. Each fork is a full, independent copy of the machine, so the agents can install packages and rewrite files without seeing each other's work. Finally, you score every fork with the same benchmark, keep the winner, and delete the rest.

## Prerequisites

Export your Archil API key, a region where sandboxes are available (`aws-us-east-1`, `aws-us-west-2`, or `aws-eu-west-1`), and an [AI Gateway](https://vercel.com/ai-gateway) key for the model:

```bash theme={null}
export ARCHIL_API_KEY=key-...
export ARCHIL_REGION=aws-us-east-1
export AI_GATEWAY_API_KEY=...
```

Install the dependencies. The TypeScript agents use the [AI SDK](https://ai-sdk.dev); the Python agents call the same model through the gateway's OpenAI-compatible API.

<CodeGroup>
  ```bash TypeScript theme={null}
  npm init -y && npm pkg set type=module
  npm install disk ai zod tsx
  ```

  ```bash Python theme={null}
  pip install archil openai
  ```
</CodeGroup>

## Create the project

The agents' task is to speed up a slow function without breaking its tests. Create these three files in a local `project` directory: the function, its tests, and a benchmark that prints the runtime in milliseconds.

```python project/primes.py theme={null}
def count_primes(n):
    """Return the number of primes less than n."""
    count = 0
    for k in range(2, n):
        if all(k % d for d in range(2, int(k**0.5) + 1)):
            count += 1
    return count
```

```python project/test_primes.py theme={null}
from primes import count_primes


def test_small():
    assert [count_primes(n) for n in range(12)] == [0, 0, 0, 1, 2, 2, 3, 3, 4, 4, 4, 4]


def test_large():
    assert count_primes(10_000) == 1229
    assert count_primes(100_000) == 9592
```

```python project/bench.py theme={null}
import timeit

from primes import count_primes

assert count_primes(1_000_000) == 78498
print(round(min(timeit.repeat(lambda: count_primes(1_000_000), number=1, repeat=3)) * 1000, 2))
```

## Prepare the base sandbox

The base sandbox gets the project in `/app`, installs `pytest`, and commits everything to git, so you can diff each agent's changes later. It also records a baseline benchmark.

The project lives on the sandbox's root filesystem rather than on an Archil disk, because the root filesystem is copied into every fork. Once it's ready, the base is stopped: it waits as a template without using compute, and forks of it boot as running sandboxes.

<CodeGroup>
  ```typescript prepare.ts theme={null}
  import { readFile } from "node:fs/promises";
  import * as archil from "disk";

  const base = await archil.createSandbox({
    name: "parallel-agents-base",
    baseImage: "python:3.13",
    vcpuCount: 2,
    memSizeMiB: 4096,
  });

  for (const file of ["primes.py", "test_primes.py", "bench.py"]) {
    await base.files.uploadFile(await readFile(`project/${file}`), `/app/${file}`);
  }

  const setup = await base.exec(
    "pip install -q --root-user-action=ignore pytest && git init -q && git add -A && " +
      "git -c user.name=base -c user.email=base@example.com commit -qm baseline && python bench.py",
    { cwd: "/app" },
  );
  if (setup.exitCode !== 0) throw new Error(setup.stderr);
  console.log(`Baseline: ${setup.stdout.trim()} ms`);

  // The base is only a template from here on; forks can be made while it's stopped
  await base.stop();
  ```

  ```python prepare.py theme={null}
  import archil

  base = archil.create_sandbox(
      name="parallel-agents-base",
      base_image="python:3.13",
      vcpu_count=2,
      mem_size_mib=4096,
  )

  for file in ["primes.py", "test_primes.py", "bench.py"]:
      base.files.upload_file(f"project/{file}", f"/app/{file}")

  setup = base.exec(
      "pip install -q --root-user-action=ignore pytest && git init -q && git add -A && "
      "git -c user.name=base -c user.email=base@example.com commit -qm baseline && python bench.py",
      cwd="/app",
  )
  if setup.exit_code != 0:
      raise RuntimeError(setup.stderr)
  print(f"Baseline: {setup.stdout.strip()} ms")

  # The base is only a template from here on; forks can be made while it's stopped
  base.stop()
  ```
</CodeGroup>

<CodeGroup>
  ```bash TypeScript theme={null}
  npx tsx prepare.ts
  ```

  ```bash Python theme={null}
  python prepare.py
  ```
</CodeGroup>

```text theme={null}
Baseline: 2295.34 ms
```

## Fork, race, and keep the winner

The race script forks the base once per approach and runs an agent in each fork at the same time. Every agent gets the same task plus a different hint, two of them run on Claude Sonnet and one on Claude Haiku, and each has a `bash` tool bound to its own fork.

When an agent finishes, the script scores its fork: the tests and benchmark must be untouched (`git diff --quiet`), the tests must pass, and the score is the benchmark time. The fastest fork wins; the others are stopped and deleted.

<CodeGroup>
  ```typescript race.ts theme={null}
  import { ToolLoopAgent, isStepCount, tool } from "ai";
  import * as archil from "disk";
  import type { Sandbox } from "disk";
  import { z } from "zod";

  const approaches = [
    { name: "parallel-agents-1", model: "anthropic/claude-sonnet-5.5", hint: "Use only the standard library." },
    { name: "parallel-agents-2", model: "anthropic/claude-sonnet-5.5", hint: "You may pip install numpy." },
    { name: "parallel-agents-3", model: "anthropic/claude-haiku-5.5", hint: "Keep the change small." },
  ];

  const task =
    "Make count_primes in /app/primes.py as fast as you can. `python bench.py` prints its runtime in ms, " +
    "and `python -m pytest -q` must keep passing. Don't edit test_primes.py or bench.py. " +
    "Keep your final answer to one sentence.";

  function bashTool(sandbox: Sandbox) {
    return tool({
      description: "Run a bash command in /app. Returns the exit status and output.",
      inputSchema: z.object({ command: z.string() }),
      execute: async ({ command }) => {
        console.log(`[${sandbox.name}] $ ${command.split("\n")[0].slice(0, 80)}`);
        const result = await sandbox.exec('bash -c "$COMMAND"', {
          cwd: "/app",
          env: { COMMAND: command },
          timeoutSeconds: 120,
        });
        return `exit ${result.exitCode ?? result.status}\n${result.stdout}${result.stderr}`.slice(-10_000);
      },
    });
  }

  async function score(sandbox: Sandbox) {
    const result = await sandbox.exec(
      "git diff --quiet HEAD -- test_primes.py bench.py && python -m pytest -q >/dev/null && python bench.py",
      { cwd: "/app" },
    );
    return result.exitCode === 0 ? Number(result.stdout.trim()) : Infinity;
  }

  const base = (await archil.listSandboxes()).find((s) => s.name === "parallel-agents-base")!;
  const forks = await Promise.all(approaches.map(({ name }) => base.fork({ name })));

  const results = await Promise.all(
    forks.map(async (sandbox, i) => {
      const agent = new ToolLoopAgent({
        model: approaches[i].model,
        tools: { bash: bashTool(sandbox) },
        stopWhen: isStepCount(15),
      });
      const { text } = await agent.generate({ prompt: `${task} ${approaches[i].hint}` });
      const ms = await score(sandbox);
      console.log(`${sandbox.name}: ${ms} ms. ${text}`);
      return { sandbox, ms };
    }),
  );

  results.sort((a, b) => a.ms - b.ms);
  const [winner, ...losers] = results;
  for (const { sandbox } of losers) {
    await sandbox.stop();
    await sandbox.delete();
  }

  console.log(`\nWinner: ${winner.sandbox.name} (${winner.ms} ms)`);
  console.log((await winner.sandbox.exec("git diff --stat", { cwd: "/app" })).stdout);
  ```

  ```python race.py theme={null}
  import json
  import os
  from concurrent.futures import ThreadPoolExecutor

  import archil
  from openai import OpenAI

  client = OpenAI(
      base_url="https://ai-gateway.vercel.sh/v1",
      api_key=os.environ["AI_GATEWAY_API_KEY"],
  )

  APPROACHES = [
      {"name": "parallel-agents-1", "model": "anthropic/claude-sonnet-5.5", "hint": "Use only the standard library."},
      {"name": "parallel-agents-2", "model": "anthropic/claude-sonnet-5.5", "hint": "You may pip install numpy."},
      {"name": "parallel-agents-3", "model": "anthropic/claude-haiku-5.5", "hint": "Keep the change small."},
  ]

  TASK = (
      "Make count_primes in /app/primes.py as fast as you can. `python bench.py` prints its runtime in ms, "
      "and `python -m pytest -q` must keep passing. Don't edit test_primes.py or bench.py. "
      "Keep your final answer to one sentence."
  )

  BASH_TOOL = {
      "type": "function",
      "function": {
          "name": "bash",
          "description": "Run a bash command in /app. Returns the exit status and output.",
          "parameters": {
              "type": "object",
              "properties": {"command": {"type": "string"}},
              "required": ["command"],
          },
      },
  }


  def bash(sandbox, command):
      print(f"[{sandbox.name}] $ {command.splitlines()[0][:80]}")
      result = sandbox.exec(
          'bash -c "$COMMAND"', cwd="/app", env={"COMMAND": command}, timeout_seconds=120
      )
      status = result.status if result.exit_code is None else result.exit_code
      return f"exit {status}\n{result.stdout}{result.stderr}"[-10_000:]


  def run_agent(sandbox, prompt, model, max_steps=15):
      messages = [{"role": "user", "content": prompt}]
      for _ in range(max_steps):
          reply = client.chat.completions.create(
              model=model,
              messages=messages,
              tools=[BASH_TOOL],
          ).choices[0].message
          messages.append(reply)
          if not reply.tool_calls:
              return reply.content
          for call in reply.tool_calls:
              command = json.loads(call.function.arguments)["command"]
              messages.append(
                  {"role": "tool", "tool_call_id": call.id, "content": bash(sandbox, command)}
              )
      return f"Stopped after {max_steps} steps."


  def score(sandbox):
      result = sandbox.exec(
          "git diff --quiet HEAD -- test_primes.py bench.py && python -m pytest -q >/dev/null && python bench.py",
          cwd="/app",
      )
      return float(result.stdout) if result.exit_code == 0 else float("inf")


  def attempt(sandbox, approach):
      text = run_agent(sandbox, f"{TASK} {approach['hint']}", approach["model"])
      ms = score(sandbox)
      print(f"{sandbox.name}: {ms} ms. {text}")
      return sandbox, ms


  base = next(s for s in archil.list_sandboxes() if s.name == "parallel-agents-base")
  with ThreadPoolExecutor() as pool:
      forks = list(pool.map(lambda approach: base.fork(name=approach["name"]), APPROACHES))
      results = list(pool.map(attempt, forks, APPROACHES))

  results.sort(key=lambda result: result[1])
  (winner, best_ms), *losers = results
  for sandbox, _ in losers:
      sandbox.stop()
      sandbox.delete()

  print(f"\nWinner: {winner.name} ({best_ms} ms)")
  print(winner.exec("git diff --stat", cwd="/app").stdout)
  ```
</CodeGroup>

<CodeGroup>
  ```bash TypeScript theme={null}
  npx tsx race.ts
  ```

  ```bash Python theme={null}
  python race.py
  ```
</CodeGroup>

The agents' commands interleave as they work in parallel, and the whole race usually takes well under a minute. Results vary from run to run; in this one, the fork that was allowed to install numpy won:

```text theme={null}
[parallel-agents-3] $ cd /app && cat primes.py && echo --- && cat bench.py && echo --- && cat test_pri
[parallel-agents-1] $ cd /app; ls; cat primes.py bench.py test_primes.py; python --version
[parallel-agents-2] $ cd /app; ls; cat primes.py bench.py test_primes.py; python -V; python -c "import
[parallel-agents-1] $ cd /app; cat > primes.py <<'E'
[parallel-agents-1] $ cd /app; python -c "
[parallel-agents-2] $ cd /app; pip install numpy 2>&1 | tail -1
[parallel-agents-1] $ cd /app; cat > primes.py <<'E'
[parallel-agents-3] $ cd /app && cat > primes.py <<'EOF'
parallel-agents-1: 1.42 ms. I rewrote `count_primes` in `/app/primes.py` as an odd-only bytearray sieve, which cuts the `bench.py` time to about 1.4 ms, and `python -m pytest -q` passes. I also checked it against the original trial-division method for every n below 3000, and the counts match.
parallel-agents-3: 3.66 ms. I replaced the trial-division loop in `/app/primes.py` with a bytearray Sieve of Eratosthenes, cutting the benchmark from about 2274 ms to 3.69 ms while `python -m pytest -q` still passes.
parallel-agents-2: 0.41 ms. I replaced `count_primes` in `/app/primes.py` with a NumPy sieve over odd numbers only. `bench.py` now reports about 0.41 ms, and `python -m pytest -q` passes (2 tests). I also spot-checked values just below and above a few primes, and they were correct.

Winner: parallel-agents-2 (0.41 ms)
 primes.py | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)
```

The winning fork is still running, with the agent's change applied and anything it installed still in place, so you can keep working in it: run more tests, push the change from it, or hand it to the next agent. The base is untouched, so you can fork it again for the next task without repeating the setup.

## Clean up

A sandbox can't be deleted while forks of it still exist; the API rejects the request with `sandbox has dependent forks`. Delete the winning fork first, then the base.

<CodeGroup>
  ```typescript cleanup.ts theme={null}
  import * as archil from "disk";

  const sandboxes = (await archil.listSandboxes()).filter((s) => s.name.startsWith("parallel-agents-"));
  const base = sandboxes.find((s) => s.name === "parallel-agents-base")!;
  const forks = sandboxes.filter((s) => s !== base);

  for (const sandbox of [...forks, base]) {
    await sandbox.stop();
    await sandbox.delete();
  }
  ```

  ```python cleanup.py theme={null}
  import archil

  sandboxes = [s for s in archil.list_sandboxes() if s.name.startswith("parallel-agents-")]
  base = next(s for s in sandboxes if s.name == "parallel-agents-base")
  forks = [s for s in sandboxes if s is not base]

  for sandbox in [*forks, base]:
      sandbox.stop()
      sandbox.delete()
  ```
</CodeGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Coding agent in a sandbox" icon="code" href="/guides/agents/coding-agent">
    Give one agent a persistent machine, run its app as a service, and pause it between sessions.
  </Card>

  <Card title="Persistent sandboxes" icon="box" href="/compute/sandboxes/introduction">
    Lifecycle, services, network access, and forking in detail.
  </Card>

  <Card title="Branches and checkpoints" icon="code-branch" href="/concepts/branches-and-checkpoints">
    Fork an Archil disk instead of a whole machine.
  </Card>

  <Card title="Python SDK" icon="python" href="/sdks/python">
    Full reference for sandboxes, disks, and `exec` options.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.