humanlayer/12-factor-agents es uno de los pocos documentos sobre ingeniería de agentes que les entregaría a ingenieros sénior sin tener que disculparme primero.
No porque sea perfectamente completo ni porque todos los factores deban implementarse al pie de la letra, sino porque se enfoca en lo que importa: la semántica del runtime, no el teatro de los prompts.
Para este artículo, revisé el repositorio original y la documentación oficial vigente de LangGraph y Agno/AgentOS al March 31, 2026. Este artículo no es una reescritura de la guía original. Es una traducción práctica para ingenieros que necesitan decidir: ¿cuáles de estos principios deben convertirse en arquitectura explícita y qué framework lo facilita?
En resumen
- Conviene leer
12-factor-agentscomo un manifiesto sobre runtimes de producción, no como un tutorial de un framework. - Los factores no tienen todos la misma importancia. En la práctica, el foco está en:
- interfaces estructuradas para herramientas y personas
- control del contexto y del estado
- pausa y reanudación, además de flujo de control explícito
- manejo de fallos y alcance acotado del agente
- LangGraph suele encajar mejor cuando quieres que el framework exponga directamente estado duradero, checkpoints, interrupciones y control a nivel de grafo.
- Agno/AgentOS suele ser la opción más práctica cuando quieres sesiones, aprobaciones, trazas, APIs y un plano de control cómodo de usar sin reconstruir esa capa del runtime por tu cuenta.
- Mi postura editorial más firme: la filosofía de HumanLayer apunta en la dirección correcta, pero también presupone que tu equipo está dispuesto a hacerse cargo de más detalles de orquestación de los que muchos equipos de producto quieren mantener.
Qué aprenderás aquí
- Qué dice realmente
12-factor-agentsuna vez que dejas de lado la marca y la numeración de los factores. - Qué factores importan más en sistemas de producción y cuáles se entienden mejor como extensiones o decisiones de estilo.
- Cómo relacionar los factores clave con conceptos de LangGraph como threads, checkpoints, interrupciones y subgrafos.
- Cómo relacionar las mismas ideas con conceptos de Agno/AgentOS como sesiones, resúmenes, aprobaciones, herramientas y el plano de control.
- Un ejemplo práctico en Python de un agente de releases y despliegues para ambos ecosistemas.
- En qué aspectos LangGraph se acerca más a la filosofía y en cuáles Agno/AgentOS simplemente facilita la entrega.
Revisión de las fuentes: qué dice realmente 12-Factor Agents
La lectura del repositorio respaldada por las fuentes es bastante consistente.
Primero, el repositorio expresa un claro escepticismo frente a la narrativa habitual de «un LLM más herramientas en un bucle». El README sostiene que los agentes sólidos en producción son, en su mayor parte, software con decisiones del LLM insertadas en los puntos adecuados, y que la mayoría de los equipos que buscan calidad terminan necesitando un control más profundo sobre los prompts, el flujo y el estado que el que los frameworks exponen por defecto.
Segundo, el repositorio trata las llamadas a herramientas como salidas estructuradas, el contexto como el verdadero runtime y el flujo de control como una responsabilidad de la aplicación. Los factores 1, 3, 4, 5, 6, 7, 8 y 9 giran en torno a esa misma idea desde distintos ángulos.
Tercero, el repositorio favorece claramente:
- estado serializable similar a un thread
- semántica explícita de pausa y reanudación
- puntos duraderos de aprobación humana
- retroalimentación compacta sobre errores
- alcance reducido del agente
Esa preferencia es evidente. Es el corazón de la guía.
Por último, el repositorio no se lee realmente como doce leyes de igual importancia. Algunos factores son orientación arquitectónica fundamental. Otros se parecen más a heurísticas de implementación. El factor 12 dice literalmente que es «principalmente por diversión», una pista útil sobre cuánto deberías llevar cada sección a la práctica.
Esta es mi interpretación del material original:
12-factor-agentsno es realmente un manifiesto para el software «agéntico» en general. Es un manifiesto para runtimes de agentes duraderos, inspeccionables, preparados para interrupciones y que incorporan a las personas.
Precisamente por eso encaja excepcionalmente bien con LangGraph, y solo de manera parcial con wrappers de agentes más simples.
Un mejor modelo mental: 12 factores como 4 áreas del runtime
En lugar de recorrer los factores uno por uno, creo que es más útil reorganizarlos en torno a cuatro áreas del runtime.
Slack / API / Cron trigger
|
v
+-----------------------------+
| Planner / structured output |
| "what should happen next?" |
+-------------+---------------+
|
v
+-----------------------------+
| State store / thread |
| events, approvals, errors, |
| summaries, session history |
+------+------+---------------+
| |
| +-------------------+
| |
v v
+-------------+ +------------------+
| Tool action | | Human approval |
| fetch/deploy | | reject/approve |
+------+------+ +---------+--------+
| |
+------------+--------------+
|
v
resume next step
| Área del runtime | Factores | Encaje de LangGraph | Encaje de Agno/AgentOS | Compensación práctica |
|---|---|---|---|---|
| Interfaces estructuradas para herramientas y personas | 1, 4, 7 | Sólido, explícito, de bajo nivel | Sólido, cómodo de usar y con todo incluido | LangGraph da más control directo; Agno exige menos configuración |
| Contexto y estado como runtime | 2, 3, 5, 12 | Muy sólido mediante estado de threads y checkpoints | Sólido mediante sesiones, resúmenes y contexto respaldado por una base de datos | LangGraph es más explícito; Agno ofrece una gestión más integrada |
| Pausa, reanudación y flujo de control | 6, 8, 11 | Excelente con interrupciones y ejecución duradera | Sólido con confirmaciones, aprobaciones y APIs de AgentOS | LangGraph se acerca más a la filosofía; Agno es más fácil de operar |
| Manejo de fallos y alcance acotado | 9, 10 | Sólido si modelas los errores deliberadamente en el estado | Sólido si mantienes acotados las herramientas y los equipos | Ninguno de los dos frameworks te salva de malas decisiones sobre el alcance |
Esa tabla es el artículo completo en forma resumida.
1. Interfaces estructuradas para herramientas y personas
El principio respaldado por las fuentes que comparten los factores 1, 4 y 7 es simple:
- el modelo debe emitir una intención estructurada
- el código determinista debe decidir cómo se ejecuta esa intención
- pedirle a una persona una aprobación o aclaración es simplemente otro tipo de intención estructurada
Es una idea a la que no se le da suficiente importancia.
Muchos frameworks todavía tratan las llamadas a herramientas como una función mágica de la API. HumanLayer propone una visión más útil: una llamada a una herramienta es simplemente una intención serializada en el límite entre el LLM y el runtime.
Eso te da la libertad de hacer cosas que los frameworks no siempre modelan directamente:
- convertir la intención de usar una herramienta en registros de aprobación
- traducir una salida del modelo en varias acciones deterministas
- convertir «contactar a una persona» en un paso de primer nivel, en lugar de texto no estructurado del asistente
- inspeccionar o rechazar una herramienta propuesta antes de que se ejecute
Por qué importa en el escenario del agente de releases
Supongamos que un agente de releases recibe un mensaje de Slack:
Despliega
release-2026.03.31en producción y avísame si fallan las comprobaciones de salud.
La primera pregunta útil no es «¿qué framework tiene herramientas?».
Es esta:
¿Puede el sistema representar esto como una intención estructurada de una manera que me permita inspeccionar, pausar, aprobar, denegar, reintentar y reanudar?
Esa es la pregunta de los 12 factores.
Ejemplos factor por factor
Factor 1: Del lenguaje natural a las llamadas a herramientas
En este planteamiento, el factor 1 significa que el modelo debe convertir una solicitud humana de release en un objeto de intención tipado antes de que se ejecute cualquier lógica de despliegue.
from typing import Literal
from pydantic import BaseModel
class ReleaseIntent(BaseModel):
action: Literal["deploy", "verify", "rollback"]
tag: str
environment: Literal["staging", "production"]
notify_on_failure: bool
intent = llm.with_structured_output(ReleaseIntent).invoke(
"Deploy release-2026.03.31 to production and page me if health checks fail."
)
En LangGraph, esta intención suele convertirse en una actualización de estado; en Agno puede convertirse en un resultado de output_schema o en una entrada para la selección de herramientas.
Factor 4: Las herramientas son solo salidas estructuradas
El factor 4 dice que el modelo decide qué debe suceder a continuación, mientras que el código determinista sigue a cargo de cómo se ejecuta esa acción.
def dispatch_release_intent(intent: ReleaseIntent) -> str:
if intent.action == "deploy":
return "request_approval" if intent.environment == "production" else "deploy_now"
if intent.action == "verify":
return "run_checks"
if intent.action == "rollback":
return "open_incident"
return "done"
next_step = dispatch_release_intent(intent)
Esa separación se relaciona de forma natural con las funciones de enrutamiento de LangGraph y con el código de Agno que rodea las herramientas, las aprobaciones o los pasos de un workflow.
Factor 7: Contacta a las personas mediante llamadas a herramientas
Según el factor 7, «preguntarle a una persona» debe ser un paso tipado en el runtime, no una frase ambigua del asistente enterrada en el historial del chat.
def build_approval_request(intent: ReleaseIntent, requested_by: str) -> dict:
return {
"intent": "request_human_input",
"question": f"Approve deploy of {intent.tag} to {intent.environment}?",
"context": "Production release with customer-facing impact.",
"options": ["approve", "reject"],
"requested_by": requested_by,
}
approval_event = build_approval_request(intent, requested_by="slack:alice")
En LangGraph esto se convierte en el payload de una interrupción o en un evento; en Agno/AgentOS corresponde directamente a herramientas sujetas a confirmación y registros de aprobación.
Cómo se interpreta en LangGraph
LangGraph encaja bien con esto porque ya parte de que las actualizaciones de estado y las transiciones entre nodos son el verdadero runtime. El modelo puede emitir una intención estructurada, pero el grafo decide qué nodo se ejecuta a continuación y si debe pausarse antes de un efecto secundario de alto impacto.
En la práctica, LangGraph fomenta una separación saludable:
- el modelo decide el siguiente paso semántico
- los nodos y las aristas deciden la semántica de ejecución
- las interrupciones deciden cuándo vuelven a intervenir las personas
Cómo se interpreta en Agno/AgentOS
Agno aplica el mismo principio desde una perspectiva más orientada a producto:
output_schemahace que la salida estructurada sea una capacidad de primer nivel@tool(requires_confirmation=True)hace que la ejecución sujeta a aprobación sea una capacidad de primer nivel- AgentOS persiste y expone el flujo de aprobación para su operación
Este es uno de los primeros puntos en los que Agno/AgentOS se siente más práctico que purista. HumanLayer preferiría que tú controlaras la semántica del runtime. Agno dice: está bien, pero la mayoría de los equipos preferiría contar con un sistema de aprobaciones con soporte.
Es una compensación real, no una falla filosófica.
2. El contexto es el runtime
Los factores 2, 3, 5 y 12 apuntan a la misma afirmación arquitectónica:
La mejor fuente de verdad para un agente suele ser el historial serializado de lo que ha ocurrido hasta ahora, no un objeto de ejecución oculto y separado.
Aquí es donde el repositorio se vuelve realmente interesante.
La mayoría de las conversaciones sobre agentes se detienen en la ingeniería de prompts o la memoria. HumanLayer va más allá y dice:
- controla el prompt
- controla el formato del contexto
- unifica el estado de ejecución y el estado de negocio cuando sea posible
- piensa en el agente como un reducer sobre eventos serializados
Es un enfoque mucho más orientado a sistemas.
El beneficio práctico
Si tu agente de releases mantiene un registro serializado de eventos como:
- trigger recibido
- metadatos de la release cargados
- aprobación solicitada
- aprobación concedida
- despliegue intentado
- comprobaciones de salud fallidas
entonces varias funcionalidades complejas de producción se vuelven más sencillas:
- reproducción
- depuración
- representación en la interfaz
- bifurcación
- capacidad de reanudación
- resúmenes y compactación
Ejemplos factor por factor
Factor 2: Controla tus prompts
El factor 2 significa que el prompt del planificador debe ser código explícito de la aplicación, no un conjunto oculto de valores predeterminados para «rol» y «objetivo».
def build_release_planner_prompt(events: list[dict], environment: str) -> str:
history = "\n".join(f"- {event['type']}: {event['data']}" for event in events[-6:])
return f"""You are a release planner for {environment}.
Rules:
- Never deploy to production without approval.
- Prefer verify before declare success.
History:
{history}
"""
Ese patrón funciona de la misma manera en ambos frameworks porque el control del prompt está por encima de la capa de orquestación.
Factor 3: Controla tu ventana de contexto
El factor 3 dice que debes serializar solo los eventos relevantes, en lugar de devolverle al modelo todos los tokens anteriores.
def event_to_prompt(event: dict) -> str:
return f"<{event['type']}>\n{event['data']}\n</{event['type']}>"
def thread_to_prompt(events: list[dict]) -> str:
keep = {"trigger", "release_context", "approval_response", "error"}
selected = [event for event in events if event["type"] in keep]
return "\n\n".join(event_to_prompt(event) for event in selected)
LangGraph fomenta esto mediante la definición explícita del estado; Agno lo admite mediante el ensamblado personalizado del contexto, además de resúmenes y controles del historial.
Factor 5: Unifica el estado de ejecución y el estado de negocio
El factor 5 sostiene que un mismo registro de eventos a menudo puede explicar tanto «qué sucedió» como «qué debe hacer el runtime a continuación».
events = [
{"type": "trigger", "data": "deploy release-2026.03.31"},
{"type": "release_context", "data": {"tag": "release-2026.03.31"}},
{"type": "approval_requested", "data": {"channel": "slack"}},
{"type": "approval_response", "data": {"approved": True}},
]
waiting_for_approval = events[-1]["type"] == "approval_requested"
ready_to_deploy = events[-1]["type"] == "approval_response" and events[-1]["data"]["approved"]
LangGraph expone este estilo de manera más directa, mientras que Agno tiende a encapsular una mayor parte dentro de abstracciones de sesión y ejecución.
Factor 12: Convierte tu agente en un reducer sin estado
El factor 12 es la interpretación más funcional de la misma idea: obtener el estado actual de la release mediante la reducción de eventos anteriores.
def reduce_release_events(events: list[dict]) -> dict:
state = {"approved": False, "deployed": False, "last_error": None}
for event in events:
if event["type"] == "approval_response":
state["approved"] = event["data"]["approved"]
elif event["type"] == "deploy_result":
state["deployed"] = event["data"]["status"] == "success"
elif event["type"] == "error":
state["last_error"] = event["data"]["message"]
return state
No tienes que implementar tu sistema de esta manera, pero el modelo de LangGraph, centrado en el estado, se acerca más a ella que la mayoría de los wrappers de agentes.
Cómo se interpreta en LangGraph
Este es el punto de mayor encaje de LangGraph en todo el artículo.
La documentación de persistencia señala explícitamente que LangGraph guarda el estado del grafo como checkpoints organizados en threads, y que estos checkpoints permiten workflows con intervención humana, memoria, viajes en el tiempo y ejecución tolerante a fallos. La documentación de ejecución duradera también señala explícitamente que, con un checkpointer configurado, un workflow puede reanudarse desde el progreso guardado en lugar de volver a ejecutarse desde cero.
Eso se acerca mucho a la visión de HumanLayer.
LangGraph no te obliga a usar un «objeto de agente oculto basado en intuiciones». Te anima a modelar el estado directamente, guardar checkpoints, recargarlo mediante thread_id y seguir avanzando desde ahí.
Cómo se interpreta en Agno/AgentOS
Agno adopta una abstracción diferente, pero igualmente útil.
La documentación actual de Agent.run() describe cómo el runtime construye el contexto a partir de instrucciones del sistema, entrada del usuario, historial del chat, memorias del usuario, estado de la sesión y otras entradas relevantes. Luego, los resúmenes de sesión comprimen los historiales largos en resúmenes acumulativos más cortos. AgentOS añade sesiones respaldadas por una base de datos, memoria, trazas y APIs sobre esa base.
Así que Agno sí admite la idea subyacente del runtime. Simplemente encapsula una mayor parte detrás de APIs centradas en sesiones.
Esta es la diferencia clave:
- LangGraph dice: el estado es el runtime del grafo; modélalo explícitamente.
- Agno dice: el estado es el runtime del agente o de la sesión; te ayudamos a administrarlo.
Si te gusta la orquestación explícita, LangGraph se siente más claro. Si quieres tener rápidamente un servicio de producción, Agno suele ser la opción más rápida.
3. Pausar y reanudar supera a la autonomía mágica
Aquí es donde los factores 6, 8 y 11 se convierten en la verdadera historia de producción.
El argumento respaldado por las fuentes es directo:
- los agentes deben iniciarse desde APIs simples
- deben pausarse cuando se necesita a una persona o una dependencia de larga duración
- deben reanudarse después a partir de un trigger externo
- se debe poder contactar a las personas desde la misma interfaz del runtime, no agregarlas después como una ocurrencia tardía
Esto importa más que la mayoría de las conversaciones sobre sistemas «multiagente».
Un agente de releases que puede elegir herramientas, pero no puede pausar antes del despliegue, esperar una aprobación y reanudar después sin perder su lugar, no está listo para producción. Es una demo con credenciales.
Ejemplos factor por factor
Factor 6: Inicia, pausa y reanuda con APIs simples
El factor 6 significa que el mundo exterior debe poder iniciar y reanudar el runtime mediante APIs pequeñas y predecibles.
def start_release_run(message: str, thread_id: str) -> dict:
state = {"events": [{"type": "trigger", "data": message}]}
return graph.invoke(state, config={"configurable": {"thread_id": thread_id}})
def resume_release_run(thread_id: str, approved: bool, approver: str) -> dict:
return graph.invoke(
Command(resume={"approved": approved, "approved_by": approver}),
config={"configurable": {"thread_id": thread_id}},
)
En LangGraph esto es literalmente un thread más Command(resume=...); en Agno el equivalente es session_id, ejecuciones pausadas y continue_run().
Factor 8: Controla tu flujo de control
El factor 8 insiste en que las reglas de enrutamiento deben permanecer en código determinista visible, no en comportamiento opaco del framework.
def route_release_step(state: dict) -> str:
if state.get("last_error"):
return "escalate"
if not state.get("context_loaded"):
return "collect_context"
if state["environment"] == "production" and not state.get("approved"):
return "approval_gate"
if not state.get("deployed"):
return "deploy"
return "verify"
Esto es idiomático en LangGraph y sigue siendo un modelo mental útil al encapsular herramientas de Agno o pasos de un workflow.
Factor 11: Activa desde cualquier lugar
El factor 11 significa que Slack, cron y las llamadas a la API deben normalizarse al mismo formato interno de eventos antes de que lleguen al bucle del agente.
def normalize_trigger(source: str, payload: dict) -> dict:
return {
"type": "trigger",
"data": {
"source": source,
"requested_by": payload.get("user", source),
"message": payload["message"],
"thread_key": payload.get("thread_id") or payload.get("session_id"),
},
}
Esto mantiene la lógica específica de cada canal fuera del núcleo del runtime, tanto si usas threads de LangGraph como sesiones de AgentOS.
Ejemplo de LangGraph: estado explícito, interrupción explícita, reanudación explícita
Este ejemplo se mantiene deliberadamente cerca de la mentalidad de los 12 factores: el modelo emite una intención estructurada, el código determinista controla la ejecución y la aprobación es un punto de pausa real.
from __future__ import annotations
from typing import Any, Literal, NotRequired, TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
class ReleaseDecision(TypedDict):
intent: Literal[
"collect_release_data",
"request_approval",
"deploy_release",
"post_deploy_check",
"done",
]
reason: str
class ReleaseState(TypedDict):
release_id: str
tag: str
environment: Literal["staging", "production"]
requested_by: str
events: list[dict[str, Any]]
next_action: NotRequired[ReleaseDecision]
approval: NotRequired[dict[str, Any]]
deployment_result: NotRequired[dict[str, Any]]
consecutive_errors: int
def fetch_release_context(release_id: str) -> dict[str, Any]:
# Deterministic tool boundary: this is not model logic.
return {
"release_id": release_id,
"tag": "release-2026.03.31",
"change_ticket": "CHG-8471",
"smoke_tests_passed": True,
}
def deploy_release(tag: str, environment: str) -> dict[str, Any]:
# Replace with your deploy system.
return {"tag": tag, "environment": environment, "status": "success"}
def build_prompt(events: list[dict[str, Any]]) -> str:
rendered = "\n".join(f"- {event['type']}: {event['data']}" for event in events)
return f"""You coordinate software releases.
Here is the execution history so far:
{rendered}
Return the next structured step only.
"""
def planner_node(state: ReleaseState) -> dict[str, Any]:
prompt = build_prompt(state["events"])
# Assume `llm` is an instantiated LangChain chat model client.
decision: ReleaseDecision = llm.with_structured_output(ReleaseDecision).invoke(prompt)
return {
"next_action": decision,
"events": state["events"]
+ [{"type": "planner_decision", "data": decision}],
}
def collect_context_node(state: ReleaseState) -> dict[str, Any]:
context = fetch_release_context(state["release_id"])
return {
"events": state["events"] + [{"type": "release_context", "data": context}],
}
def approval_gate_node(state: ReleaseState) -> dict[str, Any]:
approval = interrupt(
{
"kind": "deploy_approval",
"release_id": state["release_id"],
"tag": state["tag"],
"environment": state["environment"],
"requested_by": state["requested_by"],
"reason": state["next_action"]["reason"],
}
)
return {
"approval": approval,
"events": state["events"] + [{"type": "approval_response", "data": approval}],
}
def deploy_node(state: ReleaseState) -> dict[str, Any]:
try:
result = deploy_release(state["tag"], state["environment"])
return {
"deployment_result": result,
"consecutive_errors": 0,
"events": state["events"] + [{"type": "deploy_result", "data": result}],
}
except Exception as exc:
compact_error = {
"tool": "deploy_release",
"message": str(exc)[:240],
}
return {
"consecutive_errors": state["consecutive_errors"] + 1,
"events": state["events"] + [{"type": "error", "data": compact_error}],
}
def verify_node(state: ReleaseState) -> dict[str, Any]:
verification = {
"environment": state["environment"],
"healthy": True,
}
return {
"events": state["events"] + [{"type": "post_deploy_check_result", "data": verification}],
}
def route_from_planner(state: ReleaseState) -> str:
intent = state["next_action"]["intent"]
if intent == "collect_release_data":
return "collect_context"
if intent == "request_approval":
return "approval_gate"
if intent == "deploy_release":
return "deploy"
if intent == "post_deploy_check":
return "verify"
return END
builder = StateGraph(ReleaseState)
builder.add_node("planner", planner_node)
builder.add_node("collect_context", collect_context_node)
builder.add_node("approval_gate", approval_gate_node)
builder.add_node("deploy", deploy_node)
builder.add_node("verify", verify_node)
builder.add_edge(START, "planner")
builder.add_conditional_edges("planner", route_from_planner)
builder.add_edge("collect_context", "planner")
builder.add_edge("approval_gate", "planner")
builder.add_edge("deploy", "planner")
builder.add_edge("verify", "planner")
graph = builder.compile(checkpointer=InMemorySaver()) # Demo-only saver; use a durable checkpointer in production.
config = {"configurable": {"thread_id": "release-847"}}
initial_state: ReleaseState = {
"release_id": "rel_847",
"tag": "release-2026.03.31",
"environment": "production",
"requested_by": "slack:alice",
"events": [
{
"type": "trigger",
"data": {
"channel": "slack",
"message": "Deploy release-2026.03.31 to production",
},
}
],
"consecutive_errors": 0,
}
# First run pauses at approval_gate_node and persists thread state for this in-process demo.
graph.invoke(initial_state, config=config)
# Later, in the same process; with a durable checkpointer this can also resume from a webhook or admin UI:
graph.invoke(
Command(resume={"approved": True, "approved_by": "sre-oncall"}),
config=config,
)
Por qué importa este ejemplo:
- el estado compartido y tipado es explícito
- el límite entre nodo y herramienta es explícito
- la aprobación es una pausa real del runtime, no texto del asistente
- los errores compactos son simplemente más contexto serializado
thread_ides el identificador persistente
Esto se acerca mucho a la filosofía de HumanLayer.
Ejemplo de Agno/AgentOS: runtime centrado en sesiones con aprobaciones integradas
Ahora mira el mismo escenario desde la perspectiva de Agno/AgentOS.
from typing import Literal
from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.tools import tool
from pydantic import BaseModel, Field
db = PostgresDb(
db_url="postgresql+psycopg://ai:ai@localhost:5532/ai"
)
class ReleaseDecision(BaseModel):
action: Literal["collect_context", "wait_for_approval", "deploy", "verify", "done"]
summary: str
tag: str
environment: Literal["staging", "production"]
risk: Literal["low", "medium", "high"]
rollback_hint: str | None = Field(default=None)
@tool
def fetch_release_context(release_id: str) -> dict:
return {
"release_id": release_id,
"change_ticket": "CHG-8471",
"smoke_tests_passed": True,
"approver_group": "sre-oncall",
}
@tool(requires_confirmation=True)
def deploy_release(tag: str, environment: str) -> dict:
return {"tag": tag, "environment": environment, "status": "success"}
@tool
def run_post_deploy_checks(environment: str) -> dict:
return {"environment": environment, "healthy": True}
release_agent = Agent(
id="release-coordinator",
name="Release Coordinator",
model=OpenAIResponses(id="gpt-5.2"),
db=db,
tools=[fetch_release_context, deploy_release, run_post_deploy_checks],
output_schema=ReleaseDecision,
instructions=[
"Coordinate releases conservatively.",
"Always gather release context before any production action.",
"Treat production deploys as high risk and require confirmation.",
"After deployment, run verification before declaring success.",
],
add_history_to_context=True,
enable_session_summaries=True,
num_history_runs=3,
markdown=True,
)
agent_os = AgentOS(
id="release-agent-os",
agents=[release_agent],
db=db,
)
app = agent_os.get_app()
run_response = release_agent.run(
"Deploy rel_847 to production and verify it afterward.",
user_id="slack:alice",
session_id="release-847",
)
if run_response.is_paused:
for requirement in run_response.active_requirements:
print(requirement.tool.tool_name, requirement.tool.tool_args)
requirement.confirmed = True
final_response = release_agent.continue_run(
run_id=run_response.run_id,
requirements=run_response.requirements,
)
Y si expones el agente mediante AgentOS, el runtime adquiere aún más capacidades operativas:
# Request enters through the AgentOS API with a stable session_id
curl -X POST http://localhost:7777/agents/release-coordinator/runs \
-F "message=Deploy rel_847 to production" \
-F "user_id=slack:alice" \
-F "session_id=release-847"
# If deploy_release requires confirmation, the run pauses.
# An admin can resolve the approval in the AgentOS control plane
# or through the approvals API, then the run resumes.
Por qué importa este ejemplo:
session_ides el identificador persistente de la conversaciónoutput_schemate da decisiones tipadasrequires_confirmation=Truete da puntos de pausa integrados para herramientas de riesgo- los resúmenes de sesión ayudan a evitar que el contexto crezca sin límite
- AgentOS añade APIs, aprobaciones, trazas y una interfaz operativa alrededor del agente
Esto no es tan de bajo nivel como LangGraph, pero suele encajar de manera más realista en equipos que necesitan que el runtime sea un producto, no un proyecto de orquestación a medida.
4. Diseña para los fallos y un alcance acotado
Los factores 9 y 10 son menos llamativos, pero es en ellos donde muchos sistemas de agentes realmente tienen éxito o fracasan.
El argumento de HumanLayer es:
- devuelve al runtime un estado de error compacto y útil
- no permitas que un solo agente crezca hasta convertirse en una masa de propósito general con 40 herramientas
Eso es totalmente acertado.
Ejemplos factor por factor
Factor 9: Compacta los errores en la ventana de contexto
El factor 9 dice que los fallos deben comprimirse en un estado que el runtime pueda leer, para que el siguiente paso pueda adaptarse en lugar de empezar a ciegas.
def record_deploy_error(events: list[dict], exc: Exception, retries: int) -> tuple[list[dict], int]:
compact_error = {
"tool": "deploy_release",
"message": str(exc)[:180],
"retryable": retries < 2,
}
return (
events + [{"type": "error", "data": compact_error}],
retries + 1,
)
Este patrón encaja en ambos frameworks: en LangGraph mediante actualizaciones de estado y en Agno mediante contexto añadido y una ejecución reanudada.
Factor 10: Agentes pequeños y enfocados
El factor 10 tiene menos que ver con la etiqueta «multiagente» y más con preservar responsabilidades acotadas en el sistema de releases.
release_components = {
"planner": "decide next release step",
"deployer": "execute deployment only",
"verifier": "run post-deploy checks only",
"escalator": "page humans when risk or failure is high",
}
active_component = release_components["planner"]
En LangGraph estos pueden convertirse en nodos o subgrafos; en Agno pueden convertirse en agentes separados, pasos de workflow o miembros de un equipo sin cambiar la interfaz externa del producto.
Los errores compactos son entrada para el runtime, no solo logs
Si tu acción de despliegue falla porque falta el artefacto o porque una comprobación de salud agota el tiempo de espera, lo peor que puedes hacer es dejar esa información únicamente en la capa de observabilidad. El siguiente paso útil suele depender del propio error.
El patrón útil es:
- compactar el error
- agregarlo al estado
- dejar que el planificador decida si debe reintentar, escalar o detenerse
En otras palabras: los errores deben estar tanto en la observabilidad como en el contexto.
Los agentes pequeños y enfocados siguen siendo la mejor opción
Esta sección del repositorio original es una de las menos controversiales y más acertadas. Un contexto más grande, más herramientas y un alcance más amplio no solo aumentan el costo. También reducen la confiabilidad y hacen más difícil localizar los fallos.
Para el escenario de releases, una división práctica sería:
- planificador de releases
- ejecutor de despliegues
- verificador posterior al despliegue
- responsable de escalar incidentes
No necesariamente necesitas cuatro agentes desplegados por separado. Pero sí debes preservar esos límites en el diseño, ya sea como subgrafos de LangGraph, equipos o workflows de Agno, o módulos deterministas simples detrás de una única interfaz para el usuario.
Una regla operativa compacta
Si una herramienta puede modificar el estado de producción, normalmente debería cumplir las tres condiciones:
- La intención está tipada.
- La acción contempla la aprobación o, al menos, está sujeta a una política.
- El modo de fallo se serializa de vuelta al contexto de forma compacta.
Esa regla por sí sola mejorará muchos sistemas reales más que perseguir una abstracción de orquestación más sofisticada.
Dónde encaja mejor LangGraph con la filosofía de los 12 factores
Esta sección es deliberadamente editorial.
LangGraph encaja mejor cuando el trabajo consiste en:
- construir un runtime de ejecución duradera
- modelar transiciones de estado explícitas
- preservar una distinción clara entre la elección del modelo y la semántica del runtime
- hacer que la pausa y reanudación, la reproducción, el historial de threads y las bifurcaciones sean capacidades de primer nivel
La documentación actual respalda directamente esa interpretación:
- la persistencia se construye alrededor de threads y checkpoints
- las interrupciones pausan la ejecución y la reanudan con una entrada externa
- la ejecución duradera conserva el progreso en lugar de volver a ejecutar desde cero
- los subgrafos te permiten aislar componentes reutilizables o multiagente
Esa arquitectura está excepcionalmente alineada con los principios de HumanLayer.
Mi postura más firme es esta:
Si lees
12-factor-agentsy te entusiasma la idea de controlar la semántica de tu runtime, probablemente prefieras LangGraph a un wrapper de agentes de más alto nivel.
El costo, por supuesto, es que LangGraph te permitirá ser explícito sobre casi todo, lo que significa que también tendrás que hacerte cargo de casi todo:
- diseño del estado
- límites del grafo
- disciplina de idempotencia
- decisiones de serialización
- experiencia de usuario para la aprobación humana
- APIs de producción alrededor del grafo
Eso te da poder, pero también implica trabajo de ingeniería real.
Dónde Agno/AgentOS es más práctico que purista
Agno/AgentOS encaja con otro tipo de equipo.
La documentación actual señala explícitamente que AgentOS es un runtime de producción y un plano de control con:
- APIs listas para usar
- sesiones, memoria y trazas respaldadas por una base de datos
- aprobaciones y flujos con intervención humana
- streaming compatible con SSE
- RBAC y aislamiento de solicitudes
Esto importa porque muchos equipos realmente no quieren construir el runtime explícito perfecto a partir de componentes básicos. Quieren:
- un modelo de sesión sólido
- ejecución de herramientas que contemple aprobaciones
- un plano de control
- endpoints de producción
- visibilidad operativa
Agno te ofrece mucho más de eso desde el primer momento.
Aquí es donde discrepo ligeramente de una lectura purista de HumanLayer.
El repositorio tiene razón al decir que los equipos suelen necesitar un control más profundo del que exponen los frameworks. Pero algunos equipos se van demasiado al otro extremo y reconstruyen infraestructura de orquestación que nunca fue la ventaja competitiva de su producto.
Agno/AgentOS suele ser la respuesta adecuada cuando:
- Python es tu lenguaje principal
- quieres exponer rápidamente equipos de agentes y workflows como APIs
- necesitas sesiones, aprobaciones y trazas desde el principio
- estás dispuesto a aceptar un runtime con decisiones más predefinidas a cambio de entregar más rápido
Resumiría la diferencia así:
- LangGraph es mejor cuando tu problema más difícil es la semántica del runtime.
- Agno/AgentOS es mejor cuando tu problema más difícil es poner en operación servicios de agentes.
No es una distinción menor.
Conclusión
Lo más valioso de 12-factor-agents no es el número 12.
Es la insistencia en que los sistemas de agentes confiables son, sobre todo, una cuestión de:
- límites estructurados
- diseño del estado
- semántica de pausa y reanudación
- flujo de control explícito
- alcance acotado
Esa es la batalla correcta.
Si tuviera que condensar la traducción en una sola frase, sería esta:
HumanLayer te dice que controles el runtime; LangGraph te ayuda a hacerlo directamente; Agno/AgentOS te ayuda a entregar una versión administrada.
Para los ingenieros expertos, la decisión no es «¿qué framework es mejor?».
Es esta:
¿Queremos construir un runtime o queremos operar uno?
Esa es la bifurcación arquitectónica que se esconde debajo de la mayoría de los debates sobre frameworks de agentes.
Fuentes
- HumanLayer: README de 12-Factor Agents
- HumanLayer: Factor 1 - Del lenguaje natural a las llamadas a herramientas
- HumanLayer: Factor 2 - Controla tus prompts
- HumanLayer: Factor 3 - Controla tu ventana de contexto
- HumanLayer: Factor 4 - Las herramientas son solo salidas estructuradas
- HumanLayer: Factor 5 - Unifica el estado de ejecución y el estado de negocio
- HumanLayer: Factor 6 - Inicia, pausa y reanuda con APIs simples
- HumanLayer: Factor 7 - Contacta a las personas mediante llamadas a herramientas
- HumanLayer: Factor 8 - Controla tu flujo de control
- HumanLayer: Factor 9 - Compacta los errores en la ventana de contexto
- HumanLayer: Factor 10 - Agentes pequeños y enfocados
- HumanLayer: Factor 11 - Activa desde cualquier lugar y llega a los usuarios donde estén
- HumanLayer: Factor 12 - Convierte tu agente en un reducer sin estado
- Documentación de LangGraph: Persistencia
- Documentación de LangGraph: Ejecución duradera
- Documentación de LangGraph: Interrupciones
- Documentación de LangGraph: Subgrafos
- Documentación de Agno: Ejecución de agentes
- Documentación de Agno: Resúmenes de sesión
- Documentación de Agno: Introducción a los equipos
- Documentación de Agno: Introducción a los workflows
- Documentación de Agno: Confirmación del usuario
- Documentación de AgentOS: ¿Qué es AgentOS?
- Documentación de AgentOS: Plano de control
- Documentación de AgentOS: Aprobaciones
- Documentación de AgentOS: Ejemplo de intervención humana