¿Cómo crear tu propio arnés de agentes?

@mfpiccolo
INGLÉShace 2 meses · 28 may 2026
339K
1.9K
242
35
4.9K

TL;DR

El autor sostiene que los marcos de trabajo para agentes tradicionales son demasiado rígidos y propone un enfoque de motor modular 'iii', donde cada componente del arnés —desde las credenciales hasta los motores de políticas— es un trabajador independiente y reemplazable.

La mayoría de los equipos de agentes no construyen un arnés. Lo adoptan. LangChain, LangGraph, OpenAI Agents SDK, Anthropic SDK, CrewAI, AutoGen, el bucle, las herramientas, la memoria y la orquestación se toman del estante como una sola decisión. El arnés es un framework que importas. Si algo dentro no encaja, lo bifurcas, lo peleas o lo evitas.

Mike Piccolo - inline image

Creo que esa forma es incorrecta, y es la razón por la que todo equipo de agentes que funciona a largo plazo termina reescribiendo su arnés desde cero. El arnés no es una sola cosa. Son diez o doce cosas diferentes agrupadas porque el ecosistema circundante no te da una forma de componerlas. Los paquetes de Pi agent van por buen camino, pero todavía están en el paradigma de "Agrega otro servicio e intégralo con todos los demás". El motor de iii trata a todos los workers por igual y elimina por completo la lógica de integración. El enrutador de proveedores, la bóveda de credenciales, el motor de políticas, la puerta de aprobación, el catálogo de modelos, el almacenamiento de sesiones, el rastreador de presupuesto, el fanout de hooks posteriores a la llamada y el bucle de turno durable son preocupaciones independientes. Todos son interoperables con tu cola, servidor HTTP/API, streaming, incluso workers del navegador. Un framework que los entrega como un solo bloque te está vendiendo una compensación que no tenías que hacer.

La apuesta detrás de iii es que no deberían ser un solo bloque. Debería haber un conjunto de workers sobre un motor compartido, cada uno reemplazable, cada uno versionado de forma independiente, cada uno conectado por una primitiva única: un disparador (iii.trigger()) que todos los demás workers también usan. El arnés se convierte en una pila de workers instalables, y "construye el tuyo propio" deja de significar "bifurca un framework". Significa "intercambia algunos workers".

Este artículo explica cómo se ve eso en la práctica. La pila completa que impulsa un turno de agente de iii hoy, por qué cada capa es su propio worker y cómo reemplazar cualquiera de ellos.

Las 15 tareas que debe realizar un arnés de agente

Si reduces un arnés de agente de producción a sus responsabilidades, obtienes una lista que se ve más o menos así:

  1. Aceptar una solicitud de turno de un cliente y persistirla
  2. Resolver credenciales para el proveedor de modelo que se llamará
  3. Consultar qué puede hacer realmente el modelo elegido (visión, herramientas, streaming, ventana de contexto)
  4. Conducir la máquina de estados por turno, aprovisionar, transmitir asistente, ejecutar herramientas, dirigir, finalizar
  5. Cargar y servir cuerpos de skill que describen la forma de cada solicitud de función, códigos de error y notas de uso
  6. Ensamblar el prompt del sistema, el párrafo de modo, el preámbulo de identidad, el directorio de trabajo y el apéndice de skills predeterminadas
  7. Transmitir tokens de vuelta al cliente a medida que el modelo los produce
  8. Verificar cada llamada a herramienta (que es solo una función) contra una política antes de que se ejecute
  9. Pausar las llamadas a herramienta que necesitan una decisión humana y enrutar la respuesta al turno correcto
  10. Rastrear el gasto de LLM contra presupuestos por espacio de trabajo o por agente
  11. Ejecutar hooks antes y después de las llamadas a herramienta (registro, redacción, efectos secundarios personalizados)
  12. Persistir la sesión como un árbol ramificado para que los forks y reanudaciones funcionen
  13. Compactar el historial de sesiones cuando la ventana de contexto se llena
  14. Emitir un flujo de eventos al que la UI se suscribe
  15. Pieza faltante de cada empresa que construye agentes, veo. Llevar una traza de OpenTelemetry a través de cada paso para que puedas depurarla

Cada arnés de agente serio maneja la mayoría de estas. Los costosos hacen todas. Los baratos toman atajos y luego reconstruyen esos atajos cuando llegan a producción. Los frameworks los agrupan en un monolito y entregan una versión de cada uno. Esa última parte es la que te cuesta, porque después de un año, descubres que el motor de políticas que quieres no es el que el framework entrega, y reemplazarlo significa reemplazar el arnés.

