Powered by AppSignal & Oban Pro

Working With Sprite

guides/sprite.livemd

Working With Sprite

Mix.install([{:cia, github: "seanmor5/cia"}])

Sprites

Sprites is a remote sandbox option for CIA.

Sprite sandbox declarations support three lifecycle modes:

  • :ephemeral creates dedicated compute for the agent and destroys it on stop
  • :durable creates the named sprite if needed and keeps it on stop
  • :attached expects the named sprite to already exist and keeps it on stop

Environment

The examples below assume these environment variables are available:

sprite_name = System.fetch_env!("LB_CIA_SPRITE_NAME")
sprite_token = System.fetch_env!("LB_CIA_SPRITE_TOKEN")
openai_api_key = System.fetch_env!("LB_OPENAI_API_KEY")

:ok

Minimal Remote Agent

sandbox_opts = [name: sprite_name, token: sprite_token, lifecycle: :durable]

config =
  CIA.new()
  |> CIA.sandbox(:sprite, sandbox_opts)
  |> CIA.workspace(:directory, root: "/workspace")
  |> CIA.before_start(fn %{sandbox: sandbox} ->
    with {_, 0} <- CIA.Sandbox.cmd(sandbox, "mkdir", ["-p", "/workspace/lib"]),
         {_, 0} <-
           CIA.Sandbox.cmd(
             sandbox,
             "sh",
             ["-lc", "printf '%s\n' 'defmodule Demo do' 'end' > /workspace/lib/demo.ex"]
           ) do
      :ok
    end
  end)
  |> CIA.harness(:codex, auth: {:api_key, openai_api_key})

{:ok, agent} = CIA.start(config)
CIA.subscribe(agent)
{:ok, thread} = CIA.thread(agent,
  model: "gpt-5.4"
)

{:ok, turn} = CIA.turn(agent, thread, "Add a function to demo which returns :ok")

pretty = fn term ->
  inspect(term, pretty: true, limit: :infinity, printable_limit: :infinity)
end

terminal_turn_event? = fn
  %{"turnId" => turn_id, "status" => status} ->
    turn_id == turn.id and status in ["completed", "failed", "cancelled"]

  %{"id" => turn_id, "status" => status} ->
    turn_id == turn.id and status in ["completed", "failed", "cancelled"]

  %{"turn" => %{"id" => turn_id, "status" => status}} ->
    turn_id == turn.id and status in ["completed", "failed", "cancelled"]

  _ ->
    false
end

print_codex_event = fn
  {:server_message, %{method: method, params: params}} ->
    IO.puts("\n[codex] #{method}")
    IO.puts(pretty.(params || %{}))

  {:channel_message, %{"type" => type} = msg} ->
    IO.puts("\n[channel] #{type}")
    IO.puts(pretty.(msg))

  {:channel_message, msg} ->
    IO.puts("\n[channel]")
    IO.puts(pretty.(msg))

  {:stderr, data} ->
    IO.puts("\n[stderr]")
    IO.puts(data)

  {:transport_exit, reason} ->
    IO.puts("\n[transport_exit]")
    IO.puts(pretty.(reason))

  {:unparsed_channel_message, raw} ->
    IO.puts("\n[unparsed_channel_message]")
    IO.puts(inspect(raw, limit: :infinity, printable_limit: :infinity))
end

listen = fn listen ->
  receive do
    {:cia, ^agent, {:harness, :codex, payload}} ->
      print_codex_event.(payload)

      stop? =
        case payload do
          {:transport_exit, _reason} ->
            true

          {:server_message, %{params: params}} ->
            terminal_turn_event?.(params || %{})

          _ ->
            false
        end

      if stop? do
        :ok
      else
        listen.(listen)
      end
  after
    15_000 ->
      IO.puts("\n[idle] no more Codex events")
      :ok
  end
end

listen.(listen)