Modern Agent Engineering

12-Factor Agents en la práctica: LangChain/LangGraph y Agno/AgentOS

Una traducción, verificada contra las fuentes, de los principios de agentes de 12 factores de HumanLayer a arquitecturas prácticas con LangGraph y Agno/AgentOS, con ejemplos en Python pensados para producción.

28 min de lectura

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-agents como 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-agents una 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-agents no 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 runtimeFactoresEncaje de LangGraphEncaje de Agno/AgentOSCompensación práctica
Interfaces estructuradas para herramientas y personas1, 4, 7Sólido, explícito, de bajo nivelSólido, cómodo de usar y con todo incluidoLangGraph da más control directo; Agno exige menos configuración
Contexto y estado como runtime2, 3, 5, 12Muy sólido mediante estado de threads y checkpointsSólido mediante sesiones, resúmenes y contexto respaldado por una base de datosLangGraph es más explícito; Agno ofrece una gestión más integrada
Pausa, reanudación y flujo de control6, 8, 11Excelente con interrupciones y ejecución duraderaSólido con confirmaciones, aprobaciones y APIs de AgentOSLangGraph se acerca más a la filosofía; Agno es más fácil de operar
Manejo de fallos y alcance acotado9, 10Sólido si modelas los errores deliberadamente en el estadoSólido si mantienes acotados las herramientas y los equiposNinguno 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.31 en 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_schema hace 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_id es 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_id es el identificador persistente de la conversación
  • output_schema te da decisiones tipadas
  • requires_confirmation=True te 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:

  1. La intención está tipada.
  2. La acción contempla la aprobación o, al menos, está sujeta a una política.
  3. 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-agents y 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