Tres lecciones de Managed Agents para nuestros agentes

ArquitecturaAgentes IAAnthropicWhatsAppInfraestructura7 min de lectura
Tres lecciones de Managed Agents para nuestros agentes

Tres lecciones de Managed Agents para nuestros agentes

Anthropic publicó hace poco Managed Agents, un paper de ingeniería donde describen la arquitectura del servicio que corre agentes de larga duración dentro de la plataforma Claude. La tesis central es simple y dolorosa al mismo tiempo: los harnesses tradicionales codifican suposiciones sobre las limitaciones del modelo actual y se vuelven obsoletos cuando el modelo mejora.

El paper propone separar el agente en tres componentes que se reemplazan independientemente: brain (Claude + harness), hands (sandboxes y herramientas) y session (log append-only de eventos). Es un diseño inspirado en cómo los sistemas operativos virtualizan hardware: la interfaz permanece estable mientras la implementación cambia.

Leyéndolo con la lupa puesta en Formmy y nuestros droplets de NanoClaw, encontramos tres lecciones que aplican directo a problemas que ya teníamos en backlog. Las dejamos aquí porque creemos que aplican a cualquiera que esté operando agentes en producción.

Lección 1: la sesión debe vivir fuera del agente

En Formmy hoy, una conversación de WhatsApp luce así:

typescript
// server/ghosty/whatsapp-handler.ts (simplificado)
const messages = await getMessagesByConversationId(conversationId);
const result = streamText({
  model: openai("gpt-4o-mini"),
  messages: convertToModelMessages([...messages, newMessage]),
  tools: { search_context, save_lead },
  maxSteps: 5,
});

for await (const chunk of result.textStream) {
  await sendWhatsAppMessage(chunk);
}

El historial vive en Mongo (Conversation + Message), pero el loop del agente vive en memoria del proceso Node. Si ese proceso muere a mitad del stream — porque Fly hace rolling restart, porque la máquina se quedó sin RAM, porque el bridge de Baileys se desconectó — la respuesta queda truncada y nadie más puede retomarla. Peor todavía: nuestro caso del singleton de Baileys peleándose entre dos máquinas de Fly (conflict: replaced) es exactamente este problema escalado.

Managed Agents propone una interfaz mínima para que el harness sea stateless:

typescript
wake(sessionId): Session
getSession(id): Session
getEvents(sessionId, slice): Event[]
emitEvent(sessionId, event): void

La sesión es un log append-only que vive fuera de cualquier proceso. Cualquier worker puede llamar wake(sessionId) y reconstruir el estado. Esto convierte un agente de "proceso que recordó cosas" a "log que un proceso interpreta", y es la diferencia entre poder escalar horizontalmente o no.

En Formmy esto resuelve dos pendientes a la vez:

Problema actualCómo lo resuelve
Multi-machine Fly conflict en BaileysCualquier máquina puede hacer wake(sessionId) desde el log compartido
NanoClaw queda blind durante pausas y su messages.db pierde contextoEl log compartido es la fuente de verdad; NanoClaw lee del mismo evento stream

El cambio no es trivial — implica mover toda la lógica de tool-calling fuera del request handler y reformatearla como reducer de eventos — pero la interfaz es chiquita y se puede adoptar incrementalmente: primero el log, luego el wake, luego stateless workers.

Lección 2: las herramientas no deberían correr en el proceso del modelo

Hoy nuestra integración con EasyBits luce así:

typescript
// server/tools/vercel/easybitsDb.ts (simplificado)
export const easybitsDbTool = (chatbot) => tool({
  description: "Consulta la base de datos del agente",
  parameters: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    const { apiKey, dbId } = chatbot.integrations.easybits;
    const res = await fetch(
      `https://www.easybits.cloud/api/v2/databases/${dbId}/query`,
      { headers: { Authorization: `Bearer ${apiKey}` }, ... }
    );
    return await res.json();
  }
});

La API key vive en el mismo proceso donde el LLM está procesando tokens del usuario. No hay aislamiento. Si hay un bug de razonamiento que mezcla contextos de usuarios — como el leak de Ghosty que tenemos abierto en backlog — el modelo tiene acceso al material para hacer daño real.

Managed Agents lo separa con una interfaz tipada:

text
execute(name, input) -> string

El harness invoca herramientas por nombre. La implementación vive en otro proceso, otra máquina, otra región. Las credenciales viven en un vault accesible sólo desde un proxy dedicado. El proceso que ve los tokens del prompt nunca ve la API key.

