Gleam 1.x Erlang BEAM OTP Actors CULTIVA IA

AgentFlow — Backend Type-Safe
con Gleam / BEAM

Migración del núcleo de orquestación de agentes IA de Node.js a Gleam: concurrencia real, tipos exhaustivos y tolerancia a fallos sin boilerplate.

Cliente: AgentFlow SaaS  ·  Stack destino: Gleam + Wisp + gleam_otp · Fly.io  ·  4 módulos
Arquitectura del servicio
Cliente HTTP / React App
→
Wisp Router
router.gleam
→
AgentSupervisor
supervisor.gleam
→
AgentActor (por cliente)
agent_actor.gleam
Tipos de dominio
agent.gleam
Cada cliente tiene su propio actor BEAM — crash-safe, aislado, sin locks compartidos
1
Tipos de dominio — src/agentflow/agent.gleam
Tipos custom · Pattern matching exhaustivo · Máquina de estados
src/agentflow/agent.gleam
Gleam
// Tipos de dominio de AgentFlow — el compilador garantiza
// que CADA variant se maneja. Sin runtime surprises.

import gleam/option.{type Option, Some, None}

/// Tipo de agente — cada uno tiene capacidades distintas
pub type AgentType {
  ContentWriter          // Redacción y copy
  SeoAnalyzer            // Auditoría y keywords
  AdCreative             // Creatividades publicitarias
  EmailSequence          // Cadenas de drip
  DataExtractor          // Scraping + parsing
  Custom(name: String)    // Agentes personalizados
}

/// Estado del ciclo de vida de un agente
pub type AgentStatus {
  Idle
  Running
  Paused
  Error(message: String)
  Completed
}

pub type Agent {
  Agent(
    id:         String,
    name:       String,
    agent_type: AgentType,
    status:     AgentStatus,
    owner_id:   String,
    credits_used: Int,
    config:     Option(AgentConfig),
  )
}

pub type AgentConfig {
  AgentConfig(
    model:       String,      // "claude-sonnet-4-5", etc.
    temperature: Float,
    max_tokens:  Int,
    system_prompt: Option(String),
  )
}

/// Máquina de estados: no todas las transiciones son válidas.
/// El compilador fuerza a manejar TODOS los casos.
pub fn transition(from: AgentStatus, to: AgentStatus) -> Result(AgentStatus, String) {
  case from, to {
    Idle,     Running   -> Ok(Running)
    Running,  Paused    -> Ok(Paused)
    Running,  Completed -> Ok(Completed)
    Running,  Error(_)  -> Ok(to)
    Paused,   Running   -> Ok(Running)   // resume
    Paused,   Idle      -> Ok(Idle)      // reset
    Error(_), Idle      -> Ok(Idle)      // retry
    same, target if same == target -> Ok(target)
    _, _  -> Error("Transición inválida: " <> status_to_string(from) <> " → " <> status_to_string(to))
  }
}

/// Coste en créditos por tipo de agente — exhaustivo, sin default
pub fn credits_per_run(agent_type: AgentType) -> Int {
  case agent_type {
    ContentWriter   -> 10
    SeoAnalyzer     -> 8
    AdCreative      -> 15
    EmailSequence   -> 12
    DataExtractor   -> 20
    Custom(_)        -> 25
  }
}
2
Router HTTP — src/agentflow/router.gleam
Wisp · Type-safe routing · JSON decode/encode
src/agentflow/router.gleam
Gleam
import gleam/http.{Get, Post, Delete, Patch}
import gleam/json
import gleam/result
import wisp.{type Request, type Response}
import agentflow/agent_actor
import agentflow/json_codec

pub fn handle_request(req: Request, supervisor: agent_actor.Supervisor) -> Response {
  // Middleware: logging + body size limit (1 MB)
  use req <- wisp.log_request(req)
  use req <- wisp.rescue_crashes(req)
  use req <- wisp.max_body_size(req, 1_000_000)

  case wisp.path_segments(req) {
    // GET /api/agents — listar agentes del cliente
    ["api", "agents"] if req.method == Get ->
      list_agents(req, supervisor)

    // POST /api/agents — crear agente
    ["api", "agents"] if req.method == Post ->
      create_agent(req, supervisor)

    // GET /api/agents/:id
    ["api", "agents", id] if req.method == Get ->
      get_agent(req, supervisor, id)

    // PATCH /api/agents/:id/run — ejecutar agente
    ["api", "agents", id, "run"] if req.method == Patch ->
      run_agent(req, supervisor, id)

    // DELETE /api/agents/:id
    ["api", "agents", id] if req.method == Delete ->
      delete_agent(req, supervisor, id)

    // Health check
    ["health"] ->
      wisp.json_response(json.to_string_builder(json.object([
        #("status", json.string("ok")),
        #("vm",     json.string("beam")),
      ])), 200)

    _ -> wisp.not_found()
  }
}

