# Contents

When I started building tools for orchestrating coding agents, I came across numerous issues. Agents like Claude Code and Codex are designed to run locally on your machine and I needed them to run in the cloud triggered by API calls, persisting across requests, and in isolated environments. After running into these issues numerous times, I built the Background Agents SDK, which solves this for CLI coding agents in general and using Daytona as the sandbox provider. I evaluated several options for running agents in isolated environments, and Daytona stood out for a few reasons:

  • Instant sandbox creation: I can spin up an isolated environment with a single API call

  • Built-in git support: Cloning repos, switching branches, and pushing changes are all first-class operations

  • Long-lived persistence: Sandboxes outlive my serverless functions and can run indefinitely with Daytona

  • Security by design: Credentials like GitHub tokens stay outside the sandbox, so agents can't accidentally (or intentionally) misuse them This meant I could focus on the agent orchestration logic instead of building infrastructure.

Using the Background Agents SDK

I built the Background Agents SDK as a TypeScript library to solve a specific problem: running long-running AI coding agents from serverless applications without managing infrastructure. First, create a Daytona sandbox and clone the repo:

1import { Daytona } from "@daytonaio/sdk"
2
3const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
4
5const sandbox = await daytona.create()
6
7await sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/repo")
8
9await sandbox.git.checkoutBranch("/home/daytona/repo", "fix-auth-bug")

Then create a session and start the agent:

1import { createSession, getSession } from "background-agents"
2
3const session = await createSession("claude", {
4  sandbox,
5  cwd: "/home/daytona/repo",
6  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
7})
8
9await session.start("Refactor the auth module")

The agent is now running in the sandbox with the repo cloned into it. To use a different agent, just change "claude" to any of the supported agents:

  • "claude": Claude Code from Anthropic

  • "codex": Codex from OpenAI

  • "gemini": Gemini CLI from Google

  • "goose": Goose from Block

  • "opencode": OpenCode, open-source multi-provider agent

  • "pi": Pi, lightweight open-source agent

Long-Running Agents

Coding agents can run from a few minutes to much longer. For a serverless application, you need to be able to reconnect to running agent sessions to check their status later. When you start a session, save the sandbox ID (sandbox.id) and session ID (session.id). Later, you can reconnect:

1const sandbox = await daytona.get(sandboxId)
2
3const session = await getSession(sessionId, { sandbox })
4
5const { events, running } = await session.getEvents()
6
7for (const event of events) {
8  if (event.type === "token") process.stdout.write(event.text)
9  if (event.type === "tool_start") console.log(`\n[Tool: ${event.name}]`)
10}

The SDK tracks which events you've already seen. Each call to getEvents() returns only the new ones.

Event Types

Since every agent has its own output format, the SDK normalizes agent outputs into a common event structure: token for text output, tool_start and tool_end to mark when a tool begins (with name and input) and finishes (with output), and end when the agent completes the task. Here's what the output of the SDK looks like like when an agent edits a file:

1{ type: "token", text: "I'll update the auth module..." }
2{ type: "tool_start", name: "edit", input: { file_path: "src/auth.ts", ... } }
3{ type: "tool_end", output: "File updated successfully" }
4{ type: "token", text: "Done. The auth module now..." }
5{ type: "end" }

Pushing to Git

When getEvents() returns running: false, the agent has finished. You can then push the changes:

1if (!running) {
2  await sandbox.git.push("/home/daytona/repo", "x-access-token", githubToken)
3}

You can also create a PR using the GitHub API:

1import { Octokit } from "octokit"
2
3const octokit = new Octokit({ auth: githubToken })
4await octokit.request("POST /repos/{owner}/{repo}/pulls", {
5  owner: "user",
6  repo: "repo",
7  title: "Refactor auth module",
8  head: "fix-auth-bug",
9  base: "main",
10})

One security benefit is that because Daytona manages the git operations, my GitHub token is never stored in the sandbox. The agent can't push to branches on its own or delete repos, it only has access to the files.

Adding New Agents

Anyone can add new agents to the framework. All you have to do is provide the right information, the agent's CLI commands and documentation, and a coding agent can build the rest using the TESTING.md instructions. The sample output is auto-generated, and the whole process can often be completed in a single session.

Conclusion

The hard parts of running AI agents in the cloud is isolation, persistence, and scaling. Daytona handled all of that, enabling me to build a reusable SDK around all CLI coding agents. If you're building something similar, I'd love to hear about it, and contributions to the SDK are welcome.

Check out the Background Agents SDK on npm or GitHub!