CharClaw / Docs / Agents SDK

The CharClaw Agents SDK

@charclaw/agents is the TypeScript library at the heart of CharClaw. It runs interactive coding-agent CLIs — Claude Code, OpenAI Codex, Google Gemini, Block Goose, OpenCode, Pi — as long-running background tasks inside isolated Daytona sandboxes or directly on a developer's machine, and it streams structured events back to your web service or desktop app via a single polling API.

You can use it standalone in any Node.js project. CharClaw uses it to power its Kanban board, its chat-with-agent surface, and its autopilot scheduler.

Why this SDK exists

Coding-agent CLIs are designed for an interactive shell. To wire them into a long-running web service, a scheduled job, or a desktop app that survives sleep/wake cycles, four problems show up immediately:

  • Background execution. The agent's turn often outlives the request that started it.
  • Streaming without stdin/stdout pipes. Once the spawning process is gone, you can't read its pipes — you need a polling-friendly transport.
  • Sandbox isolation. Agent activity should not see your production secrets or your home directory.
  • Re-attachment. If a serverless function cold-starts mid-turn, the new instance has to pick up exactly where the old one left off.

@charclaw/agents handles all four with a single createSession entry point, a unified Event stream, and a sandbox abstraction that targets either a Daytona cloud sandbox or the local host.

Install

npm install @charclaw/agents @daytonaio/sdk

The @daytonaio/sdk dependency is only required if you'll run agents in Daytona sandboxes. For pure local-machine use, install just @charclaw/agents.

Quick start — Daytona sandbox

import { Daytona } from "@daytonaio/sdk"
import { adaptDaytonaSandbox, createSession } from "@charclaw/agents"

const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY! })
const raw = await daytona.create()
const sandbox = adaptDaytonaSandbox(raw)

const session = await createSession("claude", {
  sandbox,
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
  model: "sonnet",
  systemPrompt: "You are a careful, focused engineer.",
})

await session.start("Add input validation to the /signup route.")

while (true) {
  const { events, running } = await session.getEvents()
  for (const e of events) {
    if (e.type === "token") process.stdout.write(e.text)
    if (e.type === "tool_start") console.log(`\n[tool] ${e.name}`)
  }
  if (!running) break
  await new Promise(r => setTimeout(r, 1000))
}

await raw.delete()

Quick start — local sandbox

import { createLocalSandbox, createSession, localWorkdir } from "@charclaw/agents"

const sandbox = createLocalSandbox({ cwd: localWorkdir("acme", "api", "main") })
const session = await createSession("claude", {
  sandbox,
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
})
await session.start("Summarize the diff on this branch.")

localWorkdir(...segments) returns a path under ~/charclaw-workspaces. Without arguments it returns the base directory; with arguments it joins them, which is convenient for per-repo and per-branch checkouts.

Restart-tolerant turns

Every session writes its metadata, parser cursor, and current turn's PID to ~/.charclaw-sessions/<id>/ inside the sandbox. That means a second Node process can re-attach to an in-flight turn and continue polling without losing state:

import { adaptDaytonaSandbox, getSession } from "@charclaw/agents"

// Save these IDs somewhere durable when you start the turn.
const savedSandboxId = "sb_..."
const savedSessionId = "..."

// In a different process — possibly a serverless function cold-start.
const sandbox = adaptDaytonaSandbox(await daytona.get(savedSandboxId))
const session = await getSession(savedSessionId, { sandbox })
const { events, running } = await session.getEvents()

The polling cursor advances atomically; if two processes poll concurrently, each sees its own slice of new events and the cursor ends up at max(positions).

Supported agents