fn create_agent(req: Request, supervisor: agent_actor.Supervisor) -> Response {
  // use-syntax: si falla, devuelve 400 automáticamente
  use body <- wisp.require_json(req)
  use owner_id <- require_header(req, "x-owner-id")

  case json_codec.decode_create_request(body) {
    Ok(create_req) -> {
      case agent_actor.create(supervisor, owner_id, create_req) {
        Ok(agent) ->
          wisp.json_response(
            json.to_string_builder(json_codec.encode_agent(agent)),
            201,
          )
        Error(err) ->
          wisp.json_response(json.to_string_builder(error_json(err)), 422)
      }
    }
    Error(_) -> wisp.bad_request()
  }
}
3
Actor OTP — src/agentflow/agent_actor.gleam
gleam_otp · Estado aislado por cliente · Sin condiciones de carrera
src/agentflow/agent_actor.gleam
Gleam
import gleam/erlang/process.{type Subject}
import gleam/otp/actor.{type Next}
import gleam/dict.{type Dict}
import agentflow/agent.{type Agent, type AgentStatus}

/// Mensajes que acepta el actor — protocolo tipado
pub type Message {
  ListAgents(
    owner_id: String,
    reply_to: Subject(List(Agent)),
  )
  CreateAgent(
    owner_id:   String,
    create_req: CreateRequest,
    reply_to:   Subject(Result(Agent, String)),
  )
  UpdateStatus(
    agent_id:   String,
    new_status: AgentStatus,
    reply_to:   Subject(Result(Agent, String)),
  )
  DeleteAgent(
    agent_id: String,
    reply_to: Subject(Result(Nil, String)),
  )
}

/// Estado interno del actor: Dict(owner_id, List(Agent))
pub type State {
  State(agents: Dict(String, List(Agent)), next_id: Int)
}

/// Loop del actor — pure function, sin efectos ocultos
fn handle_message(msg: Message, state: State) -> Next(Message, State) {
  case msg {
    ListAgents(owner_id, reply_to) -> {
      let agents = dict.get(state.agents, owner_id) |> result.unwrap([])
      process.send(reply_to, agents)
      actor.continue(state)
    }

    CreateAgent(owner_id, create_req, reply_to) -> {
      let id = "agent-" <> int.to_string(state.next_id)
      let new_agent = Agent(
        id:           id,
        name:         create_req.name,
        agent_type:   create_req.agent_type,
        status:       agent.Idle,
        owner_id:     owner_id,
        credits_used: 0,
        config:       create_req.config,
      )
      let owner_agents = dict.get(state.agents, owner_id) |> result.unwrap([])
      let new_state = State(
        agents: dict.insert(state.agents, owner_id, [new_agent, ..owner_agents]),
        next_id: state.next_id + 1,
      )
      process.send(reply_to, Ok(new_agent))
      actor.continue(new_state)
    }

    UpdateStatus(agent_id, new_status, reply_to) -> {
      case find_and_update(state, agent_id, new_status) {
        Ok(#(updated_agent, new_state)) -> {
          process.send(reply_to, Ok(updated_agent))
          actor.continue(new_state)
        }
        Error(err) -> {
          process.send(reply_to, Error(err))
          actor.continue(state)   // estado no cambia si hay error
        }
      }
    }

    DeleteAgent(agent_id, reply_to) -> {
      let #(found, new_agents) = remove_agent(state.agents, agent_id)
      case found {
        True -> {
          process.send(reply_to, Ok(Nil))
          actor.continue(State(..state, agents: new_agents))
        }
        False -> {
          process.send(reply_to, Error("Agent not found: " <> agent_id))
          actor.continue(state)
        }
      }
    }
  }
}

