background-agents
v0.1.1
Published
A TypeScript SDK for interacting with various AI coding agents (Claude, Codex, OpenCode, Gemini)
Downloads
207
Maintainers
Readme
Coding Agents SDK
A unified TypeScript interface for AI coding agents—Claude, Codex, Gemini, and OpenCode. Commands run in secure Daytona sandboxes by default, with real-time PTY streaming.
import { Daytona } from "@daytonaio/sdk"
import { createSession } from "background-agents"
const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
const sandbox = await daytona.create({ envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY } })
const session = await createSession("claude", { sandbox })
for await (const event of session.run("Hello!")) {
if (event.type === "token") process.stdout.write(event.text)
if (event.type === "end") break
}
await sandbox.delete()Same pattern for any provider: create a sandbox, create a session, stream events, then tear down. Swap the provider name and env keys as needed.
Features
- Secure by default — Execution runs in isolated Daytona sandboxes
- Real-time streaming — PTY-based streaming for live token output
- Unified API — One interface for Claude, Codex, Gemini, and OpenCode
- Zero-friction setup — Provider CLI is installed when you create a session (
skipInstall: trueto skip). Env and Codex login run on everyrun(). - Session persistence — Resume conversations across runs
Provider support
| Provider | Status | Auth |
|----------|--------|------|
| Claude | ✅ | ANTHROPIC_API_KEY |
| Codex | ✅ | OPENAI_API_KEY |
| OpenCode | ✅ | Provider-specific (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY) |
| Gemini | 🚧 | GOOGLE_API_KEY |
Prerequisites
A Daytona API key (or run locally without a sandbox).
export DAYTONA_API_KEY=dtn_your_api_keyInstallation
npm install background-agentsFor sandboxed execution, also install the Daytona SDK:
npm install @daytonaio/sdkNext.js: Merge the SDK's Next config so native deps (e.g. ssh2 / cpu-features) are not bundled:
// next.config.js or next.config.mjs
import codeagentsdk from 'background-agents/next.config'
export default { ...codeagentsdk, ...yourConfig }Quick start
1. Create a sandbox — Pass provider API keys via the sandbox; the SDK does not read your host env.
import { Daytona } from "@daytonaio/sdk"
import { createSession } from "background-agents"
const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
const sandbox = await daytona.create({
envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
})2. Create a session — The provider CLI is installed in the sandbox (unless skipInstall: true).
const session = await createSession("claude", {
sandbox,
model: "sonnet",
timeout: 120,
systemPrompt: "You are a helpful coding assistant.",
})3. Stream responses
for await (const event of session.run("Hello!")) {
if (event.type === "token") process.stdout.write(event.text)
if (event.type === "tool_start") console.log(`\n[Tool: ${event.name}]`)
if (event.type === "end") break
}4. Cleanup
await sandbox.delete()Optional: Git workflow — Use the Daytona Git SDK to clone before and push after:
const repoPath = "workspace/repo"
await sandbox.git.clone("https://github.com/user/repo.git", repoPath)
// ... run session ...
await sandbox.git.push(repoPath)Full example
End-to-end example with event handling and cleanup:
import { Daytona } from "@daytonaio/sdk"
import { createSession } from "background-agents"
async function main() {
const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
const sandbox = await daytona.create({
envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
})
try {
const session = await createSession("claude", { sandbox })
for await (const event of session.run("List /tmp then write /tmp/out.txt with 'done'")) {
switch (event.type) {
case "token":
process.stdout.write(event.text)
break
case "tool_start":
console.log("\n🛠️", event.name, event.input ?? "")
break
case "end":
console.log("\nDone.")
break
}
}
} finally {
await sandbox.delete()
}
}
main()CLI commands (reference)
Each provider is invoked via its CLI. Optional flags in brackets.
| Provider | Command |
|----------|---------|
| Claude | claude -p --output-format stream-json --verbose --dangerously-skip-permissions [--model <m>] [--resume <id>] <prompt> |
| Codex | codex exec --json --skip-git-repo-check --yolo [--model <m>] [resume <id>] <prompt> |
| OpenCode | opencode run --format json --variant medium -m <model> [-s <id>] <prompt> (via bash -lc "…") |
| Gemini | gemini -p --output-format stream-json --yolo [--model <m>] [--resume <id>] <prompt> |
API reference
createSession(provider, options)
Creates a session with the given provider and options (e.g. sandbox, model, timeout). Installs the provider CLI in the sandbox before returning unless skipInstall: true. Codex login runs automatically on each run() when needed.
const session = await createSession("claude", {
sandbox,
model: "sonnet",
timeout: 120,
})session.run(prompt)
Returns an async iterable of events. Stream and handle them uniformly across providers.
for await (const event of session.run("Hello")) {
// event.type: "session" | "token" | "tool_start" | "tool_delta" | "tool_end" | "end" | "agent_crashed"
}Event stream
| Event | Description | Fields |
|-------|-------------|--------|
| session | Session started (for resumption) | id: string |
| token | Streamed assistant text | text: string |
| tool_start | Tool invoked | name: string, input?: unknown |
| tool_delta | Streaming tool input | text: string |
| tool_end | Tool finished | output?: string |
| end | Turn complete | — |
| agent_crashed | Process exited without completing (crash/kill) | message?: string, output?: string (raw tail of stdout/stderr; often not JSONL) |
type Event =
| { type: "session"; id: string }
| { type: "token"; text: string }
| { type: "tool_start"; name: string; input?: unknown }
| { type: "tool_delta"; text: string }
| { type: "tool_end"; output?: string }
| { type: "end" }
| { type: "agent_crashed"; message?: string; output?: string }Normalized tool names
Tool names are normalized across providers. Each has a defined tool_start input and tool_end output.
| Tool | tool_start input | Claude | Codex | OpenCode |
|------|--------------------|:------:|:-----:|:--------:|
| write | { file_path, content?, kind } | ✅ | ✅ | ✅ |
| read | { file_path } | ✅ | — | ✅ |
| edit | { file_path, ... } | ✅ | — | ✅ |
| glob | { pattern } | ✅ | — | ✅ |
| grep | { pattern, path? } | ✅ | — | ✅ |
| shell | { command, description? } | ✅ | ✅ | ✅ |
Model selection
Set model when creating the session.
| Provider | Example | Docs |
|----------|---------|------|
| Claude | model: "sonnet" or "opus", "haiku" | Claude Code models |
| Codex | model: "gpt-4o" or "o1", "o3" | Codex CLI models |
| OpenCode | model: "openai/gpt-4o" (provider/model) | OpenCode models |
| Gemini | model: "gemini-2.0-flash" or "gemini-1.5-pro" | Gemini CLI model |
Sandboxed background sessions
For long-running or restart-tolerant flows: start the agent in the sandbox, write the event stream to log files there, and poll with getEvents. All state except the session ID lives in the sandbox.
- Session ID — One UUID per background session; host stores only this.
- start() — Returns immediately with
{ executionId, pid, outputFile }; the agent runs in the background. - isRunning() — True while the turn is in progress, false after.
- Crash detection — If the process exits without completing, getEvents returns an
agent_crashedevent. You can treat it likeendto stop polling and show a warning.
Example: start, persist sandboxId and backgroundSessionId, then reattach after a restart.
import { Daytona } from "@daytonaio/sdk"
import { createBackgroundSession, getBackgroundSession } from "background-agents"
const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY! })
const sandbox = await daytona.create({
envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
})
const bgSession = await createBackgroundSession("claude", {
sandbox,
model: "sonnet",
// Optional: per-session system prompt (applied once, persisted across turns).
systemPrompt: "You are a helpful coding assistant.",
})
await bgSession.start("Do a long-running refactor...")
// Persist sandbox.id and bgSession.id, then exit.
// --- After restart ---
const sandboxAgain = await daytona.get(sandboxId)
const bgAgain = await getBackgroundSession({
sandbox: sandboxAgain,
backgroundSessionId,
// Re-apply session options so the provider is recreated with the same model
// and system prompt when reattaching.
model: "sonnet",
systemPrompt: "You are a helpful coding assistant.",
})
async function poll() {
const { events } = await bgAgain.getEvents()
for (const e of events) {
if (e.type === "token") process.stdout.write(e.text)
else if (e.type === "tool_start") console.log("[Tool]", e.name)
}
if (!(await bgAgain.isRunning())) return
setTimeout(poll, 2000)
}
poll()
await bgAgain.cancel() // kill agent in sandbox (no-op if stopped)Local mode (dangerous)
Runs the provider CLI on your machine instead of a sandbox. Only use when you fully trust the code.
const session = await createSession("claude", { dangerouslyAllowLocalExecution: true })
for await (const event of session.run("Hello")) {
if (event.type === "token") process.stdout.write(event.text)
}Interactive REPL
# Claude (default)
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/repl.ts
# Other providers
npx tsx scripts/repl.ts --provider codex # OPENAI_API_KEY
npx tsx scripts/repl.ts --provider opencode
npx tsx scripts/repl.ts --provider gemini # GEMINI_API_KEY (or GOOGLE_API_KEY)
# Polling-based (background session)
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/repl-polling.tsnpx tsx scripts/repl.ts -h # help; providers: claude, codex, opencode, geminiHow it works
- Sandbox — You create a Daytona sandbox and pass it to
createSession. - CLI — Provider CLI is installed in the sandbox at session creation (unless
skipInstall: true). Eachrun()sets env and, for Codex, runscodex login --with-api-key. - PTY — Commands run in a PTY for real-time streaming.
- Events — JSON from the CLI is parsed into typed events.
- Cleanup — You call
sandbox.delete()when done.
┌─────────────┐ ┌──────────────────────────────────────┐
│ Your App │────▶│ Daytona Sandbox │
│ │◀────│ ┌─────────────┐ ┌─────────────┐ │
│ │ │ │ PTY Stream │◀──▶│ Agent CLI │ │
│ │ │ └─────────────┘ └─────────────┘ │
└─────────────┘ └──────────────────────────────────────┘Debug mode
Set CODING_AGENTS_DEBUG=1 (or any non-empty value) to log debugging information to stderr:
- Agent lifecycle — when sessions and background sessions are created, when runs start and end
- Background agents — when a turn starts (session dir, turn number, output file), when the background process is started (pid), and each time events are polled (cursor, event count)
- Unparsed output — any CLI line that didn’t parse as an event (helps spot hangs where the agent prints something the SDK doesn’t recognize)
CODING_AGENTS_DEBUG=1 npx tsx scripts/repl-polling.tsDevelopment
npm install
npm run build
npm test # unit tests (integration/sandbox-background skipped without keys)
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npm run test -- tests/integration/sandbox-background.test.ts # real sandbox background test
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/test-sdk-full.ts # integration
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/repl.ts # REPLResources
Sandbox — Daytona Docs · Daytona GitHub
Agents — Claude Code · Codex CLI · Gemini CLI · OpenCode
License
MIT
