SandboxSDK & APIAgents & sessions

Agents and sessions

Run an agent in a sandbox with access to its files and tools. Choose a supported harness, including Claude Code, Codex, OpenCode, or Pi.

Choose an execution mode

ModeUse it forAPI
Non-interactiveRun a task and read its result.box.prompt()
Non-interactive, streamedShow messages and tool activity while a task runs.box.streamPrompt()
Interactive TUIUse the agent’s native terminal interface, with keyboard input and screen updates.session.interactive()
Questions and approvalsLet a headless agent ask a user for input in your application.backend.interactions, session.interactions()

Streaming does not require a TUI. Headless agents can also ask questions or request approvals when you enable those interactions.

Run without a terminal

Set TANGLE_API_KEY and TANGLE_SANDBOX_ID to a running sandbox you own. Install the TypeScript SDK, or pip install httpx for Python. These examples leave the sandbox running. For a budgeted fleet, obtain box with await fleet.sandbox(machineId) from the original fleet instance.

import { Sandbox } from "@tangle-network/sandbox";

const apiKey = process.env.TANGLE_API_KEY;
if (!apiKey) throw new Error("Set TANGLE_API_KEY");

const client = new Sandbox({
  apiKey,
  baseUrl: "https://sandbox.tangle.tools",
});

const sandboxId = process.env.TANGLE_SANDBOX_ID;
if (!sandboxId) throw new Error("Set TANGLE_SANDBOX_ID");
const box = await client.get(sandboxId);
if (!box) throw new Error("Sandbox not found");

const sessionId = crypto.randomUUID();
const result = await box.prompt("Report the installed Node.js version.", {
  sessionId,
  backend: { type: "codex" },
});
console.log(sessionId, result.status, result.response ?? result.error);

Use the same sessionId for follow-up messages in that conversation. Use a new session when switching to a different agent. The Python HTTP request encodes text parts as Base64.

Stream output

streamPrompt() yields messages, tool activity, usage, and execution events as the agent works. The outcome tracker distinguishes completion, failure, and requests for human input.

TypeScript
import { createAgentRunOutcomeTracker } from "@tangle-network/sandbox/runtime";
import { Sandbox } from "@tangle-network/sandbox";

const apiKey = process.env.TANGLE_API_KEY;
if (!apiKey) throw new Error("Set TANGLE_API_KEY");

const client = new Sandbox({
  apiKey,
  baseUrl: "https://sandbox.tangle.tools",
});

const sandboxId = process.env.TANGLE_SANDBOX_ID;
if (!sandboxId) throw new Error("Set TANGLE_SANDBOX_ID");
const box = await client.get(sandboxId);
if (!box) throw new Error("Sandbox not found");

const sessionId = crypto.randomUUID();
const outcome = createAgentRunOutcomeTracker();
for await (const event of box.streamPrompt("Report the installed Node.js version.", {
  sessionId,
  backend: { type: "claude-code" },
})) {
  outcome.observe(event);
  console.log(event);
}
console.log(sessionId, outcome.finish());

A closed stream alone does not mean the task succeeded. For work that must continue after the caller exits, use a durable turn.

Enable questions and approvals

Enable the interaction kinds your application handles with backend.interactions. This example lets Codex ask a question and prints requests while the agent runs. Keep this stream open while another process or your application sends the answer.

TypeScript
import { createAgentRunOutcomeTracker } from "@tangle-network/sandbox/runtime";
import { Sandbox } from "@tangle-network/sandbox";

const apiKey = process.env.TANGLE_API_KEY;
if (!apiKey) throw new Error("Set TANGLE_API_KEY");

const client = new Sandbox({
  apiKey,
  baseUrl: "https://sandbox.tangle.tools",
});

const sandboxId = process.env.TANGLE_SANDBOX_ID;
if (!sandboxId) throw new Error("Set TANGLE_SANDBOX_ID");
const box = await client.get(sandboxId);
if (!box) throw new Error("Sandbox not found");

const sessionId = crypto.randomUUID();
console.log({ sandboxId, sessionId });
const outcome = createAgentRunOutcomeTracker();
for await (const event of box.streamPrompt(
  "Ask me what to call the new project before creating files.",
  { sessionId, backend: { type: "codex", interactions: { question: true } } },
)) {
  outcome.observe(event);
  console.log(event);
}
console.log(outcome.finish());
SettingAgent behavior
question: trueAsk the user to answer or choose.
permission: trueRequest a tool or command approval.
plan: trueSubmit a plan for a decision.

Unsupported interaction kinds fail when the session initializes. Read interaction events as they arrive. session.interactions() retrieves outstanding requests when your application reconnects.

