Agent Adapters
Agent adapters translate between Runra’s runtime tool interface and each AI agent’s native communication protocol. This lets you swap agents without changing your sandbox or observability configuration.
Built-in Adapters
Section titled “Built-in Adapters”Runra ships with adapters for the most popular AI coding agents:
| Adapter | Provider Key | Description |
|---|---|---|
| Claude Code | claude-code | Anthropic’s CLI-based coding agent |
| Codex | codex | OpenAI’s coding agent |
| OpenCode | opencode | Open-source coding agent |
| Custom | custom | Bring your own agent logic |
Using an Adapter
Section titled “Using an Adapter”Claude Code
Section titled “Claude Code”import { Runra } from "@runra/runtime";
const runra = new Runra({ sandbox: { provider: "runra-sandbox", apiKey: process.env.RUNRA_API_KEY, }, agent: { provider: "claude-code", config: { model: "claude-sonnet-4-20250514", maxTurns: 50, allowedTools: ["bash", "read", "write", "edit", "glob", "grep"], permissionMode: "auto-approve", // or "prompt" }, }, observability: { provider: "axiom", token: process.env.AXIOM_TOKEN, dataset: "runra-events", },});
// Start the agent with a promptconst result = await runra.start({ prompt: "Create a TypeScript Express server with a /health endpoint", sandboxOptions: { image: "node:22", resources: { cpu: 2, memoryMb: 4096 }, },});
console.log(`Agent finished: ${result.summary}`);const runra = new Runra({ agent: { provider: "codex", config: { model: "gpt-5", maxTurns: 30, approvalMode: "auto", }, }, // ... sandbox and observability config});OpenCode
Section titled “OpenCode”const runra = new Runra({ agent: { provider: "opencode", config: { model: "anthropic/claude-sonnet-4-20250514", maxTurns: 40, workdir: "/workspace", }, }, // ... sandbox and observability config});Configuration Reference
Section titled “Configuration Reference”All adapters share a common configuration interface:
interface AgentConfig { provider: string; // Adapter to use config: { model: string; // LLM model for this agent maxTurns: number; // Max agent tool call loops (default: 50) allowedTools?: string[]; // Restrict available tools permissionMode?: "auto-approve" | "prompt" | "plan"; workdir?: string; // Agent working directory systemPrompt?: string; // Override system prompt apiKey?: string; // Agent-specific API key (falls back to LLM provider) };}Permission Modes
Section titled “Permission Modes”| Mode | Behavior |
|---|---|
auto-approve | Agent can run any allowed tool without confirmation |
prompt | Each tool call requires human approval (for interactive use) |
plan | Agent creates a plan first, then executes with auto-approve |
Tool Allowlisting
Section titled “Tool Allowlisting”Restrict which tools the agent can use:
agent: { provider: "claude-code", config: { allowedTools: ["bash", "read", "write", "edit"], // Disabled: "glob", "grep", "task", "web_search", "web_fetch" },}Adapter Interface
Section titled “Adapter Interface”If the built-in adapters don’t fit your needs, implement the AgentAdapter interface:
interface AgentAdapter { /** Unique identifier for this adapter */ readonly id: string;
/** Initialize the adapter with config */ initialize(config: AgentAdapterConfig): Promise<void>;
/** Start the agent with a prompt and sandbox context */ start(params: StartParams): Promise<AgentResult>;
/** Send a message mid-run (for interactive use) */ sendMessage?(message: string): Promise<void>;
/** Gracefully stop the agent */ stop?(): Promise<void>;
/** Clean up adapter resources */ dispose(): Promise<void>;}
interface StartParams { prompt: string; sandboxId: string; sandboxProvider: SandboxProvider; eventEmitter: EventEmitter; workdir?: string; env?: Record<string, string>;}
interface AgentResult { success: boolean; summary: string; totalTurns: number; totalDurationMs: number; totalCost: number; error?: string;}Writing a Custom Adapter
Section titled “Writing a Custom Adapter”Create a custom adapter to integrate any agent. Here’s an adapter for a hypothetical REST-based agent:
import type { AgentAdapter, AgentAdapterConfig, AgentResult, SandboxProvider, StartParams,} from "@runra/runtime";
interface MyAgentConfig { endpoint: string; apiKey: string; model: string; maxTurns: number;}
export class MyAgentAdapter implements AgentAdapter { readonly id = "my-agent";
private config!: MyAgentConfig; private sandboxProvider!: SandboxProvider; private sandboxId!: string;
async initialize(config: AgentAdapterConfig): Promise<void> { this.config = config.config as MyAgentConfig; }
async start(params: StartParams): Promise<AgentResult> { this.sandboxProvider = params.sandboxProvider; this.sandboxId = params.sandboxId;
const startTime = Date.now(); let messages = [{ role: "user", content: params.prompt }];
for (let turn = 0; turn < this.config.maxTurns; turn++) { // 1. Ask the agent what to do const response = await fetch(this.config.endpoint, { method: "POST", headers: { Authorization: `Bearer ${this.config.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: this.config.model, messages }), });
const choice = await response.json(); const agentMessage = choice.messages[0];
// Agent is done if (agentMessage.stop_reason === "end_turn") { return { success: true, summary: agentMessage.content, totalTurns: turn + 1, totalDurationMs: Date.now() - startTime, totalCost: 0, // Calculate from usage }; }
// 2. Execute the tool call in the sandbox const toolCall = agentMessage.tool_use; let toolResult: string;
switch (toolCall.name) { case "bash": { const execResult = await this.sandboxProvider.execute( this.sandboxId, toolCall.input.command, { cwd: toolCall.input.cwd, env: toolCall.input.env } ); toolResult = execResult.stdout || execResult.stderr; break; } case "read_file": { toolResult = await this.sandboxProvider.readFile( this.sandboxId, toolCall.input.path ); break; } case "write_file": { await this.sandboxProvider.writeFile( this.sandboxId, toolCall.input.path, toolCall.input.content ); toolResult = "File written"; break; } default: toolResult = `Unknown tool: ${toolCall.name}`; }
// 3. Feed result back to agent messages.push(agentMessage); messages.push({ role: "user", content: [ { type: "tool_result", tool_use_id: toolCall.id, content: toolResult, }, ], });
// Emit observability event params.eventEmitter.emit("agent.tool_call", { sandboxId: this.sandboxId, tool: toolCall.name, args: toolCall.input, result: toolResult, }); }
return { success: false, summary: "Max turns reached", totalTurns: this.config.maxTurns, totalDurationMs: Date.now() - startTime, totalCost: 0, error: "MAX_TURNS_EXCEEDED", }; }
async dispose(): Promise<void> { // Cleanup }}Registering a Custom Adapter
Section titled “Registering a Custom Adapter”import { Runra } from "@runra/runtime";import { MyAgentAdapter } from "./my-agent-adapter";
// Register the custom adapterRunra.registerAgentAdapter("my-agent", () => new MyAgentAdapter());
// Use itconst runra = new Runra({ agent: { provider: "my-agent", config: { endpoint: "https://my-agent.internal/v1/chat", apiKey: process.env.MY_AGENT_KEY, model: "gpt-5", maxTurns: 30, }, }, // ... sandbox and observability config});Tool Mapping
Section titled “Tool Mapping”Adapters map agent-native tools to Runra’s sandbox operations:
| Agent Tool | Runra Operation | Description |
|---|---|---|
bash / execute / shell | sandbox.execute() | Run shell command |
read / read_file | sandbox.readFile() | Read file contents |
write / write_file | sandbox.writeFile() | Create or overwrite file |
edit / str_replace | sandbox.readFile() + sandbox.writeFile() | Modify existing file |
glob / list_files | sandbox.listFiles() | List directory contents |
grep / search | sandbox.execute("grep ...") | Search file contents |
web_fetch / http | sandbox.execute("curl ...") | Make HTTP request |
web_search | Provider-native | Web search (agent-level, not sandbox) |
Multi-Agent Runs
Section titled “Multi-Agent Runs”Run multiple agents in the same sandbox (one at a time):
const sandbox = await runra.sandboxes.create({ image: "node:22", resources: { cpu: 4, memoryMb: 8192 },});
// Agent 1: Set up the projectawait runra.start({ prompt: "Initialize a Next.js project with TypeScript", sandboxId: sandbox.id, agent: { provider: "claude-code", config: { maxTurns: 10 } },});
// Agent 2: Add featuresawait runra.start({ prompt: "Add a /api/health endpoint and a Home page", sandboxId: sandbox.id, agent: { provider: "codex", config: { maxTurns: 10 } },});Next Steps
Section titled “Next Steps”- LLM Providers — configure AI backends
- Runtime Architecture — understand the full stack
- SDK Reference — programmatic API
- CLI Reference — run agents via CLI