El arnés de iii entrega cada una de esas trece tareas como un worker separado en el registro workers.iii.dev. Cada uno habla el mismo protocolo WebSocket. Cada uno registra funciones y disparadores en el mismo bus del motor. Cada uno se puede agregar, intercambiar y escribir con iii worker add en cualquier lenguaje que tenga un SDK.

La pila, por worker

Aquí está la pila de producción real del monorepo iii-hq/workers, con el trabajo de cada worker en una línea. Todo el paquete está en github.com/iii-hq/workers/harness:

Mike Piccolo - inline image

Once workers. Un motor. Cada uno en una versión publicada. Cada uno ejecutable de forma independiente como un proceso independiente (pnpm dev:<worker> en desarrollo, iii worker add <worker-específico> como binario de lanzamiento) o como parte del punto de entrada compuesto que los inicia juntos.

La razón por la que esto importa: cada recuadro en esa tabla es un lugar donde alguien puede darte un worker diferente y tú te quedas con el resto. ¿No te gusta el catálogo de modelos estático? Conecta un worker que registre models::list y lea de una API en vivo. ¿No te gustan las credenciales basadas en archivos? Conecta un worker que registre auth::get_token y lea de un administrador de secretos. ¿Quieres una máquina de estados de turno diferente para un flujo de trabajo que se ramifica de otra manera? Reemplaza turn-orchestrator, cada dependiente llama a run::start y lee turn_state a través del mismo bus, por lo que el resto de la pila no cambia.

Cómo se ejecuta realmente el bucle

La forma de un turno se ve así, recorriendo los workers en el orden en que se activan.

Un navegador/CLI/chat hace un POST de un turno a través de harness::trigger con {session_id, message_id, payload}. El meta-worker del arnés reenvía el payload a run::start. Ese salto existe para que el envoltorio de span de OpenTelemetry pueda sembrar los IDs de sesión y mensaje como baggage, que se propaga a cada llamada anidada de iii.trigger a través de cada worker en la pila. El árbol de traza del otro lado es un gráfico conectado.

run::start aterriza en el turn-orchestrator. Persiste la solicitud de ejecución, siembra el TurnStateRecord inicial en el estado de iii en session/<sid>/turn_state y regresa inmediatamente. El trabajo real ocurre dentro de la máquina de estados durable por turno, despertada por publicaciones en la FIFO turn-step.

Los dos estados terminales son stopped (salida limpia vía finishSession()) y failed (un lanzamiento inesperado de un manejador llega aquí, confirma la cola para que deje de reintentar y muestra message_complete{stop_reason:'error'} más agent_end para que la UI muestre la razón). La finalización es un puerto finishSession() en línea llamado desde cualquier ruta de fin de turno, no un paso encolado separado.

provisioning hace tres cosas. Inicia una microVM iii-sandbox si la ejecución necesita aislamiento. Llama a directory::skills::download por cada namespace en system_default_skills (por defecto ["iii://iii-directory/index"]) para que iii-directory pre-cachee los cuerpos de skill con los que comienza la ejecución. Y ensambla el prompt del sistema en tres capas: un párrafo de modo elegido de run_request.mode (plan, ask o agent), el preámbulo de identidad de iii que le enseña al modelo la convención agent_trigger y el patrón de descubrimiento bajo demanda directory::skills::get, y un índice adjunto de las skills predeterminadas con las que el agente arranca. Quien llama puede anular todo el prompt pasando system_prompt en run::start; de lo contrario, el orquestador lo construye. Los esquemas de función provienen del catálogo en vivo del motor.

assistant_streaming llama a provider::<name>::stream en el worker de proveedor que coincida con el campo provider de la ejecución. El worker de proveedor extrae credenciales vía auth::get_token (auth-credentials), transmite la respuesta SSE del modelo en un canal de iii, y el orquestador drena ese canal emitiendo eventos message_update en agent::events para el fanout de la UI. La creación del canal y el bucle de lectura viven detrás de un MessagePump basado en pull en provider-stream.ts, por lo que el estado de streaming se mantiene enfocado en las transiciones.