typescript
// patrón a adoptar
// proceso del modelo
const result = await toolProxy.execute("easybitsDb", { query, agentId });
// proceso del proxy (otra máquina)
async function execute(name, input) {
  const creds = await vault.get(`tools/${name}/${input.agentId}`);
  return runners[name](input, creds);
}

El costo: una red hop más por tool call. La ganancia: aislamiento real de credenciales, posibilidad de ejecutar tools en runners especializados (sandbox de código, navegador headless, máquina con GPU) y la capacidad de reemplazar la implementación de un tool sin redeployar el agente.

Para Formmy esto se vuelve crítico cuando los tools tocan datos sensibles (Stripe, WhatsApp Business API, integraciones de clientes). Hoy todo corre en el mismo binario.

Lección 3: slices posicionales en lugar de cargar todo el historial

Esta es la más concreta y la más fácil de adoptar. Hoy:

typescript
const messages = await getMessagesByConversationId(conversationId);

Carga todo el historial. Para conversaciones cortas no pasa nada. Para clientes de WhatsApp con tres meses de mensajes, esto:

  • Explota tokens del prompt (caro)
  • Aumenta latencia del primer token (mal UX)
  • Aumenta probabilidad de "context anxiety" — el modelo se distrae con material irrelevante

Managed Agents expone el log con slices posicionales:

text
getEvents(sessionId, { from, to }): Event[]
getEvents(sessionId, { fromEnd: 50 }): Event[]

El agente decide qué pedazo del historial necesita en cada turno. Para mensajes rutinarios, los últimos N. Para preguntas sobre algo viejo ("¿qué te dije la semana pasada del pedido?"), búsqueda semántica sobre el log + slice contextual. El contexto del modelo deja de ser el contexto completo.

En la práctica esto se ve así:

typescript
// hoy
const all = await getMessagesByConversationId(conversationId);

// adoptando el patrón
const recent = await getEvents(sessionId, { fromEnd: 20 });
const relevant = userMentioned("anterior") || userMentioned("la semana")
  ? await searchEvents(sessionId, userMessage.text)
  : [];
const context = [...relevant, ...recent];

No requiere reescribir nada arquitectónicamente — sólo cambiar el query y agregar un índice de búsqueda semántica sobre Message. Tenemos infra para eso (Mongo vector_index_2). Es el cambio con mejor relación valor/esfuerzo de los tres.

Lo que nuestros droplets NanoClaw no exponen hoy

NanoClaw — nuestro bridge a WhatsApp Business API que corre en droplets DigitalOcean — expone dos endpoints:

EndpointFunción
POST /admin/agents (puerto 8787)CRUD de agentes y grupos
POST /message (puerto 3940)Recibir mensajes del canal

La sesión vive dentro del droplet: un messages.db SQLite local. No hay wake, no hay getEvents(slice), no hay forma de que otro nodo recupere una sesión si el droplet muere o lo reiniciamos. Si queremos escalar a varios droplets — o simplemente queremos hacer deploy sin perder contexto — el modelo de sesión local nos bloquea.

Las tres lecciones se traducen a un roadmap concreto para NanoClaw:

FaltanteEndpoint propuesto
Recovery tras restartGET /sessions/:id + POST /sessions/:id/wake
Acceso parcial al historialGET /sessions/:id/events?fromEnd=N
Tools fuera del binarioPOST /tools/:name/execute con vault de credenciales

Cómo lo vamos a adoptar

Migrar todo a la vez sería irresponsable y caro. El plan que estamos evaluando es incremental:

  1. Empezar por la lección 3 porque no requiere cambios arquitectónicos — sólo dejar de hacer getMessagesByConversationId(id) y empezar a pedir slices.
  2. Después la lección 1 — el log append-only en Mongo. Empezamos espejeando eventos a TraceEvent (que ya existe) y eventualmente migramos Conversation/Message al modelo de log puro.
  3. La lección 2 al final — separar el tool runner es el cambio más grande y sólo vale la pena después de los otros dos.

Lo que sí queda claro leyendo el paper es que el momento de pensar esto es cuando el agente todavía es chico. Cada mes que pasa con tools en el mismo proceso y sesión en memoria es un mes más de deuda arquitectónica que va a doler cuando queramos escalar de un droplet a veinte.


Prueba Formmy

¿Construyes agentes de IA en producción? En Formmy estamos aplicando estas lecciones para que tus chatbots de WhatsApp, web y voz sean robustos sin que tengas que pensar en infraestructura.

Prueba Formmy Gratis — Agentes que venden, soportan y convierten.

¿Tienes preguntas sobre cómo construimos nuestra arquitectura? Nuestro equipo está listo para platicar.