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