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

# Browser Agent

> An LLM agent that drives a real Linux shell on a Dedalus Machine over WebSocket.

A terminal gives an agent the same primitives a human gets: a stateful shell, working directory, environment, real interactive programs. The agent writes keystrokes, reads bytes, decides the next command.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install dedalus-labs ws @anthropic-ai/sdk
```

```env .env theme={"theme":{"light":"github-light","dark":"github-dark"}}
DEDALUS_API_KEY=<your-dedalus-key>
ANTHROPIC_API_KEY=<your-anthropic-key>
```

## 1. Create a machine and open a terminal

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import Dedalus from "dedalus-labs";
import WebSocket from "ws";

const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! });

const machine = await dedalus.machines.create({ image: "ubuntu-24.04" });
const term = await dedalus.machines.terminals.create(machine.machine_id, {
  width: 100,
  height: 30,
});

const ws = new WebSocket(term.stream_url);
await new Promise((r) => ws.once("open", r));
```

## 2. Bridge stdin/stdout for the agent

The agent writes a command, you send it as a Binary frame, you collect the bytes that come back until the shell prints a fresh prompt, then you hand the captured output to the agent.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
function send(input: string) {
  ws.send(Buffer.from(input));
}

function readUntilPrompt(promptRe = /\$\s$/): Promise<string> {
  return new Promise((resolve) => {
    let buf = "";
    const onMsg = (data: WebSocket.RawData) => {
      buf += data.toString("utf8");
      if (promptRe.test(stripAnsi(buf))) {
        ws.off("message", onMsg);
        resolve(buf);
      }
    };
    ws.on("message", onMsg);
  });
}

const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
```

## 3. Loop the agent against the shell

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import Anthropic from "@anthropic-ai/sdk";
const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

await readUntilPrompt(); // drain banner

const history: { role: "user" | "assistant"; content: string }[] = [
  { role: "user", content: "Find every Python file under ~ that mentions 'asyncio' and count the matching lines." },
];

for (let step = 0; step < 8; step++) {
  const reply = await claude.messages.create({
    model: "claude-opus-4-7",
    max_tokens: 512,
    system:
      "You drive a bash shell on a Linux VM. Reply with ONE shell command on a single line. " +
      "When done, reply with the literal word DONE.",
    messages: history,
  });
  const cmd = (reply.content[0] as { text: string }).text.trim();
  history.push({ role: "assistant", content: cmd });
  if (cmd === "DONE") break;

  send(cmd + "\n");
  const output = stripAnsi(await readUntilPrompt());
  history.push({ role: "user", content: output });
}

ws.close();
await dedalus.machines.delete(machine.machine_id);
```

## Notes

* **Why a terminal and not [executions](/dcs/dm/executions)?** State persists across commands. `cd /tmp && ls` followed by `pwd` returns `/tmp`. Executions are independent and lose that.
* **Resize the PTY** before running TUI programs (`top`, `htop`, `less`): send a Text frame `{"cols": 200, "rows": 50}`.
* **Stripping ANSI** is only for the prompt-detection heuristic. If the agent itself benefits from colors and cursor codes (e.g., parsing `git status` output), pass the raw bytes through.
* **Detect the prompt** robustly by setting `PS1` to a known sentinel (`export PS1='__DEDALUS_PROMPT__$ '`) at the start of the session. Regex against that instead of `\$\s$`.
* **Timeouts**: wrap `readUntilPrompt` in a `Promise.race` against a timer; long-running commands (`apt-get install`) don't return a prompt for a while.