Answer a question

Set TANGLE_SESSION_ID to the printed session ID. Set TANGLE_QUESTION_ID to event.data.request.id from the question interaction. Set TANGLE_ANSWERS_JSON using request.answerSpec.fields and your user’s answers. For a string-array field named q0, an answer could be {"q0":["My project"]}.

TypeScript
import { Sandbox } from "@tangle-network/sandbox";

const apiKey = process.env.TANGLE_API_KEY;
if (!apiKey) throw new Error("Set TANGLE_API_KEY");

const client = new Sandbox({
  apiKey,
  baseUrl: "https://sandbox.tangle.tools",
});

const sandboxId = process.env.TANGLE_SANDBOX_ID;
if (!sandboxId) throw new Error("Set TANGLE_SANDBOX_ID");
const box = await client.get(sandboxId);
if (!box) throw new Error("Sandbox not found");

const sessionId = process.env.TANGLE_SESSION_ID;
const questionId = process.env.TANGLE_QUESTION_ID;
const answers = process.env.TANGLE_ANSWERS_JSON;
if (!sessionId || !questionId || !answers) {
  throw new Error("Set TANGLE_SESSION_ID, TANGLE_QUESTION_ID, and TANGLE_ANSWERS_JSON");
}
const session = box.session(sessionId);
const acknowledgement = await session.answer(questionId, JSON.parse(answers));
console.log(acknowledgement);

Keep the acknowledgement and inspect the session before sending another turn. Respond to permissions with respondToPermission() and plans with approvePlan() or rejectPlan(). Plan decisions can enqueue a follow-up turn; do not submit that turn again.

Isolate a task’s changes

Set TANGLE_REPO_URL to a public Git repository URL. This example creates a task session, asks the agent to write a README, and returns its file changes.

TypeScript
import { Sandbox } from "@tangle-network/sandbox";

const apiKey = process.env.TANGLE_API_KEY;
if (!apiKey) throw new Error("Set TANGLE_API_KEY");

const client = new Sandbox({
  apiKey,
  baseUrl: "https://sandbox.tangle.tools",
});

const repoUrl = process.env.TANGLE_REPO_URL;
if (!repoUrl) throw new Error("Set TANGLE_REPO_URL to a public Git repository URL");

const box = await client.create({
  maxLifetimeSeconds: 900,
  git: { url: repoUrl },
});
await box.waitFor("running");
const { session: task } = await box.createTaskSession({
  sessionId: crypto.randomUUID(),
  title: "Write a README",
  backend: { type: "opencode" },
  isolateFileWrites: true,
});
await task.sendMessage({
  parts: [{ type: "text", text: "Write a README explaining this workspace." }],
  turnId: crypto.randomUUID(),
});
console.log(await task.result());
console.log(await task.changes());
await box.delete();

Review task.changes() before applying them with task.commit(). The example deletes its sandbox after inspection. Keep its sandbox and session IDs when another process needs to continue the task.

Reconnect to a durable turn

Use dispatchPrompt() when work must continue after the client disconnects. Save the sandbox ID, session ID, and turn ID in your job record before dispatching. Set the corresponding environment variables for this example.

TypeScript
import { Sandbox } from "@tangle-network/sandbox";

const apiKey = process.env.TANGLE_API_KEY;
if (!apiKey) throw new Error("Set TANGLE_API_KEY");

const client = new Sandbox({
  apiKey,
  baseUrl: "https://sandbox.tangle.tools",
});

const sandboxId = process.env.TANGLE_SANDBOX_ID;
const sessionId = process.env.TANGLE_SESSION_ID;
const turnId = process.env.TANGLE_TURN_ID;
if (!sandboxId || !sessionId || !turnId) {
  throw new Error("Set TANGLE_SANDBOX_ID, TANGLE_SESSION_ID, and TANGLE_TURN_ID");
}

const box = await client.get(sandboxId);
if (!box) throw new Error("Sandbox not found");
const receipt = await box.dispatchPrompt("Analyze code quality", { sessionId, turnId });
console.log(receipt);

const cached = await box.findCompletedTurn(turnId, { sessionId: receipt.sessionId });
if (cached) {
  console.log(cached.result);
} else {
  if (!receipt.executionId) throw new Error("Missing execution ID");
  const result = await box.session(receipt.sessionId).result({
    executionId: receipt.executionId,
  });
  console.log(result);
}

Reuse the same turn ID and prompt when retrying the same job. A completed result is deduplicated while its cache is retained. A session ID alone does not deduplicate completed turns.

Keep the receipt’s executionId to reconnect to that specific execution. session.events({ executionId }) follows its output. Delete the sandbox after the job and its retries finish.