Provider CLI Auth env Parser status
"claude" Claude Code ANTHROPIC_API_KEY or CLAUDE_CODE_CREDENTIALS Stable
"codex" OpenAI Codex CLI OPENAI_API_KEY Tolerant — validate against your CLI version
"gemini" Google Gemini CLI GEMINI_API_KEY Tolerant
"goose" Block Goose Provider-specific Tolerant
"opencode" OpenCode Provider-specific Tolerant
"pi" Pi Provider-specific Tolerant
"mock" Built-in echo None Stable — useful for tests
Tolerant parsers: the non-Claude agents accept a superset of common JSON-Lines event shapes. End-to-end test against the version of the CLI you actually deploy and tighten the parser if you find a mismatch — pull requests welcome.

API reference

createSession(provider, options)

Creates a new session and provisions its directory inside the sandbox. The provider's CLI is auto-installed on the first session per sandbox.

const session = await createSession("claude", {
  sandbox,                                          // CodeAgentSandbox
  env: { ANTHROPIC_API_KEY: "sk-..." },             // session-scoped env vars
  model: "sonnet",                                  // optional
  systemPrompt: "You are helpful.",                 // optional
  cwd: "/workspace/my-repo",                        // optional working dir
  sessionId: "explicit-id",                         // optional override
})

session.start(prompt, options?)

Spawns the agent process for one turn. Returns immediately with the turn handle (PID, output file, turn ID).

const { pid, outputFile, turnId } = await session.start("Refactor X.", {
  env: { GITHUB_TOKEN: "ghp_..." },                 // run-scoped, cleared after
  history: priorMessages,                           // optional context hint
})

session.getEvents()

Polls for new events since the last call. Always safe to call — returns an empty events array if nothing new has appeared. Persists parser state between calls.

const { events, running, sessionId, cursor, runPhase } = await session.getEvents()

session.cancel()

SIGTERM, brief grace, SIGKILL. Updates the persisted phase to "cancelled".

getSession(sessionId, options)

Re-attaches to an existing session by reading session.json and state.json from the sandbox. Useful for continuing a turn from a different process or after a restart.

Event types

type Event =
  | { type: "session"; id: string }
  | { type: "token"; text: string }
  | { type: "tool_start"; name: string; id?: string; input?: unknown }
  | { type: "tool_delta"; id?: string; text: string }
  | { type: "tool_end"; name?: string; id?: string; output?: string; isError?: boolean }
  | { type: "end"; error?: string }
  | { type: "agent_crashed"; message?: string; output?: string }

Tool names are normalized to a canonical lowercase form (shell, read, write, edit, glob, grep, task, todo, fetch) regardless of which agent emits them. Use getToolDisplayName(name) to render the canonical UI label (Bash, Read, Write, …).

How it works under the hood

  1. Provision. createSession creates ~/.charclaw-sessions/<id>/ and writes session.json (provider, model, system prompt) and state.json (parser cursor, phase, history).
  2. Launch. session.start calls sandbox.executeBackground, which wraps the command in nohup (Daytona) or spawn(detached: true) (local). The wrapper writes the agent's stdout/stderr to a turn-specific log file and creates a .done sentinel file when the command exits.
  3. Poll. session.getEvents reads the log file's new bytes (sliced from the last cursor position), runs the agent's JSON-Lines parser, and returns the events. State is persisted back to state.json.
  4. Detect end. Either the .done file exists (clean finish) or the process is no longer alive (crash / kill / external SIGTERM). Either way, running goes false.
  5. Re-attach. Because every byte of state lives in files inside the sandbox, any process with sandbox access can call getSession and pick up where the previous one left off.

Debugging

CHARCLAW_AGENTS_DEBUG=1 node my-script.js

The SDK prints structured debug messages to stderr when this flag is set: PID acquisition, command construction, parser state advances, and CLI installation steps.

License

GNU AGPL v3 or later. See the full license text.

Network-service copyleft. AGPL-3.0 means that if you run a modified version of @charclaw/agents as a network service, you must offer the source code of your modifications to the users of that service. If that does not fit your use case, contact anitc98@gmail.com for commercial licensing.

→ Read about the launch in Launching the CharClaw Agents SDK, or browse the source on GitHub.