Cuando el asistente devuelve llamadas a herramienta, la máquina de estados entra en function_execute. Cada llamada a herramienta pasa por dispatchWithHook, el único punto de estrangulamiento en el orquestador. consultBefore llama a policy::check_permissions directamente con un tiempo de espera de 5 segundos. El worker de políticas (el meta-worker del arnés, en la pila predeterminada) lee iii-permissions.yaml, compara el function_id de la llamada con el conjunto de reglas y devuelve uno de tres resultados:

  • allow: el envío continúa; el orquestador activa la función objetivo y escribe el resultado
  • deny: el envío se cortocircuita con un DenialEnvelope, el resultado se convierte en un registro de denegación
  • needs_approval: la llamada individual se estaciona en la lista awaiting_approval del turno. El resto del lote continúa enviándose. El turno transiciona a function_awaiting_approval solo cuando una o más entradas están pendientes.

La activación de aprobación es reactiva y compartida. El orquestador registra exactamente un disparador de estado turn::on_approval en el ámbito approvals. Cuando la consola llama a approval::resolve, el worker approval-gate escribe approvals/<sid>/<cid> = {decision, reason} en el estado de iii. Esa escritura dispara turn::on_approval, que avanza la sesión afectada. function_awaiting_approval lee solo las decisiones que acaban de llegar, envía cada una a medida que llega (allow se convierte en un envío preaprobado, deny o aborted se convierte en una denegación sintética) y avanza cuando awaiting_approval[] está vacío. Sin funciones de reanudación por llamada que registrar. Sin reescaneo de inicio para recuperar aprobaciones pendientes. Un disparador cubre cada sesión.

Falla cerrado por construcción: si el worker de políticas no está disponible o el tiempo de espera de 5 segundos se agota, consultBefore deniega la llamada con un envoltorio gate_unavailable. Si iii::durable::publish mismo falló, el fanout de hooks devuelve publish_failed: true y el orquestador lo trata como una denegación.

Algunas victorias de latencia surgen de esta forma. El hook posterior a la llamada de función cortocircuita publish_collect a través de un caché de presencia de suscriptores cuando no hay un suscriptor durable registrado para el tema, eliminando aproximadamente 500ms por llamada de función ejecutada. tearing_down está en línea en finishSession(), eliminando un salto de cola durable por turno. context-compaction se suscribe a un flujo dedicado agent::turn_end que el orquestador emite en los límites de turno, por lo que las activaciones del compactador son por turno en lugar de por evento. El disparador de estado de fanout de creación de sesión se activa solo por ámbito y coincide en proceso, por lo que la llamada RPC anterior harness::session::is_create_event por escritura ha desaparecido.

Después de que el lote se completa, steering_check decide si continuar, detener o alcanzar max_turns. Si es continuar, vuelve a assistant_streaming. Si es detener o max, finishSession() se ejecuta en línea: emite agent_end, libera el sandbox, transiciona a stopped.

Durante toda la ejecución, cada worker que participa emite spans de OTel etiquetados con iii.session.id, iii.message.id y iii.function.id. Esas etiquetas son lo que engine::traces::group_by del motor lee para poblar "Agrupar por Sesión" / "Agrupar por Mensaje" / "Agrupar por Función" en la UI de trazas. La instrumentación es automática: src/runtime/worker.ts envuelve cada registerFunction en un Proxy para que ningún código por worker tenga que recordar agregar spans.

Construye el tuyo propio

La parte interesante es que ninguno de los workers anteriores es especial. Cada uno es un proceso que abre un WebSocket al motor, registra algunas funciones y disparadores, y se ejecuta. El contrato es el mismo que el contrato que usa cualquier worker de aplicación. El arnés está construido sobre la misma primitiva sobre la que está construida tu lógica de negocio.

Lo que significa que "construye tu propio arnés" se descompone en la misma operación que "escribe cualquier worker". Eliges la capa que quieres reemplazar, escribes un worker que registre las mismas funciones en el bus, ejecutas iii worker add, y el resto de la pila comienza a usar tu worker.

Dos capas no aparecen en la tabla de workers anterior, pero importan para cómo se comporta el arnés. Las skills son cómo cada worker anuncia lo que hacen sus funciones. Cada worker puede publicar una skill en iii://<worker>/<function> que el agente obtiene vía directory::skills::get antes de llamar a esa función por primera vez. El prompt del sistema se ensambla por turno a partir de un párrafo de modo, el preámbulo de identidad de iii y los cuerpos de skill predeterminados con los que se configuró la ejecución. Ambos son impulsados por el bus: las skills son servidas por el worker iii-directory, el prompt del sistema es ensamblado por el turn-orchestrator. Ambos son reemplazables.

Cinco ejemplos concretos.

