[iris]
Guides

AI Agents

Build AI agents with safe code execution

Why Iris for AI Agents?

AI agents that execute code face a fundamental problem: code can fail, corrupt state, or cause unintended side effects.

Iris solves this with instant forks:

  1. Fork before risky operations
  2. Execute AI-generated code in the branch
  3. Discard the branch on failure — original is untouched

Basic Pattern

import { Sandbox } from '@iris/sdk'
import { generateCode } from './your-llm'

async function safeExecute(sandbox: Sandbox, task: string) {
  const branch = await sandbox.fork()

  try {
    const code = await generateCode(task)
    const result = await branch.exec.run(code)

    if (!result.ok) {
      throw new Error(result.stderr)
    }

    return { success: true, result }
  } catch (error) {
    await branch.delete()
    return { success: false, error }
  }
}
# from your_llm import generate_code

def safe_execute(sandbox, task: str):
    branch = sandbox.fork()
    try:
        cmd = generate_code(task)
        result = branch.exec.run(["bash", "-c", cmd])
        if not result.ok:
            raise RuntimeError(result.stderr)
        return {"success": True, "result": result}
    except Exception as e:
        branch.delete()
        return {"success": False, "error": e}

ReAct Agent Example

import { Sandbox } from '@iris/sdk'
import Anthropic from '@anthropic-ai/sdk'

const anthropic = new Anthropic()

async function reactAgent(task: string) {
  const sandbox = await Sandbox.create()
  const messages = []

  while (true) {
    const response = await anthropic.messages.create({
      model: 'claude-opus-4-7',
      max_tokens: 4096,
      messages: [{ role: 'user', content: task }, ...messages],
      tools: [{
        name: 'execute_code',
        description: 'Execute a shell command in the sandbox',
        input_schema: {
          type: 'object',
          properties: { code: { type: 'string' } },
          required: ['code'],
        },
      }],
    })

    if (response.stop_reason === 'end_turn') break

    const toolResults = []

    for (const block of response.content) {
      if (block.type === 'tool_use') {
        const branch = await sandbox.fork()
        const result = await branch.exec.run(['bash', '-c', block.input.code])

        if (result.ok) {
          toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result.stdout })
        } else {
          await branch.delete()
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: `Error (exit ${result.exit_code}): ${result.stderr}`,
            is_error: true,
          })
        }
      }
    }

    messages.push({ role: 'assistant', content: response.content })
    if (toolResults.length > 0) messages.push({ role: 'user', content: toolResults })
  }

  await sandbox.delete()
}
import anthropic
from iris import IrisClient

iris = IrisClient()
claude = anthropic.Anthropic()

def react_agent(task: str):
    sandbox = iris.sandboxes.create()
    messages = []

    while True:
        response = claude.messages.create(
            model="claude-opus-4-7",
            max_tokens=4096,
            messages=[{"role": "user", "content": task}, *messages],
            tools=[{
                "name": "execute_code",
                "description": "Execute a shell command in the sandbox",
                "input_schema": {
                    "type": "object",
                    "properties": {"code": {"type": "string"}},
                    "required": ["code"],
                },
            }],
        )

        if response.stop_reason == "end_turn":
            break

        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                branch = sandbox.fork()
                result = branch.exec.run(["bash", "-c", block.input["code"]])

                if result.ok:
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result.stdout,
                    })
                else:
                    branch.delete()
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": f"Error (exit {result.exit_code}): {result.stderr}",
                        "is_error": True,
                    })

        messages.append({"role": "assistant", "content": response.content})
        if tool_results:
            messages.append({"role": "user", "content": tool_results})

    sandbox.delete()

Multi-Step Workflows

Checkpoint at each successful step so you can fork back to the last good state:

const checkpoints: string[] = []

for (const step of workflow) {
  const cp = await sandbox.checkpoint.create({ name: step.name })
  checkpoints.push(cp.checkpoint_id)

  const result = await sandbox.exec.run(step.code)

  if (!result.ok) {
    // Roll back to the last good checkpoint and retry or bail
    const lastGood = checkpoints[checkpoints.length - 2]
    if (lastGood) await sandbox.checkpoint.restore(lastGood)
    console.error(`Step ${step.name} failed:`, result.stderr)
    break
  }
}
checkpoints = []

for step in workflow:
    cp = sandbox.checkpoint.create(name=step.name)
    checkpoints.append(cp.checkpoint_id)

    result = sandbox.exec.run(step.cmd)

    if not result.ok:
        # Roll back to the last good checkpoint and retry or bail
        if len(checkpoints) >= 2:
            sandbox.checkpoint.restore(checkpoints[-2])
        print(f"Step {step.name} failed:", result.stderr)
        break

Parallel Exploration

Fork from a decision point to explore multiple approaches simultaneously:

await sandbox.exec.run('python3 setup.py')

const approaches = ['approach_a.py', 'approach_b.py', 'approach_c.py']

const results = await Promise.all(
  approaches.map(async (approach) => {
    const branch = await sandbox.fork()
    const result = await branch.exec.run(`python3 ${approach}`)
    await branch.delete()
    return { approach, stdout: result.stdout, ok: result.ok }
  }),
)

const best = results.find((r) => r.ok)
import asyncio
from iris import AsyncIrisClient

async def main():
    client = AsyncIrisClient()
    sandbox = await client.sandboxes.create()
    await sandbox.exec.run(["python3", "setup.py"])

    approaches = ["approach_a.py", "approach_b.py", "approach_c.py"]

    async def run_approach(approach):
        branch = await sandbox.fork()
        result = await branch.exec.run(["python3", approach])
        await branch.delete()
        return {"approach": approach, "stdout": result.stdout, "ok": result.ok}

    results = await asyncio.gather(*[run_approach(a) for a in approaches])
    best = next((r for r in results if r["ok"]), None)

asyncio.run(main())

Best Practices

Fork, don't mutate

Fork before any action the agent might want to roll back. Keep the base sandbox clean.

Use timeouts

Set timeout_ms on exec.run() to prevent runaway processes from blocking your agent.

Check result.ok

result.ok is true only when exit_code === 0. Always check before treating output as valid.

Clean up branches

Call branch.delete() on completed forks. Suspended sandboxes still consume quota.

On this page