Skip to content

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.

Runra ships with adapters for the most popular AI coding agents:

AdapterProvider KeyDescription
Claude Codeclaude-codeAnthropic’s CLI-based coding agent
CodexcodexOpenAI’s coding agent
OpenCodeopencodeOpen-source coding agent
CustomcustomBring your own agent logic
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 prompt
const 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
});
const runra = new Runra({
agent: {
provider: "opencode",
config: {
model: "anthropic/claude-sonnet-4-20250514",
maxTurns: 40,
workdir: "/workspace",
},
},
// ... sandbox and observability config
});

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)
};
}
ModeBehavior
auto-approveAgent can run any allowed tool without confirmation
promptEach tool call requires human approval (for interactive use)
planAgent creates a plan first, then executes with auto-approve

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"
},
}

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;
}

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
}
}
import { Runra } from "@runra/runtime";
import { MyAgentAdapter } from "./my-agent-adapter";
// Register the custom adapter
Runra.registerAgentAdapter("my-agent", () => new MyAgentAdapter());
// Use it
const 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
});

Adapters map agent-native tools to Runra’s sandbox operations:

Agent ToolRunra OperationDescription
bash / execute / shellsandbox.execute()Run shell command
read / read_filesandbox.readFile()Read file contents
write / write_filesandbox.writeFile()Create or overwrite file
edit / str_replacesandbox.readFile() + sandbox.writeFile()Modify existing file
glob / list_filessandbox.listFiles()List directory contents
grep / searchsandbox.execute("grep ...")Search file contents
web_fetch / httpsandbox.execute("curl ...")Make HTTP request
web_searchProvider-nativeWeb search (agent-level, not sandbox)

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 project
await runra.start({
prompt: "Initialize a Next.js project with TypeScript",
sandboxId: sandbox.id,
agent: { provider: "claude-code", config: { maxTurns: 10 } },
});
// Agent 2: Add features
await runra.start({
prompt: "Add a /api/health endpoint and a Home page",
sandboxId: sandbox.id,
agent: { provider: "codex", config: { maxTurns: 10 } },
});