Arquitectura del servicio
Cliente HTTP / React App
→
Wisp Router
router.gleam
router.gleam
→
AgentSupervisor
supervisor.gleam
supervisor.gleam
→
AgentActor (por cliente)
agent_actor.gleam
agent_actor.gleam
Tipos de dominio
agent.gleam
agent.gleam
Cada cliente tiene su propio actor BEAM — crash-safe, aislado, sin locks compartidos
1
Tipos de dominio —
src/agentflow/agent.gleamTipos 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.gleamWisp · 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.gleamgleam_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.gleamgleam_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