/// Arrancar el actor — devuelve un Subject para enviarle mensajes
pub fn start() -> Result(Subject(Message), actor.StartError) {
  actor.start(State(agents: dict.new(), next_id: 1), handle_message)
}
4
JSON Codec — src/agentflow/json_codec.gleam
gleam_json · Encode + Decode type-safe · Sin any
src/agentflow/json_codec.gleam
Gleam
import gleam/json.{type Json}
import gleam/dynamic/decode
import gleam/option.{type Option}
import agentflow/agent.{type Agent, type AgentType, type AgentStatus}

/// Serializar un agente a JSON — exhaustivo sobre AgentType y AgentStatus
pub fn encode_agent(a: Agent) -> Json {
  json.object([
    #("id",           json.string(a.id)),
    #("name",         json.string(a.name)),
    #("agent_type",   json.string(encode_agent_type(a.agent_type))),
    #("status",       encode_status(a.status)),
    #("owner_id",     json.string(a.owner_id)),
    #("credits_used", json.int(a.credits_used)),
    #("credits_per_run", json.int(agent.credits_per_run(a.agent_type))),
  ])
}

fn encode_agent_type(t: AgentType) -> String {
  case t {
    agent.ContentWriter  -> "content_writer"
    agent.SeoAnalyzer    -> "seo_analyzer"
    agent.AdCreative     -> "ad_creative"
    agent.EmailSequence  -> "email_sequence"
    agent.DataExtractor  -> "data_extractor"
    agent.Custom(name)   -> "custom:" <> name
  }
}

fn encode_status(s: AgentStatus) -> Json {
  case s {
    agent.Idle          -> json.object([#("state", json.string("idle"))])
    agent.Running       -> json.object([#("state", json.string("running"))])
    agent.Paused        -> json.object([#("state", json.string("paused"))])
    agent.Completed     -> json.object([#("state", json.string("completed"))])
    agent.Error(msg)    -> json.object([
      #("state",   json.string("error")),
      #("message", json.string(msg)),
    ])
  }
}

/// Decodificar request de creación — si falta campo, error tipado
pub fn decode_create_request(json: Dynamic) -> Result(CreateRequest, List(decode.DecodeError)) {
  decode.run(json, {
    use name       <- decode.field("name", decode.string)
    use agent_type <- decode.field("agent_type", decode_agent_type())
    use config     <- decode.optional_field("config", decode_config())
    decode.success(CreateRequest(name: name, agent_type: agent_type, config: config))
  })
}

fn decode_agent_type() -> decode.Decoder(AgentType) {
  decode.string
  |> decode.map(fn(s) {
    case s {
      "content_writer"  -> Ok(agent.ContentWriter)
      "seo_analyzer"    -> Ok(agent.SeoAnalyzer)
      "ad_creative"     -> Ok(agent.AdCreative)
      "email_sequence"  -> Ok(agent.EmailSequence)
      "data_extractor"  -> Ok(agent.DataExtractor)
      other             -> Ok(agent.Custom(other))
    }
  })
}
✓
Por qué Gleam / BEAM para AgentFlow
🔒
Zero runtime crashes por tipos
Si compila, casi nunca falla en producción. No hay undefined, null sin manejar ni any. El compilador rechaza código con casos no cubiertos.
⚡
Millones de actores concurrentes
Un actor BEAM por cliente: cada uno tiene su estado aislado. No hay locks compartidos ni race conditions. Un crash no propaga al resto.
🔀
Pattern matching exhaustivo
Máquina de estados de agentes verificada por el compilador. Si añades un nuevo AgentStatus, el compilador señala cada sitio que lo usa.
🌐
Ecosistema Erlang/OTP completo
Acceso con @external a 30 años de librerías de Erlang: supervisores, ETS, GenServer, distributed nodes, hot code reloading.
📦
Deploy nativo en Fly.io
Fly.io tiene soporte nativo para BEAM (clustering automático). gleam build --target erlang produce un release listo para desplegar sin Docker complicado.
$
Setup del proyecto
Terminal
bash
# 1. Instalar Gleam (macOS)
brew install gleam

# 2. Crear proyecto
gleam new agentflow
cd agentflow

# 3. Añadir dependencias
gleam add wisp          # web framework type-safe
gleam add gleam_otp     # actores BEAM
gleam add gleam_json    # encode/decode
gleam add gleam_http    # tipos HTTP
gleam add mist          # servidor HTTP Erlang

# 4. Desarrollar
gleam run               # arranca el servidor
gleam test              # ejecuta tests (Gleeunit)
gleam build             # build de producción

# 5. Deploy en Fly.io
fly launch --name agentflow-api
fly deploy