Agent Adapters
Connect Claude Code, Codex, OpenCode, and custom AI agents to Runra Runtime using agent adapters. Adapter interface, built-in adapters, and writing custom 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
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
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 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}`);
Codex
const runra = new Runra({
agent: {
provider: "codex",
config: {
model: "gpt-5",
maxTurns: 30,
approvalMode: "auto",
},
},
// ... sandbox and observability config
});
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
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
| 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
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
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
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
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
});
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
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 } },
});
Next Steps
- LLM Providers — configure AI backends
- Runtime Architecture — understand the full stack
- SDK Reference — programmatic API
- CLI Reference — run agents via CLI