Skip to main content

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.

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.
npm install dedalus-labs ws @anthropic-ai/sdk
.env
DEDALUS_API_KEY=<your-dedalus-key>
ANTHROPIC_API_KEY=<your-anthropic-key>

1. Create a machine and open a terminal

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.
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

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();
const fresh = await dedalus.machines.retrieve(machine.machine_id);
await dedalus.machines.delete(machine.machine_id, { "If-Match": fresh.revision });

Notes

  • Why a terminal and not 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.
Last modified on May 2, 2026