Reemplaza el catálogo de modelos con una API en vivo. Escribe un worker que registre models::list, models::get, models::supports. Que obtenga datos del endpoint del catálogo de tu proveedor cada N minutos y los almacene en caché. Publícalo. iii worker add tu-org/dynamic-models-catalog. Detén el worker models-catalog estático. El turn-orchestrator nunca nota la diferencia. Llama a iii.trigger('models::list') y el motor enruta al worker que registró ese ID de función más recientemente.

Agrega un nuevo proveedor. La forma ya está probada con provider-kimi y provider-lmstudio. Cada uno es un worker que registra provider::<name>::stream y provider::<name>::complete, drena un flujo SSE de la API upstream en un canal de iii y escribe su uso de modelo en llm-budget vía budget::record. Agregar un quinto proveedor es escribir una carpeta con un iii.worker.yaml y un register.ts. Publícalo en el registro, o mantenlo local. El turn-orchestrator elige el proveedor por el campo provider de la ejecución; los nuevos proveedores están disponibles en el instante en que el worker se conecta.

Sirve skills desde un almacén de artefactos privado. Escribe un worker que registre directory::skills::get y directory::skills::list, respaldado por tu sistema de documentación interna o un bucket S3 privado. Desconecta o renombra el worker iii-directory predeterminado. El arranque del orquestador llama a directory::skills::download por namespace; tu worker responde. El patrón de "obtener la skill por función antes de llamar a una nueva función" del agente sigue funcionando sin cambios porque la forma de comunicación es la misma.

Anula completamente el prompt del sistema. run::start acepta un campo opcional system_prompt. Pásalo y el orquestador usa tu cadena textualmente, omitiendo el ensamblaje del párrafo de modo + preámbulo de identidad + apéndice de skills. La descarga de skills aún se ejecuta en el arranque, por lo que el agente mantiene el descubrimiento bajo demanda directory::skills::get incluso con un prompt personalizado.

Reemplaza la superficie de UI de la puerta de aprobación. El worker approval-gate predeterminado registra approval::resolve. El esquema de comunicación es una llamada a función:

El manejador persiste approvals/<sid>/<cid> = {decision, reason} en el estado de iii. El único disparador de estado turn::on_approval del orquestador capta esa escritura y despierta la sesión correcta. Si quieres manejar aprobaciones desde Slack en lugar de la consola, escribe un worker de Slack que escuche comandos slash /approve <id> y /deny <id>, luego llame a approval::resolve con el payload correcto. El orquestador nunca nota la diferencia. Todo el worker approval-gate permanece intacto. Agregaste un nuevo worker; no reemplazaste el existente.

Si quieres un motor de políticas diferente (OPA, Cedar, tu propio DSL), escribe un worker que registre policy::check_permissions y devuelva { decision, rule_id?, matched_constraint? }. Desconecta el worker de políticas predeterminado (que está envuelto dentro del meta-worker del arnés, así que deshabilitarías ese manejador o ejecutarías un meta-worker reducido). El consultBefore del orquestador no nota la diferencia. Mismo tiempo de espera de 5 segundos, mismas semánticas de fallo cerrado, misma forma de comunicación.

El punto de estos ejemplos no son los reemplazos específicos. Es la forma de la operación. Cada capa del arnés en la pila de iii es accesible a través de uno o dos IDs de función en el bus. Reemplazar una capa es escribir un worker que registre esos IDs. El resto del sistema permanece.

El arnés es un control deslizante, no una bifurcación en el camino

El debate clásico sobre arneses se presenta como delgado vs grueso. El bucle delgado de Anthropic versus el DAG explícito de LangGraph. El marco asume que eliges un lado y vives con él.

Cuando el arnés está compuesto por workers en el mismo bus, delgado vs grueso es solo un recuento de cuántos workers instalas. Un arnés delgado es turn-orchestrator más provider-anthropic más auth-credentials más un meta-worker de arnés mínimo. Eso es todo. Sin aprobaciones, sin presupuestos, sin motor de políticas, sin fanout de hooks. Ejecuta cualquier cosa. Confía en el modelo. Útil para agentes de investigación autónomos, bucles experimentales, cualquier cosa interna.

Un arnés grueso son los trece workers más context-compaction más un worker de políticas personalizado más una puerta de aprobación personalizada más una superficie de aprobación integrada con Slack más el worker de presupuesto aplicando límites por espacio de trabajo. Útil para un agente que ejecuta flujos de trabajo de clientes donde cada llamada a herramienta debe ser auditable y cada gasto de modelo debe consolidarse en un panel financiero.

