Runtime de agentes
El runtime de agentes es un servicio en Python (FastAPI y CrewAI) que convierte una tarea y su contexto en un resultado estructurado; solo habla con proveedores de modelo. Esta página es para quien opera o integra el runtime: su contrato HTTP, cómo elige proveedor, qué datos salen del proceso y cómo se controla el costo.
En esta página
- Rol y contrato HTTP
- Simulación y modo real
- Proveedores de modelo
- Orden de proveedores y respaldo
- Política de datos: ALLOWED_PROVIDERS
- Qué sale del proceso y redacción
- Herramientas: el modelo pide, el backend ejecuta
- Sandbox de código
- Contabilidad de costos y presupuestos
Rol y contrato HTTP#
El backend llama al runtime por HTTP con una tarea, el perfil del agente y el contexto que armó. El runtime devuelve una salida estructurada validada y, si el agente quiere actuar, una lista de tool_requests. Nunca ejecuta una herramienta ni llama a un sistema externo: el modelo puede ser engañado por texto no confiable, pero solo puede proponer.
Cuando RUNTIME_TOKEN está definido, el runtime exige Authorization: Bearer <token> en todas las rutas salvo GET /healthz, comparado en tiempo constante; sin él responde 401. Los endpoints son:
curl http://localhost:8000/healthz
# {"status":"ok","mode":"live","provider":"anthropic",
# "configured_providers":["anthropic","custom"],"default_provider":"anthropic"}
| Endpoint | Propósito |
|---|---|
POST /v1/plan, /v1/plan-phases, /v1/plan-phase, /v1/replan | Convierten una solicitud o un proyecto en un plan de tareas para los agentes disponibles; replan reemplaza un nodo fallido |
POST /v1/run-task | Ejecuta una tarea para un agente; devuelve output, consults, tool_requests y usage |
POST /v1/consult, /v1/review, /v1/synthesize, /v1/summarize | Consultas entre agentes, revisión de calidad, informe final y resúmenes |
POST /v1/counterargument | Contraargumento consultivo para una aprobación de alto riesgo |
POST /v1/route, /v1/chat-reply | Enrutamiento de chat y respuestas conversacionales |
POST /v1/estimate, /v1/context-sizes | Rango de costo de un plan y tamaño del prompt; ninguno llama al modelo |
POST /v1/provider-test | Una llamada real pequeña para probar la clave de una organización; siempre responde 200 con un código estable |
GET /healthz | Estado, modo (simulation o live), proveedores configurados y proveedor por defecto; nunca claves |
Atención
Define RUNTIME_TOKEN con el mismo valor en el backend y en el runtime. Sin él, cualquiera que alcance el puerto del runtime puede gastar tus claves de proveedor; el backend lo advierte en el log al arrancar.
Simulación y modo real#
El contrato HTTP es el mismo en ambos modos; solo cambia el motor.
- Simulación: se activa con
SIMULATION=trueo cuando no hay ningún proveedor configurado. Devuelve planes, resultados, tokens y costos plausibles con latencia simulada (SIM_LATENCY_SCALE=0elimina la espera), sin red ni costo, e ignora la política de proveedores porque ningún dato sale del proceso. - Real: el motor CrewAI llama al proveedor y valida la salida contra el esquema
StructuredOutput.LLM_DIRECT_CALL=truecambia el bucle de CrewAI por una sola llamada de chat. - Degradado: si hay un proveedor configurado pero el motor real no arranca, el runtime responde en simulación y
GET /healthzdevuelvestatus: degradedcon unwarning.
Nota
Los campos opcionales provider y usage.failed_providers solo aparecen cuando contesta un proveedor real, así que el cliente no necesita distinguir el modo.
Consejo
SIMULATION=true siempre gana, incluso sobre la clave propia de una organización: no se hace ninguna llamada de red.
Proveedores de modelo#
Hay cinco proveedores. Uno queda configurado cuando su clave está en el entorno (para custom basta la URL base) o cuando la solicitud trae la clave propia de la organización. Un modelo custom se pasa como openai/<nombre>, y un modelo local sin costo puede tarificarse en 0 con CUSTOM_PRICE_IN_PER_M y CUSTOM_PRICE_OUT_PER_M.
Las organizaciones pueden traer su propia clave. Se sella con AES-256-GCM bajo una clave envuelta por CONNECTIONS_KEK, ninguna respuesta de la API la devuelve salvo sus últimos cuatro caracteres (key_last4) y solo viaja en claro por el salto interno del backend al runtime.
| Proveedor | Configuración | Modelo por defecto |
|---|---|---|
deepseek | DEEPSEEK_API_KEY, DEEPSEEK_MODEL | deepseek/deepseek-chat |
anthropic | ANTHROPIC_API_KEY, ANTHROPIC_MODEL | anthropic/claude-sonnet-5-5 |
openai | OPENAI_API_KEY, OPENAI_MODEL, OPENAI_BASE_URL opcional | openai/gpt-4.1 |
kimi | KIMI_API_KEY, KIMI_MODEL, KIMI_BASE_URL opcional | openai/kimi-k2-0905-preview (Moonshot, compatible con OpenAI) |
custom | CUSTOM_LLM_BASE_URL (obligatoria), CUSTOM_LLM_API_KEY, CUSTOM_LLM_MODEL | El de CUSTOM_LLM_MODEL: cualquier endpoint compatible con OpenAI, como vLLM, Ollama o una pasarela propia |
Orden de proveedores y respaldo#
Para cada llamada el runtime arma una lista de intentos. Cada fuente agrega solo los proveedores que aún no están, así que todo proveedor configurado y permitido acaba siendo un respaldo:
MODEL_PROVIDER_ORDER_<ROL>: elroledel agente en mayúsculas (por ejemploMODEL_PROVIDER_ORDER_LEGAL) o el tipo de llamada:PLAN,REVIEW,CONSULT,SYNTHESIZE,SUMMARIZE,ROUTE,COUNTERARGUMENT.role_providersde la política de modelos de la organización.preferred_providersde la solicitud.- El orden global
MODEL_PROVIDER_ORDER(por defectodeepseek,anthropic,openai,kimi,custom).
# Runtime (agent-runtime)
SIMULATION=false # true: scripted engine, no network, no cost
DEEPSEEK_API_KEY=... # one configured provider is enough
ANTHROPIC_API_KEY=...
CUSTOM_LLM_BASE_URL=https://llm.internal.example/v1 # OpenAI-compatible
MODEL_PROVIDER_ORDER=anthropic,deepseek,custom
MODEL_PROVIDER_ORDER_LEGAL=anthropic # per role / call kind
ALLOWED_PROVIDERS=anthropic,custom # operator ceiling
RUNTIME_TOKEN=<shared secret> # same value in the backend
# Backend
RUNTIME_URL=http://agent-runtime:8000
RUNTIME_TOKEN=<shared secret>
BUDGET_USD=50 # organization budget (default 50)
REQUEST_BUDGET_CAP_USD=5 # default cap per request
AGENT_BUDGET_USD=20 # default monthly cap per agent
Nota
Un fallo transitorio (timeout, límite de tasa, 5xx) se reintenta en el mismo proveedor PROVIDER_RETRIES veces (1 por defecto, con espera PROVIDER_RETRY_BACKOFF_S) antes de pasar al siguiente; PROVIDER_TIMEOUT_S (120 por defecto) acota cada intento. La política de la organización puede cambiar el modelo por rol (role_models), pero solo de un proveedor ya configurado y permitido.
Política de datos: ALLOWED_PROVIDERS#
La política de datos se aplica antes de enviar nada. La lista efectiva es ALLOWED_PROVIDERS del operador intersectado con allowed_providers de la solicitud: un proveedor fuera de la lista ni se llama ni se usa como respaldo, y una solicitud puede reducir el techo del operador pero nunca ampliarlo.
El backend completa allowed_providers desde la política de modelos de cada organización (GET/PUT /api/v1/settings/model-policy, solo owner y admin, y cada cambio queda auditado). Si no queda ningún proveedor permitido envía el valor centinela none, así que el runtime rechaza la llamada en lugar de ampliar el conjunto. Con byo_only, solo las claves propias de la organización pueden contestar. Los fallos son explícitos:
{
"detail": {
"code": "no_allowed_provider",
"message": "No allowed model provider is configured for this request; nothing was sent.",
"providers_tried": [],
"allowed_providers": ["anthropic"],
"configured_providers": ["deepseek"]
}
}
| Código | HTTP | Significado |
|---|---|---|
no_allowed_provider | 422 | Ningún proveedor configurado está permitido; no se envió nada |
all_providers_failed | 502 | Fallaron todos los proveedores permitidos; providers_tried los lista |
provider_no_balance | 402 | Las cuentas de proveedor probadas no tienen saldo; reintentar no sirve |
call_budget_exceeded | 402 | La estimación de la llamada supera max_cost_usd, el margen que dio el backend; no se envió nada |
Qué sale del proceso y redacción#
A un proveedor solo llegan los prompts de cada llamada. El contrato no tiene campos para credenciales, connection_id, URLs de API ni tokens, así que no pueden enviarse. Un prompt puede contener:
- El texto de la solicitud del usuario y la persona, responsabilidades y herramientas del agente.
- Salidas de tareas dependientes, memoria del agente y contenido externo.
- Resultados de herramientas que el backend ya filtró y redactó.
- Preguntas de consulta o de síntesis.
Nota
Los secretos se redactan dos veces. El backend redacta primero los resultados de herramientas; el módulo redact.py del runtime es la segunda barrera y cubre claves privadas, JWT, tokens de GitHub, AWS y Google, claves de API, tokens bearer, asignaciones tipo password, URLs con tokens y números de tarjeta válidos por Luhn. Se aplica a la salida del modelo y a los mensajes de error de los proveedores. El contenido externo, las salidas de dependencias y la memoria van solo en el prompt de usuario, dentro de bloques marcados como DATOS NO CONFIABLES con la instrucción de no obedecerlos; el system prompt se arma solo con lo que da el backend.
Herramientas: el modelo pide, el backend ejecuta#
Una solicitud de herramienta es una propuesta. El backend revisa la lista de herramientas del agente, los argumentos, el motor de políticas y el nivel de autonomía, y puede crear una aprobación para una persona. Solo entonces la ejecuta, con credenciales de la bóveda cifrada que nunca salen del backend. El resultado vuelve al modelo redactado y delimitado como datos no confiables.
Las capacidades marcadas como irreversibles en el manifiesto del proveedor, como email.send de Gmail, siempre requieren una decisión humana, sea cual sea la autonomía del agente. Una acción aprobada se reclama como máximo una vez, así que un reinicio no puede ejecutarla dos veces. Las herramientas sin conexión real usan adaptadores falsos en memoria, que es como funciona la simulación de punta a punta.
{
"tool_requests": [
{ "tool": "email", "action": "send", "risk": "high",
"args": { "to": "customer@example.com", "subject": "Proposal", "body": "..." } }
]
}
Sandbox de código#
La acción code.run permite que un agente pida ejecutar Python. Solo está disponible si el backend tiene SANDBOX_URL y SANDBOX_TOKEN; sin ellos se deniega con sandbox_unavailable. Por defecto cada ejecución es una aprobación que muestra el código, los archivos de entrada y el propósito, y queda ligada al hash del código y al SHA-256 de cada entrada: cambiar cualquiera exige una aprobación nueva.
Tras la aprobación el backend envía la ejecución a un runner aparte que inicia un contenedor desechable:
- Sin red (
--network none), raíz de solo lectura, usuario no root y todas las capabilities eliminadas. - Límites de tiempo, memoria, procesos y tamaño de salida (
SANDBOX_MAX_RUNTIME_SECONDS,SANDBOX_MEMORY_MB,SANDBOX_PIDS,SANDBOX_MAX_OUTPUT_BYTES) y una lista de tipos de archivo de salida permitidos. - El entorno del runner no se reenvía, así que ningún secreto llega al código.
- El kill switch, la pausa del agente o el modo de solo lectura abortan una ejecución en curso.
Nota
El modo escritorio es opcional y está apagado por defecto: permite que una organización deje correr sin aprobación el código que solo usa archivos del propio agente, con topes más bajos de código, entradas y ejecuciones por tarea.
Atención
El runner monta el socket de Docker, lo que equivale a root en ese host. Ejecútalo en una máquina dedicada y sin puertos publicados.
Contabilidad de costos y presupuestos#
Los precios viven en una sola tabla, model_prices.json, en USD por millón de tokens, idéntica byte a byte en el backend y en el runtime (una prueba en cada servicio lo exige). Un modelo ausente se cobra a la tarifa por defecto de la tabla y se avisa una vez en el log. PRICE_IN_PER_M y PRICE_OUT_PER_M fuerzan una tarifa para todos los modelos.
Antes de cada llamada el backend reserva la estimación máxima contra la solicitud, el agente y la organización. Después registra el mayor entre el costo que reporta el runtime y su propio recálculo, así que no depende de la exactitud del runtime. El gasto se lee de un ledger en Postgres y sobrevive a reinicios.
| Control | Por defecto | Efecto al excederse |
|---|---|---|
BUDGET_USD (organización) | 50 | La llamada no se hace; no se reanuda subiendo un tope desde la API |
REQUEST_BUDGET_CAP_USD (por solicitud; budget_cap_usd lo sustituye) | Sin tope | La solicitud se pausa con budget.exceeded; sube el tope para continuar |
AGENT_BUDGET_USD (mensual por agente) | Sin tope | Las llamadas del agente se pausan; sube el tope para continuar |
COST_CONFIRM_THRESHOLD_USD | 1.0 | Una persona confirma el costo estimado antes de ejecutar |
MAX_PARALLEL_PER_ORG | 8 | Las llamadas de la organización esperan en cola |
Nota
La tabla de precios es una referencia para presupuestar, no lo que cobra tu propio contrato con el proveedor.