La distancia arquitectónica entre delgado y grueso no es una reescritura. Es un cambio de configuración. Mismo protocolo de comunicación, misma forma de traza, misma historia de observabilidad. El control deslizante se mueve agregando y eliminando workers de tu config.yaml. Todo lo demás se mantiene.

Esto se aplica también dentro de un solo worker. El turn-orchestrator acaba de enviar una refactorización que colapsó su máquina de estados de once estados a siete, eliminó el mecanismo turn::approval_resume::<sid>/<cid> por llamada en favor de un disparador de estado reactivo turn::on_approval en el ámbito de approvals, e integró tearing_down en un puerto finishSession(). Cada otro worker en la pila (approval-gate, session, llm-budget, providers, models-catalog, auth-credentials, hook-fanout, context-compaction) permaneció sin cambios. La forma de comunicación de approval::resolve no se movió. Los contratos se mantuvieron. Esa es la propiedad que te da la composición: una reescritura interna importante de un worker es un cambio autocontenido porque cada vecino se comunica con él a través de IDs de función a nivel de bus.

Esta es la parte que el modelo de framework no puede darte. Un framework elige una posición en el control deslizante por ti y te bloquea. El modelo de worker deja el control deslizante en tu mano.

Qué significa esto en la práctica

Si has estado ejecutando un agente sobre un framework y sintiendo los mismos problemas de límite que la mayoría de los equipos encuentran a escala, la respuesta probablemente no sea "reescribir el arnés en nuestro propio framework". El motor de políticas no se extiende como necesitas. La UI de aprobación está cableada a la superficie de chat del framework. El almacén de credenciales no puede hablar con tu administrador de secretos. El rastreador de presupuesto está en una base de datos sidecar que la traza no puede ver. La respuesta es cambiarse a un sustrato donde el arnés esté descompuesto desde el principio.

La forma más rápida de experimentar la idea es clonar github.com/iii-hq/workers, ejecutar pnpm install, pnpm build, y ejecutar el punto de entrada compuesto. Obtendrás el arnés completo de catorce workers apuntando a un motor de iii. Puedes deshabilitar cualquier worker eliminando su entrada de la lista de inicio. Puedes intercambiar cualquier worker escribiendo un reemplazo que registre los mismos IDs de función. Puedes extender cualquier worker agregando un suscriptor a sus temas de hook. hook-fanout::publish_collect es el genérico sobre el que se construye cada hook de iii.

La documentación está en iii.dev/docs. El motor está en github.com/iii-hq/iii. El registro de workers está en workers.iii.dev. El paquete del arnés está en github.com/iii-hq/workers/harness.

La apuesta

Un arnés no es algo que instalas. Un arnés es un conjunto de tareas que tu sistema debe realizar para que un agente funcione de manera duradera, segura y observable. La era de los frameworks agrupaba esas tareas porque nada subyacente te daba una forma de componerlas.

La apuesta de iii es que una primitiva —un worker que se conecta al motor vía WebSocket y registra funciones y disparadores— es lo suficientemente pequeña para absorber cada una de esas tareas por separado, y que la pila resultante es más útil que cualquier framework porque cada capa es reemplazable de forma independiente.

No adoptas el arnés de iii. Instalas los workers que quieres, escribes los que necesitas y terminas con un arnés con la forma exacta de tu sistema. Mismo protocolo en cada capa. Misma traza en cada llamada. Mismo iii worker add para las partes que tomas del registro que para las que publicas tú mismo.

Así es como se ve "construye tu propio arnés de agente" cuando el sustrato tiene la forma correcta. Elige los workers. Escribe los que faltan. Compón. El arnés es la composición.

Únete a nosotros para construir el arnés de agente perfecto que el mundo moderno necesita: discord.gg/iiidev

iii es de código abierto. Comienza en iii.dev/docs. Los workers del arnés están en github.com/iii-hq/workers y el motor en github.com/iii-hq/iii.

— Mike Piccolo, Fundador y CEO de @iiidevs

Guardar con un clic

Lee artículos virales en profundidad con IA en YouMind

Guarda la fuente, haz preguntas concretas, resume el argumento y convierte un artículo viral en notas reutilizables en un único espacio de trabajo con IA.

Explora YouMind
Para creadores

Convierte tu Markdown en un artículo de 𝕏 impecable

Cuando publicas tus propios textos largos, dar formato en 𝕏 a imágenes, tablas y bloques de código es un fastidio. YouMind convierte un borrador completo en Markdown en un artículo de 𝕏 impecable y listo para publicar.

Prueba Markdown a 𝕏

Más patrones por descifrar

Artículos virales recientes

Explorar más artículos virales